From fd68014edc79ec63f4cdc156770eb5548f661838 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Fri, 18 Sep 2026 16:38:45 -0300 Subject: [PATCH] feat(OPS127): launch do worktree nasce na variante max (Closes #1149) --- .agents/shell/worktree.sh | 7 +- .agents/skills/local-database/SKILL.md | 2 +- .agents/skills/worktree-next-issue/SKILL.md | 2 +- .opencode/commands/worktree.md | 2 +- docs/AGENT-OPS.md | 4 +- docs/changelog/2026-09-18-ops127.md | 1 + docs/plans/worktree-go-variant-max-impl.md | 144 ++++++++++++++++++++ scripts/lib/agent-session.mjs | 23 +++- scripts/lib/worktree.mjs | 27 +++- scripts/worktree.mjs | 13 +- tests/unit/agentSession.unit.spec.ts | 17 ++- tests/unit/worktree.unit.spec.ts | 4 +- 12 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 docs/changelog/2026-09-18-ops127.md create mode 100644 docs/plans/worktree-go-variant-max-impl.md diff --git a/.agents/shell/worktree.sh b/.agents/shell/worktree.sh index bc4fc7b5..2a257a1b 100644 --- a/.agents/shell/worktree.sh +++ b/.agents/shell/worktree.sh @@ -17,9 +17,10 @@ # `/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 -# máquina, via Ctrl+T). Modelo por invocação: `--cheap` +# encerre com `pnpm agent:session stop`. Sem `--variant` na diretiva nem no attach +# (OPS95: o yargs do TUI rejeita o flag). OPS127: a sessão nasce na variante `max` +# — ela vai no body do `POST /session` e no argv do `opencode run` do +# driver/headless (o TUI anexado herda), nunca na linha do launch. Modelo por invocação: `--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), diff --git a/.agents/skills/local-database/SKILL.md b/.agents/skills/local-database/SKILL.md index 7c8f80c1..b73ebb3c 100644 --- a/.agents/skills/local-database/SKILL.md +++ b/.agents/skills/local-database/SKILL.md @@ -26,7 +26,7 @@ VMs Cloud **não rodam** `docker compose` / `pnpm db:start`. Postgres nativo: `. ## Per-worktree environments (`pnpm worktree next`) -Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]` provisions each worktree with a deterministic isolated env (`next`/`plan`/`new` print `cd ` by default — apply it, or pass `--stay` to suppress; `--cheap/--pro/--zen/--go/--alibaba/--glm/--free` selects the model per invocation via `WORKTREE_MODEL_MAP`): +Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]` provisions each worktree with a deterministic isolated env (`next`/`plan`/`new` print `cd ` by default — apply it, or pass `--stay` to suppress; `--cheap/--pro/--zen/--go/--alibaba/--glm/--free` selects the model per invocation via `WORKTREE_MODEL_MAP` — the session is created in variant `max` (OPS127)): - `slot` = numeric part of the issue code (`C15` → 15, `B164` → 164; hash fallback without digits; bumped on collision among live worktrees). - Dev server port `3100+slot`; databases `teqo_wt` (dev) and `teqo_wt_test` (tests), created on the **one shared container** with the full migration chain **and the minimal seed** applied (one-time, ~30 s each; OPS28 parity with CI, which runs `migrate → seed:minimal` before e2e — a worktree's `pnpm test:e2e` mirrors CI). diff --git a/.agents/skills/worktree-next-issue/SKILL.md b/.agents/skills/worktree-next-issue/SKILL.md index 54079d5a..0d192a3f 100644 --- a/.agents/skills/worktree-next-issue/SKILL.md +++ b/.agents/skills/worktree-next-issue/SKILL.md @@ -31,7 +31,7 @@ No opencode, isso é o comando **`/worktree next`** / **`/worktree kill`** (`.op **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` 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.** +**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 nem no attach (OPS95: o yargs do TUI rejeita o flag desconhecido e só imprime o helper). OPS127: a sessão nasce na variante `max` — ela viaja no body do `POST /session` e no argv do `opencode run` do driver/headless (o TUI anexado herda), nunca na linha do launch. 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 ff8439ef..6635864d 100644 --- a/.opencode/commands/worktree.md +++ b/.opencode/commands/worktree.md @@ -10,7 +10,7 @@ Rode o script determinístico abaixo (não improvise a lógica) e reporte o resu - `fix [bag] [--stay] [--auto] [--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. -> **OPS122 — flags honradas ou falha alto.** `--auto` (opt-out do GATE da skill) transposta para a invocation do driver: `/work-issue --issue --auto` (next), `/plan-issue --auto ` (plan com bag), `/bug-fix --auto ` (fix). Vale em `next`/`fix` e em `plan` COM bag; falha alto em `new`, `plan` sem bag, `--stay --auto`, `--headless --auto` e `--auto` **neste comando** (sem `TEQO_WORKTREE_TERMINAL` não há launch — rode `pnpm worktree` no terminal). Flag desconhecida falha alto com sugestão, nunca é descartada. O modelo pedido (`--zen/--go/…`) agora é gravado na própria SESSÃO do servidor, então o TUI anexado o herda inclusive nos driverless (`new`, `plan` sem bag). +> **OPS122 — flags honradas ou falha alto.** `--auto` (opt-out do GATE da skill) transposta para a invocation do driver: `/work-issue --issue --auto` (next), `/plan-issue --auto ` (plan com bag), `/bug-fix --auto ` (fix). Vale em `next`/`fix` e em `plan` COM bag; falha alto em `new`, `plan` sem bag, `--stay --auto`, `--headless --auto` e `--auto` **neste comando** (sem `TEQO_WORKTREE_TERMINAL` não há launch — rode `pnpm worktree` no terminal). Flag desconhecida falha alto com sugestão, nunca é descartada. O modelo pedido (`--zen/--go/…`) agora é gravado na própria SESSÃO do servidor — com a variante `max` (OPS127: body do `POST /session` e argv do driver/headless; o `attach` segue sem flag) —, então o TUI anexado herda modelo e variante inclusive nos driverless (`new`, `plan` sem bag). > **Este comando roda SEM o marcador `TEQO_WORKTREE_TERMINAL`, então nunca recebe a diretiva `launch`** — só o `cd`. O launch (OPS26/OPS33+OPS93+OPS95+**OPS110**: delega ao `scripts/agent-session.mjs`, que sobe o servidor compartilhado, dispara o driver destacado e anexa o TUI — fechar o terminal não encerra o run) acontece apenas na função `worktree()` de `.agents/shell/worktree.sh` (terminal interativo); nunca abra um TUI aninhado a partir daqui. diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 96f30cfd..f055c8fb 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -62,12 +62,12 @@ Labels (GitHub): `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, ` ## Sessões persistentes (attach/detach — OPS110) -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. +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 `). A sessão nasce na variante `max` (OPS127): ela vai no body do `POST /session` e no argv do `opencode run` do driver/headless — o `opencode attach` não emite flag. 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á comando a auto-submeter — `plan` sem bag é driverless) e anexa o TUI | +| `pnpm agent:session start --purpose= --dir= --model= [--issue=N] [--argument=""] [--new]` | Cria a sessão (variante `max` no body — OPS127), 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 | diff --git a/docs/changelog/2026-09-18-ops127.md b/docs/changelog/2026-09-18-ops127.md new file mode 100644 index 00000000..61e88889 --- /dev/null +++ b/docs/changelog/2026-09-18-ops127.md @@ -0,0 +1 @@ +**OPS127 — launch do worktree nasce na variante `max` do modelo selecionado (2026-09-18):** todo lançamento (`worktree next|plan|new|fix`, incluindo `fix --headless`/auto-unblock e o preset sem flag) passa a pedir a variante `max` do modelo escolhido, sem flag nova no terminal. A variante viaja no **body do `POST /session`** (`buildCreateSessionBody` → `Session.model.variant`, herdada pelo TUI anexado) e no argv do `opencode run` do driver destacado (`driverArgs`) e do headless (`opencodeHeadlessArgs`); o `opencode attach` e a diretiva `launch` continuam **sem** `--variant` (o yargs do TUI rejeita o flag — veto OPS95 preservado) e a config global da máquina fica intocada. Reabre o veto OPS95 apenas para o caminho da sessão: o fato novo é que o opencode 1.18.31 aceita `variant` dentro de `model` no `POST /session` e persiste. Dono único do literal é `MODEL_VARIANT` em `scripts/lib/agent-session.mjs`; o cardápio/preset de modelos (`WORKTREE_MODEL_MAP`, `OPENCODE_PRESET_MODEL`) e o mapa de flags não mudam — `--go` segue `opencode-go/deepseek-v4.1-flash` (OPS112). Sem migration, sem schema, sem UI. Verificação: `pnpm test:unit` (pins de `agentSession.unit.spec.ts` e `worktree.unit.spec.ts` atualizados) e `pnpm gate:fast`. diff --git a/docs/plans/worktree-go-variant-max-impl.md b/docs/plans/worktree-go-variant-max-impl.md new file mode 100644 index 00000000..a6e47ade --- /dev/null +++ b/docs/plans/worktree-go-variant-max-impl.md @@ -0,0 +1,144 @@ +# Impl: `pnpm worktree … --go` abre a sessão no DeepSeek V4.1 Flash (`opencode-go`) na variante `max` + +Status: aprovado +Atualizado em: 2026-09-18 +Issue: #1149 +Intenção: docs/plans/worktree-go-variant-max.md +Appetite restante: herdado (~0,5 dia eng — plumbing de launch + textos + pins). Sem migration, sem schema, sem UI, sem DB. +Modo: autônomo (`--auto`) — impl plan nasce aprovado pelo agente. + +## Leitura da intenção + +- **Outcome:** todo lançamento de worktree (`next`, `plan`, `new`, `fix` — inclusive `fix --headless`/auto-unblock e o preset sem flag) cria a sessão do servidor com `Session.model.variant === 'max'` para o modelo selecionado, e `--go` continua em `opencode-go/deepseek-v4.1-flash`. Verificável no body de `POST /session` virado `Session.model.variant`, no argv do driver/headless e nos pins unitários. A TUI anexada (`opencode attach`, sem flag) herda. +- **O que NÃO negociar:** + - Nenhuma flag `--variant` no `opencode attach` nem na diretiva `launch` (o yargs do TUI rejeita o flag — veto OPS95). + - Config global da máquina (`~/.config/opencode/…` / Ctrl+T) intocada; `opencode.json` do repo intocado. + - Lançamento nunca quebra por variante: nenhum lookup/`throw` local por capacidade de modelo; o literal é constante. + - Cardápio/modelos tocados **só** na variante — `WORKTREE_MODEL_MAP`/`OPENCODE_PRESET_MODEL` inalterados (OPS112 deixa `go = opencode-go/deepseek-v4.1-flash`). + - Docs históricos congelados (`docs/plans/*` anteriores, HISTORY) intocados. +- **O que reavaliar:** a hipótese "variante nunca entra na diretiva" permanece correta, mas o dono da variante **não** é `resolveWorktreeModel` (que segue devolvendo string `provider/model`); é `scripts/lib/agent-session.mjs` (dono do ciclo de vida da sessão). Reavalia-se também a hipótese de guardar `variant` no estado: **não** guardar (ver D2). O sintoma "nem no provider correto" já foi atacado pela OPS122 (`createSession` passa o modelo no body); a variante não altera esse caminho — a verificação viva fica no aceite. + +## Abordagem recomendada + +```mermaid +flowchart LR + W["pnpm worktree next|plan|new|fix (--go …)"] --> R["resolveWorktreeModel\n→ 'provider/model' (string, sem variante)"] + W --> H["fix --headless\n(scripts/worktree.mjs:761)"] + R --> D["opencodeLaunchDirective\n--model (SEM --variant)"] + D --> S["agent-session start --model "] + S --> B["buildCreateSessionBody({ model })\n→ model:{ providerID, id, variant:'max' }"] + S --> DR["driverArgs → --model --variant max"] + B --> SV["POST /session\ndirectory=…"] + SV --> P["Session.model.variant = 'max'"] + P --> TUI["opencode attach (sem flags) herda"] + H --> HA["opencodeHeadlessArgs\n→ --model --variant max"] + C["MODEL_VARIANT='max'\n(dono: lib/agent-session.mjs)"] --> B + C --> DR + C --> HA +``` + +**Opções consideradas:** A | B | C | D +**Recomendação:** **A — variante como constante `MODEL_VARIANT = 'max'` aplicada **dentro** de `scripts/lib/agent-session.mjs`** (body da sessão + argv do driver) e reusada por `opencodeHeadlessArgs` em `scripts/lib/worktree.mjs`. A diretiva `launch`/`attach` permanece model-only. É a única opção que (1) faz a variante chegar à sessão pelo caminho já validado pela OPS122 (`POST /session`), (2) mantém o pin "diretiva sem `--variant`" verdadeiro (nenhum flag novo no TUI), (3) cobre **todos** os propósitos — terminal, driverless, driver e headless — com um único dono de literal, e (4) cabe no appetite (poucas linhas + textos/pins). + +**Rejeitadas:** + +- **B — `--variant` roteado pela diretiva `opencodeLaunchDirective` até o CLI de sessão.** Rejeitada: reintroduz o flag no canal tokenizado da diretiva/TUI (a classe exata da quebra OPS95 — yargs do TUI rejeita flag desconhecida) e torna a diretiva inconsistente com o attach, que continua sem o flag. A variante é atributo do _modelo da sessão_, não um parâmetro de linha do TUI. +- **C — mapa de capacidade por modelo (só enviar `max` a quem suporta).** Rejeitada: over-engineering; a intenção corta explicitamente a revisão do cardápio e diz que `max` é a única variante, em todos os propósitos. Um mapa seria mais um lugar para divergir do provider real. +- **D — variante só no body da sessão, sem argv no driver/headless.** Rejeitada: o driver é um `opencode run` separado (`driverArgs`, `opencodeHeadlessArgs`) — sem `--variant` ele abriria no variant default e divergiria da sessão; o aceite exige **todos** os lançamentos. + +**Decisões caras registradas:** + +- **D1 — Dono do literal:** `scripts/lib/agent-session.mjs` exporta `MODEL_VARIANT = 'max'`; `scripts/lib/worktree.mjs` o importa no import já existente com `assertKnownFlags, SKILL_AUTO_FLAG` (`lib/worktree.mjs:10`) e o usa em `opencodeHeadlessArgs`. `worktree.mjs` e o CLI `agent-session.mjs` **não** re-spelam `'max'` (single source). +- **D2 — Estado da sessão continua `model` string, variante implícita.** Não gravar `variant` no estado: a variante é constante global; registrá-la forçaria migrar estados antigos (sem `variant`) e a checagem de reuso (`agent-session.mjs:520`) passaria a falhar fechado por diferença de `undefined`, quebrando reuso de runs vivos. **Gatilho de revisitação:** se um dia existir uma segunda variante real ou seleção por sessão, aí sim gravar e comparar. +- **D3 — Posição do flag no argv headless:** `['opencode','run','--model',model,'--variant',MODEL_VARIANT,'--auto','--command',command, report]` — `--variant` **antes** do `report` posicional (não pode ir depois, viraria argumento do report). Pin de array exato no teste. +- **D4 — Guardrail "nunca quebra":** a variante entra como propriedade string constante; helpers seguem puros (aceitam/devolvem string, sem lookup que lance). Se um modelo do cardápio não declarar `max`, o servidor persiste o que recebe e o provider decide — comportamento do upstream, não do nosso launch. Verificação viva dos modelos usados nos gates. + +### Componentes / mudanças + +- **`MODEL_VARIANT`** (`scripts/lib/agent-session.mjs`, novo, acima de `buildCreateSessionBody`): `export const MODEL_VARIANT = 'max'` + docblock citando 1.18.31 (`Session.model.variant`) e o porquê de não ir no attach (OPS95/OPS127). +- **`buildCreateSessionBody`** (`scripts/lib/agent-session.mjs:245-248`): assinatura inalterada `({ model = null } = {})`; quando há modelo, retorna `{ model: { ...parseModelRef(model), variant: MODEL_VARIANT } }` (variante incondicional — sem parâmetro de override); sem modelo segue `{}`. Trocar o comentário "No `variant` (OPS95: variants stay on the machine's global config)" (`:242`) pela nova semântica. +- **`driverArgs`** (`scripts/lib/agent-session.mjs:368-394`): inserir `'--variant', MODEL_VARIANT` imediatamente após `--model ` (antes de `--auto`), incondicional. Nenhum parâmetro de override, nenhum `throw` novo (simplify pós-revisão: override era generalidade especulativa — sem caller de produção). +- **`opencodeHeadlessArgs`** (`scripts/lib/worktree.mjs:333-345`): inserir `'--variant', MODEL_VARIANT` após o modelo; importar `MODEL_VARIANT` de `./agent-session.mjs` na linha `:10`. `headlessDirective` (`:351-356`) herda o argv sem mudança própria. +- **CLI `createSession`/`cmdStart`** (`scripts/agent-session.mjs:272-279`, `:468-572`): **sem mudança de código** — `buildCreateSessionBody({ model })` já ganha o default e `driverArgs({ …, model, invocation })` também; o estado `model` (`:538`) permanece string (D2). +- **Migration:** sem migration — tooling de launch; nenhum schema/coleção Payload muda. +- **Access / Consent:** N/A — nenhum dado de cidadão, login de campanha ou Consent envolvido. +- **UI:** N/A — nenhum componente Next/Payload. O TUI do opencode é consumidor externo (sem superfície de design; Impeccable A). + +### Dados → forma (se aplicável) + +N/A — tooling. Não há dado de negócio modelado; a mudança é uma constante de variante aplicada ao body da sessão e ao argv do driver. Perguntas de data-presentation não se aplicam. + +## Pins de teste a atualizar (valores exatos) + +**`tests/unit/agentSession.unit.spec.ts`:** + +- **L328-337** (`buildCreateSessionBody` com modelo) — passa a esperar: + `{ model: { providerID: 'opencode-go', id: 'deepseek-v4.1-flash', variant: MODEL_VARIANT } }` (referência à constante; o literal é pinado à parte). + Os casos `null`/`undefined`/`{}`/`{}` (L332-335) seguem `{}`; o `throw` de `'no-slash'` (L336) inalterado. +- **L339-342** — o `it` "never emits a variant (OPS95)" vira o pin único da constante: `expect(MODEL_VARIANT).toBe('max')` (o body exato de L328 já cobre a emissão). +- **L458-483** (`driverArgs` com invocation) — inserir `'--variant', 'max'` logo após `'--model', 'deepseek/deepseek-flash'` (L476-477), antes de `--auto`. +- **L486-509** (`driverArgs` sem argumentos, model `'m'`) — inserir `'--variant', 'max'` após `'--model', 'm'`. +- **L550-560** (`attachArgs`) — manter o array exato e acrescentar `expect(argv).not.toContain('--variant')` (guardrail OPS127/OPS95). +- **Import novo no topo** — `MODEL_VARIANT` (usado no pin e no `toEqual` do body). + +**`tests/unit/worktree.unit.spec.ts`:** + +- **L296-303** — a asserção `not.toContain('--variant')` **permanece** (a diretiva segue model-only); atualizar só a descrição do `it` para "diretiva segue sem --variant; a variante viaja no body/driver (OPS127)". +- **L426-439** (`opencodeHeadlessArgs`) — array esperado vira: + `['opencode','run','--model','deepseek/deepseek-flash','--variant','max','--auto','--command',OPENCODE_HEADLESS_COMMAND,'run 1 falhou']`. +- **L442-450, L452-463** — sem mudança estrutural (o `:452` só confere `argv[0]`). +- **L466-479** (mapa/preset) e **L482-519** (resolução/diretiva) — **inalterados** (cardápio intocado). +- Nenhuma mudança em `tests/unit/autoUnblock.unit.spec.ts` / `autoUnblockWorkflow.unit.spec.ts` — `parseHeadlessDirective` valida só `dir/branch/argv` e o wrapper passa `directive.argv` adiante; o `--variant max` flui sem contrato novo. + +**`tests/unit/opencodeCommands.unit.spec.ts`** — **não tocar** (pina frontmatter dos comandos; anti-goal). + +## Textos / superfícies a sincronizar (mesma entrega) + +- **`scripts/lib/agent-session.mjs:238-248`** — docblock de `buildCreateSessionBody`: remove "No `variant` (OPS95)" e explica `variant: 'max'` no body (1.18.31), attach sem flag. +- **`scripts/lib/agent-session.mjs:360-367`** — docblock de `driverArgs`: acrescentar `--variant max`. +- **`scripts/lib/worktree.mjs:31-38`** (OPS95 no header), **`:43-52`** (mapa "No `--variant` is emitted") e **`:333-345`** — nota OPS127: diretiva/attach sem flag; variante no body e no argv run. +- **`scripts/worktree.mjs:33-49`** (docblock do launch) e **help `:991-1004`** — acrescentar que a sessão nasce em variante `max` (body/driver), sem alterar a linha `--model`. +- **`.agents/shell/worktree.sh:20-27`** e **`:76-77`** — trocar a frase "Sem `--variant` (OPS95… config global/Ctrl+T)" por: variante `max` aplicada na criação da sessão; o attach não leva flag. +- **`.opencode/commands/worktree.md:13`** — nota OPS122 estendida: modelo e variante `max` gravados na sessão (TUI anexado herda, inclusive driverless). +- **`.agents/skills/worktree-next-issue/SKILL.md:34`** — parágrafo de launch: substituir o trecho OPS95 de variante/global por `max` no body da sessão + driver. +- **`.agents/skills/local-database/SKILL.md:29`** — menção curta: o modelo selecionado é gravado na sessão na variante `max`. +- **`docs/AGENT-OPS.md:65`** (parágrafo "Sessões persistentes") e **`:70`** (linha da tabela `agent:session start`) — registrar que a criação fixa a variante `max` (ex.: `--model ` + variante `max` no body). **Não** tocar `:24` (ladder de design — só cita o modelo `opencode-go/…`, sem vínculo com variante). +- **Changelog novo:** `docs/changelog/2026-09-18-ops127.md` (uma entrada curta; nunca editar `docs/CHANGELOG-AGENTS.md`/HISTORY). + +## Fases verificáveis + +1. **Tracer (dono da variante) — quota ~40%:** `scripts/lib/agent-session.mjs` (`MODEL_VARIANT`, `buildCreateSessionBody`, `driverArgs`) + `tests/unit/agentSession.unit.spec.ts` (pins L328-342, L458-509, L550-560 + novo pin da constante). Rodar `pnpm vitest run tests/unit/agentSession.unit.spec.ts` verde antes de seguir. +2. **Launch (headless + textos de código) — quota ~30%:** `scripts/lib/worktree.mjs` import `:10` + `opencodeHeadlessArgs` `:333-345`; pins de `tests/unit/worktree.unit.spec.ts` (L426-439; descrição L296-303). Rodar `pnpm vitest run tests/unit/worktree.unit.spec.ts`. +3. **Superfícies/labels — quota ~20%:** textos listados acima (`worktree.mjs` docblock/help, `worktree.sh`, `worktree.md`, 2 skills, `AGENT-OPS.md`) + `docs/changelog/2026-09-18-ops127.md`. +4. **Gates — quota ~10%:** `pnpm gate:fast` (lint+typecheck+unit); verificação viva: criar sessão de teste (`pnpm worktree plan --go` em branch descartável ou `agent-session start --model opencode-go/deepseek-v4.1-flash`), ler `~/.local/state/teqo/agent-sessions/.json` e a sessão no servidor (`Session.model.variant === 'max'` / TUI mostrando `max`); `pnpm push` (origin GitHub) → PR `--base main`. **Atenção CI:** `scripts/worktree.mjs` é `HIGH_RISK_EXACT` — o PR roda unit/int full + e2e curado (esperado, não é surpresa). + +## Rabbit holes / Não escopo (engenharia) + +- **Não** emitir `--variant` na diretiva `launch` nem no `opencode attach` (classe OPS95; attach sem flags por design). +- **Não** criar mapa de capacidade modelo→variante nem lookup que lance (opção C rejeitada). +- **Não** gravar/derivar `variant` no estado da sessão nem na checagem de reuso (D2; gatilho de revisitação registrado). +- **Não** alterar `WORKTREE_MODEL_MAP`/`OPENCODE_PRESET_MODEL` nem o cardápio de flags (intenção corta). +- **Não** tocar `opencode.json` do repo nem a config global da máquina / Ctrl+T. +- **Não** mudar `opencodeCommands.unit.spec.ts` nem frontmatter de comandos de execução. +- **Não** editar docs históricos congelados (`docs/plans/*` antigos, `CHANGELOG-AGENTS.md`, HISTORY). +- **Não** adicionar flag nova de variante, nem parâmetro de CLI `--variant` no `agent-session start` (a variante é default interno). + +## Riscos e mitigação + +- **Sessão reusada de antes do OPS127 não tem `variant`.** `start` com driver vivo/sessão busy reusa sem re-aplicar o body (`agent-session.mjs:515-526`). Mitigação: worktrees de `plan`/`new` nascem com sessão nova; para runs longevos, `--new` força recriação; documentar o efeito no changelog. Revisitar se virar recorrente. +- **`--variant` em posição errada no argv do `run`.** Se cair depois do `report` posicional vira parte do report. Mitigação: D3 + pin de array exato (L426-439). +- **Servidor/provider rejeitar `max` em algum modelo do cardápio.** Guardrail pede "nunca quebra"; sem mapa (opção C rejeitada), o risco residual existe. Mitigação: verificação viva no PR para o preset e `--go` (os usados no fluxo) e flag de revisitação; o body é string constante, sem `throw` local. +- **Divergência do sintoma "nem sempre no provider correto".** O caminho de modelo da sessão é o da OPS122; a variante não o altera. Mitigação: o aceite de engenharia exige sessão viva conferindo provider/modelo **e** variante antes de marcar aprovado. +- **Deriva de texto vs. constante.** Vários docblocks citavam OPS95. Mitigação: pins unit + os pontos de texto são atualizados na mesma fase (3), e `MODEL_VARIANT` é importado (não re-digitado) em `lib/worktree.mjs`. +- **PR dispara suíte ampla por `worktree.mjs` ser HIGH_RISK_EXACT.** Esperado; não é regressão. Mitigação: rodar `gate:fast` local antes do push. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: `next`/`plan`/`new`/`fix` (inclui `fix --headless`) e o preset criam a sessão com `Session.model.variant === 'max'`; `--go` segue em `opencode-go/deepseek-v4.1-flash`. +- [ ] Guardrails: **nenhum** `--variant` em `attachArgs`/diretiva `launch`; `opencode.json` e config global intocados; variante é constante sem lookup que lance; cardápio/preset de modelos inalterados. +- [ ] Invariantes AGENTS/engineering-standards: single owner do literal (`lib/agent-session.mjs`), sem twinning, sem migration/schema/Consent/access. +- [ ] Pins unitários atualizados conforme lista acima (`agentSession.unit.spec.ts`, `worktree.unit.spec.ts`); `tests/unit/opencodeCommands.unit.spec.ts` intacto. +- [ ] Superfícies sincronizadas na mesma entrega (código, help/docblocks, shell, command, skills, AGENT-OPS, changelog novo). +- [ ] `pnpm test:unit` (specs citados) e `pnpm gate:fast` verdes; verificação viva de sessão em `max` no PR; push via `pnpm push`. + +**Self-score decision-quality: 5/5.** (1) Decisão cara — dono do literal e formato do body/argv — tem opções e rejeitadas explícitas (A recomendada; B/C/D rejeitadas com motivo técnico). (2) Cabe no appetite: poucas linhas funcionais + textos + pins, sem schema/UI. (3) Rabbit holes nomeados (mapa de capacidade, attach, estado da sessão, config global, cardápio, docs congelados). (4) Depth check: reusa o caminho OPS122 `buildCreateSessionBody`/`driverArgs`/`opencodeHeadlessArgs` em vez de criar passe-through novo; literal em single owner. (5) Outcome preservado: a engenharia cobre todos os propósitos via body/argv e mantém o guardrail do attach, sem reescrever o aceite. diff --git a/scripts/lib/agent-session.mjs b/scripts/lib/agent-session.mjs index 09e6b76f..681ec4d4 100644 --- a/scripts/lib/agent-session.mjs +++ b/scripts/lib/agent-session.mjs @@ -235,16 +235,27 @@ export const parseModelRef = (model) => { return { providerID: model.slice(0, slash), id: model.slice(slash + 1) } } +/** + * Variante única pedida em todo lançamento de worktree (OPS127). Opencode + * 1.18.31 aceita `variant` dentro de `model` no `POST /session` e persiste em + * `Session.model.variant`; a TUI anexada herda — por isso a variante viaja no + * body da sessão (e no `opencode run` do driver/headless), nunca como flag no + * `opencode attach`/diretiva do TUI (o yargs rejeita flag desconhecida — + * OPS95). `max` é a única variante usada, em todos os propósitos. + */ +export const MODEL_VARIANT = 'max' + /** * Body of `POST /session` — `{}` when no model was requested, or - * `{ model: { providerID, id } }` when one was (the session then reports the - * model, so the attached TUI inherits it even on a driverless launch). No - * `variant` (OPS95: variants stay on the machine's global config). + * `{ model: { providerID, id, variant } }` when one was (the session then + * reports the model+variant, so the attached TUI inherits them even on a + * driverless launch). The variant rides the body (OPS127); the machine's + * global config stays untouched. * @param {{ model?: string | null }} [options] */ export const buildCreateSessionBody = ({ model = null } = {}) => { if (model === null || model === undefined || model === '') return {} - return { model: parseModelRef(model) } + return { model: { ...parseModelRef(model), variant: MODEL_VARIANT } } } /** @@ -363,6 +374,8 @@ export const validateSessionFlags = ({ subcommand = null, flags = {} } = {}) => * argument starting with `-` (`--issue 1019`) is not parsed as an opencode flag * by yargs — verified live on 1.18.31. No `-p`: the credential rides * `OPENCODE_SERVER_PASSWORD` in the inherited env (never in `ps`). + * `--variant` (OPS127) keeps the run on the same variant as the session body — + * `opencode run` supports it; the attached TUI does not. * @param {{ url: string, sessionID: string, dir: string, model: string, invocation: { command: string, arguments?: string } | null }} options */ export const driverArgs = ({ url, sessionID, dir, model, invocation }) => { @@ -385,6 +398,8 @@ export const driverArgs = ({ url, sessionID, dir, model, invocation }) => { dir, '--model', model, + '--variant', + MODEL_VARIANT, '--auto', '--command', invocation.command, diff --git a/scripts/lib/worktree.mjs b/scripts/lib/worktree.mjs index 7f02dce0..2720d449 100644 --- a/scripts/lib/worktree.mjs +++ b/scripts/lib/worktree.mjs @@ -7,7 +7,7 @@ */ import { slugify } from '../../src/lib/slug.ts' -import { assertKnownFlags, SKILL_AUTO_FLAG } from './agent-session.mjs' +import { assertKnownFlags, MODEL_VARIANT, SKILL_AUTO_FLAG } from './agent-session.mjs' /** * Prefix of every `/plan-issue` planning-worktree branch (`pnpm worktree @@ -47,9 +47,10 @@ export const OPENCODE_PRESET_MODEL = * `--zen` → Muse Spark 1.3 Free. * The directive picks `WORKTREE_MODEL_MAP[flag]` when * a single flag is present, otherwise falls back to `OPENCODE_PRESET_MODEL`. - * No `--variant` is emitted (the TUI yargs rejects the flag — OPS95): - * variants live on the machine's global config, selectable via Ctrl+T - * (`opencode run --variant max` remains the headless path). + * No `--variant` is emitted in the directive (the TUI yargs rejects the flag — + * OPS95). OPS127: the session is created in variant `max` — it rides the + * `POST /session` body and the `opencode run` argv of the driver/headless + * (`MODEL_VARIANT` in `./agent-session.mjs`), never the directive/attach. */ export const WORKTREE_MODEL_MAP = { cheap: 'cheapestinference/deepseek-v4-flash', @@ -327,7 +328,10 @@ export const opencodeLaunchDirective = ({ export const OPENCODE_HEADLESS_COMMAND = 'bug-fix' /** - * opencode `run` argv for the auto-unblock agent (model + report, no shell). + * opencode `run` argv for the auto-unblock agent (model + variant + report, no + * shell). `--variant` (OPS127) rides before the positional report — after it, + * it would be swallowed into the report argument. Unlike the attached TUI, + * `opencode run` accepts the flag. * @param {{ model?: string, report?: string, command?: string }} [options] */ export const opencodeHeadlessArgs = ({ @@ -341,7 +345,18 @@ export const opencodeHeadlessArgs = ({ if (typeof report !== 'string' || report.trim().length === 0) { throw new Error('opencodeHeadlessArgs: report vazio') } - return ['opencode', 'run', '--model', model, '--auto', '--command', command, report] + return [ + 'opencode', + 'run', + '--model', + model, + '--variant', + MODEL_VARIANT, + '--auto', + '--command', + command, + report, + ] } /** diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index 4313efbc..e415f24d 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -43,9 +43,12 @@ * (`opencode attach -s `) — fechar o * terminal NÃO encerra o run; encerrar é * `pnpm agent:session stop`. OPS95 continua: sem - * `--variant`; variantes ficam na config global da - * máquina (Ctrl+T). Presets e mapa em - * scripts/lib/worktree.mjs; ciclo de vida em + * `--variant` na diretiva nem no attach (o yargs do + * TUI rejeita o flag). OPS127: a sessão nasce na + * variante `max` — ela viaja no body do + * `POST /session` e no argv do `opencode run` do + * driver/headless, nunca na diretiva. Presets e mapa + * em scripts/lib/worktree.mjs; ciclo de vida em * scripts/lib/agent-session.mjs. * OPS122: `--auto` (opt-out do GATE da skill) * transposta para a invocation; flag desconhecida @@ -988,10 +991,10 @@ if (!subcommand) { console.log(' opencode command, ou a função `worktree()` de .agents/shell/worktree.sh);') console.log(' no terminal (TEQO_WORKTREE_TERMINAL=1) imprime também a diretiva') console.log( - ' `launch node scripts/agent-session.mjs start --purpose=next --dir= --model --issue=` (OPS26+OPS33+OPS93+OPS95+OPS110:', + ' `launch node scripts/agent-session.mjs start --purpose=next --dir= --model --issue=` (OPS26+OPS33+OPS93+OPS95+OPS110+OPS127:', ) console.log( - ` abre a sessão com ${OPENCODE_PRESET_MODEL} (sem flag) ou com o mapa ${[ + ` abre a sessão (variante \`max\`, herdada pelo TUI anexado) com ${OPENCODE_PRESET_MODEL} (sem flag) ou com o mapa ${[ ...WORKTREE_MODEL_FLAGS, ] .map((flag) => `--${flag}=${WORKTREE_MODEL_MAP[flag]}`) diff --git a/tests/unit/agentSession.unit.spec.ts b/tests/unit/agentSession.unit.spec.ts index 3d1c9412..7ddd1f4e 100644 --- a/tests/unit/agentSession.unit.spec.ts +++ b/tests/unit/agentSession.unit.spec.ts @@ -15,6 +15,7 @@ import { formatSessionList, isLoopbackHost, isStaleLock, + MODEL_VARIANT, nearestAcceptedFlag, parseModelRef, parseServerState, @@ -325,9 +326,9 @@ describe('parseModelRef + buildCreateSessionBody (OPS122 — o modelo chega à S expect(() => parseModelRef(null as unknown as string)).toThrow() }) - it('builds the POST /session body with the model ref, or an empty body without one', () => { + it('builds the POST /session body with the model ref + variant, or an empty body without one', () => { expect(buildCreateSessionBody({ model: 'opencode-go/deepseek-v4.1-flash' })).toEqual({ - model: { providerID: 'opencode-go', id: 'deepseek-v4.1-flash' }, + model: { providerID: 'opencode-go', id: 'deepseek-v4.1-flash', variant: MODEL_VARIANT }, }) expect(buildCreateSessionBody({ model: null })).toEqual({}) expect(buildCreateSessionBody({ model: undefined })).toEqual({}) @@ -336,9 +337,8 @@ describe('parseModelRef + buildCreateSessionBody (OPS122 — o modelo chega à S expect(() => buildCreateSessionBody({ model: 'no-slash' })).toThrow(/inválido|provider/) }) - it('never emits a variant (OPS95 — variants live on the machine config)', () => { - const body = buildCreateSessionBody({ model: 'opencode-go/deepseek-v4.1-flash' }) - expect(body.model).not.toHaveProperty('variant') + it('pins max as the single launch variant (OPS127 — the TUI inherits, no attach flag)', () => { + expect(MODEL_VARIANT).toBe('max') }) }) @@ -475,6 +475,8 @@ describe('driverArgs + attachArgs (o argv verificado ao vivo)', () => { '/work/OPS110', '--model', 'deepseek/deepseek-flash', + '--variant', + 'max', '--auto', '--command', 'work-issue', @@ -502,6 +504,8 @@ describe('driverArgs + attachArgs (o argv verificado ao vivo)', () => { '/work/fix', '--model', 'm', + '--variant', + 'max', '--auto', '--command', 'bug-fix', @@ -553,11 +557,12 @@ describe('driverArgs + attachArgs (o argv verificado ao vivo)', () => { expect(() => attachArgs({ url: 'http://x', sessionID: 'ses_a', dir: '' })).toThrow(/dir/) }) - it('attach resumes exactly the same session/dir (never passes --auto/--model)', () => { + it('attach resumes exactly the same session/dir (never passes --auto/--model/--variant)', () => { const argv = attachArgs({ url: 'http://127.0.0.1:4199', sessionID: 'ses_abc', dir: '/work/x' }) expect(argv).toEqual(['attach', 'http://127.0.0.1:4199', '--dir', '/work/x', '-s', 'ses_abc']) expect(argv).not.toContain('--auto') expect(argv).not.toContain('--model') + expect(argv).not.toContain('--variant') }) it('serverArgs is loopback-flagged and never enables --mdns', () => { diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index 6ca46e4c..b2ee3e48 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -293,7 +293,7 @@ describe('opencodeLaunchDirective (terminal-only agent-session launch, OPS26 + O ) }) - it('never emits --variant — the TUI yargs rejects it (OPS95); variants live on machine config (Ctrl+T)', () => { + it('never emits --variant in the directive — the TUI yargs rejects it (OPS95); the variant rides the session body/driver (OPS127)', () => { for (const model of Object.values(WORKTREE_MODEL_MAP)) { for (const purpose of ['next', 'plan', 'new']) { const directive = opencodeLaunchDirective({ dir, purpose, terminal: true, model }) @@ -432,6 +432,8 @@ describe('opencodeHeadlessArgs + headlessDirective (OPS106 auto-unblock)', () => 'run', '--model', 'deepseek/deepseek-flash', + '--variant', + 'max', '--auto', '--command', OPENCODE_HEADLESS_COMMAND,