Contexto
Durante o release de v4.1.0-rc.1 (resolve NI-3/NI-4/NI-5 — ver CHANGELOG), três items foram identificados pelo reviewer adversarial como non-blocking mas reais e diferidos para v4.2.
Cada um é endereçável independentemente. Recomendo executar na ordem listada abaixo (menor → maior complexidade).
Item 1 — Pruning de gate-decisions.jsonl
Sintoma: gate-decisions.jsonl cresce monotonicamente ao longo da vida de um projeto. Toda invocação de pipeline adiciona entradas (EXEC_WINDOW_OPEN, EXEC_WINDOW_CLOSE, outros gates). Não há política de limpeza.
Impacto:
- O(N) scan linear em
findPairingEntry (.claude/hooks/edit-guard-hook.cjs) a cada Edit/Write hook event.
- Em projetos com 100+ pipeline runs, a latência passa a ser perceptível.
- Amplifica o risco de stale-OPEN reuse (apesar da mitigação intermediate-CLOSE do v4.1 — a janela de +60s mais ainda existe).
Proposta:
- Rotação por tamanho (ex: quando
.jsonl > 10MB, mover para gate-decisions.{YYYY-MM}.archive.jsonl e iniciar novo).
- OU truncation por idade (entradas >30 dias vão para archive).
findPairingEntry lê apenas o arquivo ativo (não o archive) — reduz scan mas mantém forensics.
Tests: adicionar contract test validando que pairing ainda funciona após rotação; e benchmark smoke que mede latência antes/depois.
Files: .claude/hooks/edit-guard-hook.cjs, novo hook Stop que dispara pruning (ou cron), tests em __tests__/.
Item 2 — buildBlockMessage com hint de pairing
Sintoma: quando getActiveExecWindow rejeita uma janela por ausência de pairing (ou qualquer motivo silencioso), o edit-guard-hook retorna o block message genérico. Controllers usando o path raw Write (não os helpers openExecWindow) não sabem que o motivo foi pairing — têm que inspecionar stderr.
Impacto: UX pobre para debugging. Hook é silencioso sobre o motivo real.
Proposta:
getActiveExecWindow retorna tupla {window, rejection_reason?} em vez de apenas window | null.
shouldBlock usa rejection_reason para enriquecer buildBlockMessage.
- Exemplo de mensagem enriquecida:
"Pipeline session is active. Your exec-window was found BUT rejected: missing paired EXEC_WINDOW_OPEN audit entry. If using raw Write path, also append an audit line to .pipeline/docs/Pre-*/*/gate-decisions.jsonl. Or call openExecWindow() helper which handles both."
Tests: adicionar tests por cada rejection reason verificando que a mensagem contém a hint específica.
Files: .claude/hooks/edit-guard-hook.cjs, tests em __tests__/edit-guard-hook.test.cjs.
Item 3 — Bloquear NaN/Infinity no opts.ttl_minutes explicitamente
Sintoma: hoje:
- Read side:
getActiveExecWindow calcula declaredTtl = expires_at - opened_at. Se qualquer um for NaN, o resultado é NaN, que não é > 0 nem <= 60min — rejeitado por acidente via short-circuit. Funciona mas é fragilidade de linguagem.
- Write side (helper):
openExecWindow hoje valida Number.isFinite(ttlMinutes) && ttlMinutes > 0 && ttlMinutes <= 60. Já rejeita NaN explicitamente.
- Write side (raw Write controller): nenhum check — um controller pode escrever
.exec-window com expires_at: NaN. Read side rejeita silentemente.
Impacto: defense-in-depth ausente no read side. Inconsistência entre helper (strict) e raw-Write (leniente-por-acidente).
Proposta:
- Adicionar
Number.isFinite() check explícito em getActiveExecWindow para opened_at e expires_at antes de calcular declaredTtl.
- Stderr log se detectado:
edit-guard: skipping window with non-finite opened_at/expires_at.
- Test que escreve window com
opened_at: NaN e valida rejeição.
Files: .claude/hooks/edit-guard-hook.cjs (~5 linhas), 1 novo test.
Estimativa de esforço
- Item 1 (pruning): MÉDIA — precisa definir política (tamanho vs idade), implementar rotação, testar migração. ~2-3h.
- Item 2 (hint message): SIMPLES — refactor de 1 função + enriquecer testes. ~30min.
- Item 3 (NaN finite check): TRIVIAL — 5 linhas + 1 test. ~10min.
Sugiro fazer Item 3 primeiro (cheap win), depois Item 2, depois Item 1 (maior design decision).
Critérios de aceite para v4.2.0-rc.1
Referência: v4.1.0-rc.1 CHANGELOG seção "Known limitations remaining (deferred to v4.2)".
Contexto
Durante o release de v4.1.0-rc.1 (resolve NI-3/NI-4/NI-5 — ver CHANGELOG), três items foram identificados pelo reviewer adversarial como non-blocking mas reais e diferidos para v4.2.
Cada um é endereçável independentemente. Recomendo executar na ordem listada abaixo (menor → maior complexidade).
Item 1 — Pruning de
gate-decisions.jsonlSintoma:
gate-decisions.jsonlcresce monotonicamente ao longo da vida de um projeto. Toda invocação de pipeline adiciona entradas (EXEC_WINDOW_OPEN,EXEC_WINDOW_CLOSE, outros gates). Não há política de limpeza.Impacto:
findPairingEntry(.claude/hooks/edit-guard-hook.cjs) a cada Edit/Write hook event.Proposta:
.jsonl> 10MB, mover paragate-decisions.{YYYY-MM}.archive.jsonle iniciar novo).findPairingEntrylê apenas o arquivo ativo (não o archive) — reduz scan mas mantém forensics.Tests: adicionar contract test validando que pairing ainda funciona após rotação; e benchmark smoke que mede latência antes/depois.
Files:
.claude/hooks/edit-guard-hook.cjs, novo hook Stop que dispara pruning (ou cron), tests em__tests__/.Item 2 —
buildBlockMessagecom hint de pairingSintoma: quando
getActiveExecWindowrejeita uma janela por ausência de pairing (ou qualquer motivo silencioso), oedit-guard-hookretorna o block message genérico. Controllers usando o path raw Write (não os helpersopenExecWindow) não sabem que o motivo foi pairing — têm que inspecionar stderr.Impacto: UX pobre para debugging. Hook é silencioso sobre o motivo real.
Proposta:
getActiveExecWindowretorna tupla{window, rejection_reason?}em vez de apenaswindow | null.shouldBlockusarejection_reasonpara enriquecerbuildBlockMessage.Tests: adicionar tests por cada rejection reason verificando que a mensagem contém a hint específica.
Files:
.claude/hooks/edit-guard-hook.cjs, tests em__tests__/edit-guard-hook.test.cjs.Item 3 — Bloquear NaN/Infinity no
opts.ttl_minutesexplicitamenteSintoma: hoje:
getActiveExecWindowcalculadeclaredTtl = expires_at - opened_at. Se qualquer um for NaN, o resultado é NaN, que não é> 0nem<= 60min— rejeitado por acidente via short-circuit. Funciona mas é fragilidade de linguagem.openExecWindowhoje validaNumber.isFinite(ttlMinutes) && ttlMinutes > 0 && ttlMinutes <= 60. Já rejeita NaN explicitamente..exec-windowcomexpires_at: NaN. Read side rejeita silentemente.Impacto: defense-in-depth ausente no read side. Inconsistência entre helper (strict) e raw-Write (leniente-por-acidente).
Proposta:
Number.isFinite()check explícito emgetActiveExecWindowparaopened_ateexpires_atantes de calculardeclaredTtl.edit-guard: skipping window with non-finite opened_at/expires_at.opened_at: NaNe valida rejeição.Files:
.claude/hooks/edit-guard-hook.cjs(~5 linhas), 1 novo test.Estimativa de esforço
Sugiro fazer Item 3 primeiro (cheap win), depois Item 2, depois Item 1 (maior design decision).
Critérios de aceite para v4.2.0-rc.1
v4.2.0-rc.1+ push +claude plugin updateverificaReferência: v4.1.0-rc.1 CHANGELOG seção "Known limitations remaining (deferred to v4.2)".