feat(OPS119): worktree plan abre driverless sem bag e auto-envia /plan-issue <bag> (Closes #1066)

`worktree plan` sem argumento cria a sessão persistente sem driver (contrato do
`new`); com bag auto-submete uma única `/plan-issue <bag>` e usa o mesmo texto
como rótulo do branch, espelhando o `fix`.

- purposeInvocation (lib/agent-session): ramo plan reusa a sanitização do fix e
  devolve null quando o bag fica vazio (driverless por desenho).
- opencodeLaunchDirective emite --argument para fix||plan; cmdPlan repassa
  argument: bag; cmdStart isenta plan do aviso "sem comando".
- next/new/fix, --stay e o comando /worktree intactos.
- Specs repinados; textos vivos e changelog atualizados. Débito de risk map das
  libs + pin do wiring registrado em #1071.
This commit is contained in:
Francisco Solla 2026-09-16 00:41:45 -03:00
parent 7b840e7dc7
commit d70a1b5072
14 changed files with 253 additions and 51 deletions

View file

@ -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 <N>` no `next`, `/plan-issue` no `plan`, `/bug-fix <bag>`
# no `fix`; `new` abre só a sessão) e ANEXA o TUI (`opencode attach -s <id>`) —
# `/work-issue --issue <N>` no `next`, `/plan-issue <bag>` no `plan` com bag,
# `/bug-fix <bag>` no `fix`; `new` e `plan` sem bag abrem só a sessão) e ANEXA
# o TUI (`opencode attach -s <id>`) —
# 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

View file

@ -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-<bag>` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-<n>` 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 `<Code>-<slug>` 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/<bag>` (sufixo `-2`/`-3` em colisão) ou `work/<n>` 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-<bag>` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-<n>` 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 `<Code>-<slug>` 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/<bag>` (sufixo `-2`/`-3` em colisão) ou `work/<n>` sequencial, prefixo `work/…` que não colide com `next` nem `plan`. No terminal, `plan <bag>` usa o bag também como mensagem de abertura (auto-submete `/plan-issue <bag>`, 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 <dir>` 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 <main>` 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 <abs>/scripts/agent-session.mjs start --purpose=<next|plan|new|fix> --dir=<dir> --model <preset|map> [--issue <N>] [--argument "<bag>"]` **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 <N>` no `next`, `/plan-issue` no `plan`, `/bug-fix <bag>` no `fix`; `new` abre só a sessão) e anexa o TUI (`opencode attach -s <id>`) — 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 <abs>/scripts/agent-session.mjs start --purpose=<next|plan|new|fix> --dir=<dir> --model <preset|map> [--issue <N>] [--argument "<bag>"]` **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 <N>` no `next`, `/plan-issue <bag>` no `plan` com bag, `/bug-fix <bag>` no `fix`; `new` e `plan` sem bag abrem só a sessão) e anexa o TUI (`opencode attach -s <id>`) — 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)

View file

@ -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 <dir>` na última linha — aplique-o na sessão de shell atual (`cd <dir>`) 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-<bag>`, ou `plans/plan-issue-<n>` 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 <dir>` 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-<bag>`, ou `plans/plan-issue-<n>` 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="<bag>"` e o driver auto-submete `/plan-issue <bag>`; **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 <dir>` 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/<bag>`, ou `work/<n>` 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 <dir>` 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/<bag>`, ou `fix/<n>` 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="<bag>"` e o driver destacado auto-submete `/bug-fix <bag>` (a descrição chega com a skill). Mesmo contrato de `cd` do `plan`: aplique a última linha `cd <dir>` 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 <main>` na última linha — aplique-o (`cd <main>`) para a sessão sair do diretório destruído.

View file

@ -72,4 +72,4 @@ src/
## Per-worktree environments
`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt<slot>` / `teqo_wt<slot>_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<slot>` / `teqo_wt<slot>_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 <bag>` 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.

View file

@ -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 <skill>`, que auto-aprova permissões enquanto vive) e **anexa** o TUI (`opencode attach -s <sessionID>`). 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=<next\|plan\|new\|fix> --dir=<D> --model=<M> [--issue=N] [--argument="<bag>"] [--new]` | Cria a sessão, dispara o driver (quando há skill) e anexa o TUI |
| `pnpm agent:session attach [--session=<ses_…>\|--branch=<B>\|--issue=N]` | (Re)entra na sessão — default: branch do cwd; sobe o servidor se ele estiver fora |
| `pnpm agent:session stop [--session=<ses_…>\|--branch=<B>\|--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=<next\|plan\|new\|fix> --dir=<D> --model=<M> [--issue=N] [--argument="<bag>"] [--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=<ses_…>\|--branch=<B>\|--issue=N]` | (Re)entra na sessão — default: branch do cwd; sobe o servidor se ele estiver fora |
| `pnpm agent:session stop [--session=<ses_…>\|--branch=<B>\|--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/<slug>.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,<leader>q`).

View file

@ -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 "<mensagem>"` segue auto-submetendo uma única `/plan-issue <mensagem>` e usa a mesma mensagem como bag do branch (`plans/plan-issue-<slug>`), espelhando o `fix <bag>`. 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`.

View file

@ -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.

View file

@ -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 "<mensagem>"` auto-submete **uma** `/plan-issue <mensagem>` e usa a mesma mensagem como bag do branch — simétrico a `fix <bag>`. `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 <N>`; `new` segue driverless; `fix <bag>` segue `/bug-fix <bag>`; `--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-<n>`.
## 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 <msg>)"]
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 <msg>`; `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 <bag>` ≡ `fix <bag>` 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 <msg>` = `{command:'plan-issue', arguments:<sanitized>}`; `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.

View file

@ -9,7 +9,8 @@
* pnpm agent:session start --purpose=<next|plan|new|fix> --dir=<D> [--model=<M>] [--issue=N] [--argument="<bag>"] [--new] [--hostname=H] [--port=P]
* Cria a sessão endereçável, sobe o driver destacado
* (`opencode run --attach … --auto --command <skill> -- …`) quando o
* purpose tem skill, e abre o TUI anexado (`opencode attach -s <id>`).
* purpose tem skill invocável (`plan` sem bag é driverless), e abre o
* TUI anexado (`opencode attach -s <id>`).
* 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 <serve|start|attach|stop|list> [flags]
stop [--session=<ses_…>|--branch=<B>|--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.`,
)

View file

@ -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: '' }

View file

@ -188,11 +188,11 @@ const namespaceBranchName = ({ prefix, bag = '', taken = new Set(), fallback })
* is always `<root without spaces>/<slugified branch>`, so the line never needs
* quoting for the path. `next` with an `issueNumber` carries `--issue=<N>`
* (OPS33: the launch delivers the claimed issue to the agent; the skill reads
* the rest from GitHub), `fix` with an `argument` carries `--argument="<bag>"`
* (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="<bag>"` (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)}`)
}

View file

@ -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 <map>` quando a flag de
* modelo está presente (OPS110: attach/detach).
* terminal, mesma diretiva `launch` — `--model`
* `<map>` quando a flag está presente (OPS110:
* attach/detach). Com `bag`, o driver destacado
* auto-submete `/plan-issue <bag>`; 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 <dir>` 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 <code>-<slug> 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 <map> quando a flag está presente',
' mesma diretiva `launch` — --model <map> quando a flag está presente; com bag o driver auto-submete /plan-issue <bag>, 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]`,

View file

@ -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',
])
})

View file

@ -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')