feat(worktree): subcomando fix + skill /bug-fix (diagnóstico → fix → verificação → prevenção → post-mortem)

This commit is contained in:
Francisco Solla 2026-08-26 12:35:14 -03:00
parent a9f0c61edf
commit 8dedf1028c
12 changed files with 428 additions and 20 deletions

View file

@ -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 <N>` 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 <bag>` 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/<n> 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/<bag> (sufixo -2/-3 se o nome já existir),
# sem bag o próximo fix/<n> sequencial; o launch envia
# `/bug-fix <bag>` (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}"

View file

@ -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/<slug>` 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/<data>-<slug>.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/<slug>)
- [ ] 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/<slug>` 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/<slug>` 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/<data>-<slug>.md` conforme `postmortem-template.md`.
2. Entrada curta em `docs/changelog/<data>-<slug>.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.

View file

@ -0,0 +1,58 @@
# Post-mortem: <título curto do bug>
> 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: `<nome do teste>` — falha sem o fix, passa com
- Suíte: `<resultado>` (`pnpm gate:fast` / `test:int` / e2e afetado)
- CI: verde no PR #N
- Prod: `<confirmação do humano, data/hora>` 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.

View file

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

View file

@ -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 <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` 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-<bag>`, ou `plans/plan-issue-<n>` sequencial sem bag; sufixo `-2`/`-3` em colisão) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. Não claima nada. Mesmo contrato de `cd` do `next`: aplique a última linha `cd <dir>` por padrão; `--stay` suprime; flag de modelo como no `next`.
- `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/<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]`: 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 envia `--prompt "/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á 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.
> **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.

View file

@ -72,4 +72,4 @@ src/
## Per-worktree environments
`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt<slot>` / `teqo_wt<slot>_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. The skill `local-database` covers the full workflow.
`pnpm worktree next` provisions isolated envs per worktree: dev port `3100+slot`, databases `teqo_wt<slot>` / `teqo_wt<slot>_test` on the shared container with full migration chain + minimal seed, `.env.local` + `.env.test.local` (gitignored), copies of `PAYLOAD_SECRET`/`S3_*`/VAPID from main repo. Same provisioning for `plan [bag]`, `new [bag]` and `fix [bag]` worktrees (`plans/plan-issue-…`, `work/…`, `fix/…` namespaces — `fix` opens `/bug-fix` with the bug description delivered; no claim, no Issue). The skill `local-database` covers the full workflow.

View file

@ -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/<slug>`, 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 |

View file

@ -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/<data>-<slug>.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/<bag>` ou `fix/<n>` 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 <bag>"` — 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`).

View file

@ -77,7 +77,8 @@ export const resolveWorktreeModel = (flags = {}) => {
* sends `/work-issue` (the OPS25 command executes the full cycle; the OPS33
* launch appends `--issue <N>` — 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 `<Code>-<slug>`,
* 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 })
* `<root without spaces>/<slugified branch>`, so the line never needs quoting
* for the path. `next` with an `issueNumber` sends `/work-issue --issue <N>`
* (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 <argument>`
* (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' })

View file

@ -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/<bag>` (e `-2`, `-3`, … se o nome
* já estiver vivo); sem bag, o próximo sequencial
* `fix/<n>` livre. Prefixo minúsculo `fix/…` —
* nunca colide com `<code>-<slug>` 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 <bag>"` — 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 <map>` 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 <dir>` line by default (`--stay` suppresses). Every
* invocation creates a DIFFERENT worktree — parallel sessions never share one.
* `argument` (optional) rides the launch prompt — for `fix`, the bug
* description the `/bug-fix` skill receives.
*/
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/<bag>`
* (suffixed `-2`, `-3`, … on collision); without, the next free sequential
* `fix/<n>`. The lowercase `fix/…` prefix can never collide with a `next`
* branch (uppercase-led `<code>-<slug>`), 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 <map> 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/<bag> (sufixo -2/-3 se o')
console.log(' nome já existir); sem bag, o próximo fix/<n> 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 <bag>" (a descrição chega com a skill) e --model <map> 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 <main>` 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))

View file

@ -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) => {

View file

@ -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/<n>', () => {
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/<slug> 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' })