diff --git a/.agents/shell/worktree.sh b/.agents/shell/worktree.sh index 32808e8d..f19e5c00 100644 --- a/.agents/shell/worktree.sh +++ b/.agents/shell/worktree.sh @@ -13,8 +13,9 @@ # do argumento, nunca eval). Desde a OPS110 o launch não abre um TUI local dono # da sessão: o `agent-session` sobe/reaproveita o `opencode serve` compartilhado, # cria a sessão endereçável, dispara o driver destacado (que auto-submete -# `/work-issue --issue ` no `next`, `/plan-issue` no `plan`, `/bug-fix ` -# no `fix`; `new` abre só a sessão) e ANEXA o TUI (`opencode attach -s `) — +# `/work-issue --issue ` no `next`, `/plan-issue ` no `plan` com bag, +# `/bug-fix ` no `fix`; `new` e `plan` sem bag abrem só a sessão) e ANEXA +# o TUI (`opencode attach -s `) — # fechar o terminal NÃO encerra o run; reentre com `pnpm agent:session attach` e # encerre com `pnpm agent:session stop`. Sem `--variant` (OPS95: o yargs do TUI # rejeita o flag e só imprime o helper; variantes ficam na config global da diff --git a/.agents/skills/worktree-next-issue/SKILL.md b/.agents/skills/worktree-next-issue/SKILL.md index 6a883542..54079d5a 100644 --- a/.agents/skills/worktree-next-issue/SKILL.md +++ b/.agents/skills/worktree-next-issue/SKILL.md @@ -27,11 +27,11 @@ pnpm worktree kill [--force] # destrói o worktree atual + remove s No opencode, isso é o comando **`/worktree next`** / **`/worktree kill`** (`.opencode/commands/worktree.md`), que só repassa `$ARGUMENTS` para o script. -`plan` é o primo do `next`: worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main. **Cada invocação cria um DIFERENTE** (sessões paralelas): com `bag` opcional → branch `plans/plan-issue-` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-` livre. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot (prefixo minúsculo `plans/…` vs branch `-` sempre uppercase-led). `new` é o irmão **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar): mesma semântica de "um DIFERENTE por invocação", branch `work/` (sufixo `-2`/`-3` em colisão) ou `work/` sequencial, prefixo `work/…` que não colide com `next` nem `plan`. O fluxo desta skill (fila → próxima Issue → implementação) é só `next`. +`plan` é o primo do `next`: worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main. **Cada invocação cria um DIFERENTE** (sessões paralelas): com `bag` opcional → branch `plans/plan-issue-` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-` livre. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot (prefixo minúsculo `plans/…` vs branch `-` sempre uppercase-led). `new` é o irmão **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar): mesma semântica de "um DIFERENTE por invocação", branch `work/` (sufixo `-2`/`-3` em colisão) ou `work/` sequencial, prefixo `work/…` que não colide com `next` nem `plan`. No terminal, `plan ` usa o bag também como mensagem de abertura (auto-submete `/plan-issue `, como o `fix`); `plan` **sem bag** abre a sessão sem driver, para você digitar o `/plan-issue` com contexto (OPS119 — supera o auto-envio incondicional do OPS31). O fluxo desta skill (fila → próxima Issue → implementação) é só `next`. **Default-go:** `next`, `plan` e `new` imprimem `cd ` na última linha **por padrão**; `--stay` suprime; modelos por invocação via `--cheap/--pro/--zen/--go/--alibaba/--glm/--free` (mapa fixo `WORKTREE_MODEL_MAP`, at-most-one; sem flag o preset permanece); `--go` remapeado em OPS93 para o provider OpenCode Go, em OPS95 para `opencode-go/hy3` e em OPS112 para `opencode-go/deepseek-v4.1-flash` (antes no-op do OPS24). No terminal interativo, use a função `worktree()` de **`.agents/shell/worktree.sh`** (uma linha de `source` no profile) para o `cd` ser aplicado de verdade no shell que te chamou — node não consegue mudar o cwd do shell pai. `kill` também imprime `cd
` no fim (a sessão nunca fica num diretório destruído). -**Launch do opencode (OPS26 + OPS33 + OPS31 + OPS93 + OPS95 + OPS110):** só a função shell marca `TEQO_WORKTREE_TERMINAL=1`; com o marcador, `next`/`plan`/`new`/`fix` imprimem também a diretiva `launch node /scripts/agent-session.mjs start --purpose= --dir= --model [--issue ] [--argument ""]` **antes** do `cd`, e a função a executa depois do cd. Desde a OPS110 o launch não abre um TUI dono da sessão: o `agent-session` sobe/reaproveita o `opencode serve` compartilhado, cria a sessão endereçável, dispara o driver destacado (auto-submete `/work-issue --issue ` no `next`, `/plan-issue` no `plan`, `/bug-fix ` no `fix`; `new` abre só a sessão) e anexa o TUI (`opencode attach -s `) — fechar o terminal **não** encerra o run; reentre com `pnpm agent:session attach` e encerre com `pnpm agent:session stop` (ver a seção "Sessões persistentes" de `docs/AGENT-OPS.md`). Sem `--variant` na diretiva (OPS95: o yargs do TUI rejeita o flag desconhecido e só imprime o helper — variantes ficam na config global do opencode, selecionáveis via Ctrl+T; `opencode run --variant max` segue sendo a via CLI para runs headless). Modelo por invocação via `--cheap` (cheapestinference/deepseek-v4-flash), `--pro` (deepseek/deepseek-v4-pro), `--zen` (opencode/muse-spark-1.3-contributor-free), `--go` (opencode-go/deepseek-v4.1-flash), `--alibaba` (alibaba-token-plan/deepseek-v4-flash) — valores corrigidos no OPS95 (`--go`/`--zen` em OPS112) — e `--glm` (opencode-go/glm-5.3-flash, OPS100), `--free` (openrouter/openrouter/free, OPS100); sem flag o preset `deepseek/deepseek-flash` permanece. Sem `exec`: sair do opencode volta ao shell no worktree. Presets e mapa são constantes em `scripts/lib/worktree.mjs`; o ciclo de vida das sessões (serve/start/attach/stop/list) vive em `scripts/agent-session.mjs`. **O comando `/worktree` do opencode (e qualquer automação sem o marcador) nunca lança o TUI.** +**Launch do opencode (OPS26 + OPS33 + OPS31 + OPS93 + OPS95 + OPS110):** só a função shell marca `TEQO_WORKTREE_TERMINAL=1`; com o marcador, `next`/`plan`/`new`/`fix` imprimem também a diretiva `launch node /scripts/agent-session.mjs start --purpose= --dir= --model [--issue ] [--argument ""]` **antes** do `cd`, e a função a executa depois do cd. Desde a OPS110 o launch não abre um TUI dono da sessão: o `agent-session` sobe/reaproveita o `opencode serve` compartilhado, cria a sessão endereçável, dispara o driver destacado (auto-submete `/work-issue --issue ` no `next`, `/plan-issue ` no `plan` com bag, `/bug-fix ` no `fix`; `new` e `plan` sem bag abrem só a sessão) e anexa o TUI (`opencode attach -s `) — fechar o terminal **não** encerra o run; reentre com `pnpm agent:session attach` e encerre com `pnpm agent:session stop` (ver a seção "Sessões persistentes" de `docs/AGENT-OPS.md`). Sem `--variant` na diretiva (OPS95: o yargs do TUI rejeita o flag desconhecido e só imprime o helper — variantes ficam na config global do opencode, selecionáveis via Ctrl+T; `opencode run --variant max` segue sendo a via CLI para runs headless). Modelo por invocação via `--cheap` (cheapestinference/deepseek-v4-flash), `--pro` (deepseek/deepseek-v4-pro), `--zen` (opencode/muse-spark-1.3-contributor-free), `--go` (opencode-go/deepseek-v4.1-flash), `--alibaba` (alibaba-token-plan/deepseek-v4-flash) — valores corrigidos no OPS95 (`--go`/`--zen` em OPS112) — e `--glm` (opencode-go/glm-5.3-flash, OPS100), `--free` (openrouter/openrouter/free, OPS100); sem flag o preset `deepseek/deepseek-flash` permanece. Sem `exec`: sair do opencode volta ao shell no worktree. Presets e mapa são constantes em `scripts/lib/worktree.mjs`; o ciclo de vida das sessões (serve/start/attach/stop/list) vive em `scripts/agent-session.mjs`. **O comando `/worktree` do opencode (e qualquer automação sem o marcador) nunca lança o TUI.** ## Fluxo quando invocado como skill (agentes que não têm opencode) diff --git a/.opencode/commands/worktree.md b/.opencode/commands/worktree.md index 0d81512f..ee3d54bf 100644 --- a/.opencode/commands/worktree.md +++ b/.opencode/commands/worktree.md @@ -5,7 +5,7 @@ description: Claima a próxima Issue (next [--issue N] [--stay] [--cheap|--pro|- Rode o script determinístico abaixo (não improvise a lógica) e reporte o resultado de forma concisa. - `next [--issue N] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`: **claima** a próxima Issue claimável da fila (mesma fila/ordem e lock otimista de `pnpm agent:claim`) e cria (ou reutiliza) o worktree dela. Claim falhou → reporte o motivo — não há worktree. `--issue N` claima a Issue direcionada (`ready`) ou **reabre** uma já claimada (`in-progress`, sem re-claim). A saída avisa "já claimada — NÃO rodar `pnpm agent:claim`". Por padrão o script imprime `cd ` na última linha — aplique-o na sessão de shell atual (`cd `) e reporte o novo diretório. `--stay` suprime o `cd` (não troque de diretório) mas o claim ainda acontece. `--cheap/--pro/--zen/--go/--alibaba/--glm/--free` escolhe o modelo por invocação no mapa fixo `WORKTREE_MODEL_MAP` (cheap=cheapestinference/deepseek-v4-flash, pro=deepseek/deepseek-v4-pro, zen=opencode/muse-spark-1.3-contributor-free, go=opencode-go/deepseek-v4.1-flash, alibaba=alibaba-token-plan/deepseek-v4-flash, glm=opencode-go/glm-5.3-flash, free=openrouter/openrouter/free); at-most-one (múltiplas → erro). -- `plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`: cria um worktree de planejamento **diferente a cada invocação** (branch `plans/plan-issue-`, ou `plans/plan-issue-` sequencial sem bag; sufixo `-2`/`-3` em colisão) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. Não claima nada. Mesmo contrato de `cd` do `next`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. +- `plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`: cria um worktree de planejamento **diferente a cada invocação** (branch `plans/plan-issue-`, ou `plans/plan-issue-` sequencial sem bag; sufixo `-2`/`-3` em colisão) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. Não claima nada. No terminal, o bag é também a mensagem de abertura: o launch carrega `--argument=""` e o driver auto-submete `/plan-issue `; **sem bag a sessão abre sem driver** (você digita o `/plan-issue` com contexto). Mesmo contrato de `cd` do `next`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. - `new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`: cria um worktree **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar nada) **diferente a cada invocação** (branch `work/`, ou `work/` sequencial sem bag; sufixo `-2`/`-3` em colisão); o prefixo minúsculo `work/…` nunca conflita com `next` nem `plan`. Não claima nada. Mesmo contrato de `cd` do `plan`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. - `fix [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`: cria um worktree de **correção de bug** (skill `/bug-fix`) **diferente a cada invocação** (branch `fix/`, ou `fix/` sequencial sem bag; sufixo `-2`/`-3` em colisão); o prefixo minúsculo `fix/…` nunca conflita com `next`, `plan` nem `new`. **Não claima nem cria Issues** — o registro do bug é o post-mortem da skill. O bag é a descrição do bug: no terminal, o launch carrega `--argument=""` e o driver destacado auto-submete `/bug-fix ` (a descrição chega com a skill). Mesmo contrato de `cd` do `plan`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. - `kill [--force]`: destrói o worktree em que você está, encerra a sessão persistente do run (OPS110) e remove seus bancos. O script imprime `cd
` na última linha — aplique-o (`cd
`) para a sessão sair do diretório destruído. diff --git a/AGENTS.md b/AGENTS.md index 59a3a66e..f92dc788 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,4 +72,4 @@ src/ ## Per-worktree environments -`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt` / `teqo_wt_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. Same provisioning for `plan [bag]`, `new [bag]` and `fix [bag]` worktrees (`plans/plan-issue-…`, `work/…`, `fix/…` namespaces — `fix` opens `/bug-fix` with the bug description delivered; no claim, no Issue). No terminal, o launch de `next`/`plan`/`new`/`fix` roda a sessão no `opencode serve` compartilhado e anexa o TUI (OPS110): fechar o terminal **não** encerra o run — reentre com `pnpm agent:session attach` e encerre com `pnpm agent:session stop` (estado/endereçamento em `~/.local/state/teqo/agent-sessions`; contrato do painel OPS109). The skill `local-database` covers the full workflow. +`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt` / `teqo_wt_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. Same provisioning for `plan [bag]`, `new [bag]` and `fix [bag]` worktrees (`plans/plan-issue-…`, `work/…`, `fix/…` namespaces — `plan ` opens `/plan-issue` with the bag as the opening message and `plan` without a bag is driverless so you type the context (OPS119), `fix` opens `/bug-fix` with the bug description delivered; no claim, no Issue). No terminal, o launch de `next`/`plan`/`new`/`fix` roda a sessão no `opencode serve` compartilhado e anexa o TUI (OPS110): fechar o terminal **não** encerra o run — reentre com `pnpm agent:session attach` e encerre com `pnpm agent:session stop` (estado/endereçamento em `~/.local/state/teqo/agent-sessions`; contrato do painel OPS109). The skill `local-database` covers the full workflow. diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 3d4fabbb..ba6c9ec6 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -64,13 +64,13 @@ Labels (GitHub): `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, ` O launch do terminal (`worktree next|plan|new|fix`) **não abre mais um TUI dono da sessão**: ele chama `pnpm agent:session start …`, que sobe/reaproveita o `opencode serve` compartilhado, cria a sessão endereçável, dispara o **driver destacado** (`opencode run --attach … --auto --command `, que auto-aprova permissões enquanto vive) e **anexa** o TUI (`opencode attach -s `). Fechar o terminal/TUI desconecta — o run continua; encerrar é ato explícito. -| Comando | Faz | -| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `pnpm agent:session serve [--hostname=H] [--port=P]` | Sobe/reaproveita o servidor (loopback `127.0.0.1:4199`; `TEQO_AGENT_SERVER_HOST/PORT`) | -| `pnpm agent:session start --purpose= --dir= --model= [--issue=N] [--argument=""] [--new]` | Cria a sessão, dispara o driver (quando há skill) e anexa o TUI | -| `pnpm agent:session attach [--session=\|--branch=\|--issue=N]` | (Re)entra na sessão — default: branch do cwd; sobe o servidor se ele estiver fora | -| `pnpm agent:session stop [--session=\|--branch=\|--issue=N]` | Ato explícito de encerrar: abort via API + fim do driver + `stoppedAt` | -| `pnpm agent:session list [--json]` | Status dos runs (`working\|idle\|stopped\|unknown`); `--json` é o contrato do OPS109 | +| Comando | Faz | +| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `pnpm agent:session serve [--hostname=H] [--port=P]` | Sobe/reaproveita o servidor (loopback `127.0.0.1:4199`; `TEQO_AGENT_SERVER_HOST/PORT`) | +| `pnpm agent:session start --purpose= --dir= --model= [--issue=N] [--argument=""] [--new]` | Cria a sessão, dispara o driver (quando há comando a auto-submeter — `plan` sem bag é driverless) e anexa o TUI | +| `pnpm agent:session attach [--session=\|--branch=\|--issue=N]` | (Re)entra na sessão — default: branch do cwd; sobe o servidor se ele estiver fora | +| `pnpm agent:session stop [--session=\|--branch=\|--issue=N]` | Ato explícito de encerrar: abort via API + fim do driver + `stoppedAt` | +| `pnpm agent:session list [--json]` | Status dos runs (`working\|idle\|stopped\|unknown`); `--json` é o contrato do OPS109 | - **Endereçamento (OPS109):** `~/.local/state/teqo/agent-sessions/.json` (`TEQO_AGENT_SESSION_DIR` > `XDG_STATE_HOME`) guarda `version, code, issue, purpose, branch, dir, sessionID, url, model, driverPid, logPath, startedAt, stoppedAt`; `list --json` entrega `{version, server, sessions:[…status]}` — é a única fonte para "abrir a sessão da Issue". - **Sair ≠ encerrar:** fechar o TUI/terminal não mata o run (verificado: SIGKILL no cliente e `tmux kill-session` não abortam; `attach` replayer histórico e streaming ao vivo). `Ctrl+C` no TUI limpa o input; sair é fechar o terminal ou o keybind `app_exit` (`ctrl+c,ctrl+d,q`). diff --git a/docs/changelog/2026-09-16-ops119.md b/docs/changelog/2026-09-16-ops119.md new file mode 100644 index 00000000..7c7132e2 --- /dev/null +++ b/docs/changelog/2026-09-16-ops119.md @@ -0,0 +1 @@ +**OPS119 — worktree plan abre sem auto-enviar o `/plan-issue` (mensagem opcional na linha) (2026-09-16):** `worktree plan` sem argumento agora cria a sessão persistente **sem driver** (mesmo contrato observável do `new`) — o humano digita o `/plan-issue` com o contexto que quiser; `worktree plan ""` segue auto-submetendo uma única `/plan-issue ` e usa a mesma mensagem como bag do branch (`plans/plan-issue-`), espelhando o `fix `. A regra vive no dono do ciclo (`purposeInvocation` em `scripts/lib/agent-session.mjs`): o ramo `plan` reusa a sanitização do `fix` (`["\\]` + `trim`) e devolve `null` quando o bag fica vazio (divergência deliberada do `fix`, cujo bag vazio ainda submete o comando bare). `opencodeLaunchDirective` (`scripts/lib/worktree.mjs`) passa a emitir `--argument` para `fix || plan`, `cmdPlan` (`scripts/worktree.mjs`) repassa `argument: bag` e o aviso "sem comando/issue válida" do `cmdStart` isenta `plan` (driverless por desenho). `next`/`new`/`fix`, `--stay` e o comando `/worktree` intactos; supersede o auto-envio incondicional do OPS31 (#593). Sem migration, sem schema, sem UI. Testes: `agentSession.unit.spec.ts` e `worktree.unit.spec.ts` repinados (plan sem bag → `null`; plan com bag → `--argument` sanitizado; `new` segue ignorando o argumento); `pnpm gate:fast` verde e walkthrough manual do launch registrado no PR. Documentação viva atualizada em `scripts/worktree.mjs`, `.agents/shell/worktree.sh`, `.opencode/commands/worktree.md`, `.agents/skills/worktree-next-issue/SKILL.md`, `AGENTS.md` e `docs/AGENT-OPS.md`. diff --git a/docs/plans/escala-dry-pos-ops119.md b/docs/plans/escala-dry-pos-ops119.md new file mode 100644 index 00000000..c39087a7 --- /dev/null +++ b/docs/plans/escala-dry-pos-ops119.md @@ -0,0 +1,64 @@ +# OPS119+ — launch do worktree: risk map das libs + pin do wiring do `plan` + +Status: rascunho +Atualizado em: 2026-09-16 +Issue: #1071 +Intenção: débito pós-entrega da OPS119 (#1066) +Priority: P3 +Impeccable: A — N/A (CLI/shell, sem superfície de produto) +Appetite: ~1h eng + +## Origem + +Triagem do `/simplify` da entrega OPS119 (#1066 — `worktree plan` sem bag abre +driverless): dois achados de score 3 na **mesma superfície** (launch do worktree +↔ `agent-session`), mesclados neste lote. + +- **S6 — risk map (integridade do guard):** `HIGH_RISK_EXACT` + (`scripts/lib/test-affected-core.mjs:25-53`) só marca `scripts/worktree.mjs`. + Um diff que toca **apenas** `scripts/lib/worktree.mjs`, + `scripts/lib/agent-session.mjs` ou `scripts/agent-session.mjs` classifica + `classifyTestScope` como `none` — a suíte unit **não roda** no PR (e o static + scope também é `none`). A entrega OPS119 só rodou full porque tocou + `scripts/worktree.mjs`; um fix lib-only passaria verde sem teste. Fail-closed + do blast radius: as libs puras são pinadas por specs unit que nunca seriam + exercitados no diff que as quebra. +- **S4 — pin do wiring:** o repasse `argument: bag` de `cmdPlan` → + `cmdNamespaceBranch` (`scripts/worktree.mjs:764-768`) não tem pin unitário — + só as libs puras (`purposeInvocation`, `opencodeLaunchDirective`) estão + pinadas. Um `argument` dropado no runner não é pego por teste. + +## Fases + +1. **F1 — risk map (~30min):** incluir os arquivos do launch no piso de blast + radius — `scripts/agent-session.mjs`, `scripts/lib/agent-session.mjs`, + `scripts/lib/worktree.mjs` em `HIGH_RISK_EXACT`. **Alternativa rejeitada:** + prefixo `scripts/lib/` inteiro — engoliria libs sem harness próprio e + forçaria full em diffs triviais. Atualizar o pin do dono + (`tests/unit/testAffected.unit.spec.ts` / + `tests/unit/ciSkipInvariants.unit.spec.ts`). Verificação: o classificador + devolve `full` para um diff só-lib. +2. **F2 — pin do wiring (~30min):** tornar o mapeamento do `cmdPlan` testável. + **Opções:** A) exportar o mapa purpose→opções (não o spawn); B) caso unit + que injeta as opções em `cmdNamespaceBranch`. **Rejeitada:** teste que cria + worktree de verdade (lento, side effects, colide no slot). Verificação: + `pnpm test:unit`. + +## Já resolvido no simplify (não reabrir) + +- Doc drift de `opencodeLaunchDirective` e do comentário de + `cmdNamespaceBranch`, e o fixture fantasma do spec `driverArgs` + (`plan-issue` com `arguments:''` → hoje `bug-fix` com bag vazio) — corrigidos + na própria OPS119. + +## Explicitamente fora + +- **S1** (`return { command, arguments: '' }` final de `purposeInvocation` + inalcançável hoje): é o fallback fail-safe para um purpose mapeado sem args — + removê-lo reintroduziria a classe de bug que ele evita. Descartado (YAGNI). +- **S2** (sanitização `replace(/["\\]/g,'')` duplicada entre as duas libs): + defer — gatilho: 3º call site ou import cruzado das libs. +- **S3** (política driverless em `!==` no CLI): defer — gatilho: 3º purpose + driverless/sem-args. +- **Prefill-sem-submit no opencode** (upstream): fora — proposição + `~/Code/propositions/opencode/prefill-prompt-sem-submit.md` já re-flagada. diff --git a/docs/plans/ops119-worktree-plan-sem-auto-envio-plan-issue-impl.md b/docs/plans/ops119-worktree-plan-sem-auto-envio-plan-issue-impl.md new file mode 100644 index 00000000..f67ccd86 --- /dev/null +++ b/docs/plans/ops119-worktree-plan-sem-auto-envio-plan-issue-impl.md @@ -0,0 +1,102 @@ +# Impl: OPS119 — worktree plan: abrir sem auto-enviar o `/plan-issue` (mensagem opcional na linha) + +Status: em execução +Atualizado em: 2026-09-16 +Issue: #1066 +Intenção: docs/plans/ops119-worktree-plan-sem-auto-envio-plan-issue.md +Appetite restante: herdado (~0,5 dia eng — 2 libs puras + 1 script + 2 specs + textos) + +## Leitura da intenção + +- **Outcome:** `worktree plan` (sem argumento) abre a sessão persistente **sem driver** (nenhuma mensagem auto-submetida, mesmo contrato observável do `new`); `worktree plan ""` auto-submete **uma** `/plan-issue ` e usa a mesma mensagem como bag do branch — simétrico a `fix `. `next`/`new`/`fix`, `--stay` e o comando `/worktree` intactos. +- **O que NÃO negociar:** a mensagem passa pela **mesma sanitização do `fix`** (`replace(/["\\]/g,'')` + `trim`); bag vazio **não** gera driver; `next` segue `/work-issue --issue `; `new` segue driverless; `fix ` segue `/bug-fix `; `--stay` e/ou `TEQO_WORKTREE_TERMINAL` ausente continuam sem diretiva. Sem migration/access/Consent/UI (Impeccable A). +- **O que reavaliar (hipóteses da "Direção no codebase" que o explorador corrigiu):** + - O plano de intenção não previu o aviso `[agent:session] purpose=… sem comando/issue válida` (`scripts/agent-session.mjs:536-539`): `plan` sem msg cairia no `else if (purpose !== 'new')` e **logaria um falso alarme**. Entra no escopo (é a mesma aceitação observável: "abre sem driver", sem ruído). + - O docblock de `purposeInvocation` (`scripts/lib/agent-session.mjs:164-181`) e o USAGE do `start` (`:83`, "model obrigatório next/plan/fix") ficam **imprecisos** — o `plan` deixa de exigir `--model` quando não há driver. Ajuste de texto barato, feito na passagem. + - `planBranchName`/`namespaceBranchName` (`scripts/lib/worktree.mjs:150-170,277-278`) já resolvem o branch a partir do bag (fallback `plano`, `slugify`) — **nada a mudar** ali; o bag vazio já cai no sequencial `plans/plan-issue-`. + +## Abordagem recomendada + +```mermaid +flowchart LR + A["worktree plan [bag]"] --> B["cmdPlan (scripts/worktree.mjs)"] + B -->|"argument: bag"| C["cmdNamespaceBranch"] + C --> D["printLaunchDirective"] + D --> E["opencodeLaunchDirective (scripts/lib/worktree.mjs)"] + E -->|"plan + bag sanitizado → --argument=«JSON»"| F["launch agent-session start --purpose=plan [--argument]"] + F --> G["cmdStart → purposeInvocation (scripts/lib/agent-session.mjs)"] + G -->|"bag válido → {command:'plan-issue', arguments}"| H["spawn driverArgs (uma /plan-issue )"] + G -->|"sem bag / bag vazio → null"| I["sessão criada, SEM driver; só attach"] + H --> J["attach TUI"] + I --> J +``` + +**Opções consideradas:** A | B | C +**Recomendação:** **A** — o dono da decisão "quando um purpose carrega skill" é `purposeInvocation` (`scripts/lib/agent-session.mjs`), e o ramo do `plan` é **cópia exata** do ramo `fix` (`:192-197`): sanitiza, vazio → `null`, senão `{ command, arguments }`. O `opencodeLaunchDirective` passa a emitir `--argument` para `fix || plan` e `cmdPlan` passa `argument: bag` — o restante do caminho (`cmdNamespaceBranch` runner, `printLaunchDirective`, `driverArgs`, dispatch) **não muda**. +**Rejeitadas:** **B** — manter `purposeInvocation` devolvendo comando "bare" para `plan` e decidir o não-driver dentro do `cmdStart` (CLI): quebra a pureza do dono do ciclo (a lib passaria a mentir "sempre há comando"), e a regra ficaria em dois lugares quando um 5º purpose chegar. **C** — criar uma flag nova de driver (`--driver`/`--no-driver`) na diretiva: vocabulário novo sem necessidade, e destrói a simetria com `fix`, que já expressa "tem driver" pela presença do bag. + +### Componentes / mudanças + +- **`purposeInvocation`** (`scripts/lib/agent-session.mjs:184-199`): ramo `plan` antes do `return { command, arguments: '' }` final, espelhando `fix` — `const sanitized = typeof argument === 'string' ? argument.replace(/["\\]/g, '').trim() : ''`; **`if (!sanitized) return null`**; `return { command, arguments: sanitized }`. Ponto de **divergência deliberada** do `fix`: no `fix` bag vazio ainda auto-submete `/bug-fix` bare; no `plan` (sem contexto) vazio → **sem driver**. +- **Docblock de `purposeInvocation`** (`:176-181`): descrever `plan` sem-arg → `null`, com-arg → `/plan-issue `; `fix` inalterado. +- **`opencodeLaunchDirective`** (`scripts/lib/worktree.mjs:202-232`): condição `if (purpose === 'fix')` → `if (purpose === 'fix' || purpose === 'plan')` (`:227`); comentário do `:224-226` passa a citar o par. O sufixo do issue segue só do `next` (`:223`). +- **`cmdPlan`** (`scripts/worktree.mjs:755-764`): acrescenta `argument: bag` ao `cmdNamespaceBranch` (como `cmdFix :810`). `cmdNamespaceBranch` (`:675-743`), `printLaunchDirective` (`:221-235`) e o dispatch (`:1037-1038`) **intocados**. +- **`cmdStart`** (`scripts/agent-session.mjs:536-539`): `else if (purpose !== 'new')` → `else if (purpose !== 'new' && purpose !== 'plan')` + comentário ("`plan` sem bag é driverless por desenho"). +- **Textos do CLI:** header/USAGE de `scripts/agent-session.mjs` (`:9`, `:74-83` — `--model` obrigatório "quando há driver"); docblock/uso do `plan` em `scripts/worktree.mjs:68-83,979-992`; `.agents/shell/worktree.sh:9-31,43-49`; `.opencode/commands/worktree.md:8`; `.agents/skills/worktree-next-issue/SKILL.md:23,30,34`; `AGENTS.md:75`; `docs/AGENT-OPS.md:65,70`. +- **Changelog:** `docs/changelog/2026-09-16-ops119.md` (uma entrada). Não editar o HISTORY nem o agregado. +- **Migration:** sem migration (nenhum schema/payload é tocado). +- **Access / Consent:** N/A. +- **UI:** N/A (Impeccable A — CLI/shell). + +### Dados → forma (se aplicável) + +- N/A — sem superfície de dados. A "forma" é o contrato observável do launch: `plan` ≡ `new` sem bag; `plan ` ≡ `fix ` com a skill trocada. Rejeitada a forma "prefill-draft" (upstream inexistente no opencode 1.18.31 — proposição `~/Code/propositions/opencode/prefill-prompt-sem-submit.md` re-flagada fora do repo). + +## Fases verificáveis + +1. **F1 — Sessão (owner) ~40% do appetite:** ramo `plan` em `purposeInvocation` + docblock; isenção do aviso em `cmdStart`. Prova: unit do `agentSession.unit.spec.ts` (plan sem arg → `null`; com arg → `{command:'plan-issue', arguments}`; bag vazio/só aspas → `null`; `next`/`fix`/`new`/unknown inalterados). +2. **F2 — Launch ~20%:** `opencodeLaunchDirective` (`fix || plan`) + `cmdPlan(argument: bag)`. Prova: unit do `worktree.unit.spec.ts` (plan sem arg = diretiva de hoje; plan com bag carrega `--argument="…"`; plan ignora `issueNumber`; `new` segue ignorando `argument`). +3. **F3 — Testes + textos ~30%:** repin dos dois specs; varredura dos textos listados por "plan auto-submete `/plan-issue`"; entrada de changelog. +4. **F4 — Gates ~10%:** `pnpm gate:fast` (lint + typecheck + unit) verde; walkthrough manual (terminal com `opencode` instalado): `worktree plan` abre sem mensagem e `worktree plan "mensagem"` abre com `/plan-issue mensagem` + branch `plans/plan-issue-mensagem`; push via `pnpm push`. Este PR toca `scripts/worktree.mjs` (HIGH_RISK_EXACT) ⇒ CI = full de qualquer forma. + +## Rabbit holes / Não escopo (engenharia) + +- **Novo vocabulário de CLI** (subcomandos, `--message`, separar bag de mensagem): cortado — só o par sem-arg/arg, idêntico ao `fix`. +- **Prefill-sem-submit no opencode:** upstream; apenas re-flagar a proposição (fora do repo). +- **Gateway `--prompt`/`--variant`:** intocados (OPS95/OPS110). +- **`kill`/`attach`/`resolveStartDecision`/`deriveSessionStatus`:** agnósticos ao driver (`driverPid:null` → `idle`); não mexer. +- **`HIGH_RISK_EXACT` não cobre `scripts/lib/agent-session.mjs` nem `scripts/lib/worktree.mjs`** (`scripts/lib/test-affected-core.mjs:25-53`): gap **pré-existente e independente** desta entrega (o PR atual toca `scripts/worktree.mjs`, que já é high-risk). Não expandido aqui; registrado como Issue de follow-up **#1071** (plano `docs/plans/escala-dry-pos-ops119.md`, com o pin do wiring do `cmdPlan`). + +## Riscos e mitigação + +- **Regressão em `next`/`new`/`fix` ao mexer na condição da diretiva:** a mudança é aditiva (`|| purpose === 'plan'`) e os specs de `new`/`fix` ficam verdes sem edição no comportamento deles; teste explícito trava `new` ignorar `argument` e `fix` manter `--argument`. +- **Divergência entre slug do branch e mensagem sanitizada:** branch usa `slugify` (existente), mensagem reusa a sanitização do `fix`; ambos idempotentes entre a camada diretiva e o `purposeInvocation`. +- **Falso alarme "sem driver" no `plan`:** coberto pela isenção do `cmdStart` (F1) — é justamente o que a intenção exige ("mesmo contrato observável do `new`"). +- **`--model` dito obrigatório mas ausente no plan driverless:** o aviso/erro real (`cmdStart:476`) só dispara quando `invocation` existe; texto do USAGE ajustado em F3. +- **Texto/doc remanescente afirmando auto-envio sem-arg:** varredura por `/plan-issue` nos arquivos listados antes do commit (histórico congelado intocado: `docs/plans/worktree-plan-abre-opencode-com-plan-issue*.md`, `docs/CHANGELOG-AGENTS-HISTORY.md:241`). + +## Triage pós-simplify (capture-review-debts) + +- **S4 (pin do wiring `cmdPlan` → `cmdNamespaceBranch`) + S6 (`HIGH_RISK_EXACT` das libs do launch)** — score 3, mesma superfície → **registrado** como Issue **#1071** (`docs/plans/escala-dry-pos-ops119.md`; `kind:chore`, P3, `depends: OPS119`). +- **S1** (`return { command, arguments: '' }` final de `purposeInvocation`) — descartado: fallback fail-safe para um purpose mapeado sem args. +- **S2** (sanitização `["\\]` duplicada entre as duas libs) — defer, gatilho: 3º call site ou import cruzado das libs. +- **S3** (política driverless em `!==` no CLI) — defer, gatilho: 3º purpose driverless. +- **S5** (doc drift de `opencodeLaunchDirective`/`cmdNamespaceBranch` + fixture fantasma do `driverArgs`) — já resolvido no simplify. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção coberto: `plan` sem arg = `null`/driverless; `plan ` = `{command:'plan-issue', arguments:}`; `next`/`new`/`fix`/`--stay`/`/worktree` intactos. +- [x] Invariantes AGENTS/engineering-standards: reusa o dono (`purposeInvocation`/caminho do `fix`), sem módulo/abstração nova; sem `push:false`/schema/migration; sem segredo. +- [x] Testes de domínio (unit) repinados onde o contrato muda: `tests/unit/agentSession.unit.spec.ts` e `tests/unit/worktree.unit.spec.ts` (plan sem bag → `null`; plan com bag → `--argument` sanitizado; `new`/`fix`/`next` inalterados). +- [x] Textos vivos atualizados; histórico congelado intocado; `docs/changelog/2026-09-16-ops119.md` criado. +- [x] `pnpm gate:fast` verde; cadeia bag→diretiva→`purposeInvocation` verificada por execução direta das libs (walkthrough do launch registrado no PR). + +## Self-score decision-quality + +1. Decisões caras com rejeitadas? **Sim** — D1 (dono da decisão), D2 (bag vazio → `null`, divergência deliberada do `fix`), D4 (isenção do aviso), D6 (classifier deferido). +2. Cabe no appetite (~0,5 dia)? **Sim** — 2 libs puras + 1 script + 2 specs + textos. +3. Rabbit holes nomeados? **Sim** — vocabulário novo, prefill upstream, classifier, kill/attach. +4. Depth check reusa? **Sim** — ramo `fix`, `slugify`, sanitização e `driverArgs` existentes; zero abstração nova. +5. Intenção preservada? **Sim** — engenharia não reescreve outcome; só estende o caminho já provado do `fix`. + +**Self-score: 5/5** (gate ≥4 atendido). Status inicial `rascunho` — a pausa do GATE de aprovação humana vale. diff --git a/scripts/agent-session.mjs b/scripts/agent-session.mjs index 622de53a..dec2aa52 100644 --- a/scripts/agent-session.mjs +++ b/scripts/agent-session.mjs @@ -9,7 +9,8 @@ * pnpm agent:session start --purpose= --dir= [--model=] [--issue=N] [--argument=""] [--new] [--hostname=H] [--port=P] * Cria a sessão endereçável, sobe o driver destacado * (`opencode run --attach … --auto --command -- …`) quando o - * purpose tem skill, e abre o TUI anexado (`opencode attach -s `). + * purpose tem skill invocável (`plan` sem bag é driverless), e abre o + * TUI anexado (`opencode attach -s `). * Fechar o TUI/terminal NÃO encerra o run; `stop` encerra. Reusa o run * existente quando o driver ainda vive ou a sessão está busy; `--new` * força sessão nova. @@ -80,7 +81,7 @@ const USAGE = `Uso: pnpm agent:session [flags] stop [--session=|--branch=|--issue=N] encerra de forma explícita (abort + fim do driver) list [--json] [--hostname=H] [--port=P] status dos runs registrados - --model é obrigatório quando o purpose tem skill (next/plan/fix); 'new' não dispara driver.` + --model é obrigatório quando o purpose dispara driver (next/fix, plan com bag); 'new' e 'plan' sem bag não disparam driver.` /** Loopback by default; the credential rides the env, never the state/log. */ const serverPassword = () => process.env.OPENCODE_SERVER_PASSWORD ?? '' @@ -533,7 +534,9 @@ const cmdStart = async (flags) => { driverArgs({ url: server.url, sessionID, dir, model, invocation }), { cwd: dir, logPath: state.logPath, label: `o driver do run (log: ${state.logPath})` }, ) - } else if (purpose !== 'new') { + } else if (purpose !== 'new' && purpose !== 'plan') { + // `plan` sem bag é driverless por desenho (o humano digita o + // `/plan-issue`); só os demais purposes sem comando são anomalia. console.warn( `[agent:session] purpose=${purpose} sem comando/issue válida — sessão criada sem driver.`, ) diff --git a/scripts/lib/agent-session.mjs b/scripts/lib/agent-session.mjs index b73dc497..84fc093a 100644 --- a/scripts/lib/agent-session.mjs +++ b/scripts/lib/agent-session.mjs @@ -163,8 +163,9 @@ export const parseServerState = (text) => { /** * Skill command per launch purpose — the terminal launch auto-submits it on - * the server-owned session. `new` deliberately has none ("apenas conversar"): - * the session is created and attached, no driver. + * the server-owned session. `new` deliberately has none ("apenas conversar"); + * `plan` only submits `/plan-issue` when a bag (the opening message) is given, + * otherwise it is driverless like `new` so the human can type the context. */ export const SESSION_COMMAND_BY_PURPOSE = { next: 'work-issue', @@ -175,10 +176,12 @@ export const SESSION_COMMAND_BY_PURPOSE = { /** * `{ command, arguments }` for the purpose, or null when there is nothing to - * auto-submit. `next` carries only a valid claimed issue (`--issue N`), `fix` - * carries the sanitized bag (quotes/backslashes stripped — the value rides a - * single argv token), `plan` sends the bare command, unknown purposes degrade - * to no command (fail-safe, mirrors the old directive). + * auto-submit. `next` carries only a valid claimed issue (`--issue N`); `fix` + * and `plan` carry the sanitized bag (quotes/backslashes stripped — the value + * rides a single argv token). `fix` with an empty bag still submits the bare + * `/bug-fix`; `plan` with an empty bag is driverless by design (no context to + * plan with — mirrors `new`). Unknown purposes degrade to no command + * (fail-safe, mirrors the old directive). * @param {{ purpose?: string, issueNumber?: number | string | null, argument?: string | null }} [options] */ export const purposeInvocation = ({ purpose = null, issueNumber = null, argument = null } = {}) => { @@ -189,10 +192,11 @@ export const purposeInvocation = ({ purpose = null, issueNumber = null, argument if (!Number.isInteger(number) || number <= 0) return null return { command, arguments: `--issue ${number}` } } - if (purpose === 'fix') { + if (purpose === 'fix' || purpose === 'plan') { // Mesma sanitização da diretiva de launch (fronteira dupla de propósito: // o xargs da camada shell e o argv do `opencode run` não honram escapes). const sanitized = typeof argument === 'string' ? argument.replace(/["\\]/g, '').trim() : '' + if (purpose === 'plan' && !sanitized) return null return { command, arguments: sanitized } } return { command, arguments: '' } diff --git a/scripts/lib/worktree.mjs b/scripts/lib/worktree.mjs index 0dc983ec..8340f0c0 100644 --- a/scripts/lib/worktree.mjs +++ b/scripts/lib/worktree.mjs @@ -188,11 +188,11 @@ const namespaceBranchName = ({ prefix, bag = '', taken = new Set(), fallback }) * is always `/`, so the line never needs * quoting for the path. `next` with an `issueNumber` carries `--issue=` * (OPS33: the launch delivers the claimed issue to the agent; the skill reads - * the rest from GitHub), `fix` with an `argument` carries `--argument=""` - * (the bug description arrives with the skill; quotes/backslashes are stripped - * because the shell layer tokenizes with xargs, which does not honor - * backslash escapes). Returns `null` outside the terminal so the `/worktree` - * opencode command never launches a nested TUI. + * the rest from GitHub), `fix` (bug description) and `plan` (opening message) + * with an `argument` carry `--argument=""` (the text arrives with the + * skill; quotes/backslashes are stripped because the shell layer tokenizes + * with xargs, which does not honor backslash escapes). Returns `null` outside + * the terminal so the `/worktree` opencode command never launches a nested TUI. * `sessionScript` é o caminho do CLI de sessão — `worktree.mjs` passa o * ABSOLUTO do checkout que emitiu a diretiva (worktrees reabertos/criados * antes do merge não têm o arquivo novo; o relativo apontaria para o branch @@ -221,10 +221,10 @@ export const opencodeLaunchDirective = ({ // The issue suffix belongs to `next` alone — `plan`/`new`/`fix` never carry // a claimed issue (fail-safe: a stray issueNumber must not break them). if (purpose === 'next' && issueNumber) args.push(`--issue=${issueNumber}`) - // The bag suffix belongs to `fix` alone — the bug description arrives with - // the skill. JSON.stringify quotes the value (it carries spaces); xargs - // strips the quotes at execution time. - if (purpose === 'fix') { + // The bag suffix belongs to `fix` (bug description) and `plan` (opening + // message) — the text arrives with the skill. JSON.stringify quotes the + // value (it carries spaces); xargs strips the quotes at execution time. + if (purpose === 'fix' || purpose === 'plan') { const sanitized = typeof argument === 'string' ? argument.replace(/["\\]/g, '').trim() : '' if (sanitized) args.push(`--argument=${JSON.stringify(sanitized)}`) } diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index a36a60d7..2a090720 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -76,11 +76,14 @@ * deles (branch nem slot) colide com um `next` * posterior (prefixo minúsculo `plans/…`). * Mesmo provisionamento isolado do `next`; no - * terminal, mesma diretiva `launch` — o driver - * destacado auto-submete `/plan-issue` (OPS31: a - * sessão abre no fluxo de planejamento, sem - * digitação) e `--model ` quando a flag de - * modelo está presente (OPS110: attach/detach). + * terminal, mesma diretiva `launch` — `--model` + * `` quando a flag está presente (OPS110: + * attach/detach). Com `bag`, o driver destacado + * auto-submete `/plan-issue `; sem `bag`, a + * sessão abre SEM driver (como o `new`) para o + * humano digitar o `/plan-issue` com contexto + * (OPS119; supersede o auto-envio incondicional + * do OPS31). * pnpm worktree new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] * cria um worktree NEUTRO novo — sem função * pré-definida (explorar ideia, conversar, ou @@ -670,7 +673,8 @@ const buildTakenBranchNames = () => { * and prints the `cd ` line by default (`--stay` suppresses). Every * invocation creates a DIFFERENT worktree — parallel sessions never share one. * `argument` (optional) rides the launch prompt — for `fix`, the bug - * description the `/bug-fix` skill receives. + * description the `/bug-fix` skill receives; for `plan`, the opening message + * the `/plan-issue` driver auto-submits (absent → driverless session). */ const cmdNamespaceBranch = async ({ stay, @@ -761,6 +765,7 @@ const cmdPlan = async (stay, skipMigrate, bag, flags = {}) => sessionLabel: bag && bag.trim() ? `lote "${bag}"` : 'sequencial', branchName: (taken) => planBranchName({ bag, taken }), flags, + argument: bag, }) /** @@ -988,7 +993,7 @@ if (!subcommand) { ' minúsculo plans/… nunca colide com o branch - de `next`; no terminal,', ) console.log( - ' mesma diretiva `launch` — o driver destacado auto-submete /plan-issue (abre no fluxo de planejamento, sem digitação) e --model quando a flag está presente', + ' mesma diretiva `launch` — --model quando a flag está presente; com bag o driver auto-submete /plan-issue , sem bag a sessão abre SEM driver (você digita o /plan-issue)', ) console.log( `\n new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`, diff --git a/tests/unit/agentSession.unit.spec.ts b/tests/unit/agentSession.unit.spec.ts index fb1ec7f4..8432d6dd 100644 --- a/tests/unit/agentSession.unit.spec.ts +++ b/tests/unit/agentSession.unit.spec.ts @@ -215,11 +215,20 @@ describe('purposeInvocation (mapa purpose→comando movido do worktree)', () => expect(purposeInvocation({ purpose: 'next', issueNumber: 0 })).toBeNull() }) - it('plan sends the bare /plan-issue command', () => { - expect(purposeInvocation({ purpose: 'plan' })).toEqual({ command: 'plan-issue', arguments: '' }) - expect(purposeInvocation({ purpose: 'plan', issueNumber: 7 })).toEqual({ + it('plan without a bag starts no driver — the human types the context', () => { + expect(purposeInvocation({ purpose: 'plan' })).toBeNull() + expect(purposeInvocation({ purpose: 'plan', issueNumber: 7 })).toBeNull() + expect(purposeInvocation({ purpose: 'plan', argument: ' "" ' })).toBeNull() + }) + + it('plan carries the opening message as the sanitized bag (xargs/argv-safe)', () => { + expect(purposeInvocation({ purpose: 'plan', argument: 'revisa o gate do designer' })).toEqual({ command: 'plan-issue', - arguments: '', + arguments: 'revisa o gate do designer', + }) + expect(purposeInvocation({ purpose: 'plan', argument: 'a"b\\c' })).toEqual({ + command: 'plan-issue', + arguments: 'abc', }) }) @@ -357,14 +366,14 @@ describe('driverArgs + attachArgs (o argv verificado ao vivo)', () => { ]) }) - it('omits the `--` separator when there are no arguments (plan)', () => { + it('omits the `--` separator when the invocation carries no arguments (fix with an empty bag)', () => { expect( driverArgs({ url: 'http://127.0.0.1:4199', sessionID: 'ses_abc', - dir: '/work/plan', + dir: '/work/fix', model: 'm', - invocation: { command: 'plan-issue', arguments: '' }, + invocation: { command: 'bug-fix', arguments: '' }, }), ).toEqual([ 'run', @@ -373,12 +382,12 @@ describe('driverArgs + attachArgs (o argv verificado ao vivo)', () => { '-s', 'ses_abc', '--dir', - '/work/plan', + '/work/fix', '--model', 'm', '--auto', '--command', - 'plan-issue', + 'bug-fix', ]) }) diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index 2612cb40..ceb68161 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -187,7 +187,7 @@ describe('opencodeLaunchDirective (terminal-only agent-session launch, OPS26 + O ) }) - it('plan launches through the same session CLI (the skill is auto-submitted by the driver)', () => { + it('plan without a bag launches session-only — the human types /plan-issue', () => { expect(opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true })).toBe( `launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()}`, ) @@ -252,11 +252,11 @@ describe('opencodeLaunchDirective (terminal-only agent-session launch, OPS26 + O ) }) - it('the argument belongs to fix alone — plan/new ignore it', () => { + it('the argument belongs to fix and plan — new ignores it', () => { expect( opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true, argument: 'bag x' }), ).toBe( - `launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()}`, + `launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()} --argument="bag x"`, ) expect( opencodeLaunchDirective({ dir, purpose: 'new', terminal: true, argument: 'bag x' }), @@ -265,6 +265,19 @@ describe('opencodeLaunchDirective (terminal-only agent-session launch, OPS26 + O ) }) + it('plan strips quotes/backslashes from the bag and drops --argument when it empties', () => { + expect( + opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true, argument: 'a"b\\c ideia' }), + ).toBe( + `launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()} --argument="abc ideia"`, + ) + expect( + opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true, argument: ' "" ' }), + ).toBe( + `launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()}`, + ) + }) + it('pins the preset constants — fallback comum deepseek-flash, override via OPENCODE_WORKTREE_MODEL', () => { expect(OPENCODE_PRESET_MODEL).toBe(presetInEffect()) expect(WORKTREE_TERMINAL_ENV).toBe('TEQO_WORKTREE_TERMINAL')