mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
fix(OPS122): launch do worktree honra o modelo na sessão e o --auto (Closes #1088)
This commit is contained in:
parent
8483387034
commit
42f013317a
12 changed files with 653 additions and 34 deletions
|
|
@ -8,7 +8,7 @@
|
|||
#
|
||||
# No terminal (esta função), o script também imprime a diretiva `launch node
|
||||
# scripts/agent-session.mjs start --purpose=<next|plan|new|fix> --dir=<dir>
|
||||
# --model=<preset|map> [--issue=<N>] [--argument="<bag>"]` (OPS26+OPS33+OPS93+OPS95+OPS110):
|
||||
# --model=<preset|map> [--skill-auto] [--issue=<N>] [--argument="<bag>"]` (OPS26+OPS33+OPS93+OPS95+OPS110+OPS122):
|
||||
# a função executa o cd e então a linha (tokenizada por xargs — honra as aspas
|
||||
# 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,
|
||||
|
|
@ -31,6 +31,14 @@
|
|||
# TEQO_WORKTREE_TERMINAL=1 é o que separa esta superfície da do comando
|
||||
# `/worktree` do opencode (que nunca lança TUI). `--stay` suprime cd e launch.
|
||||
#
|
||||
# OPS122: `--auto` (o opt-out do GATE da skill) transposta para a invocation do
|
||||
# driver — `/work-issue --issue <N> --auto`, `/plan-issue --auto <bag>`,
|
||||
# `/bug-fix --auto <bag>`. Vale em `next`/`fix` e em `plan` COM bag; flag
|
||||
# desconhecida, `--auto` sem destino (`new`, `plan` sem bag), `--stay --auto`,
|
||||
# `--headless --auto` ou `--auto` fora do terminal falham alto (nunca descarte
|
||||
# silencioso). O modelo pedido passa a ser gravado na própria SESSÃO do servidor
|
||||
# (o TUI anexado herda), inclusive driverless — não só no argv do driver.
|
||||
#
|
||||
# Instalação (uma linha no profile; requer bash ou zsh — usa BASH_SOURCE, arrays e here-strings):
|
||||
# source <repo>/.agents/shell/worktree.sh
|
||||
#
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ Fases pesadas são delegadas a sub-agentes com contexto mínimo — o agente pri
|
|||
- [ ] 3. Dispatch sub-agente investigador → causa-raiz
|
||||
- [ ] 4. Fix da causa-raiz + teste de regressão (main agent iterativo)
|
||||
- [ ] 5. Dispatch sub-agente verificador → prova verde
|
||||
- [ ] 6. Gates + PR → merge; bug de prod: deploy (staging automático, produção após approve) → confirmação do humano
|
||||
- [ ] 6. Gates + PR → merge; bug de prod: deploy (staging automático, produção após approve humano — `--auto` NÃO aprova produção) → confirmação do humano
|
||||
- [ ] 7. Dispatch sub-agente estrategista → prevenção classificada
|
||||
- [ ] 8. Implementar prevenção barata agora (a cara fica no post-mortem)
|
||||
- [ ] 9. Dispatch sub-agente escritor → post-mortem + entrada de changelog
|
||||
|
|
@ -77,6 +77,17 @@ O relatório chega no `$ARGUMENTS` (a descrição passada ao `worktree fix`), do
|
|||
|
||||
Ausente → **uma** pergunta ao humano ("Qual o bug?"). Se houver Issue no GitHub (`github.com/fsolla/teqo/issues`), leia-a via `pnpm issue` / `scripts/issue.mjs` — mas bug-fix não exige Issue (o post-mortem é o registro).
|
||||
|
||||
## Modo autônomo (`--auto`)
|
||||
|
||||
Opt-out da pausa por invocação. Detecta-se em `$ARGUMENTS`: `/bug-fix --auto <bug>` (a flag vive dentro do prompt, junto da descrição; sem parser dedicado). O transporte padrão é o `pnpm worktree fix --auto <bag>`, que auto-submete exatamente `/bug-fix --auto <bag>`.
|
||||
|
||||
- **Sem a flag (ou flag desconhecida):** o fluxo supervisionado atual vale idêntico — inclusive a pergunta única do Passo 1 quando o relatório não chegou.
|
||||
- **Com a flag:** a descrição chega no `$ARGUMENTS` e o fluxo segue direto do Passo 2 ao 9 sem pedir confirmação. Relatório ausente mesmo com a flag → **pare** (sem sintoma não há o que investigar — mesmo fail-closed do Passo 1).
|
||||
- **Pode auto-aprovar:** o diagnóstico de causa-raiz, a escolha e a implementação do fix, a classificação da prevenção, a prevenção barata, o post-mortem e o push/PR.
|
||||
- **Continua parando (vale mesmo com a flag):** DB de prod/`ALLOW_REMOTE_DB`; escrita em prod (repro de bug de prod é read-only); Consent/LGPD fail-closed; migração de schema; contrato de URL público; shapes públicos; **aprovação humana da produção** (o deploy de prod nunca é automático — `--auto` não aprova o environment `production`); merge sem CI green; divergência material de produto → pare, registre o pendente no post-mortem e reporte no resumo final. Nunca declare bug de prod corrigido sem a confirmação do humano.
|
||||
- **Não confundir:** `--auto` aqui é flag da skill dentro do prompt; o `--auto` do CLI opencode (auto-aprovar permissões de ferramenta) é outro conceito.
|
||||
- **Convivência:** `agent-work-issue` continua sendo o contrato do pool dormente; este modo é opt-in da sessão humana.
|
||||
|
||||
## Passo 2 — Reproduzir
|
||||
|
||||
Antes de mexer em código: fazer o bug acontecer de forma confiável.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ description: Corrige um bug de ponta a ponta: diagnóstico → causa-raiz → fi
|
|||
|
||||
Carregue a skill `bug-fix` (ferramenta de skills, nome exato) e siga o fluxo dela de ponta a ponta. A fonte canônica é `.agents/skills/bug-fix/SKILL.md` — não a transcreva nem recrie o fluxo.
|
||||
|
||||
Uso: /bug-fix [--auto] <descrição do bug>. Com `--auto`, a skill dispensa a pausa do Passo 1 e segue até o PR sem confirmação (o bug de prod ainda exige approve humano na produção); sem ela, o fluxo supervisionado atual vale.
|
||||
|
||||
$ARGUMENTS
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
---
|
||||
description: Claima a próxima Issue (next [--issue N] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), cria o worktree dela e provisiona o ambiente; também cria worktree de planejamento (plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), worktree neutro (new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), worktree de correção de bug (fix [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]) ou destrói o atual (kill [--force])
|
||||
description: Claima a próxima Issue (next [--issue N] [--stay] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), cria o worktree dela e provisiona o ambiente; também cria worktree de planejamento (plan [bag] [--stay] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), worktree neutro (new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]), worktree de correção de bug (fix [bag] [--stay] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]) ou destrói o atual (kill [--force])
|
||||
---
|
||||
|
||||
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. 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`.
|
||||
- `next [--issue N] [--stay] [--auto] [--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] [--auto] [--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`.
|
||||
- `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/<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.
|
||||
|
||||
> **OPS122 — flags honradas ou falha alto.** `--auto` (opt-out do GATE da skill) transposta para a invocation do driver: `/work-issue --issue <N> --auto` (next), `/plan-issue --auto <bag>` (plan com bag), `/bug-fix --auto <bag>` (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).
|
||||
|
||||
> **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.
|
||||
|
||||
!`node scripts/worktree.mjs $ARGUMENTS`
|
||||
|
|
|
|||
1
docs/changelog/2026-09-16-ops122.md
Normal file
1
docs/changelog/2026-09-16-ops122.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**OPS122 — Launch do worktree honra as flags do humano: modelo na sessão + `--auto` (2026-09-16):** fecha os dois sintomas do mesmo defeito — o que a pessoa digita na linha do `pnpm worktree` (`--zen`/`--go`/…, `--auto`) é o que a sessão usa, ou a flag falha alto. **(1) Modelo na SESSÃO:** `createSession` (`scripts/agent-session.mjs`) passa `model: { providerID, id }` no `POST /session` — construção pura em `parseModelRef`/`buildCreateSessionBody` (`scripts/lib/agent-session.mjs`, split no PRIMEIRO `/`, sem `variant` — OPS95) — então o TUI anexado herda o modelo pedido **inclusive nos driverless** (`new`, `plan` sem bag), onde antes o `--model` morria no argv do driver e o anexo caía no pin global. Verificado ao vivo no `opencode serve` 1.18.31: `GET /session/<id>` reporta o model criado. Reuso (`resolveStartDecision` só devolve `reuse` com driver vivo/sessão busy) com modelo divergente → `die` claro ("modelo não registrado" inclusive em estado antigo) — nunca descarte silencioso; a rota `/api/session/:id/model` fica como gatilho de revisão, não código. **(2) `--auto` do humano chega à invocation:** anexado em `purposeInvocation` (dono único) nas formas exatas `/work-issue --issue <N> --auto` (next), `/plan-issue --auto <bag>` (plan com bag) e `/bug-fix --auto <bag>` (fix); o transporte interno usa a const `SKILL_AUTO_FLAG='skill-auto'` (nome distinto do `--auto` de PERMISSÕES que o `driverArgs` emite para o `opencode run`). **(3) Flag desconhecida/não-honrável falha alto (antes sumia):** `WORKTREE_FLAG_ALLOWLIST` + `validateWorktreeFlags` (`scripts/lib/worktree.mjs`) por subcomando, com sugestão de vizinho (Damerau ≤1) — a mensagem/sugestão ganha dono único em `assertKnownFlags` (extraído de `validateSessionFlags`, OPS110-F1) — e `assertSkillAutoSupported` (matriz determinística: `next`/`fix` sempre; `plan` só com bag; nunca `new`, `plan` sem bag, `--stay`, `--headless` ou fora do terminal interativo), checada no dispatch **antes de qualquer git/claim** para não deixar worktree órfão. **Extensão aprovada pelo humano em 2026-09-16:** a revisão provou que a skill `bug-fix` não tinha modo `--auto` (só `work-issue`/`plan-issue` tinham) — transpor `/bug-fix --auto <bag>` seria honra de mentira e sujaria a descrição do bug; a opção escolhida foi implementar a seção **§Modo autônomo (`--auto`)** na skill `bug-fix` espelhando as outras duas (auto-aprova diagnóstico/fix/prevenção/post-mortem/PR; **continua parando** em DB/prod, Consent/LGPD, migração, URL/shapes públicos, **approve humano da produção** e divergência material), com `Uso:` documentado em `.opencode/commands/bug-fix.md` e checklist/Passo 6 ajustados. Unit: `tests/unit/worktree.unit.spec.ts` + `tests/unit/agentSession.unit.spec.ts` (formas exatas da invocation com `auto`, allowlist por verbo, matriz do `assertSkillAutoSupported` incl. `--stay`/`--headless`/fora do terminal, `parseModelRef`/`buildCreateSessionBody` com e sem model, `assertKnownFlags`) — 164 testes nos dois specs; `pnpm gate:fast` verde. Preset `deepseek/deepseek-flash` intocado, OPS119 (`plan`/`new` sem bag driverless) e OPS110 (`purposeInvocation`, ciclo serve/start/attach/stop) preservados; sem mudança de provider/auth, sem migration/schema/UI/access/Consent. Issue #1088; intenção [`worktree-launch-honra-flags.md`](../plans/worktree-launch-honra-flags.md), impl [`worktree-launch-honra-flags-impl.md`](../plans/worktree-launch-honra-flags-impl.md).
|
||||
110
docs/plans/worktree-launch-honra-flags-impl.md
Normal file
110
docs/plans/worktree-launch-honra-flags-impl.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Impl: Launch do worktree honra as flags do humano: o modelo pedido e o `--auto` chegam onde deveriam
|
||||
|
||||
Status: em execução
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1088
|
||||
Intenção: docs/plans/worktree-launch-honra-flags.md
|
||||
Appetite restante: herdado ~1 dia (parser + transporte + modelo na sessão; sem UI)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** digitar `--zen/--go/…` abre a SESSÃO (a que o TUI anexa) no modelo pedido em **todos** os purposes — inclusive driverless (`new`, `plan` sem bag); `--auto` chega à mensagem inicial nas formas `/work-issue --issue <N> --auto`, `/plan-issue --auto <bag>`, `/bug-fix --auto <bag>`; flag desconhecida ou não-honrável falha alto com sugestão, nunca descarte silencioso.
|
||||
- **O que NÃO negociar:** preset `deepseek/deepseek-flash` intocado; OPS119 (`plan`/`new` sem bag driverless); ciclo OPS110 (`serve`/`start`/`attach`/`stop`, `TEQO_WORKTREE_TERMINAL`) e contrato `purposeInvocation` preservados; sem mudança de provider/auth; `--auto` do humano ≠ `--auto` de permissões do `opencode run`.
|
||||
- **O que reavaliar:** (i) a rota `/api/session/:id/model` **não** é necessária no caminho principal — `resolveStartDecision` só devolve `reuse` com driver vivo/sessão busy ([lib/agent-session.mjs:393-417](../../scripts/lib/agent-session.mjs)), então não há reuso ocioso para trocar de modelo; (ii) o item "modelo na sessão" exige tocar `createSession` (HTTP) — manter a decisão testável extraindo a construção do body para helper puro.
|
||||
|
||||
### Extensão de escopo (aprovada pelo humano em 2026-09-16)
|
||||
|
||||
A revisão de simplify provou que a skill `bug-fix` **não tinha** modo `--auto` (só `work-issue`/`plan-issue` têm): transpor `/bug-fix --auto <bag>` seria honra de mentira e sujaria a descrição do bug com `--auto`. O humano decidiu (opção "Implementar --auto no bug-fix") **estender o item**: a skill `bug-fix` ganha a seção §Modo autônomo (`--auto`), espelhando `work-issue`/`plan-issue` (auto-aprova diagnóstico/fix/prevenção/post-mortem/PR; **continua parando** em DB/prod, Consent/LGPD, migração, URL/shapes públicos, aprovação humana da produção e divergência material — `--auto` nunca aprova o environment `production`), e `.opencode/commands/bug-fix.md` documenta o uso. Isso sobrepõe o "Fora de escopo" da intenção por decisão explícita do dono do produto.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
H["worktree plan --zen --auto bag"] --> P[parseArgs]
|
||||
P --> V["validateWorktreeFlags<br/>allowlist + nearestAcceptedFlag"]
|
||||
V -->|desconhecida| D1["die + sugestão"]
|
||||
V --> S["assertSkillAutoSupported(purpose,bag)"]
|
||||
S -->|não honrável| D2["die + forma válida"]
|
||||
S --> M["resolveWorktreeModel (at-most-one)"]
|
||||
M --> L["opencodeLaunchDirective<br/>--model + --skill-auto"]
|
||||
L --> SH["shell xargs"]
|
||||
SH --> ST["agent-session start"]
|
||||
ST --> CS["createSession POST /session {model}"]
|
||||
ST --> PI["purposeInvocation(auto) → /work-issue --issue N --auto"]
|
||||
CS --> TUI["opencode attach herda o model"]
|
||||
PI --> DR["driverArgs run --auto -- …"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A | B | C (por decisão abaixo)
|
||||
**Recomendação:** modelo na **sessão** via `POST /session` (B), `--auto` anexado em `purposeInvocation` (A), validação fail-high espelhando OPS110-F1 — ataca a raiz (descarte silencioso / ausência de model na sessão) sem processo fantasma nem dono duplicado.
|
||||
**Rejeitadas:** driver mínimo só para fixar modelo (A da intenção — reintroduz submissão espúria no driverless, contra OPS119); deixar driverless quebrado (C); `--auto` na diretiva de launch (dois donos do comando).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`WORKTREE_FLAG_ALLOWLIST` + `validateWorktreeFlags`** (`scripts/lib/worktree.mjs`, novo): allowlist por subcomando; **reusa `nearestAcceptedFlag` importado de `scripts/lib/agent-session.mjs`** (direção `lib→lib`; `agent-session.mjs` só importa `slug`/builtins — sem ciclo; `check:cycles` cobre só `src`). Chamado no dispatch ([scripts/worktree.mjs:1031](../../scripts/worktree.mjs)) após `subcommand`, antes de `cmdNext`/git. Allowlist: `next:[issue,stay,no-migrate,+modelFlags,+auto]`, `plan/new:[stay,no-migrate,+modelFlags,+auto]`, `fix:[stay,no-migrate,headless,directive,+modelFlags,+auto]`, `kill:[force,stay]` (`stay` fica p/ preservar a mensagem específica de `kill`, [:1054](../../scripts/worktree.mjs)).
|
||||
- **`assertSkillAutoSupported({purpose, bag})`** (`scripts/lib/worktree.mjs`, novo, puro): matriz determinística — `next` sempre; `fix` sempre; `plan` só com bag; `new` nunca; `--stay` + `--auto` nunca (o launch é suprimido). Chamado no dispatch (mesmo ponto), antes de qualquer git. Sem migration, sem access, sem Consent.
|
||||
- **`opencodeLaunchDirective`** ([scripts/lib/worktree.mjs:202-232](../../scripts/lib/worktree.mjs)): novo parâmetro `skillAuto` → emite `--skill-auto` (nome interno, decisão C) só quando `true`. `--model=` continua **sempre** (driverless inclusive).
|
||||
- **`printLaunchDirective`** ([scripts/worktree.mjs:225-239](../../scripts/worktree.mjs)) + call sites [:643](../../scripts/worktree.mjs) (`next`) e [:747](../../scripts/worktree.mjs) (`cmdNamespaceBranch`): repassar `skillAuto: Boolean(flags.auto)`.
|
||||
- **`parseModelRef(model)` + `buildCreateSessionBody({model})`** (`scripts/lib/agent-session.mjs`, novos, puros): split no PRIMEIRO `/` → `{providerID,id}`; throw se não houver `/`; sem `variant` (OPS95); body `{}` sem model. **Colocados aqui** (não em `lib/worktree.mjs`) porque `lib/worktree.mjs` passará a importar de `lib/agent-session.mjs` (decisão A) — evita ciclo.
|
||||
- **`createSession(url, dir, model)`** ([scripts/agent-session.mjs:269-276](../../scripts/agent-session.mjs)): usa `buildCreateSessionBody`; `POST /session?directory=…` com `{ model: {providerID,id} }` (verificado ao vivo no `serve` 1.18.31: a sessão passa a reportar o model).
|
||||
- **`cmdStart`** ([scripts/agent-session.mjs:465-559](../../scripts/agent-session.mjs)): passa `model` ao `createSession` ([:515](../../scripts/agent-session.mjs)); `purposeInvocation({..., auto: Boolean(flags['skill-auto'])})` ([:472](../../scripts/agent-session.mjs)); decisão D no ramo `reuse` ([:511](../../scripts/agent-session.mjs)). `driverArgs` ([:291-317](../../scripts/lib/agent-session.mjs)) **intocado** — segue emitindo o `--auto` de permissões; o `--auto` do humano viaja como texto da invocation.
|
||||
- **`purposeInvocation`** ([scripts/lib/agent-session.mjs:187-203](../../scripts/lib/agent-session.mjs)): novo `auto=false`; formas exatas `--issue <N>[ --auto]`, `[--auto ]<bag>`, `[--auto ][<bag>]`.
|
||||
- **`SESSION_FLAG_ALLOWLIST.start`** ([scripts/lib/agent-session.mjs:214](../../scripts/lib/agent-session.mjs)): incluir `skill-auto`.
|
||||
- **`assertSessionState`** ([scripts/lib/agent-session.mjs:113-125](../../scripts/lib/agent-session.mjs)): **não** exige `model` (decisão E); só doc.
|
||||
- **Docs/comando:** `.agents/shell/worktree.sh` (comentário; shell não muda) e `.opencode/commands/worktree.md` (documentar `--auto` e o fail-high).
|
||||
- **Migration:** sem migration. **Access/Consent/UI:** N/A (Impeccable A — sem UI).
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
||||
N/A — CLI; o efeito é observável no `model` do estado/servidor (`~/.local/state/teqo/agent-sessions/<slug>.json` e `GET /session/<id>`) e no log do driver.
|
||||
|
||||
## Decisões de engenharia (caro de reverter)
|
||||
|
||||
| # | Decisão | Opções / Rejeitadas |
|
||||
| --- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| A | `validateWorktreeFlags` em `lib/worktree.mjs`, reusando `nearestAcceptedFlag` | **Rec:** reusar (um dono da distância OSA). **Rej:** duplicar OSA (drift); extrair p/ `lib/cli.mjs` (churn, 1 call site). |
|
||||
| B | `--auto` não-honrável falha alto via `assertSkillAutoSupported({purpose,bag})` puro chamado no dispatch antes do git | **Rec:** helper puro (testável, falha antes de criar worktree). **Rej:** embutir em `purposeInvocation` (tarde, já provisionou); validar na skill (fora do launcher). |
|
||||
| C | Nome do transporte interno no `start` | **Rec:** `--skill-auto` — inequívoco, separa do `--auto` de permissões que `driverArgs` já emite. **Rej:** reusar `--auto` no `start` (dois significados no mesmo pipeline). |
|
||||
| D | Sessão reusada com `state.model` ≠ pedido | **Rec:** `die` claro ("run já em andamento no modelo X; encerre com `pnpm agent:session stop` e reabra para trocar") — `reuse` só ocorre com driver vivo/sessão busy, então não há janela para trocar; nunca descarte silencioso. **Rej:** trocar via `/api/session/:id/model` (complexidade extra; o ramo "ocioso" é inalcançável por `resolveStartDecision`); ignorar (quebra "honra as flags"). Gatilho de revisão: se reabrir sessão viva virar caso de uso, implementar a troca pela rota. |
|
||||
| E | `assertSessionState` exige `model`? | **Rec:** não exigir; só documentar `model`. **Rej:** exigir (estados antigos; `model` nunca foi obrigatório). |
|
||||
| F | Parse do modelo | **Rec:** `parseModelRef` puro em `lib/agent-session.mjs`, throw sem `/`, sem variant (OPS95). **Rej:** em `lib/worktree.mjs` (ciclo com A); aceitar sem `/` (servidor rejeita remoto). |
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer — modelo na sessão:** `parseModelRef`/`buildCreateSessionBody` + `createSession(model)` + `cmdStart` passando `model`. Prova: unit puro do body e criação real contra o `serve` local. `<0,3 dia>`
|
||||
2. **Transporte `--auto`:** `purposeInvocation(auto)` nas 3 formas + `SESSION_FLAG_ALLOWLIST.start` + `opencodeLaunchDirective({skillAuto})` + repasse nos call sites. Unit das formas exatas. `<0,3 dia>`
|
||||
3. **Fail-high no parser:** `WORKTREE_FLAG_ALLOWLIST`/`validateWorktreeFlags`/`assertSkillAutoSupported` + dispatch + decisão D. Unit de flag desconhecida com sugestão, das células falsas da matriz e do reuso com modelo divergente. `<0,3 dia>`
|
||||
4. **Skill `bug-fix` §Modo autônomo** (extensão aprovada): seção na skill + `Uso:` no comando, espelhando `work-issue`/`plan-issue`. `<0,1 dia>`
|
||||
5. **Docs + gates:** atualizar comentários/`.opencode/commands/worktree.md`; `pnpm gate:fast`; push via `pnpm push`. `<0,1 dia>`
|
||||
|
||||
## Adiado com gatilho (triage de simplify, 2026-09-16)
|
||||
|
||||
Nenhum achado atingiu o piso de registro (score ≥3); os dois abaixo ficam adiados com gatilho:
|
||||
|
||||
- **`assertKnownFlags`/`nearestAcceptedFlag` como helpers neutros de CLI** hoje moram em `scripts/lib/agent-session.mjs` e são importados por `scripts/lib/worktree.mjs` (acopla launcher→domínio de sessão; sem ciclo, sem risco de runtime). **Gatilho:** um 3º consumidor de validação de flag, ou `scripts/lib/cli.mjs` ganhar o dono do parser — então mover os três para `cli.mjs`.
|
||||
- **Branch de reuso divergente** (`scripts/agent-session.mjs`, decisão D) sem teste unit — o CLI de sessão não tem harness impuro; a decisão pura está coberta por `resolveStartDecision`. **Gatilho:** o CLI ganhar harness impuro, ou a próxima mudança nesse ramo.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Seletor de modelo no TUI / mexer em `WORKTREE_MODEL_MAP` ou preset.
|
||||
- `--auto` de permissões do `opencode run`; semântica das skills; `--variant`.
|
||||
- Caminho `--headless` (OPS106): usa `opencodeHeadlessArgs`, não a invocation de skill — `--auto` do humano não se aplica; documentar.
|
||||
- `--model` combinado com `--stay`: mantém comportamento atual (gatilho: aplicar a mesma regra depois se incomodar).
|
||||
- Validar flags do comando `/worktree` como camada separada (a validação é no script, único dono; shell e comando não ganham allowlist própria).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **TUI não herda o model da sessão criada** → validar cedo no Tracer (fase 1); fallback já desenhado e documentado (`POST /api/session/:id/model`), sem mudar contrato público.
|
||||
- **Quebra dos specs que pinam a diretiva** ([tests/unit/worktree.unit.spec.ts:153-320](../../tests/unit/worktree.unit.spec.ts)) → `--skill-auto` é aditivo; strings sem `auto` permanecem idênticas.
|
||||
- **`--auto` duplicado confundir replay de argv** → nome interno distinto (C) e comentário no `driverArgs`.
|
||||
- **Reuse + modelo divergente** (D) → `die` claro; se estourar o appetite, manter o `die` (mais simples que funciona).
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto (modelo em todos os purposes; `--auto` nas 3 formas; fail-high)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (preset/OPS110/OPS119 intactos; sem provider/auth)
|
||||
- [ ] Testes de domínio previstos (unit puro; int N/A justificado)
|
||||
|
||||
## Self-score (decision-quality)
|
||||
|
||||
5/5 — (1) decisões caras A–F com rejeitadas; (2) cabe no appetite ~1 dia com fases ≤0,3 dia; (3) rabbit holes nomeados (seletor, headless, `--stay`); (4) depth check: reusa `nearestAcceptedFlag` e o padrão `validateSessionFlags`, sem módulo pass-through; (5) outcome de produto preservado — engenharia não reescreveu o aceite.
|
||||
|
|
@ -44,6 +44,7 @@ import { dirname, join, resolve } from 'node:path'
|
|||
|
||||
import {
|
||||
attachArgs,
|
||||
buildCreateSessionBody,
|
||||
codeFromBranch,
|
||||
deriveSessionStatus,
|
||||
driverArgs,
|
||||
|
|
@ -63,6 +64,7 @@ import {
|
|||
sessionDirFromEnv,
|
||||
sessionListPayload,
|
||||
sessionStatePath,
|
||||
SKILL_AUTO_FLAG,
|
||||
startLockPath,
|
||||
STATE_VERSION,
|
||||
validateServerBind,
|
||||
|
|
@ -75,13 +77,14 @@ const die = dieWithLabel('agent:session')
|
|||
const USAGE = `Uso: pnpm agent:session <serve|start|attach|stop|list> [flags]
|
||||
|
||||
serve [--hostname=H] [--port=P] sobe/reaproveita o opencode serve compartilhado
|
||||
start --purpose=<next|plan|new|fix> --dir=<D> [--model=<M>] [--issue=N] [--argument="<bag>"] [--new] [--hostname=H] [--port=P]
|
||||
start --purpose=<next|plan|new|fix> --dir=<D> [--model=<M>] [--issue=N] [--argument="<bag>"] [--skill-auto] [--new] [--hostname=H] [--port=P]
|
||||
cria a sessão, sobe o driver destacado e anexa o TUI
|
||||
attach [--session=<ses_…>|--branch=<B>|--issue=N] (re)entra na sessão (default: branch do cwd)
|
||||
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 dispara driver (next/fix, plan com bag); 'new' e 'plan' sem bag não disparam driver.`
|
||||
--model é obrigatório quando o purpose dispara driver (next/fix, plan com bag); 'new' e 'plan' sem bag não disparam driver.
|
||||
--skill-auto é o --auto do HUMANO (opt-out do GATE da skill), transposto para o texto da invocation; não confundir com o --auto de permissões que o driver recebe.`
|
||||
|
||||
/** Loopback by default; the credential rides the env, never the state/log. */
|
||||
const serverPassword = () => process.env.OPENCODE_SERVER_PASSWORD ?? ''
|
||||
|
|
@ -266,10 +269,10 @@ const sessionExists = async (url, sessionID) => {
|
|||
|
||||
const sessionStatuses = async (url) => (await request(url, '/session/status')) ?? {}
|
||||
|
||||
const createSession = async (url, dir) => {
|
||||
const createSession = async (url, dir, model = null) => {
|
||||
const result = await request(url, `/session?directory=${encodeURIComponent(dir)}`, {
|
||||
method: 'POST',
|
||||
body: {},
|
||||
body: buildCreateSessionBody({ model }),
|
||||
})
|
||||
if (!result?.id) die(`não consegui criar a sessão em ${url} (resposta sem id).`)
|
||||
return result.id
|
||||
|
|
@ -473,6 +476,7 @@ const cmdStart = async (flags) => {
|
|||
purpose,
|
||||
issueNumber,
|
||||
argument: flags.argument ?? null,
|
||||
auto: Boolean(flags[SKILL_AUTO_FLAG]),
|
||||
})
|
||||
if (invocation && !model) die('--model é obrigatório para iniciar o run (driver).')
|
||||
|
||||
|
|
@ -509,10 +513,19 @@ const cmdStart = async (flags) => {
|
|||
)
|
||||
}
|
||||
if (decision.action === 'reuse') {
|
||||
// OPS122: `reuse` só acontece com driver vivo/sessão busy (ver
|
||||
// `resolveStartDecision`), então não há janela para aplicar outro modelo.
|
||||
// Falha alto sempre que o modelo pedido diverge do registrado — inclusive
|
||||
// quando o estado é antigo e não tem `model` (nunca descarta em silêncio).
|
||||
if (model && decision.state.model !== model) {
|
||||
die(
|
||||
`run já em andamento em ${decision.state.model ?? 'modelo não registrado'} — --model ${model} não se aplica a uma sessão ativa. Encerre com \`pnpm agent:session stop --branch=${branch}\` e reabra para trocar.`,
|
||||
)
|
||||
}
|
||||
reused = true
|
||||
state = decision.state
|
||||
} else {
|
||||
const sessionID = await createSession(server.url, dir)
|
||||
const sessionID = await createSession(server.url, dir, model)
|
||||
state = {
|
||||
version: STATE_VERSION,
|
||||
code: codeFromBranch(branch),
|
||||
|
|
|
|||
|
|
@ -182,26 +182,79 @@ export const SESSION_COMMAND_BY_PURPOSE = {
|
|||
* `/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]
|
||||
*
|
||||
* `auto` (OPS122) is the HUMAN's skill flag (`--auto` on the launcher line),
|
||||
* NEVER the `opencode run --auto` permission flag that `driverArgs` emits on
|
||||
* its own. When true it transposes into the skill text exactly as documented:
|
||||
* `/work-issue --issue <N> --auto`, `/plan-issue --auto <bag>`,
|
||||
* `/bug-fix --auto <bag>` (bare `/bug-fix --auto` with an empty bag). It is a
|
||||
* text argument, not an argv flag, because the skill parses it from its
|
||||
* invocation.
|
||||
* @param {{ purpose?: string, issueNumber?: number | string | null, argument?: string | null, auto?: boolean }} [options]
|
||||
*/
|
||||
export const purposeInvocation = ({ purpose = null, issueNumber = null, argument = null } = {}) => {
|
||||
export const purposeInvocation = ({
|
||||
purpose = null,
|
||||
issueNumber = null,
|
||||
argument = null,
|
||||
auto = false,
|
||||
} = {}) => {
|
||||
const command = SESSION_COMMAND_BY_PURPOSE[purpose] ?? null
|
||||
if (!command) return null
|
||||
if (purpose === 'next') {
|
||||
const number = Number(issueNumber)
|
||||
if (!Number.isInteger(number) || number <= 0) return null
|
||||
return { command, arguments: `--issue ${number}` }
|
||||
return { command, arguments: `--issue ${number}${auto ? ' --auto' : ''}` }
|
||||
}
|
||||
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
|
||||
if (auto) return { command, arguments: sanitized ? `--auto ${sanitized}` : '--auto' }
|
||||
return { command, arguments: sanitized }
|
||||
}
|
||||
return { command, arguments: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a `provider/model` string (the format of `WORKTREE_MODEL_MAP` and the
|
||||
* opencode `--model` flag) into the `{ providerID, id }` pair the server's
|
||||
* `POST /session` expects. The split is on the FIRST slash: model ids may
|
||||
* themselves carry slashes (`openrouter/openrouter/free`). Fail high on a
|
||||
* string without both halves — never send the server a half ref.
|
||||
* @param {string} model
|
||||
*/
|
||||
export const parseModelRef = (model) => {
|
||||
if (typeof model !== 'string' || model.length === 0) {
|
||||
throw new Error('parseModelRef: model ausente')
|
||||
}
|
||||
const slash = model.indexOf('/')
|
||||
if (slash <= 0 || slash === model.length - 1) {
|
||||
throw new Error(`parseModelRef: modelo inválido "${model}" (esperado provider/model)`)
|
||||
}
|
||||
return { providerID: model.slice(0, slash), id: model.slice(slash + 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* @param {{ model?: string | null }} [options]
|
||||
*/
|
||||
export const buildCreateSessionBody = ({ model = null } = {}) => {
|
||||
if (model === null || model === undefined || model === '') return {}
|
||||
return { model: parseModelRef(model) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal `start` argv flag carrying the HUMAN's skill `--auto` (opt-out of the
|
||||
* skill GATE) — deliberately distinct from the `opencode run --auto` permission
|
||||
* flag `driverArgs` emits. Single owner of the name: the worktree launch
|
||||
* directive, the session allowlist and the CLI all use this constant.
|
||||
*/
|
||||
export const SKILL_AUTO_FLAG = 'skill-auto'
|
||||
|
||||
/**
|
||||
* Accepted flag NAMES per subcommand (OPS110-F1). Names only — the `=`/boolean
|
||||
* form stays `parseEqualsFlags`' concern. `list`/`start`/`serve` legitimately
|
||||
|
|
@ -211,7 +264,17 @@ export const purposeInvocation = ({ purpose = null, issueNumber = null, argument
|
|||
*/
|
||||
export const SESSION_FLAG_ALLOWLIST = {
|
||||
serve: ['hostname', 'port'],
|
||||
start: ['purpose', 'dir', 'model', 'issue', 'argument', 'new', 'hostname', 'port'],
|
||||
start: [
|
||||
'purpose',
|
||||
'dir',
|
||||
'model',
|
||||
'issue',
|
||||
'argument',
|
||||
'new',
|
||||
SKILL_AUTO_FLAG,
|
||||
'hostname',
|
||||
'port',
|
||||
],
|
||||
attach: ['session', 'branch', 'issue'],
|
||||
stop: ['session', 'branch', 'issue'],
|
||||
list: ['json', 'hostname', 'port'],
|
||||
|
|
@ -264,11 +327,13 @@ export const nearestAcceptedFlag = (name, accepted) => {
|
|||
* a suggestion when a near neighbor exists; unknown subcommands return silently
|
||||
* (the CLI prints the USAGE for those). A bare `--` parses to an empty name and
|
||||
* is ignored, not reported as a typo.
|
||||
* @param {{ subcommand?: string | null, flags?: Record<string, unknown> }} [options]
|
||||
*
|
||||
* Single owner of the fail-high message: both the session CLI
|
||||
* (`validateSessionFlags`) and the worktree launcher (`validateWorktreeFlags`)
|
||||
* delegate here, so the wording and the suggestion rule never drift.
|
||||
* @param {{ flags?: Record<string, unknown>, accepted?: readonly string[] }} [options]
|
||||
*/
|
||||
export const validateSessionFlags = ({ subcommand = null, flags = {} } = {}) => {
|
||||
const accepted = SESSION_FLAG_ALLOWLIST[subcommand]
|
||||
if (!accepted) return
|
||||
export const assertKnownFlags = ({ flags = {}, accepted = [] } = {}) => {
|
||||
for (const name of Object.keys(flags)) {
|
||||
if (name === '' || accepted.includes(name)) continue
|
||||
const suggestion = nearestAcceptedFlag(name, accepted)
|
||||
|
|
@ -280,6 +345,18 @@ export const validateSessionFlags = ({ subcommand = null, flags = {} } = {}) =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail high on any flag the subcommand does not accept (names only — the
|
||||
* `=`/boolean form stays `parseEqualsFlags`' concern). Unknown subcommands
|
||||
* return silently (the CLI prints the USAGE for those).
|
||||
* @param {{ subcommand?: string | null, flags?: Record<string, unknown> }} [options]
|
||||
*/
|
||||
export const validateSessionFlags = ({ subcommand = null, flags = {} } = {}) => {
|
||||
const accepted = SESSION_FLAG_ALLOWLIST[subcommand]
|
||||
if (!accepted) return
|
||||
assertKnownFlags({ flags, accepted })
|
||||
}
|
||||
|
||||
/**
|
||||
* argv of the detached run driver (sem o binário — o caller usa
|
||||
* `spawn('opencode', …)`). The `--` separator is required so a command
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { slugify } from '../../src/lib/slug.ts'
|
||||
import { assertKnownFlags, SKILL_AUTO_FLAG } from './agent-session.mjs'
|
||||
|
||||
/**
|
||||
* Prefix of every `/plan-issue` planning-worktree branch (`pnpm worktree
|
||||
|
|
@ -79,6 +80,85 @@ export const resolveWorktreeModel = (flags = {}) => {
|
|||
return OPENCODE_PRESET_MODEL
|
||||
}
|
||||
|
||||
const hasBag = (bag) => typeof bag === 'string' && bag.trim().length > 0
|
||||
|
||||
/**
|
||||
* Accepted flag NAMES per worktree subcommand (OPS122). Names only — the
|
||||
* `=`/value form stays `parseArgs`' concern. Before OPS122 an unknown flag
|
||||
* (`--auto`, a typo) parsed to `true` and was silently dropped; now the CLI
|
||||
* fails high with a neighbor suggestion, mirroring OPS110-F1. `stay` is kept
|
||||
* for `kill` so its specific "não se aplica" message still fires.
|
||||
*/
|
||||
export const WORKTREE_FLAG_ALLOWLIST = {
|
||||
next: ['issue', 'stay', 'no-migrate', 'auto', ...WORKTREE_MODEL_FLAGS],
|
||||
plan: ['stay', 'no-migrate', 'auto', ...WORKTREE_MODEL_FLAGS],
|
||||
new: ['stay', 'no-migrate', 'auto', ...WORKTREE_MODEL_FLAGS],
|
||||
fix: ['stay', 'no-migrate', 'headless', 'directive', 'auto', ...WORKTREE_MODEL_FLAGS],
|
||||
kill: ['force', 'stay'],
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail high on any flag the worktree subcommand does not accept. Delegates the
|
||||
* message/suggestion rule to `assertKnownFlags` (single owner, shared with the
|
||||
* session CLI). Unknown subcommands return silently — the CLI prints the usage.
|
||||
* @param {{ subcommand?: string | null, flags?: Record<string, unknown> }} [options]
|
||||
*/
|
||||
export const validateWorktreeFlags = ({ subcommand = null, flags = {} } = {}) => {
|
||||
const accepted = WORKTREE_FLAG_ALLOWLIST[subcommand]
|
||||
if (!accepted) return
|
||||
assertKnownFlags({ flags, accepted })
|
||||
}
|
||||
|
||||
/**
|
||||
* The human's `--auto` (opt-out of the skill GATE) only makes sense when there
|
||||
* IS a skill invocation to auto-submit. Deterministic matrix, checked BEFORE
|
||||
* any git/provisioning so an unhonorable flag never leaves an orphan worktree:
|
||||
* `next`/`fix` always; `plan` only with a bag (OPS119 keeps it driverless
|
||||
* otherwise); `new` never; `--stay` never (the launch is suppressed); outside
|
||||
* the interactive terminal never (the `/worktree` command only applies `cd`, so
|
||||
* the launch — and thus the auto-submit — does not exist there). The purpose
|
||||
* matrix is checked before the terminal so `new --auto` reports the actionable
|
||||
* "neutra" message. Throws a clear message pointing at the valid form.
|
||||
* @param {{ purpose?: string | null, bag?: string | null, stay?: boolean, headless?: boolean, terminal?: boolean }} [options]
|
||||
*/
|
||||
export const assertSkillAutoSupported = ({
|
||||
purpose = null,
|
||||
bag = null,
|
||||
stay = false,
|
||||
headless = false,
|
||||
terminal = true,
|
||||
} = {}) => {
|
||||
if (headless) {
|
||||
throw new Error(
|
||||
'`--headless` (auto-unblock) roda `opencode run --command` sem a invocation de skill — `--auto` não se aplica. Remova a flag.',
|
||||
)
|
||||
}
|
||||
if (stay) {
|
||||
throw new Error(
|
||||
'`--stay` suprime o launch — `--auto` ficaria sem efeito. Remova `--auto` ou `--stay`.',
|
||||
)
|
||||
}
|
||||
const supported = purpose === 'next' || purpose === 'fix' || (purpose === 'plan' && hasBag(bag))
|
||||
if (!supported) {
|
||||
if (purpose === 'plan') {
|
||||
throw new Error(
|
||||
'`--auto` exige um comando para auto-submeter: `plan` sem bag abre a sessão sem driver (OPS119). Use `plan <bag>` ou remova `--auto`.',
|
||||
)
|
||||
}
|
||||
if (purpose === 'new') {
|
||||
throw new Error(
|
||||
'`new` é uma sessão neutra, sem skill — `--auto` não se aplica. Remova a flag ou use `next`/`plan`/`fix`.',
|
||||
)
|
||||
}
|
||||
throw new Error(`\`--auto\` não se aplica a \`${purpose}\`.`)
|
||||
}
|
||||
if (!terminal) {
|
||||
throw new Error(
|
||||
'`--auto` só é honrado no terminal interativo — o comando `/worktree` só aplica o `cd`, sem launch. Rode `pnpm worktree` no terminal ou remova `--auto`.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix of every neutral-worktree branch (`pnpm worktree new`). Lowercase-led
|
||||
* `work/…` — structurally disjoint from `next`'s uppercase-led `<Code>-<slug>`
|
||||
|
|
@ -132,8 +212,6 @@ export const branchNameForIssue = (issue, maxLength = 60) => {
|
|||
return `${code}-${slug.slice(0, keep)}`
|
||||
}
|
||||
|
||||
const hasBag = (bag) => typeof bag === 'string' && bag.trim().length > 0
|
||||
|
||||
/**
|
||||
* Shared branch naming for namespace worktrees NOT tied to the claim queue
|
||||
* (`plan`, `new`). Every invocation must land on a DIFFERENT branch so
|
||||
|
|
@ -197,7 +275,12 @@ const namespaceBranchName = ({ prefix, bag = '', taken = new Set(), fallback })
|
|||
* 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
|
||||
* errado). O default relativo existe para os specs.
|
||||
* @param {{ dir: string, purpose: string, terminal?: boolean, issueNumber?: number | null, model?: string | null, argument?: string | null, sessionScript?: string }} options
|
||||
*
|
||||
* OPS122: `skillAuto` (o `--auto` do HUMANO) vira `--skill-auto` no argv do
|
||||
* `start` — nome interno distinto do `--auto` de PERMISSÕES que o `driverArgs`
|
||||
* emite para o `opencode run` (são coisas diferentes). O CLI de sessão o
|
||||
* transpõe para o texto da invocation (`purposeInvocation`).
|
||||
* @param {{ dir: string, purpose: string, terminal?: boolean, issueNumber?: number | null, model?: string | null, argument?: string | null, sessionScript?: string, skillAuto?: boolean }} options
|
||||
*/
|
||||
export const opencodeLaunchDirective = ({
|
||||
dir,
|
||||
|
|
@ -207,6 +290,7 @@ export const opencodeLaunchDirective = ({
|
|||
model = null,
|
||||
argument = null,
|
||||
sessionScript = 'scripts/agent-session.mjs',
|
||||
skillAuto = false,
|
||||
}) => {
|
||||
if (!terminal) return null
|
||||
const selectedModel = model ?? OPENCODE_PRESET_MODEL
|
||||
|
|
@ -218,6 +302,7 @@ export const opencodeLaunchDirective = ({
|
|||
`--dir=${dir}`,
|
||||
`--model=${selectedModel}`,
|
||||
]
|
||||
if (skillAuto) args.push(`--${SKILL_AUTO_FLAG}`)
|
||||
// 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}`)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* `pnpm worktree` — worktree management determinístico em torno da fila de
|
||||
* claim do projeto (mesma fila de `agent:claim` / agent pool).
|
||||
*
|
||||
* pnpm worktree next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* pnpm worktree next [--issue N] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* claima a próxima Issue claimável ANTES de criar
|
||||
* o worktree a partir de origin/main (mesma fila
|
||||
* e lock otimista de `pnpm agent:claim`; claim
|
||||
|
|
@ -47,6 +47,9 @@
|
|||
* máquina (Ctrl+T). 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
|
||||
* ou `--auto` sem destino falha alto.
|
||||
* Sem o marcador (comando `/worktree` do opencode),
|
||||
* a diretiva não é impressa — nunca abre TUI aninhado.
|
||||
* Também PROVISIONA o ambiente isolado do worktree:
|
||||
|
|
@ -65,7 +68,7 @@
|
|||
* recusa a abrir com `{file:…}` pendurado.
|
||||
* `--no-migrate` pula migrations E o seed (que
|
||||
* depende do catálogo migrado).
|
||||
* pnpm worktree plan [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* pnpm worktree plan [bag] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* cria um worktree de PLANEJAMENTO novo para rodar
|
||||
* a skill /plan-issue sem ocupar o main — cada
|
||||
* invocação cria UM DIFERENTE, para sessões de
|
||||
|
|
@ -103,7 +106,7 @@
|
|||
* modelo por invocação (sem flag o preset permanece).
|
||||
* Migrations E seed mínimo nos dois bancos, como
|
||||
* o `next`/`plan` (OPS28).
|
||||
* pnpm worktree fix [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* pnpm worktree fix [bag] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]
|
||||
* cria um worktree de CORREÇÃO DE BUG (skill
|
||||
* /bug-fix) DIFERENTE a cada invocação: com bag,
|
||||
* branch `fix/<bag>` (e `-2`, `-3`, … se o nome
|
||||
|
|
@ -181,6 +184,7 @@ import {
|
|||
worktreeEnvironment,
|
||||
} from './lib/worktree-env.mjs'
|
||||
import {
|
||||
assertSkillAutoSupported,
|
||||
branchNameForIssue,
|
||||
fixBranchName,
|
||||
headlessDirective,
|
||||
|
|
@ -189,6 +193,7 @@ import {
|
|||
opencodeLaunchDirective,
|
||||
planBranchName,
|
||||
resolveWorktreeModel,
|
||||
validateWorktreeFlags,
|
||||
workBranchName,
|
||||
WORKTREE_MODEL_FLAGS,
|
||||
WORKTREE_MODEL_MAP,
|
||||
|
|
@ -231,6 +236,8 @@ const printLaunchDirective = ({ dir, purpose, issueNumber, argument = null, flag
|
|||
issueNumber,
|
||||
model,
|
||||
argument,
|
||||
// OPS122: o `--auto` do humano viaja como flag interna (`--skill-auto`).
|
||||
skillAuto: Boolean(flags.auto),
|
||||
// Absoluto de propósito (OPS110): worktrees reabertos/criados antes do
|
||||
// merge não têm `scripts/agent-session.mjs`; o relativo quebraria o launch.
|
||||
sessionScript: join(REPO_ROOT, 'scripts', 'agent-session.mjs'),
|
||||
|
|
@ -238,6 +245,23 @@ const printLaunchDirective = ({ dir, purpose, issueNumber, argument = null, flag
|
|||
if (line) console.log(line)
|
||||
}
|
||||
|
||||
/**
|
||||
* OPS122: falha alto antes de qualquer git quando `--auto` não tem para onde
|
||||
* ir (purpose sem driver/skill ou `--stay`, que suprime o launch). Um worktree
|
||||
* provisionado com a flag descartada seria órfão. O `die` do dispatch externo
|
||||
* formata o erro.
|
||||
*/
|
||||
const assertAutoFlag = ({ purpose, bag = null, flags }) => {
|
||||
if (!flags.auto) return
|
||||
assertSkillAutoSupported({
|
||||
purpose,
|
||||
bag,
|
||||
stay: Boolean(flags.stay),
|
||||
headless: Boolean(flags.headless),
|
||||
terminal: terminalShell,
|
||||
})
|
||||
}
|
||||
|
||||
/** Maintenance database on the shared local container — only ever localhost. */
|
||||
const MAINTENANCE_DATABASE_URL = 'postgresql://teqo:teqo@localhost:5432/postgres'
|
||||
|
||||
|
|
@ -950,10 +974,10 @@ const subcommand = positional[0]
|
|||
|
||||
if (!subcommand) {
|
||||
console.log(
|
||||
'Uso: pnpm worktree next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | plan [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | fix [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | kill [--force]',
|
||||
'Uso: pnpm worktree next [--issue N] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | plan [bag] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | fix [bag] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free] | kill [--force]',
|
||||
)
|
||||
console.log(
|
||||
' next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]',
|
||||
' next [--issue N] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]',
|
||||
)
|
||||
console.log(' CLAIMA a próxima Issue claimável (mesma fila/ordem e lock otimista do')
|
||||
console.log(' `pnpm agent:claim`) e cria o worktree dela (branch <code>-<slug>),')
|
||||
|
|
@ -980,7 +1004,7 @@ if (!subcommand) {
|
|||
' seed mínimo (db:seed:minimal) nos bancos novos (OPS28: paridade com a CI); at-most-one de --cheap/--pro/--zen/--go/--alibaba/--glm/--free (múltiplas → erro)',
|
||||
)
|
||||
console.log(
|
||||
`\n plan [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`,
|
||||
`\n plan [bag] [--stay] [--no-migrate] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`,
|
||||
)
|
||||
console.log(
|
||||
' cria um worktree de planejamento DIFERENTE a cada invocação (sessões /plan-issue',
|
||||
|
|
@ -1004,7 +1028,7 @@ if (!subcommand) {
|
|||
' terminal, mesma diretiva `launch` — sessão sem skill (apenas conversar) e --model <map> quando a flag está presente',
|
||||
)
|
||||
console.log(
|
||||
`\n fix [bag] [--stay] [--no-migrate] [--headless --directive <path>] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`,
|
||||
`\n fix [bag] [--stay] [--no-migrate] [--headless --directive <path>] [--auto] [--cheap|--pro|--zen|--go|--alibaba|--glm|--free]`,
|
||||
)
|
||||
console.log(' cria um worktree de CORREÇÃO DE BUG (skill /bug-fix) DIFERENTE a cada')
|
||||
console.log(' invocação: com bag (a descrição do bug), branch fix/<bag> (sufixo -2/-3 se o')
|
||||
|
|
@ -1025,10 +1049,19 @@ if (!subcommand) {
|
|||
console.log(' driver; estado/lock removidos) e remove os bancos gerados do')
|
||||
console.log(' worktree (best-effort); imprime')
|
||||
console.log(' `cd <main>` no fim — o shell sempre volta ao worktree principal')
|
||||
console.log(' --auto opt-out do GATE da skill: o driver auto-submete')
|
||||
console.log(' /work-issue --issue <N> --auto | /plan-issue --auto <bag> |')
|
||||
console.log(' /bug-fix --auto <bag>. Vale em next/fix e em plan COM bag; falha')
|
||||
console.log(' alto em new, em plan sem bag, com --stay, com --headless e fora')
|
||||
console.log(' do terminal interativo.')
|
||||
console.log(' Flag desconhecida falha alto com sugestão (OPS122) — nunca é descartada.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
// OPS122: falha alto ANTES de qualquer git/claim — flag desconhecida ou
|
||||
// `--auto` sem destino não pode deixar worktree órfão (espelha OPS110-F1).
|
||||
validateWorktreeFlags({ subcommand, flags })
|
||||
if (subcommand === 'next') {
|
||||
if ('issue' in flags && flags.issue === undefined) {
|
||||
die('`--issue` requer um número (ex.: `--issue 595`).')
|
||||
|
|
@ -1036,12 +1069,16 @@ try {
|
|||
if (positional.length > 1) {
|
||||
die('`next` não aceita argumento posicional — use `--issue <N>` para direcionar a Issue.')
|
||||
}
|
||||
assertAutoFlag({ purpose: 'next', flags })
|
||||
await cmdNext(Boolean(flags.stay), Boolean(flags['no-migrate']), flags.issue ?? null, flags)
|
||||
} else if (subcommand === 'plan')
|
||||
} else if (subcommand === 'plan') {
|
||||
assertAutoFlag({ purpose: 'plan', bag: positional[1], flags })
|
||||
await cmdPlan(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1], flags)
|
||||
else if (subcommand === 'new')
|
||||
} else if (subcommand === 'new') {
|
||||
assertAutoFlag({ purpose: 'new', flags })
|
||||
await cmdNew(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1], flags)
|
||||
else if (subcommand === 'fix')
|
||||
} else if (subcommand === 'fix') {
|
||||
assertAutoFlag({ purpose: 'fix', bag: positional[1], flags })
|
||||
await cmdFix(
|
||||
Boolean(flags.stay),
|
||||
Boolean(flags['no-migrate']),
|
||||
|
|
@ -1050,7 +1087,7 @@ try {
|
|||
Boolean(flags.headless),
|
||||
flags.directive ?? null,
|
||||
)
|
||||
else if (subcommand === 'kill') {
|
||||
} else if (subcommand === 'kill') {
|
||||
if (flags.stay) die('`--stay` não se aplica a `kill` — ele sempre volta ao main.')
|
||||
await cmdKill(Boolean(flags.force))
|
||||
} else die(`subcomando desconhecido: ${subcommand} (esperado: next | plan | new | fix | kill)`)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
assertKnownFlags,
|
||||
attachArgs,
|
||||
buildCreateSessionBody,
|
||||
codeFromBranch,
|
||||
DEFAULT_AGENT_SERVER_HOST,
|
||||
DEFAULT_AGENT_SERVER_PORT,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
isLoopbackHost,
|
||||
isStaleLock,
|
||||
nearestAcceptedFlag,
|
||||
parseModelRef,
|
||||
parseServerState,
|
||||
parseSessionState,
|
||||
purposeInvocation,
|
||||
|
|
@ -251,13 +254,126 @@ describe('purposeInvocation (mapa purpose→comando movido do worktree)', () =>
|
|||
expect(purposeInvocation({ purpose: 'bogus' })).toBeNull()
|
||||
expect(purposeInvocation({})).toBeNull()
|
||||
})
|
||||
|
||||
it('auto=true transposes to the exact skill flags — /work-issue --issue N --auto', () => {
|
||||
expect(purposeInvocation({ purpose: 'next', issueNumber: 1088, auto: true })).toEqual({
|
||||
command: 'work-issue',
|
||||
arguments: '--issue 1088 --auto',
|
||||
})
|
||||
})
|
||||
|
||||
it('auto=true prefixes plan/fix bags — /plan-issue --auto <bag> and /bug-fix --auto <bag>', () => {
|
||||
expect(purposeInvocation({ purpose: 'plan', argument: 'revisa o gate', auto: true })).toEqual({
|
||||
command: 'plan-issue',
|
||||
arguments: '--auto revisa o gate',
|
||||
})
|
||||
expect(purposeInvocation({ purpose: 'fix', argument: '500 no autosave', auto: true })).toEqual({
|
||||
command: 'bug-fix',
|
||||
arguments: '--auto 500 no autosave',
|
||||
})
|
||||
})
|
||||
|
||||
it('auto=true with an empty fix bag still submits the bare flag — /bug-fix --auto', () => {
|
||||
expect(purposeInvocation({ purpose: 'fix', auto: true })).toEqual({
|
||||
command: 'bug-fix',
|
||||
arguments: '--auto',
|
||||
})
|
||||
})
|
||||
|
||||
it('auto=false (default) keeps the supervised forms byte-identical', () => {
|
||||
expect(purposeInvocation({ purpose: 'next', issueNumber: 1088 })).toEqual({
|
||||
command: 'work-issue',
|
||||
arguments: '--issue 1088',
|
||||
})
|
||||
expect(purposeInvocation({ purpose: 'fix', argument: 'b' })).toMatchObject({
|
||||
arguments: 'b',
|
||||
})
|
||||
expect(purposeInvocation({ purpose: 'fix' })).toMatchObject({ arguments: '' })
|
||||
expect(purposeInvocation({ purpose: 'plan', argument: 'b' })).toMatchObject({
|
||||
arguments: 'b',
|
||||
})
|
||||
})
|
||||
|
||||
it('auto=true never resurrects a missing invocation (new / plan without bag stay null)', () => {
|
||||
expect(purposeInvocation({ purpose: 'new', auto: true })).toBeNull()
|
||||
expect(purposeInvocation({ purpose: 'plan', auto: true })).toBeNull()
|
||||
expect(purposeInvocation({ purpose: 'plan', argument: ' ', auto: true })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseModelRef + buildCreateSessionBody (OPS122 — o modelo chega à SESSÃO)', () => {
|
||||
it('splits provider/model at the FIRST slash', () => {
|
||||
expect(parseModelRef('opencode/muse-spark-1.3-contributor-free')).toEqual({
|
||||
providerID: 'opencode',
|
||||
id: 'muse-spark-1.3-contributor-free',
|
||||
})
|
||||
expect(parseModelRef('openrouter/openrouter/free')).toEqual({
|
||||
providerID: 'openrouter',
|
||||
id: 'openrouter/free',
|
||||
})
|
||||
expect(parseModelRef('opencode-go/deepseek-v4.1-flash')).toEqual({
|
||||
providerID: 'opencode-go',
|
||||
id: 'deepseek-v4.1-flash',
|
||||
})
|
||||
})
|
||||
|
||||
it('fails high on a model without provider/id — never sends a half ref', () => {
|
||||
expect(() => parseModelRef('deepseek-flash')).toThrow(/provider\/model|inválido/)
|
||||
expect(() => parseModelRef('/deepseek-flash')).toThrow(/inválido/)
|
||||
expect(() => parseModelRef('deepseek/')).toThrow(/inválido/)
|
||||
expect(() => parseModelRef('')).toThrow()
|
||||
expect(() => parseModelRef(null as unknown as string)).toThrow()
|
||||
})
|
||||
|
||||
it('builds the POST /session body with the model ref, 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' },
|
||||
})
|
||||
expect(buildCreateSessionBody({ model: null })).toEqual({})
|
||||
expect(buildCreateSessionBody({ model: undefined })).toEqual({})
|
||||
expect(buildCreateSessionBody({})).toEqual({})
|
||||
expect(buildCreateSessionBody()).toEqual({})
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertKnownFlags (dono único da mensagem fail-high)', () => {
|
||||
it('is silent when every flag is accepted or a bare `--`', () => {
|
||||
expect(() =>
|
||||
assertKnownFlags({ flags: { '': true, stay: true }, accepted: ['stay'] }),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('throws with a neighbor suggestion, or as-is without one', () => {
|
||||
expect(() => assertKnownFlags({ flags: { stya: true }, accepted: ['stay'] })).toThrow(
|
||||
/--stya[\s\S]*--stay/,
|
||||
)
|
||||
expect(() => assertKnownFlags({ flags: { nope: true }, accepted: ['stay'] })).toThrow(
|
||||
'flag desconhecida: --nope.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateSessionFlags + nearestAcceptedFlag (OPS110-F1 — flag desconhecida falha alto)', () => {
|
||||
it('pins the accepted flag names per verb', () => {
|
||||
expect(SESSION_FLAG_ALLOWLIST).toEqual({
|
||||
serve: ['hostname', 'port'],
|
||||
start: ['purpose', 'dir', 'model', 'issue', 'argument', 'new', 'hostname', 'port'],
|
||||
start: [
|
||||
'purpose',
|
||||
'dir',
|
||||
'model',
|
||||
'issue',
|
||||
'argument',
|
||||
'new',
|
||||
'skill-auto',
|
||||
'hostname',
|
||||
'port',
|
||||
],
|
||||
attach: ['session', 'branch', 'issue'],
|
||||
stop: ['session', 'branch', 'issue'],
|
||||
list: ['json', 'hostname', 'port'],
|
||||
|
|
@ -275,6 +391,7 @@ describe('validateSessionFlags + nearestAcceptedFlag (OPS110-F1 — flag desconh
|
|||
issue: '1',
|
||||
argument: 'x',
|
||||
new: true,
|
||||
'skill-auto': true,
|
||||
hostname: 'h',
|
||||
port: '1',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
worktreeEnvironment,
|
||||
} from '../../scripts/lib/worktree-env.mjs'
|
||||
import {
|
||||
assertSkillAutoSupported,
|
||||
branchNameForIssue,
|
||||
FIX_BRANCH_PREFIX,
|
||||
fixBranchName,
|
||||
|
|
@ -28,8 +29,10 @@ import {
|
|||
PLAN_BRANCH_PREFIX,
|
||||
planBranchName,
|
||||
resolveWorktreeModel,
|
||||
validateWorktreeFlags,
|
||||
WORK_BRANCH_PREFIX,
|
||||
workBranchName,
|
||||
WORKTREE_FLAG_ALLOWLIST,
|
||||
WORKTREE_MODEL_FLAGS,
|
||||
WORKTREE_MODEL_MAP,
|
||||
WORKTREE_TERMINAL_ENV,
|
||||
|
|
@ -317,6 +320,43 @@ describe('opencodeLaunchDirective (terminal-only agent-session launch, OPS26 + O
|
|||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('skillAuto carries --skill-auto to the session CLI (OPS122 — o --auto do humano)', () => {
|
||||
expect(
|
||||
opencodeLaunchDirective({
|
||||
dir,
|
||||
purpose: 'next',
|
||||
terminal: true,
|
||||
issueNumber: 1088,
|
||||
skillAuto: true,
|
||||
}),
|
||||
).toBe(
|
||||
`launch node scripts/agent-session.mjs start --purpose=next --dir=${dir} --model=${presetInEffect()} --skill-auto --issue=1088`,
|
||||
)
|
||||
expect(opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true, skillAuto: true })).toBe(
|
||||
`launch node scripts/agent-session.mjs start --purpose=plan --dir=${dir} --model=${presetInEffect()} --skill-auto`,
|
||||
)
|
||||
expect(
|
||||
opencodeLaunchDirective({
|
||||
dir,
|
||||
purpose: 'fix',
|
||||
terminal: true,
|
||||
skillAuto: true,
|
||||
argument: 'bug x',
|
||||
}),
|
||||
).toBe(
|
||||
`launch node scripts/agent-session.mjs start --purpose=fix --dir=${dir} --model=${presetInEffect()} --skill-auto --argument="bug x"`,
|
||||
)
|
||||
})
|
||||
|
||||
it('omits --skill-auto by default and outside the terminal (supervised stays byte-identical)', () => {
|
||||
expect(opencodeLaunchDirective({ dir, purpose: 'next', terminal: true })).not.toContain(
|
||||
'--skill-auto',
|
||||
)
|
||||
expect(
|
||||
opencodeLaunchDirective({ dir, purpose: 'next', terminal: false, skillAuto: true }),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('namespaceLaunchDescriptor (purpose→options single source, OPS119+)', () => {
|
||||
|
|
@ -481,6 +521,122 @@ describe('resolveWorktreeModel + WORKTREE_MODEL_MAP (OPS93 menu, OPS95 values, O
|
|||
})
|
||||
})
|
||||
|
||||
describe('validateWorktreeFlags + WORKTREE_FLAG_ALLOWLIST (OPS122 — flag desconhecida falha alto)', () => {
|
||||
it('pins the accepted names per verb (model flags included)', () => {
|
||||
const modelFlags = [...WORKTREE_MODEL_FLAGS]
|
||||
expect(WORKTREE_FLAG_ALLOWLIST).toEqual({
|
||||
next: ['issue', 'stay', 'no-migrate', 'auto', ...modelFlags],
|
||||
plan: ['stay', 'no-migrate', 'auto', ...modelFlags],
|
||||
new: ['stay', 'no-migrate', 'auto', ...modelFlags],
|
||||
fix: ['stay', 'no-migrate', 'headless', 'directive', 'auto', ...modelFlags],
|
||||
kill: ['force', 'stay'],
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts every documented flag per verb', () => {
|
||||
expect(() =>
|
||||
validateWorktreeFlags({
|
||||
subcommand: 'next',
|
||||
flags: { issue: '1088', stay: true, 'no-migrate': true, auto: true, zen: true },
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: 'plan', flags: { auto: true, go: true } }),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
validateWorktreeFlags({
|
||||
subcommand: 'fix',
|
||||
flags: { headless: true, directive: '/d', auto: true, free: true },
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: 'kill', flags: { force: true } }),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('suggests the neighbor: --zen flag typo and --stay typo', () => {
|
||||
expect(() => validateWorktreeFlags({ subcommand: 'next', flags: { zenm: true } })).toThrow(
|
||||
/--zenm[\s\S]*--zen/,
|
||||
)
|
||||
expect(() => validateWorktreeFlags({ subcommand: 'plan', flags: { stya: true } })).toThrow(
|
||||
/--stya[\s\S]*--stay/,
|
||||
)
|
||||
})
|
||||
|
||||
it('reports an unknown flag without a suggestion as-is', () => {
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: 'next', flags: { totallyWrong: true } }),
|
||||
).toThrow('flag desconhecida: --totallyWrong.')
|
||||
})
|
||||
|
||||
it('rejects a flag that belongs to another verb', () => {
|
||||
expect(() => validateWorktreeFlags({ subcommand: 'plan', flags: { issue: '1' } })).toThrow(
|
||||
/--issue/,
|
||||
)
|
||||
expect(() => validateWorktreeFlags({ subcommand: 'next', flags: { headless: true } })).toThrow(
|
||||
/--headless/,
|
||||
)
|
||||
expect(() => validateWorktreeFlags({ subcommand: 'kill', flags: { auto: true } })).toThrow(
|
||||
/--auto/,
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores a bare `--` and stays silent for an unknown/absent verb', () => {
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: 'next', flags: { '': true, stay: true } }),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: 'bogus', flags: { whatever: true } }),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
validateWorktreeFlags({ subcommand: null, flags: { whatever: true } }),
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertSkillAutoSupported (OPS122 — `--auto` não-honrável falha alto)', () => {
|
||||
it('honors next/fix always and plan only with a bag', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'next' })).not.toThrow()
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'fix' })).not.toThrow()
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'fix', bag: 'bug' })).not.toThrow()
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'plan', bag: 'uma ideia' })).not.toThrow()
|
||||
})
|
||||
|
||||
it('fails high on new (neutral session, no skill to auto-submit)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'new' })).toThrow(/new|neutra/)
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'new', bag: 'ideia' })).toThrow(/new|neutra/)
|
||||
})
|
||||
|
||||
it('fails high on plan without a bag (OPS119 keeps it driverless)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'plan' })).toThrow(/bag|driverless|sem/)
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'plan', bag: ' ' })).toThrow(
|
||||
/bag|driverless|sem/,
|
||||
)
|
||||
})
|
||||
|
||||
it('fails high on --stay (the launch is suppressed, so --auto would be dead)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'next', stay: true })).toThrow(/--stay/)
|
||||
})
|
||||
|
||||
it('fails high on --headless (the auto-unblock path has no skill invocation)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'fix', bag: 'bug', headless: true })).toThrow(
|
||||
/--headless/,
|
||||
)
|
||||
})
|
||||
|
||||
it('fails high outside the interactive terminal (the /worktree command never launches)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'next', terminal: false })).toThrow(/terminal/)
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'plan', bag: 'x', terminal: false })).toThrow(
|
||||
/terminal/,
|
||||
)
|
||||
})
|
||||
|
||||
it('fails high on an unknown purpose (never invents a command)', () => {
|
||||
expect(() => assertSkillAutoSupported({ purpose: 'bogus' })).toThrow(/bogus/)
|
||||
expect(() => assertSkillAutoSupported({})).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('workBranchName (per-invocation neutral worktrees)', () => {
|
||||
it('no bag → first free sequential work/<n>', () => {
|
||||
expect(workBranchName({})).toBe('work/1')
|
||||
|
|
|
|||
Loading…
Reference in a new issue