mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
feat(worktree): subcomando fix + skill /bug-fix (diagnóstico → fix → verificação → prevenção → post-mortem)
This commit is contained in:
parent
a9f0c61edf
commit
8dedf1028c
12 changed files with 428 additions and 20 deletions
|
|
@ -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}"
|
||||
|
|
|
|||
131
.agents/skills/bug-fix/SKILL.md
Normal file
131
.agents/skills/bug-fix/SKILL.md
Normal 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.
|
||||
58
.agents/skills/bug-fix/postmortem-template.md
Normal file
58
.agents/skills/bug-fix/postmortem-template.md
Normal 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.
|
||||
8
.opencode/commands/bug-fix.md
Normal file
8
.opencode/commands/bug-fix.md
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
1
docs/changelog/2026-08-26-bug-fix-skill.md
Normal file
1
docs/changelog/2026-08-26-bug-fix-skill.md
Normal 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`).
|
||||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
Loading…
Reference in a new issue