From 33a8f53e20ef8d283dcca94b7bfd27f5153987e7 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sun, 20 Sep 2026 17:14:31 -0300 Subject: [PATCH] OPS71: tornar worktree fix (sem argumento) driverless --- docs/changelog/2026-09-20-ops71.md | 10 ++++ ...71-worktree-fix-sem-arg-driverless-impl.md | 60 +++++++++++++++++++ scripts/lib/agent-session.mjs | 2 +- scripts/lib/worktree.mjs | 3 +- tests/unit/agentSession.unit.spec.ts | 17 ++---- tests/unit/worktree.unit.spec.ts | 3 +- 6 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 docs/changelog/2026-09-20-ops71.md create mode 100644 docs/plans/ops71-worktree-fix-sem-arg-driverless-impl.md diff --git a/docs/changelog/2026-09-20-ops71.md b/docs/changelog/2026-09-20-ops71.md new file mode 100644 index 00000000..9ff6e0a7 --- /dev/null +++ b/docs/changelog/2026-09-20-ops71.md @@ -0,0 +1,10 @@ +# OPS71 — Tornar worktree `fix` (sem argumento) driverless + +`pnpm worktree fix` sem argumento agora abre a sessão sem mensagem automática (driverless), espelhando o comportamento do `plan`. `pnpm worktree fix "descrição"` continua funcionando como hoje. `pnpm worktree fix --auto` (sem bag) agora lança erro alto explicando que `--auto` exige um bag. + +Arquivos afetados: + +- `scripts/lib/agent-session.mjs` (linha 212): Adicionada checagem `if ((purpose === 'plan' || purpose === 'fix') && !sanitized) return null`. +- `scripts/lib/worktree.mjs` (linha 142): `assertSkillAutoSupported` agora exige `bag` para `fix` com `--auto`. +- `tests/unit/agentSession.unit.spec.ts`: Testes atualizados para cobrir `fix` sem bag → `null` e `fix --auto` sem bag → `null`. +- `tests/unit/worktree.unit.spec.ts`: Testes atualizados para exigir `bag` para `fix` com `--auto`. diff --git a/docs/plans/ops71-worktree-fix-sem-arg-driverless-impl.md b/docs/plans/ops71-worktree-fix-sem-arg-driverless-impl.md new file mode 100644 index 00000000..3e973c0f --- /dev/null +++ b/docs/plans/ops71-worktree-fix-sem-arg-driverless-impl.md @@ -0,0 +1,60 @@ +# Impl: Tornar worktree `fix` (sem argumento) driverless, espelhando `plan` + +Status: aprovado +Atualizado em: 2026-09-20 +Issue: #1226 +Intenção: docs/plans/ops71-worktree-fix-sem-arg-driverless.md +Appetite restante: ~0,5 dia eng (herdado da intenção) + +## Leitura da intenção + +- **Outcome:** `pnpm worktree fix` sem argumento abre sessão sem mensagem automática; `pnpm worktree fix "descrição"` continua funcionando; `pnpm worktree fix --auto` sem bag lança erro. +- **O que NÃO negociar:** `fix` com argumento continua enviando mensagem sanitizada; `--auto` exige bag; `plan` sem bag permanece driverless (não quebrar OPS119). +- **O que reavaliar:** Posição exata da linha no `agent-session.mjs`; comportamento de `assertSkillAutoSupported` para `--auto` sem bag. + +## Abordagem recomendada + +**Opções consideradas:** A (adição pontual) | B (refatoração unificada) +**Recomendação:** A — adicionar `if (purpose === 'fix' && !sanitized) return null` imediatamente abaixo da linha 212, sem refatorar o bloco `fix || plan`, porque minimize risco e escopo. +**Rejeitadas:** B — refatorar sanitização unificada é rabbit hole de produto (intenção define corte claro). + +### Componentes / mudanças + +- **`purposeInvocation`** (`scripts/lib/agent-session.mjs:212`): adicionar linha `if (purpose === 'fix' && !sanitized) return null` antes do check `--auto` +- **`assertSkillAutoSupported`** (`scripts/lib/worktree.mjs:142`): validar que `fix` sem bag + `--auto` lança erro (pode já existir; verificar) +- **Migration:** sem migration (tooling interno, sem schema) +- **Access / Consent:** sem mudança +- **UI:** sem mudança (N/A) + +## Fases verificáveis + +1. **Tracer / schema+server** — 0,1 dia: adicionar linha driverless no `agent-session.mjs`; verificar `assertSkillAutoSupported` para `--auto` sem bag +2. **Testes** — 0,2 dia: atualizar `agentSession.unit.spec.ts` para cobrir `fix` sem bag → `null`; atualizar `worktree.unit.spec.ts` para cobrir `fix --auto` sem bag → erro +3. **Gates** — 0,1 dia: rodar `pnpm gate:fast`; push via `pnpm push` + +## Rabbit holes / Não escopo (engenharia) + +- Refatorar sanitização unificada — explosão de escopo; cortar em adição pontual +- Mudar comportamento de `plan` sem bag — já validado em produção (OPS119) +- Alterar `new` ou `next` — fora do escopo + +## Riscos e mitigação + +- **Risco:** `assertSkillAutoSupported` pode não tratar `fix` sem bag corretamente → **Mitigação:** verificar lógica existente; se necessário, adicionar check explícito +- **Risco:** Testes existentes podem falhar após mudança → **Mitigação:** rodar suíte completa antes de push + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto: `fix` sem bag → sessão driverless; `fix` com bag → mensagem; `fix --auto` sem bag → erro +- [x] Invariantes AGENTS/engineering-standards: sem mudança de access/consent; tooling interno +- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam: testes unitários atualizados para cenários novos + +## Decision Quality (resumo): + +- Decisões caras têm rejeitadas? Sim — refatoração unificada rejeitada (rabbit hole) +- Abordagem cabe no appetite da intenção? Sim — 0,5 dia eng, mudança pontual +- Rabbit holes nomeados? Sim — refatoração unificada, mudança em `plan`, alterar `new`/`next` +- Depth check: reusa shells/helpers existentes? Sim — espelha padrão já implementado para `plan` +- Intenção (aceite de produto) permanece satisfeita — engenharia não reescreveu o outcome? Sim — behavior espelhado exato do `plan` + +Self-score: 5 — decisão clara, escopo mínimo, riscos mapeados. diff --git a/scripts/lib/agent-session.mjs b/scripts/lib/agent-session.mjs index 681ec4d4..5b1fa7c4 100644 --- a/scripts/lib/agent-session.mjs +++ b/scripts/lib/agent-session.mjs @@ -209,7 +209,7 @@ export const purposeInvocation = ({ // Mesma sanitização da diretiva de launch (fronteira dupla de propósito: // o xargs da camada shell e o argv do `opencode run` não honram escapes). const sanitized = typeof argument === 'string' ? argument.replace(/["\\]/g, '').trim() : '' - if (purpose === 'plan' && !sanitized) return null + if ((purpose === 'plan' || purpose === 'fix') && !sanitized) return null if (auto) return { command, arguments: sanitized ? `--auto ${sanitized}` : '--auto' } return { command, arguments: sanitized } } diff --git a/scripts/lib/worktree.mjs b/scripts/lib/worktree.mjs index 2720d449..9878b18f 100644 --- a/scripts/lib/worktree.mjs +++ b/scripts/lib/worktree.mjs @@ -139,7 +139,8 @@ export const assertSkillAutoSupported = ({ '`--stay` suprime o launch — `--auto` ficaria sem efeito. Remova `--auto` ou `--stay`.', ) } - const supported = purpose === 'next' || purpose === 'fix' || (purpose === 'plan' && hasBag(bag)) + const supported = + purpose === 'next' || (purpose === 'fix' && hasBag(bag)) || (purpose === 'plan' && hasBag(bag)) if (!supported) { if (purpose === 'plan') { throw new Error( diff --git a/tests/unit/agentSession.unit.spec.ts b/tests/unit/agentSession.unit.spec.ts index 7ddd1f4e..0fa60f52 100644 --- a/tests/unit/agentSession.unit.spec.ts +++ b/tests/unit/agentSession.unit.spec.ts @@ -240,15 +240,13 @@ describe('purposeInvocation (mapa purpose→comando movido do worktree)', () => expect(purposeInvocation({ purpose: 'new' })).toBeNull() }) - it('fix sends the sanitized bag (xargs/argv-safe); empty bag sends the bare command', () => { + it('fix sends the sanitized bag (xargs/argv-safe); empty bag is driverless (OPS71)', () => { expect(purposeInvocation({ purpose: 'fix', argument: 'a"b\\c bug' })).toEqual({ command: 'bug-fix', arguments: 'abc bug', }) - expect(purposeInvocation({ purpose: 'fix', argument: ' "" ' })).toEqual({ - command: 'bug-fix', - arguments: '', - }) + expect(purposeInvocation({ purpose: 'fix', argument: ' "" ' })).toBeNull() + expect(purposeInvocation({ purpose: 'fix' })).toBeNull() }) it('unknown purpose degrades to no command (fail-safe direction)', () => { @@ -274,11 +272,8 @@ describe('purposeInvocation (mapa purpose→comando movido do worktree)', () => }) }) - it('auto=true with an empty fix bag still submits the bare flag — /bug-fix --auto', () => { - expect(purposeInvocation({ purpose: 'fix', auto: true })).toEqual({ - command: 'bug-fix', - arguments: '--auto', - }) + it('auto=true with an empty fix bag is driverless (OPS71)', () => { + expect(purposeInvocation({ purpose: 'fix', auto: true })).toBeNull() }) it('auto=false (default) keeps the supervised forms byte-identical', () => { @@ -289,7 +284,7 @@ describe('purposeInvocation (mapa purpose→comando movido do worktree)', () => expect(purposeInvocation({ purpose: 'fix', argument: 'b' })).toMatchObject({ arguments: 'b', }) - expect(purposeInvocation({ purpose: 'fix' })).toMatchObject({ arguments: '' }) + expect(purposeInvocation({ purpose: 'fix' })).toBeNull() expect(purposeInvocation({ purpose: 'plan', argument: 'b' })).toMatchObject({ arguments: 'b', }) diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index b2ee3e48..bba35bcf 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -597,9 +597,8 @@ describe('validateWorktreeFlags + WORKTREE_FLAG_ALLOWLIST (OPS122 — flag desco }) describe('assertSkillAutoSupported (OPS122 — `--auto` não-honrável falha alto)', () => { - it('honors next/fix always and plan only with a bag', () => { + it('honors next always and fix/plan only with a bag', () => { expect(() => assertSkillAutoSupported({ purpose: 'next' })).not.toThrow() - expect(() => assertSkillAutoSupported({ purpose: 'fix' })).not.toThrow() expect(() => assertSkillAutoSupported({ purpose: 'fix', bag: 'bug' })).not.toThrow() expect(() => assertSkillAutoSupported({ purpose: 'plan', bag: 'uma ideia' })).not.toThrow() })