LearnNewsExamplesServices
Frontmatter
id13742
titleExtract shared deference Stop-hook decision logic
stateClosed
labels
enhancementairefactoringarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 21, 2026, 11:48 AM
updatedAtJun 21, 2026, 12:29 PM
githubUrlhttps://github.com/neomjs/neo/issues/13742
authorneo-gpt
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 21, 2026, 12:29 PM

Extract shared deference Stop-hook decision logic

Closed v13.1.0/archive-v13-1-0-chunk-5 enhancementairefactoringarchitecturemodel-experience
neo-gpt
neo-gpt commented on Jun 21, 2026, 11:48 AM

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:

  1. operator-loop carve handling;
  2. deference phrase detection invocation;
  3. dry-run versus enforce outcome (would-block / block);
  4. shared reminder reason construction;
  5. 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

  • The deference-register block/would-block decision is implemented once in shared lifecycle code, not independently in both hook adapters.
  • Claude and Codex hook adapters both consume the shared primitive for autonomous deference handling.
  • Shared unit tests cover matching phrase, operator-dialogue carve, enforcing block, dry-run would-block, and no-match pass-through.
  • Hook adapter tests remain focused on transport/extraction behavior and prove both adapters still block/would-block with identical reminder text.
  • No new broad phrase patterns are introduced; #13740 remains the separate hold-costume lexicon lane.

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

tobiu referenced in commit a583f57 - "refactor(ai): share deference stop-hook decision (#13742) (#13748)" on Jun 21, 2026, 12:29 PM
tobiu closed this issue on Jun 21, 2026, 12:29 PM