15 KiB
Impl: OPS119+ — launch do worktree: risk map das libs + pin do wiring do plan
Status: em execução Atualizado em: 2026-09-16 Issue: #1071 Intenção: docs/plans/escala-dry-pos-ops119.md Appetite restante: herdado (~1h eng) — F1 ~30min + F2 ~30min, sem corte
Leitura da intenção
- Outcome: fechar os dois achados de score 3 do
/simplifyda OPS119 na mesma superfície (launch do worktree ↔ agent-session). F1: um diff que toca sóscripts/lib/worktree.mjs,scripts/lib/agent-session.mjsouscripts/agent-session.mjshoje classificanone(a unit não roda e o static scope também énone) — o risk map deve forçarfull, fail-closed. F2: o repasseargument: bagdecmdPlan→cmdNamespaceBranchnão pode mais ser dropado em silêncio — o wiring vira estrutura pinada por unit. - O que NÃO negociar: o outcome é integridade de guard (blast radius fail-closed) e pin do wiring, não refactor de arquitetura; Impeccable A (CLI/shell, sem superfície de produto); appetite ~1h; S1/S2/S3 e o prefill upstream ficam fora; a alternativa rejeitada na intenção (prefixo
scripts/lib/inteiro emHIGH_RISK_EXACT) não pode voltar; migration/Consent/UI não se aplicam. - O que reavaliar: (1) “o F2 precisa tornar
cmdNamespaceBranchtestável” — não precisa: derivar o descriptor dentro do runner elimina o parâmetro dropável por construção, sem importarscripts/worktree.mjs(que hoje dispara o CLI no import —:950,:1043-1062); (2) “o descriptor deveria sanear o bag” — a sanitização já tem dois donos (opencodeLaunchDirective:228,purposeInvocation:198) e S2 é defer, então sanear de novo reintroduziria a duplicação; (3) “o descriptor pertence ascripts/lib/agent-session.mjs” — apesar deSESSION_COMMAND_BY_PURPOSEmorar lá,noun/sessionLabelsão vocabulário de branch/namespace do worktree (decisão abaixo).
Abordagem recomendada
flowchart LR
cmdPlan -->|purpose plan + bag| cmdNamespaceBranch
cmdNew -->|purpose new + bag| cmdNamespaceBranch
cmdFix -->|purpose fix + bag| cmdNamespaceBranch
cmdNamespaceBranch -->|deriva| namespaceLaunchDescriptor
namespaceLaunchDescriptor -->|noun, sessionLabel, argument| cmdNamespaceBranch
cmdNamespaceBranch --> opencodeLaunchDirective
cmdNamespaceBranch --> headlessDirective
worktree.unit.spec --> namespaceLaunchDescriptor
testAffected.unit.spec --> HIGH_RISK_EXACT
ciSkipInvariants.unit.spec --> HIGH_RISK_EXACT
Opções consideradas: A | B | C
Recomendação: A — extrair namespaceLaunchDescriptor({ purpose, bag }) para scripts/lib/worktree.mjs e fazer cmdNamespaceBranch derivar o descriptor internamente a partir de { purpose, bag }, removendo noun/sessionLabel/argument da assinatura; os 3 callers passam só purpose+bag (+ branchName, flags, headless, directivePath). Porque a classe do S4 (parâmetro dropado no caller) deixa de existir estruturalmente: não há argument para dropar. O pin unitário cai na lib pura que o spec já importa (tests/unit/worktree.unit.spec.ts:17-35), sem exigir guard de importabilidade em worktree.mjs.
Rejeitadas: B — tornar cmdNamespaceBranch injetável/importável e testar as opções com mocks: exigiria adicionar guard isMain a scripts/worktree.mjs e costurar seams de git/fs/provision que hoje são execFileSync/readFileSync diretos (:696-746) — refactor grande para ~0 teste novo além do descriptor; C — manter os parâmetros e exportar só o mapa: o pin pegaria o descriptor, mas o caller continuaria podendo dropar argument, ou seja, a classe do bug sobrevive; o teste que cria worktree de verdade segue rejeitado (lento, side effects, colide no slot, já descartado na intenção).
Componentes / mudanças
namespaceLaunchDescriptor(scripts/lib/worktree.mjs, novo): mapa puropurpose+bag→{ noun, sessionLabel, argument }, absorvendo os literais hoje inline nos 3 callers (nounpor purpose;sessionLabel=lote "…"/bag "…"/bug "…"com bag, senãosequencial;argument= bag paraplan/fix,nullparanew). Não reimplementa regra de branch nem sanitização — reusa a convenção já existente no módulo (prefixos enamespaceBranchNameficam intocados).cmdNamespaceBranch(scripts/worktree.mjs:679-747): removenoun/sessionLabel/argumentda assinatura, recebebag, e chamanamespaceLaunchDescriptor({ purpose, bag })uma vez; os usos em:709(erro),:716(sessão exibida),:737(headlessDirective({ report })) e:744(printLaunchDirective({ argument })) passam a ler o descriptor.cmdPlan/cmdNew/cmdFix(scripts/worktree.mjs:759-769,:780-789,:806-818): passampurpose+bag;branchName,flags,headless,directivePathseguem iguais.HIGH_RISK_EXACT(scripts/lib/test-affected-core.mjs:25-53): acrescentarscripts/agent-session.mjs,scripts/lib/agent-session.mjs,scripts/lib/worktree.mjs.- Pins de teste:
tests/unit/testAffected.unit.spec.ts(loop:22-35),tests/unit/ciSkipInvariants.unit.spec.ts(:100-116),tests/unit/worktree.unit.spec.ts(novodescribedo descriptor). - Migration: sem migration.
- Access / Consent: n/a.
- UI: Impeccable A — N/A.
Decisões de engenharia
- Dono do descriptor:
scripts/lib/worktree.mjsvsscripts/lib/agent-session.mjs. Opções: A)worktree.mjs; B)agent-session.mjs(junto deSESSION_COMMAND_BY_PURPOSE). Recomendação: A —noun/sessionLabelsão vocabulário de branch/namespace do worktree (mesmo módulo deplanBranchName/workBranchName/fixBranchNamee doopencodeLaunchDirectiveque consomeargument), e o spec que já importa o módulo (worktree.unit.spec.ts:17-35) pina sem import cruzado novo. Rejeitadas: B porque espalharia metade do contrato de launch (label de branch) para o dono do ciclo de vida de sessão, criando dependência invertida; o que é genuinamente compartilhado comagent-sessioné só o flag--argumentna borda CLI, que os dois specs já pinam de cada lado. - Bag no descriptor: cru vs saneado. Opções: A) cru (trim só para decidir o label); B) aplicar
replace(/["\\]/g,'')no descriptor. Recomendação: A — a sanitização é dos donos (opencodeLaunchDirective:228no caminho terminal,purposeInvocation:198no caminho do driver) e S2 é defer explícito. Rejeitadas: B porque criaria o terceiro call site da sanitização — exatamente o gatilho de S2 — e faria oreportdoheadlessDirectivedivergir do comportamento atual (que sanitiza só na montagem do argv). purposedesconhecido: fail-high vs fail-safe. Opções: A)namespaceLaunchDescriptorlança em purpose não mapeado; B) degradar para um descriptor neutro. Recomendação: A — os únicos callers sãoplan/new/fix; um purpose novo sem descriptor é erro de wiring e deve falhar alto (mesmo padrão deresolveWorktreeModel). Rejeitadas: B porque inventarianoun/label silenciosamente, que é a própria classe de bug que o pin fecha.- Corte do pin de F1: membership-only vs membership+existência em disco. Opções: A) só
HIGH_RISK_EXACT.has(path); B) membership +fsconfirmando que cada um dos 3 paths resolve. Recomendação: B, noitdedicado deciSkipInvariants— um literal high-risk morto (typo/rename) é exatamente a integridade que o S6 protege e não existe invariante de existência hoje. Rejeitadas: A deixa o valor do F1 degradar em silêncio; o invariante de existência repo-wide sobre toda a lista (que revelaria entradas antigas mortas) fica como follow-up nomeado, fora do appetite. - Cross-pin
purposeInvocation× descriptor (2ª perna). Opções: A) pinar os dois juntos num spec; B) manter os dois pins independentes. Recomendação: B — o contrato entre worktree e agent-session é o flag--argument, já pinado nas duas pontas (opencodeLaunchDirectivepor purpose epurposeInvocationpor argument); um teste combo só reafirmaria os dois. Rejeitadas: A por duplicação e por acoplar dois módulos puros num spec; gatilho de revisitação: se o flag mudar de transporte (env/stdin) ou se S2 consolidar a sanitização num único dono.
Fases verificáveis
- F1 — risk map (~30min). Adicionar os 3 arquivos a
HIGH_RISK_EXACT(scripts/lib/test-affected-core.mjs:25-53); acrescentar os 3 ao loop detests/unit/testAffected.unit.spec.ts:22-35; novoitemtests/unit/ciSkipInvariants.unit.spec.tscom nome de contrato (“launch/agent-session modules are high-risk — fail-closed blast radius”) pinando membership + presença em disco dos 3. Verificação:classifyTestScope([{ path: 'scripts/lib/worktree.mjs', status: 'M' }]).mode === 'full'para cada um dos 3 (coberto pelo loop) epnpm test:unitverde. - F2 — descriptor + wiring (~30min). Extrair
namespaceLaunchDescriptorparascripts/lib/worktree.mjs; fazercmdNamespaceBranchderivar internamente e removernoun/sessionLabel/argumentda assinatura; simplificar os 3 callers parapurpose+bag. Adicionardescribe('namespaceLaunchDescriptor …')emtests/unit/worktree.unit.spec.tscobrindo plan com/sem bag (argument threaded + labellote "…"vssequencial), new com/sem bag (argument semprenull), fix com/sem bag, e purpose desconhecido → throw. Verificação:pnpm test:unitverde etsc --noEmitlimpo (assinatura nova sem sobras). - Gates.
pnpm gate:fast(lint + typecheck + unit); push viapnpm push. Sem migration e sem e2e novo (Impeccable A, sem superfície de produto).
Rabbit holes / Não escopo (engenharia)
- Guard
isMainemscripts/worktree.mjs(padrão descripts/check-test-locations.mjs:68-70): só seria preciso na opção B; com o descriptor derivado internamente, não é necessário — se um diaworktree.mjsprecisar de teste direto, é Issue própria. - Prefixo
scripts/lib/inteiro emHIGH_RISK_EXACT: rejeitado na intenção (engoliria as ~40 libsscripts/lib/*.mjssem harness e forçariafullem diffs triviais). O resíduo “lib-only diff → unitnone” para as demais libs fica nomeado como follow-up, não implementado. - Invariante de existência em disco sobre a lista inteira de
HIGH_RISK_EXACT/HIGH_RISK_PREFIXES: escopo maior que os 3 alvos; follow-up. - S1 (fallback fail-safe de
purposeInvocation:202), S2 (sanitização duplicada, gatilho 3º call site), S3 (política driverless), prefill-sem-submit upstream: fora, conforme a intenção. - Cross-pin
purposeInvocation× descriptor: fora (decisão acima).
Adiado com gatilho (triage pós-simplify)
- Generalização do risk map para as demais
scripts/lib/*.mjsunit-pinadas (ex.:scripts/lib/worktree-env.mjs, pinada portests/unit/worktree.unit.spec.ts:5-16): a intenção só pediu os 3 arquivos de launch/agent-session e rejeitou o prefixoscripts/lib/inteiro. A classe remanescente (diff só-lib → unitnone) virou a IssueOPS119++(#1077,depends: [OPS119+]), planodocs/plans/escala-dry-pos-scripts-blast-radius.md. - 3ª codificação de “purpose carrega bag” (
entry.carriesBagemnamespaceLaunchDescriptor×opencodeLaunchDirective×purposeInvocation): defer. Gatilho: 4º encoding ou mudança de transporte do--argument(env/stdin) → consolidar membership+sanitização num dono único. classifyStaticScopeseguenoneparascripts/*.mjs(lint/typecheck/knip pulados num diff só-scripts): entrou como fase 2 da IssueOPS119++.
Explicitamente fora (triage pós-simplify)
- Bag em branco →
argumentcru no descriptor ×opencodeHeadlessArgslançando “report vazio”: comportamento pré-existente e intencional (fail-high do headless); sem reuso bloqueado. Descartado. - Drift cosmético do docblock/reason de
HIGH_RISK_EXACT(“schema/lockfile/test harness”): descartado (cheap_polish score ≤2, sem impacto funcional). nouncomo misnomer ('de planejamento'/'neutro'/'de correção de bug'são qualificadores): rename de pureza mantido por continuidade com o código original. Descartado.- Duplicação dos pins de membership entre
testAffectedeciSkipInvariants: já resolvido/aceito no plano (camadas distintas — classificaçãofull× membership+existência).
Riscos e mitigação
- O descriptor não cobre o caminho
next.cmdNextnão usacmdNamespaceBranch;namespaceLaunchDescriptorsó recebeplan/new/fix. Mitigação:purposedesconhecido lança (fail-high), então um futuro uso indevido falha alto em vez de gerar label errado. - Mudança de assinatura de
cmdNamespaceBranchdeixa parâmetro morto. Mitigação:tsc --noEmite a leitura dos 3 callers; nenhum outro caller existe (verificado — sócmdPlan/cmdNew/cmdFix). - Label/id divergirem do texto atual exibido. Mitigação: os casos do novo
describefixam os três labels e osequencialbyte a byte, contra os literais de:765/:786/:812. - Entrada high-risk morta (path renomeado). Mitigação: asserção de existência em disco no
itnovo deciSkipInvariantspara os 3 alvos. - F1 forçar
fullem diffs envolvendo essas libs aumenta custo de CI. Esperado e desejado: são o harness de launch/agent-session; fail-closed é o outcome.
Aceite de engenharia
- Aceite de produto da intenção coberto: diff só-lib de launch/agent-session classifica
full(F1) e o repasse do bag não pode mais ser dropado por parâmetro (F2). - Invariantes AGENTS/engineering-standards: identifiers em inglês, prosa pt-BR, sem comentários de código; nenhuma migration; nenhum acesso/Consent/UI tocado.
- Testes de domínio previstos:
tests/unit/testAffected.unit.spec.ts,tests/unit/ciSkipInvariants.unit.spec.ts,tests/unit/worktree.unit.spec.ts— todos unit, sem harness novo. pnpm gate:fastverde;pnpm pushno fluxo normal de PR.
Self-score decision-quality
- Decisões caras têm rejeitadas? 5/5 — dono do descriptor, cru-vs-saneado, fail-high-vs-neutro, corte do pin de F1 e cross-pin registraram Opções/Recomendação/Rejeitadas.
- Cabe no appetite (~1h)? 5/5 — F1 ~30min (3 literais + 2 pins) e F2 ~30min (extração pequena em lib pura + rewire de 3 callers + 1 describe); sem migration, sem e2e.
- Rabbit holes nomeados? 5/5 — guard
isMain, prefixoscripts/lib/inteiro, existência repo-wide, S1/S2/S3, cross-pin e prefill. - Depth check reusa shells/helpers? 4/5 — reusa o spec que já importa
scripts/lib/worktree.mjs, os padrões de pin deHIGH_RISK_EXACTe a convenção existente do módulo; não cria pass-through (o descriptor é dono real da derivação, não repassa nada). - Intenção (aceite de produto) intacta? 5/5 — a engenharia não reescreveu o outcome; o F2 foi feito mais forte que a opção A original (o parâmetro some), sem ampliar escopo.
Score: 24/25 — aprovado para execução.