Context
#13657 extracted the shared no-hold Stop-hook decision primitives into ai/scripts/lifecycle/stopHookDecision.mjs — now the cross-harness source-of-authority consumed by both the Claude (.claude/hooks/laneStateStopHook.mjs) and Codex (.codex/hooks/codex-lane-state-stop.mjs) hooks. My cross-family review of #13657 flagged this as a non-blocking follow-up.
The gap
The helper is the explicit anti-drift mechanism ("Claude and Codex cannot drift again") — yet it has no dedicated spec; it's covered only indirectly through the two hooks' specs. Its blockInjectionSupported:false + blockUnsupportedReason-suffix branches are exercised only via the Codex path. A future harness wiring the helper with blockInjectionSupported:false, or a refactor of either hook, could silently drift the shared layer with the hook-level tests still green. The shared primitive should be tested directly.
AC
- A dedicated
test/playwright/unit/hooks/stopHookDecision.spec.mjs exercises the three pure functions directly (no hook, no I/O):
parseOutcomeToVerdict: malformed / absent / valid / invalid-with-violations / invalid-no-violations.
isOperatorInLoop: stopHookActive forced-continuation, empty-prompt fail-closed, [WAKE] autonomous, genuine operator → true.
decideStopHookAction: operatorInLoop→allow; enforce+block-supported→block; enforce+block-unsupported(+suffix / bare)→would-block; dry-run→would-block; defaults→would-block.
- Green under
npm run test-unit.
Out of scope
- Any behavior change to the helper or either hook (this is test-only hardening).
Refs #13657, #13655.
Context
#13657 extracted the shared no-hold Stop-hook decision primitives into
ai/scripts/lifecycle/stopHookDecision.mjs— now the cross-harness source-of-authority consumed by both the Claude (.claude/hooks/laneStateStopHook.mjs) and Codex (.codex/hooks/codex-lane-state-stop.mjs) hooks. My cross-family review of #13657 flagged this as a non-blocking follow-up.The gap
The helper is the explicit anti-drift mechanism ("Claude and Codex cannot drift again") — yet it has no dedicated spec; it's covered only indirectly through the two hooks' specs. Its
blockInjectionSupported:false+blockUnsupportedReason-suffix branches are exercised only via the Codex path. A future harness wiring the helper withblockInjectionSupported:false, or a refactor of either hook, could silently drift the shared layer with the hook-level tests still green. The shared primitive should be tested directly.AC
test/playwright/unit/hooks/stopHookDecision.spec.mjsexercises the three pure functions directly (no hook, no I/O):parseOutcomeToVerdict: malformed / absent / valid / invalid-with-violations / invalid-no-violations.isOperatorInLoop:stopHookActiveforced-continuation, empty-prompt fail-closed,[WAKE]autonomous, genuine operator → true.decideStopHookAction:operatorInLoop→allow; enforce+block-supported→block; enforce+block-unsupported(+suffix / bare)→would-block; dry-run→would-block; defaults→would-block.npm run test-unit.Out of scope
Refs #13657, #13655.