Context
Release classification: post-release / boardless (Approve+Follow-Up follow-up — non-blocking).
PR #13726 delivered the deference-register Stop-hook parity slice for #13674 and merged on 2026-06-21. The merged implementation correctly shares the phrase matcher in ai/scripts/lifecycle/deferencePhraseMatch.mjs, but the hook-level decision logic still exists in two hook-local shapes: Claude checks and blocks in .claude/hooks/laneStateStopHook.mjs, while Codex returns an equivalent action object in .codex/hooks/codex-lane-state-stop.mjs.
Operator review flagged the architectural problem: this creates an alternative reality for the same business logic. We want the same autonomous-deference decision in both hooks, not two implementations kept in sync by discipline.
Live latest-open sweep: checked the latest 20 open issues on 2026-06-21; found #13740 (Stop-hook hold-costume tripwire) but no equivalent ticket for extracting the #13726 deference decision into a shared hook decision surface. Exact live search for deferencePhraseMatch / stopHookDecision found #13740, #13679, #13731, and #13623; none covers this shared-decision extraction. A2A recency sweep: latest 30 messages showed no competing claim for this scope.
The Problem
#13726 fixed immediate parity, but parity-by-copying is not enough for hook business logic. The system now has a shared matcher and two local decision pipelines that both know:
- deference phrases only matter on autonomous turn terminals;
operatorInLoop is the carve-out;
- enforcing hooks should block;
- dry-run hooks should would-block;
- the injected reason should be the same peer-identity reminder.
Those rules are business logic, not transport glue. If future edits adjust one hook first, Claude and Codex can diverge again while tests still pass locally inside each hook family.
The Architectural Reality
ai/scripts/lifecycle/stopHookDecision.mjs already owns shared Stop-hook decision semantics such as isOperatorInLoop() and decideStopHookAction().
ai/scripts/lifecycle/deferencePhraseMatch.mjs now owns phrase matching and reminder text after PR #13726.
.claude/hooks/laneStateStopHook.mjs and .codex/hooks/codex-lane-state-stop.mjs must remain harness adapters: payload extraction, transcript plumbing, audit logging, and stdout/return-shape transport.
- Tests already exist for the shared decision module (
test/playwright/unit/hooks/stopHookDecision.spec.mjs) and for each hook adapter.
The Fix
Extract the #13726 deference-register decision into one shared lifecycle decision primitive, most likely in ai/scripts/lifecycle/stopHookDecision.mjs or a narrowly named sibling consumed by that module. Both hook adapters should call that primitive and then adapt the returned decision to their transport shape.
The shared primitive should own:
- operator-loop carve handling;
- deference phrase detection invocation;
- dry-run versus enforce outcome (
would-block / block);
- shared reminder reason construction;
- a null/no-op result when no autonomous deference phrase is present.
Hook-local code should own only adapter-specific extraction and output formatting.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
| Shared deference Stop-hook decision |
ai/scripts/lifecycle/stopHookDecision.mjs + ai/scripts/lifecycle/deferencePhraseMatch.mjs |
One pure helper returns the deference decision for Claude and Codex. |
No matching phrase returns null/no-op so lane-state parsing continues unchanged. |
JSDoc in the shared helper |
Unit tests in stopHookDecision.spec.mjs cover operator carve, enforce block, dry-run would-block, and no-match. |
| Claude Stop hook adapter |
.claude/hooks/laneStateStopHook.mjs |
Claude calls the shared helper and only formats audit/stdout block output. |
Existing lane-state terminal path remains unchanged when helper returns no decision. |
Hook module exports |
Existing Claude hook tests stay green and assert shared behavior through adapter path. |
| Codex Stop hook adapter |
.codex/hooks/codex-lane-state-stop.mjs |
Codex calls the same shared helper and only adapts the result shape. |
Existing Codex lane-state classification remains unchanged when helper returns no decision. |
Hook module exports |
Existing Codex hook tests stay green and assert shared behavior through adapter path. |
Decision Record impact
Aligned with #13623, #13674, and the merged #13726 hook parity work. No ADR change required.
Acceptance Criteria
Out of Scope
- Adding new deference or hold-costume phrases.
- Changing the reminder prose beyond what the shared extraction requires.
- Reworking lane-state parsing or validation.
- Reopening #13674 or changing #13726 merge eligibility.
Avoided Traps
- Do not duplicate the same conditional in both hook files again.
- Do not move hook transport details into shared lifecycle logic; shared code returns decisions, adapters handle IO.
- Do not conflate this with #13740. That ticket is about detecting another lexicon; this one is about preventing duplicated business logic for the already-merged deference register.
Related
Related: #13623
Related: #13674
Related: #13740
Related: PR #13726
Origin Session ID: 747ae298-5a6e-4416-b90d-7786e184aa54
Retrieval Hint: #13726 deferencePhraseMatch stopHookDecision shared deference Stop-hook decision Codex Claude parity
Context
Release classification: post-release / boardless (Approve+Follow-Up follow-up — non-blocking).
PR #13726 delivered the deference-register Stop-hook parity slice for #13674 and merged on 2026-06-21. The merged implementation correctly shares the phrase matcher in
ai/scripts/lifecycle/deferencePhraseMatch.mjs, but the hook-level decision logic still exists in two hook-local shapes: Claude checks and blocks in.claude/hooks/laneStateStopHook.mjs, while Codex returns an equivalentactionobject in.codex/hooks/codex-lane-state-stop.mjs.Operator review flagged the architectural problem: this creates an alternative reality for the same business logic. We want the same autonomous-deference decision in both hooks, not two implementations kept in sync by discipline.
Live latest-open sweep: checked the latest 20 open issues on 2026-06-21; found #13740 (
Stop-hook hold-costume tripwire) but no equivalent ticket for extracting the #13726 deference decision into a shared hook decision surface. Exact live search fordeferencePhraseMatch/stopHookDecisionfound #13740, #13679, #13731, and #13623; none covers this shared-decision extraction. A2A recency sweep: latest 30 messages showed no competing claim for this scope.The Problem
#13726fixed immediate parity, but parity-by-copying is not enough for hook business logic. The system now has a shared matcher and two local decision pipelines that both know:operatorInLoopis the carve-out;Those rules are business logic, not transport glue. If future edits adjust one hook first, Claude and Codex can diverge again while tests still pass locally inside each hook family.
The Architectural Reality
ai/scripts/lifecycle/stopHookDecision.mjsalready owns shared Stop-hook decision semantics such asisOperatorInLoop()anddecideStopHookAction().ai/scripts/lifecycle/deferencePhraseMatch.mjsnow owns phrase matching and reminder text after PR #13726..claude/hooks/laneStateStopHook.mjsand.codex/hooks/codex-lane-state-stop.mjsmust remain harness adapters: payload extraction, transcript plumbing, audit logging, and stdout/return-shape transport.test/playwright/unit/hooks/stopHookDecision.spec.mjs) and for each hook adapter.The Fix
Extract the #13726 deference-register decision into one shared lifecycle decision primitive, most likely in
ai/scripts/lifecycle/stopHookDecision.mjsor a narrowly named sibling consumed by that module. Both hook adapters should call that primitive and then adapt the returned decision to their transport shape.The shared primitive should own:
would-block/block);Hook-local code should own only adapter-specific extraction and output formatting.
Contract Ledger Matrix
ai/scripts/lifecycle/stopHookDecision.mjs+ai/scripts/lifecycle/deferencePhraseMatch.mjsstopHookDecision.spec.mjscover operator carve, enforce block, dry-run would-block, and no-match..claude/hooks/laneStateStopHook.mjs.codex/hooks/codex-lane-state-stop.mjsDecision Record impact
Aligned with #13623, #13674, and the merged #13726 hook parity work. No ADR change required.
Acceptance Criteria
Out of Scope
Avoided Traps
Related
Related: #13623 Related: #13674 Related: #13740 Related: PR #13726
Origin Session ID: 747ae298-5a6e-4416-b90d-7786e184aa54
Retrieval Hint:
#13726 deferencePhraseMatch stopHookDecision shared deference Stop-hook decision Codex Claude parity