diff --git a/.agents/shell/worktree.sh b/.agents/shell/worktree.sh index 6c07e5d3..98f6dbca 100644 --- a/.agents/shell/worktree.sh +++ b/.agents/shell/worktree.sh @@ -11,7 +11,9 @@ # a função executa o cd e então a linha (tokenizada por xargs — honra as aspas # do prompt, nunca eval), e o TUI do opencode abre no worktree — `next` com # `/work-issue --issue ` já enviado (OPS33: a Issue claimada vai no prompt), -# `plan` com `/plan-issue` já enviado (OPS31), `new` sem prompt (apenas +# `plan` com `/plan-issue` já enviado (OPS31), `fix` com `/bug-fix ` já +# enviado (a descrição do bug vai no prompt — aspas/barra-invertida do bag são +# removidas porque o xargs não honra escapes), `new` sem prompt (apenas # conversar). 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` (cheapestinference/deepseek-v4-flash), @@ -46,11 +48,20 @@ # (sufixo -2/-3 se o nome já existir), sem bag o próximo # work/ sequencial; cd para dentro dele por padrão; # --stay não troca; flag de modelo como no `next` +# worktree fix [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba] +# cria um worktree de CORREÇÃO DE BUG (skill /bug-fix) +# DIFERENTE a cada chamada: com bag (a descrição do bug), +# branch fix/ (sufixo -2/-3 se o nome já existir), +# sem bag o próximo fix/ sequencial; o launch envia +# `/bug-fix ` (a descrição chega com a skill); não +# claima nem cria Issues — o registro é o post-mortem; +# cd para dentro dele por padrão; --stay não troca; +# flag de modelo como no `next` # worktree kill [--force] destrói o worktree atual e cd para o main por padrão # # `--go` remapeado em OPS93 para o provider OpenCode Go e em OPS95 para o modelo `opencode-go/hy3` # (antes no-op do OPS24). Claim determinístico: `next` claima antes de criar o worktree (mesma fila e -# lock de `pnpm agent:claim`); `plan`/`new`/`kill` não tocam Issues. +# lock de `pnpm agent:claim`); `plan`/`new`/`fix`/`kill` não tocam Issues. worktree() { local src="${BASH_SOURCE[0]:-$0}" diff --git a/.agents/skills/bug-fix/SKILL.md b/.agents/skills/bug-fix/SKILL.md new file mode 100644 index 00000000..87808914 --- /dev/null +++ b/.agents/skills/bug-fix/SKILL.md @@ -0,0 +1,131 @@ +--- +name: bug-fix +description: 'Corrige bugs de ponta a ponta: diagnóstico → causa-raiz → fix → verificação → prevenção → post-mortem.' +disable-model-invocation: true +--- + +# Bug-fix + +Corrige um bug de ponta a ponta e fecha o ciclo com prevenção e post-mortem. A sessão nasce no contrato: worktree `fix/` criado por `pnpm worktree fix [bug]` — o launch entrega `/bug-fix` com a descrição do bug no prompt (a skill lê o resto do contexto). + +**Proibido:** DB de prod / `ALLOW_REMOTE_DB`; fix de sintoma (máscara) em vez de causa-raiz; merge sem CI green; fix sem teste de regressão; declarar bug de prod "corrigido" antes da confirmação em prod; pular o post-mortem. + +## Decomposição em sub-agentes + +Fases pesadas são delegadas a sub-agentes com contexto mínimo — o agente principal não engorda com a exploração, verificação, análise nem redação. O agente principal orquestra e implementa o fix. + +### Sub-agente: Investigador de causa-raiz + +**Quando:** Passo 3, antes de qualquer código. +**Input:** relatório do bug (sintoma, URL/rota, erro/log/stack, quando começou, ambiente) + dica de onde procurar. +**Task:** Encontrar a ORIGEM: reproduzir (se possível), localizar a camada (UI/API/DB/infra/teste), apontar a causa-raiz provável com `file:line`, propor repro mínimo. **Não escrever código nem fix.** +**Output:** ≤25 linhas: evidência, repro, origem `file:line`, hipótese de causa-raiz. + +### Sub-agente: Verificador + +**Quando:** Passo 5, após o fix. +**Input:** diff do fix + causa-raiz + cenário original do bug. +**Task:** Provar que o fix funciona: rodar o teste de regressão (falha sem o fix, passa com), a suíte afetada e o cenário original end-to-end (e2e quando a superfície for de UI). **Não alterar código — reportar falhas para o orquestrador.** +**Output:** relatório de verificação com evidência (comandos rodados + resultados). + +### Sub-agente: Estrategista de prevenção + +**Quando:** Passo 7, após a verificação verde. +**Input:** causa-raiz + fix + relato de verificação. +**Task:** Analisar por que e como o bug aconteceu (5-whys) e classificar a estratégia de prevenção: **barata** (teste de regressão, guard, lint, validação — implementar AGORA no mesmo PR) vs **cara** (refactor estrutural, nova infra de observabilidade, mudança de contrato — documentar no post-mortem, NÃO implementar aqui). +**Output:** ≤25 linhas: análise causal + estratégia classificada + passos concretos de cada lado. + +### Sub-agente: Escritor do post-mortem + +**Quando:** Passo 9, por último. +**Input:** relatório, causa-raiz, fix, verificação, análise de prevenção. +**Task:** Escrever `docs/postmortems/-.md` conforme `postmortem-template.md`. Não inventar fato ausente — marcar "não apurado". +**Output:** conteúdo markdown completo do documento. + +## Checklist + +``` +- [ ] 0. Prep: `pnpm i` se preciso; confirmar worktree fix/ (senão criar branch fix/) +- [ ] 1. Contexto: relatório do bug ($ARGUMENTS / bag / Issue / humano) +- [ ] 2. Reproduzir o bug (ou registrar por que não reproduz) +- [ ] 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 manual → 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 +``` + +## Passo 0 — Prep + +```bash +pnpm i # se node_modules ausente/stale +``` + +O worktree `fix/` já foi criado pelo `pnpm worktree fix` (mesmo provisionamento isolado de `next`/`plan`/`new`). Se o `/bug-fix` foi invocado fora de um worktree `fix/` (ex.: no main), crie a branch `fix/` de `origin/main` você mesmo — bug-fix não toca a fila de claim, nunca rode `pnpm agent:claim`. + +## Passo 1 — Contexto do bug + +O relatório chega no `$ARGUMENTS` (a descrição passada ao `worktree fix`), do humano no chat, ou de uma Issue do GitHub referenciada. Junte o mínimo para investigar: + +- **Sintoma:** o que acontece (mensagem de erro, comportamento errado, tela branca…) +- **Onde:** rota/URL, coleção/feature, camada +- **Quando começou:** deploy/commit suspeito, "sempre esteve assim" ou regressão +- **Ambiente:** prod / dev / CI / worktree +- **Severidade/impacto:** quem afeta, bloqueia usuário? + +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). + +## Passo 2 — Reproduzir + +Antes de mexer em código: fazer o bug acontecer de forma confiável. + +- **Bug local:** dev server do worktree (porta própria do `.env.local`), banco dev isolado. +- **Bug de prod:** verifique o comportamento em prod de forma **read-only** (browser/curl na URL pública — nunca escreva em prod, nunca aponte DATABASE_URL para `teqo_1313`). Nunca tente reproduzir bug de prod escrevendo em prod. +- **Não reproduz:** registre as condições testadas e prossiga com a hipótese baseada em evidência (logs, código, git log/bisect na área suspeita). Documente a não-reprodução no post-mortem. + +## Passo 3 — Investigar (sub-agente) + +Dispatch o **investigador de causa-raiz** com o relatório do Passo 1 + condições do Passo 2. Receba ≤25 linhas: evidência, repro, origem `file:line`, hipótese de causa-raiz. + +O agente principal decide a causa-raiz com base nos achados — o investigador sugere, não decide. + +## Passo 4 — Fix da causa-raiz (main agent) + +`SwitchMode` → `agent`. Corrija a **causa-raiz**, nunca o sintoma: + +- **Teste de regressão primeiro** (que falha sem o fix) — o kernel do guard de recorrência. +- Convenções do repo: transação para escrita multi-coleção (`req: { transactionID }`), access control, migrations commitadas se houver mudança de schema (`pnpm migrate:create` — nunca `push`). +- Itere com `pnpm gate:fast` (lint + typecheck + unit). E2E afetado quando a superfície for de UI (OPS72: discricionário, rode os que criou + mesma superfície). + +## Passo 5 — Verificar (sub-agente) + +Dispatch o **verificador** com o diff + causa-raiz + cenário original. Ele roda a prova: teste de regressão (sem o fix falha, com o fix passa), suíte afetada, cenário end-to-end. Falhas → volte ao Passo 4. Não declare corrigido sem a prova. + +## Passo 6 — Fechar e confirmar em prod + +- `pnpm push` → PR no GitHub **Ready** base `main` (`Closes #N` se houver Issue; `Related #N` para PR só de docs — plans-only guard) → auto-merge nativo com o required check `CI (PR) / checks` verde. Siga `agent-pr-workflow.mdc`. +- **Bug de prod:** o merge NÃO é o fim. O deploy é **manual** (`workflow_dispatch` de `deploy.yml`, só humano — AGENT-OPS). Peça ao humano para disparar o deploy e **espere a confirmação de que prod se comporta** (o humano relata; você nunca toca o homeserver). Só então o bug está corrigido — registre data/hora da confirmação. + +## Passo 7 — Prevenção (sub-agente) + +Dispatch o **estrategista de prevenção** com causa-raiz + fix + verificação. Receba a análise causal e a estratégia classificada (barata vs cara). + +## Passo 8 — Aplicar a prevenção + +- **Barata** → implemente AGORA no mesmo PR (o teste de regressão do Passo 4 já é o guard mínimo). +- **Cara** → NÃO implemente neste fluxo (evite scope creep e risco sem evidência): registre no post-mortem como a estratégia que preveniria a recorrência — o documento vira o gatilho para uma Issue futura (`pnpm agent:register` é caminho do humano/plan-issue). + +## Passo 9 — Post-mortem (sub-agente) + changelog + +Dispatch o **escritor do post-mortem** com todo o material. Crie: + +1. `docs/postmortems/-.md` conforme `postmortem-template.md`. +2. Entrada curta em `docs/changelog/-.md` (convenção de entrega — uma entrada por arquivo; nunca edite `docs/CHANGELOG-AGENTS.md` nem o HISTORY). + +Revise o documento contra os fatos apurados antes do push. O post-mortem entra no MESMO PR do fix (ou num PR só de docs `Related #N` se o fix já mergeou). + +## Resumo final + +Bug · causa-raiz · fix · verificação (CI + prod) · prevenção · post-mortem. diff --git a/.agents/skills/bug-fix/postmortem-template.md b/.agents/skills/bug-fix/postmortem-template.md new file mode 100644 index 00000000..1359c71b --- /dev/null +++ b/.agents/skills/bug-fix/postmortem-template.md @@ -0,0 +1,58 @@ +# Post-mortem: + +> Template do `/bug-fix`. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente. + +## Registro + +| Campo | Valor | +| ----- | ----- | +| Data do post-mortem | YYYY-MM-DD | +| Severidade | crítica / alta / média / baixa | +| Ambiente | prod / dev / CI | +| Issue(s) | #N ou "sem Issue" | +| PR do fix | #N | +| Detectado por | humano / teste / log / usuário | + +## Timeline + +| Momento | Data/hora | Evento | +| ------- | --------- | ------ | +| Início provável | | quando o bug começou (commit/entrega que o introduziu, se apurado) | +| Detecção | | | +| Correção mergeada | | | +| Deploy | | (bug de prod: dispatch manual do deploy.yml) | +| Verificado em prod | | (bug de prod: confirmação do humano) | + +## O bug + +O que acontecia, como se manifestava, quem afetava. **Sintoma — não a causa.** + +## Causa-raiz + +Por que e como aconteceu (5-whys). Origem `file:line` / commit quando apurado. Se a reprodução falhou, registre as condições testadas e a base da hipótese. + +## Correção + +O que mudou e por que resolve a **causa** (não o sintoma). Referências: arquivos, PR, migration se houver. + +## Verificação + +- Teste de regressão: `` — falha sem o fix, passa com +- Suíte: `` (`pnpm gate:fast` / `test:int` / e2e afetado) +- CI: verde no PR #N +- Prod: `` ou "não se aplica (bug local)" + +## Prevenção + +| Estratégia | Custo | Estado | +| ---------- | ----- | ------ | +| | barata | implementada agora (commit/PR) | +| | cara | documentada — não implementada neste fluxo | + +**Estratégia implementada:** o que foi adicionado para o bug não voltar (teste de regressão, guard, lint, validação…). + +**Estratégia documentada (cara):** o que preveniria a recorrência mas exige engenharia relevante — candidata a Issue futura. + +## Lições + +Observações livres: padrões que geraram o bug, sinais precoces ignorados, o que fazer diferente. diff --git a/.opencode/commands/bug-fix.md b/.opencode/commands/bug-fix.md new file mode 100644 index 00000000..d7e4fce2 --- /dev/null +++ b/.opencode/commands/bug-fix.md @@ -0,0 +1,8 @@ +--- +description: Corrige um bug de ponta a ponta: diagnóstico → causa-raiz → fix → verificação → prevenção → post-mortem +model: deepseek/deepseek-v4-flash +--- + +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. + +$ARGUMENTS diff --git a/.opencode/commands/worktree.md b/.opencode/commands/worktree.md index 0338c148..7cf40046 100644 --- a/.opencode/commands/worktree.md +++ b/.opencode/commands/worktree.md @@ -1,5 +1,5 @@ --- -description: Claima a próxima Issue (next [--issue N] [--stay] [--cheap|--pro|--zen|--go|--alibaba]), cria o worktree dela e provisiona o ambiente; também cria worktree de planejamento (plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]), worktree neutro (new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]) ou destrói o atual (kill [--force]) +description: Claima a próxima Issue (next [--issue N] [--stay] [--cheap|--pro|--zen|--go|--alibaba]), cria o worktree dela e provisiona o ambiente; também cria worktree de planejamento (plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]), worktree neutro (new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]), worktree de correção de bug (fix [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]) 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. @@ -7,6 +7,7 @@ Rode o script determinístico abaixo (não improvise a lógica) e reporte o resu - `next [--issue N] [--stay] [--cheap|--pro|--zen|--go|--alibaba]`: **claima** a próxima Issue claimável da fila (mesma fila/ordem e lock otimista de `pnpm agent:claim`) e cria (ou reutiliza) o worktree dela. Claim falhou → reporte o motivo — não há worktree. `--issue N` claima a Issue direcionada (`ready`) ou **reabre** uma já claimada (`in-progress`, sem re-claim). A saída avisa "já claimada — NÃO rodar `pnpm agent:claim`". Por padrão o script imprime `cd ` na última linha — aplique-o na sessão de shell atual (`cd `) e reporte o novo diretório. `--stay` suprime o `cd` (não troque de diretório) mas o claim ainda acontece. `--cheap/--pro/--zen/--go/--alibaba` escolhe o modelo por invocação no mapa fixo `WORKTREE_MODEL_MAP` (cheap=cheapestinference/deepseek-v4-flash, pro=deepseek/deepseek-v4-pro, zen=opencode-go/ox-alpha-free, go=opencode-go/hy3, alibaba=alibaba-token-plan/deepseek-v4-flash); at-most-one (múltiplas → erro). - `plan [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]`: cria um worktree de planejamento **diferente a cada invocação** (branch `plans/plan-issue-`, ou `plans/plan-issue-` sequencial sem bag; sufixo `-2`/`-3` em colisão) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. Não claima nada. Mesmo contrato de `cd` do `next`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. - `new [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]`: cria um worktree **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar nada) **diferente a cada invocação** (branch `work/`, ou `work/` sequencial sem bag; sufixo `-2`/`-3` em colisão); o prefixo minúsculo `work/…` nunca conflita com `next` nem `plan`. Não claima nada. Mesmo contrato de `cd` do `plan`: aplique a última linha `cd ` por padrão; `--stay` suprime; flag de modelo como no `next`. +- `fix [bag] [--stay] [--cheap|--pro|--zen|--go|--alibaba]`: 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 envia `--prompt "/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á e remove seus bancos. O script imprime `cd
` na última linha — aplique-o (`cd
`) para a sessão sair do diretório destruído. > **Este comando roda SEM o marcador `TEQO_WORKTREE_TERMINAL`, então nunca recebe a diretiva `launch`** — só o `cd`. O launch do opencode (OPS26/OPS33+OPS93+OPS95, sem `--variant` — o TUI rejeita o flag) acontece apenas na função `worktree()` de `.agents/shell/worktree.sh` (terminal interativo); nunca abra um TUI aninhado a partir daqui. diff --git a/AGENTS.md b/AGENTS.md index 210389d2..0159c516 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,4 +72,4 @@ src/ ## Per-worktree environments -`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt` / `teqo_wt_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. The skill `local-database` covers the full workflow. +`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt` / `teqo_wt_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. Same provisioning for `plan [bag]`, `new [bag]` and `fix [bag]` worktrees (`plans/plan-issue-…`, `work/…`, `fix/…` namespaces — `fix` opens `/bug-fix` with the bug description delivered; no claim, no Issue). The skill `local-database` covers the full workflow. diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 977de8ee..13d4395b 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -38,6 +38,7 @@ main → (opcional, deliberado) workflow_dispatch deploy.yml → verify full sui | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief (GitHub) | | `pnpm worktree next [--issue N] [--stay]` | Claim determinístico (mesma fila/lock do claim) → worktree + provisionamento | +| `pnpm worktree fix [bug] [--stay]` | Worktree de correção de bug (`fix/`, skill `/bug-fix` — o launch envia a descrição do bug); não claima nem cria Issues | | `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue no GitHub (`--plan` ⇒ `blocked` até `agent:ready`; sem plano ⇒ `ready`) | | `pnpm agent:ready -- --issue N[,N…]` | Pós-merge do plano: `blocked`→`ready` (só Issues com link `docs/plans/`) | | `pnpm agent:status` | Overview / fila / mermaid | diff --git a/docs/changelog/2026-08-26-bug-fix-skill.md b/docs/changelog/2026-08-26-bug-fix-skill.md new file mode 100644 index 00000000..747b40d7 --- /dev/null +++ b/docs/changelog/2026-08-26-bug-fix-skill.md @@ -0,0 +1 @@ +**bug-fix (2026-08-26) — nova skill `/bug-fix` + subcomando `pnpm worktree fix` para correção de bugs de ponta a ponta com post-mortem:** a skill (`.agents/skills/bug-fix/SKILL.md` + `postmortem-template.md`, comando `.opencode/commands/bug-fix.md`) cobre o ciclo completo — reprodução → investigação de causa-raiz → fix (nunca sintoma) com teste de regressão → verificação (regressão + suíte + CI, e para bug de prod a confirmação do humano após o deploy manual) → análise de prevenção classificada em barata (implementar no mesmo PR) vs cara (documentada no post-mortem, vira Issue futura) → **post-mortem** em `docs/postmortems/-.md` + entrada de changelog. Token-optimized: 4 sub-agentes (investigador, verificador, estrategista de prevenção, escritor do post-mortem) fazem as fases pesadas com contexto mínimo — o agente principal só orquestra e implementa o fix. `pnpm worktree fix [bug] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba]` cria worktrees `fix/` ou `fix/` sequenciais (namespace minúsculo novo, nunca colide com `next`/`plans/`/`work/`) com o mesmo provisionamento isolado dos demais; no terminal, o launch envia `--prompt "/bug-fix "` — a descrição do bug chega com a skill (aspas/barra-invertida removidas do bag: o xargs da camada shell não honra escapes; `opencodeLaunchDirective` ganhou `argument`, exclusivo do purpose `fix`); nenhuma Issue é claimada nem criada — o registro do bug é o post-mortem. Sem migration/dados/access/UI; specs novos no `worktree.unit.spec.ts` (namespace `fix/`, launch com/sem bag, sanitização, isolamento do `argument`). diff --git a/scripts/lib/worktree.mjs b/scripts/lib/worktree.mjs index b48c5924..8a040315 100644 --- a/scripts/lib/worktree.mjs +++ b/scripts/lib/worktree.mjs @@ -77,7 +77,8 @@ export const resolveWorktreeModel = (flags = {}) => { * sends `/work-issue` (the OPS25 command executes the full cycle; the OPS33 * launch appends `--issue ` — the claimed issue — via * `opencodeLaunchDirective`); `plan` sends `/plan-issue` (OPS31 — auto-submit - * is the real need of the flow) and `new` sends nothing ("apenas conversar", + * is the real need of the flow), `fix` sends `/bug-fix` (with the bug + * description as the argument) and `new` sends nothing ("apenas conversar", * no skill at all). The opencode CLI's `--prompt` always auto-submits, there * is no prefill-without-submit flag; prefill stays an upstream-only feature * request (proposition annotated), not a Teqo fallback. @@ -86,6 +87,7 @@ export const OPENCODE_SKILL_COMMAND_BY_PURPOSE = { next: '/work-issue', plan: '/plan-issue', new: null, + fix: '/bug-fix', } /** @@ -95,6 +97,14 @@ export const OPENCODE_SKILL_COMMAND_BY_PURPOSE = { */ export const WORK_BRANCH_PREFIX = 'work' +/** + * Prefix of every bug-fix worktree branch (`pnpm worktree fix`). Lowercase-led + * `fix/…` — structurally disjoint from `next`'s uppercase-led `-`, + * from `plan`'s `plans/plan-issue-…` and from `new`'s `work/…`, in branch name + * and slot space alike. + */ +export const FIX_BRANCH_PREFIX = 'fix' + /** Total branch-name budget — mirrors `branchNameForIssue` (60). */ const NAMESPACE_BRANCH_MAX_LENGTH = 60 @@ -183,10 +193,11 @@ const namespaceBranchName = ({ prefix, bag = '', taken = new Set(), fallback }) * `/`, so the line never needs quoting * for the path. `next` with an `issueNumber` sends `/work-issue --issue ` * (OPS33: the launch delivers the claimed issue to the agent; the skill reads - * the rest from GitHub), the prompt value is quoted in the line because it - * now carries a space. Returns `null` outside the terminal so the `/worktree` - * opencode command never launches a nested TUI. - * @param {{ dir: string, purpose: string, terminal?: boolean, issueNumber?: number | null, model?: string | null }} options + * the rest from GitHub), `fix` with an `argument` sends `/bug-fix ` + * (the bug description arrives with the skill), the prompt value is quoted in + * the line because it now carries a space. Returns `null` outside the terminal + * so the `/worktree` opencode command never launches a nested TUI. + * @param {{ dir: string, purpose: string, terminal?: boolean, issueNumber?: number | null, model?: string | null, argument?: string | null }} options */ export const opencodeLaunchDirective = ({ dir, @@ -194,15 +205,22 @@ export const opencodeLaunchDirective = ({ terminal = false, issueNumber = null, model = null, + argument = null, }) => { if (!terminal) return null const prompt = OPENCODE_SKILL_COMMAND_BY_PURPOSE[purpose] const selectedModel = model ?? OPENCODE_PRESET_MODEL const args = [dir, '--model', selectedModel, '--auto'] if (prompt) { - // The issue suffix belongs to `next` alone — `plan`/`new` never carry a - // claimed issue (fail-safe: a stray issueNumber must not break /plan-issue). - const value = purpose === 'next' && issueNumber ? `${prompt} --issue ${issueNumber}` : prompt + // The issue suffix belongs to `next` alone — `plan`/`new`/`fix` never carry + // a claimed issue (fail-safe: a stray issueNumber must not break them). + const issueSuffix = purpose === 'next' && issueNumber ? `--issue ${issueNumber}` : null + // The bag suffix belongs to `fix` alone — the bug description arrives with + // the skill. Quotes/backslashes are stripped: the shell layer tokenizes the + // directive with xargs, which does not honor backslash escapes. + const sanitized = typeof argument === 'string' ? argument.replace(/["\\]/g, '').trim() : null + const bagSuffix = purpose === 'fix' && sanitized ? sanitized : null + const value = [prompt, issueSuffix, bagSuffix].filter(Boolean).join(' ') // JSON.stringify quotes the value — the directive carries spaces now. args.push('--prompt', JSON.stringify(value)) } @@ -222,3 +240,10 @@ export const planBranchName = ({ bag = '', taken = new Set() }) => */ export const workBranchName = ({ bag = '', taken = new Set() }) => namespaceBranchName({ prefix: `${WORK_BRANCH_PREFIX}/`, bag, taken, fallback: 'work' }) + +/** + * Branch for a bug-fix worktree (`pnpm worktree fix`) — see + * `namespaceBranchName` (namespace `fix/…`, fallback label `fix`). + */ +export const fixBranchName = ({ bag = '', taken = new Set() }) => + namespaceBranchName({ prefix: `${FIX_BRANCH_PREFIX}/`, bag, taken, fallback: 'fix' }) diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index 9d65509a..a0dfb423 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -95,6 +95,24 @@ * 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] + * cria um worktree de CORREÇÃO DE BUG (skill + * /bug-fix) DIFERENTE a cada invocação: com bag, + * branch `fix/` (e `-2`, `-3`, … se o nome + * já estiver vivo); sem bag, o próximo sequencial + * `fix/` livre. Prefixo minúsculo `fix/…` — + * nunca colide com `-` de `next`, + * com `plans/plan-issue-…` de `plan` nem com + * `work/…` de `new`. Mesmo provisionamento + * isolado do `next`/`plan`/`new`; no terminal, a + * diretiva `launch` envia `--prompt + * "/bug-fix "` — a descrição do bug chega ao + * agente junto com a skill (aspas/barra-invertida + * do bag são removidas — o xargs da camada shell + * não honra escapes); `--model ` quando a + * flag de modelo está presente; `--stay` suprime + * cd e launch (nenhuma Issue é claimada nem + * criada — o registro do bug é o post-mortem). * pnpm worktree kill [--force] destrói o worktree em que o shell atual está * (recusa worktree sujo sem `--force`) e remove os * bancos gerados do worktree (best-effort); por @@ -107,7 +125,7 @@ * Read-only no GitHub? NÃO — desde o OPS33 `next` CLAIMA: claim determinístico * antes do worktree (mesma fila/ordem e lock otimista do `pnpm agent:claim`; * `--issue N` claim direcionado ou reabre sessão já claimada). `plan`/`new`/ - * `kill` não tocam Issues. O claim do pool (supervisor coordenado) é intacto. + * `fix`/`kill` não tocam Issues. O claim do pool (supervisor coordenado) é intacto. * Dir raiz: `~/.cursor/worktrees/teqo/` (mesma casa dos worktrees do Cursor). */ @@ -150,6 +168,7 @@ import { } from './lib/worktree-env.mjs' import { branchNameForIssue, + fixBranchName, OPENCODE_PRESET_MODEL, opencodeLaunchDirective, planBranchName, @@ -181,7 +200,7 @@ const resolveLaunchModel = (flags) => { } /** Print the `launch` directive (only exists from the terminal shell); the `cd` line stays last. */ -const printLaunchDirective = ({ dir, purpose, issueNumber, flags = {} }) => { +const printLaunchDirective = ({ dir, purpose, issueNumber, argument = null, flags = {} }) => { const model = resolveLaunchModel(flags) const line = opencodeLaunchDirective({ dir, @@ -189,6 +208,7 @@ const printLaunchDirective = ({ dir, purpose, issueNumber, flags = {} }) => { terminal: terminalShell, issueNumber, model, + argument, }) if (line) console.log(line) } @@ -623,11 +643,13 @@ const buildTakenBranchNames = () => { } /** - * Shared runner for namespace worktrees NOT tied to the claim queue (`plan` - * and `new`): fetches origin, picks a FRESH branch in the namespace (via + * Shared runner for namespace worktrees NOT tied to the claim queue (`plan`, + * `new` and `fix`): fetches origin, picks a FRESH branch in the namespace (via * `branchName`), provisions the same isolated env as `next` (with `purpose`) * and prints the `cd ` line by default (`--stay` suppresses). Every * invocation creates a DIFFERENT worktree — parallel sessions never share one. + * `argument` (optional) rides the launch prompt — for `fix`, the bug + * description the `/bug-fix` skill receives. */ const cmdNamespaceBranch = async ({ stay, @@ -637,6 +659,7 @@ const cmdNamespaceBranch = async ({ sessionLabel, branchName, flags = {}, + argument = null, }) => { // Fail-high on conflicting model flags before touching git. resolveLaunchModel(flags) @@ -679,7 +702,7 @@ const cmdNamespaceBranch = async ({ console.log(` banco test: postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`) if (!stay) { - printLaunchDirective({ dir, purpose, flags }) + printLaunchDirective({ dir, purpose, argument, flags }) console.log(`cd ${dir}`) } } @@ -725,6 +748,28 @@ const cmdNew = async (stay, skipMigrate, bag, flags = {}) => flags, }) +/** + * `fix` — a bug-fix worktree for the `/bug-fix` skill. Same detached-namespace + * shape as `new`: with `bag` (the bug description), branch `fix/` + * (suffixed `-2`, `-3`, … on collision); without, the next free sequential + * `fix/`. The lowercase `fix/…` prefix can never collide with a `next` + * branch (uppercase-led `-`), nor with `plans/…` or `work/…`. The + * launch prompt carries the bag as the skill's argument — the bug description + * arrives with `/bug-fix` (sanitized in the directive builder). Never claims + * nor creates Issues: the bug record is the skill's post-mortem. + */ +const cmdFix = async (stay, skipMigrate, bag, flags = {}) => + cmdNamespaceBranch({ + stay, + skipMigrate, + purpose: 'fix', + noun: 'de correção de bug', + sessionLabel: bag && bag.trim() ? `bug "${bag}"` : 'sequencial', + branchName: (taken) => fixBranchName({ bag, taken }), + flags, + argument: bag, + }) + /** Generated database names referenced by a worktree's own env files. */ const worktreeDatabaseNamesOf = (dir) => { const names = [] @@ -811,7 +856,7 @@ const subcommand = positional[0] if (!subcommand) { console.log( - 'Uso: pnpm worktree next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | plan [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | kill [--force]', + 'Uso: pnpm worktree next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | plan [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | new [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | fix [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba] | kill [--force]', ) console.log(' next [--issue N] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba]') console.log(' CLAIMA a próxima Issue claimável (mesma fila/ordem e lock otimista do') @@ -858,6 +903,14 @@ if (!subcommand) { console.log( ' terminal, mesma diretiva `launch` porém sem --prompt (apenas conversar) e --model quando a flag está presente', ) + console.log(`\n fix [bag] [--stay] [--no-migrate] [--cheap|--pro|--zen|--go|--alibaba]`) + 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/ (sufixo -2/-3 se o') + console.log(' nome já existir); sem bag, o próximo fix/ sequencial livre; o prefixo') + console.log(' minúsculo fix/… nunca colide com `next`, `plan` nem `new`; não claima nem cria') + console.log( + ' Issues — o registro do bug é o post-mortem da skill; no terminal, a diretiva `launch` envia --prompt "/bug-fix " (a descrição chega com a skill) e --model quando a flag está presente', + ) console.log(' kill [--force] destrói o worktree em que você está (recusa sujo sem --force),') console.log(' remove os bancos gerados do worktree (best-effort) e imprime') console.log(' `cd
` no fim — o shell sempre volta ao worktree principal') @@ -877,10 +930,12 @@ try { await cmdPlan(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1], flags) else if (subcommand === 'new') await cmdNew(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1], flags) + else if (subcommand === 'fix') + await cmdFix(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1], flags) 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 | kill)`) + } else die(`subcomando desconhecido: ${subcommand} (esperado: next | plan | new | fix | kill)`) } catch (error) { if (error?.stderr) die(error.stderr.toString().trim()) die(error?.message ?? String(error)) diff --git a/tests/unit/opencodeCommands.unit.spec.ts b/tests/unit/opencodeCommands.unit.spec.ts index 010bd335..30a5ba76 100644 --- a/tests/unit/opencodeCommands.unit.spec.ts +++ b/tests/unit/opencodeCommands.unit.spec.ts @@ -10,7 +10,7 @@ import { describe, expect, it } from 'vitest' const repoRoot = process.cwd() -const commands = ['work-issue', 'plan-issue', 'testing-audit'] as const +const commands = ['work-issue', 'plan-issue', 'testing-audit', 'bug-fix'] as const describe('opencode commands couple to their skill by exact name', () => { it.each(commands)('command /%s references the existing skill of the same name', (name) => { diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index 794d9fc0..cab8a183 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -16,6 +16,8 @@ import { } from '../../scripts/lib/worktree-env.mjs' import { branchNameForIssue, + FIX_BRANCH_PREFIX, + fixBranchName, issueCodeAndSubject, OPENCODE_PRESET_MODEL, OPENCODE_SKILL_COMMAND_BY_PURPOSE, @@ -191,6 +193,55 @@ describe('opencodeLaunchDirective (terminal-only opencode launch, OPS26 + OPS33 ) }) + it('fix launches with /bug-fix sent and the bag as the argument (quoted value)', () => { + expect( + opencodeLaunchDirective({ + dir, + purpose: 'fix', + terminal: true, + argument: '500 no autosave de estimativas', + }), + ).toBe( + `launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/bug-fix 500 no autosave de estimativas"`, + ) + }) + + it('fix without a bag sends the bare /bug-fix', () => { + expect(opencodeLaunchDirective({ dir, purpose: 'fix', terminal: true })).toBe( + `launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/bug-fix"`, + ) + }) + + it('fix strips quotes/backslashes from the bag (xargs-safe) and drops it when it empties', () => { + expect( + opencodeLaunchDirective({ dir, purpose: 'fix', terminal: true, argument: 'a"b\\c' }), + ).toBe(`launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/bug-fix abc"`) + expect(opencodeLaunchDirective({ dir, purpose: 'fix', terminal: true, argument: ' "" ' })).toBe( + `launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/bug-fix"`, + ) + }) + + it('fix ignores the issueNumber — fix worktrees never carry a claimed issue', () => { + expect( + opencodeLaunchDirective({ + dir, + purpose: 'fix', + terminal: true, + issueNumber: 7, + argument: 'bug x', + }), + ).toBe(`launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/bug-fix bug x"`) + }) + + it('the argument belongs to fix alone — plan/new ignore it', () => { + expect( + opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true, argument: 'bag x' }), + ).toBe(`launch opencode ${dir} --model ${presetInEffect()} --auto --prompt "/plan-issue"`) + expect( + opencodeLaunchDirective({ dir, purpose: 'new', terminal: true, argument: 'bag x' }), + ).toBe(`launch opencode ${dir} --model ${presetInEffect()} --auto`) + }) + it('pins the preset constants — fallback comum deepseek-v4-flash, override via OPENCODE_WORKTREE_MODEL', () => { expect(OPENCODE_PRESET_MODEL).toBe(presetInEffect()) expect(WORKTREE_TERMINAL_ENV).toBe('TEQO_WORKTREE_TERMINAL') @@ -198,6 +249,7 @@ describe('opencodeLaunchDirective (terminal-only opencode launch, OPS26 + OPS33 next: '/work-issue', plan: '/plan-issue', new: null, + fix: '/bug-fix', }) }) @@ -362,6 +414,71 @@ describe('workBranchName (per-invocation neutral worktrees)', () => { }) }) +describe('fixBranchName (per-invocation bug-fix worktrees)', () => { + it('no bag → first free sequential fix/', () => { + expect(fixBranchName({})).toBe('fix/1') + expect(fixBranchName({ bag: '' })).toBe('fix/1') + expect(fixBranchName({ bag: ' ' })).toBe('fix/1') + }) + + it('no bag → skips taken sequential names (parallel sessions)', () => { + const taken = new Set(['fix/1', 'fix/2']) + expect(fixBranchName({ taken })).toBe('fix/3') + }) + + it('named bag → fix/ when free (the bug description slugs the branch)', () => { + expect(fixBranchName({ bag: '500 no autosave de estimativas' })).toBe( + 'fix/500-no-autosave-de-estimativas', + ) + }) + + it('named bag whose name is taken → suffixed -2, -3, …', () => { + const taken = new Set(['fix/bug']) + expect(fixBranchName({ bag: 'bug', taken })).toBe('fix/bug-2') + + taken.add('fix/bug-2') + expect(fixBranchName({ bag: 'bug', taken })).toBe('fix/bug-3') + }) + + it('each invocation returns a different branch for the same bag (live name)', () => { + const first = fixBranchName({ bag: 'teste' }) + expect(fixBranchName({ bag: 'teste', taken: new Set([first]) })).toBe('fix/teste-2') + }) + + it('never collides with a `next` branch nor with a `plan` nor with a `new` branch', () => { + const taken = new Set([ + 'C15-fullcalendar-em-campanha-agenda', + 'plans/plan-issue-agenda', + 'work/agenda', + ]) + expect(FIX_BRANCH_PREFIX).toBe('fix') + expect(`${FIX_BRANCH_PREFIX}/`).not.toMatch(/^[A-Z][A-Za-z0-9]*-/) + for (const bag of ['agenda', 'municipios', 'C15']) { + const branch = fixBranchName({ bag, taken }) + expect(branch).toMatch(/^fix\//) + expect(branch).not.toMatch(/^[A-Z][A-Za-z0-9]*-/) + expect(branch).not.toMatch(/^plans\//) + expect(branch).not.toMatch(/^work\//) + } + }) + + it('a numeric bag shares the sequential namespace (uniform)', () => { + expect(fixBranchName({ bag: '3' })).toBe('fix/3') + }) + + it('unslugifiable bag falls back to the namespace label', () => { + expect(fixBranchName({ bag: '!!!' })).toBe('fix/fix') + }) + + it('truncates long slugs within the branch budget', () => { + const branch = fixBranchName({ bag: 'palavra '.repeat(20).trim() }) + expect(branch.startsWith('fix/')).toBe(true) + expect(branch.length).toBeLessThanOrEqual(60) + // fix/ leaves a 56-char slug budget — 7 exact 8-char "palavra-" chunks. + expect(branch).toBe(`fix/${'palavra-'.repeat(7)}`) + }) +}) + describe('worktreeEnvironment (per-worktree ports and databases)', () => { it('derives slot, port and database names deterministically from the branch', () => { const env = worktreeEnvironment({ branch: 'C15-foo', code: 'C15' })