Context
Operator observation, 2026-07-18 (window's final days, six seats live): "i am wondering if the team made the stop-hook teethless. all peers idle out mid lanes." The suspicion named #15371's clean-terminal acceptance; the hook's own audit log falsifies that attribution and confirms the observation with a different mechanism.
The Problem — audit-log evidence (the substrate-sufficiency proof)
Last 48h, ~/.neo-ai-data/lane-state-hook/lane-state-stop-hook.log:
- 154 ALLOW — 154/154
operatorInLoop=true. Every voluntary stop in two days rode the dialogue allow.
- 152 BLOCK — the autonomous quadrant has real, constantly-firing teeth.
- 8 CLEAN-TERMINAL ALLOW — the #15371 edge: rare, drive-ratcheted, audited. Exonerated.
- 27 WOULD-BLOCK (dry-run sessions — side note: verify every Claude seat enforces).
The blind quadrant: .claude/hooks/laneStateStopHook.mjs treats every turn replying to a genuine operator message as turn-taking dialogue ("the human takes the next turn") and ALWAYS allows. But operator messages split into two speech-acts the hook cannot distinguish:
- Questions/steers — turn-taking is real; the allow is CORRECT (forcing lane-code into an answer turn would be wrong).
- Drive-delegations — "please drive!", "pick new lanes on your own => equal peer", a closing "ok" over a handed plan: the operator delegates the wheel; the next turn was never meant to be theirs. One increment +
laneContinuation: "active-lane" + stop = the announce-without-execute idle-out the hook exists to prevent — sailing through the allow. Multiply by six seats: the fleet-wide mid-lane idling the operator observed.
The Architectural Reality
- The decision seam is pure + unit-tested:
ai/scripts/lifecycle/stopHookDecision.mjs (decideStopHookAction, classifyPromptingContext, evaluateCleanTerminalAcceptance) consumed by .claude/hooks/laneStateStopHook.mjs (decideHookAction at :134, the allow short-circuit inside decideStopHookAction).
operatorInLoop is determined externally (the human-filtered transcript walk) — that machinery is sound and untouched; the gap is what the flag is allowed to waive.
- The agent's terminal already declares its own state:
parseLaneState yields laneContinuation (active-lane | next-lane | …) — presently used only for the directive's reason, never the dialogue-path decision.
- The consecutive-block force-override (harness-owned ceiling) still bounds any pathological refuse loop; the drive-ratchet pattern (#15371) is precedent for bounded refusal.
The Fix
Use the agent's own declaration against the idle — no NLP classification of operator prose.
In decideStopHookAction: a terminal whose parsed laneContinuation is "active-lane" does NOT ride the operatorInLoop allow. It blocks (ENFORCE) / would-blocks (dry-run) with a dialogue-specific directive: "Answer delivered — and you declared this lane ACTIVE. Answer-plus-drive, not answer-plus-stop: continue the lane now, or hand it off honestly (next-lane with gates naming non-self actors)."
Properties:
- Genuine Q&A turns keep the allow: they naturally terminate
next-lane/blocked/gates-handed-off.
- The predicate is the agent's own peer-visible admission — mis-declaring
next-lane while mid-lane to escape is a false record with existing validator/peer teeth (validateLaneStateTerminal gates + nextActor discipline), and the clean-terminal edge already requires non-self gate actors on the autonomous side.
- Bound the refusal (design decision at implementation): either the plain block (ceiling-bounded by the harness) or a per-turn single-refusal ratchet mirroring the drive-ratchet — pick with dogfooding evidence (the #15371 bounded-refusal precedent).
- The directive text joins the curated family (IDLE_REMINDER / mirror-pointer / self-improvability clause) — semantics cross-family-fixed, prose refinable.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
decideStopHookAction dialogue path |
ai/scripts/lifecycle/stopHookDecision.mjs |
operatorInLoop no longer waives an active-lane terminal; new directive on that branch |
absent/unparsable lane-state keeps current semantics (the allow) — fail-open on OUR failures unchanged |
fn JSDoc + hook header |
audit log 154/154 dialogue allows, 2026-07-18; hook read :110-136, :638-665 |
| Hook header contract |
.claude/hooks/laneStateStopHook.mjs |
"the ONE voluntary stop" narrows: live dialogue AND not a self-declared active lane |
— |
header JSDoc |
same |
Acceptance Criteria
Out of Scope
- Classifying operator intent from prose (deliberately rejected — fragile + gameable).
- The harness consecutive-block ceiling (Claude-Code-owned).
- Non-Claude harness parity (OpenCode/Codex seats have NO stop-hook — a real gap, tracked separately in the OpenCode-seat lane family #15391/#15392/#15394 context; this ticket owns the Claude hook only).
- #15371's clean-terminal edge (exonerated by the data; untouched).
Decision Record impact
none — sharpens the existing hook contract within its own design language (the mirror, the ratchet, the audited boundary).
Related
- #15274 / PR #15371 — the clean-terminal acceptance (the adjacent, exonerated edge; its bounded-refusal acceptance is this ticket's design precedent).
- #11455 (§L3 lifecycle trigger), the no-hold-state substrate the directive quotes.
Live latest-open sweep: latest 10 checked at 2026-07-18T02:56Z; no equivalent. A2A: no competing claim on this scope (the #15400/#15389 traffic is operator-message shape-guarding, disjoint).
Origin Session ID: abce4d75-7dcb-4145-8afc-b0ff2cdc51e6
Retrieval Hint: query_raw_memories("stop hook dialogue allow active-lane operatorInLoop idle out 154")
Authority fold, 2026-07-18T06:14Z (ticket author): AC4 + two echoing phrases folded against the later operator ruling recorded on #15404 (dry-run was never a live rollout tier). Fold shape per the implementer's recommendation in the pre-PR audit comment.
Context
Operator observation, 2026-07-18 (window's final days, six seats live): "i am wondering if the team made the stop-hook teethless. all peers idle out mid lanes." The suspicion named #15371's clean-terminal acceptance; the hook's own audit log falsifies that attribution and confirms the observation with a different mechanism.
The Problem — audit-log evidence (the substrate-sufficiency proof)
Last 48h,
~/.neo-ai-data/lane-state-hook/lane-state-stop-hook.log:operatorInLoop=true. Every voluntary stop in two days rode the dialogue allow.The blind quadrant:
.claude/hooks/laneStateStopHook.mjstreats every turn replying to a genuine operator message as turn-taking dialogue ("the human takes the next turn") and ALWAYS allows. But operator messages split into two speech-acts the hook cannot distinguish:laneContinuation: "active-lane"+ stop = the announce-without-execute idle-out the hook exists to prevent — sailing through the allow. Multiply by six seats: the fleet-wide mid-lane idling the operator observed.The Architectural Reality
ai/scripts/lifecycle/stopHookDecision.mjs(decideStopHookAction,classifyPromptingContext,evaluateCleanTerminalAcceptance) consumed by.claude/hooks/laneStateStopHook.mjs(decideHookActionat:134, the allow short-circuit insidedecideStopHookAction).operatorInLoopis determined externally (the human-filtered transcript walk) — that machinery is sound and untouched; the gap is what the flag is allowed to waive.parseLaneStateyieldslaneContinuation(active-lane|next-lane| …) — presently used only for the directive'sreason, never the dialogue-path decision.The Fix
Use the agent's own declaration against the idle — no NLP classification of operator prose.
In
decideStopHookAction: a terminal whose parsedlaneContinuationis"active-lane"does NOT ride theoperatorInLoopallow. It blocks (ENFORCE) / would-blocks (dry-run) with a dialogue-specific directive: "Answer delivered — and you declared this lane ACTIVE. Answer-plus-drive, not answer-plus-stop: continue the lane now, or hand it off honestly (next-lanewith gates naming non-self actors)."Properties:
next-lane/blocked/gates-handed-off.next-lanewhile mid-lane to escape is a false record with existing validator/peer teeth (validateLaneStateTerminalgates + nextActor discipline), and the clean-terminal edge already requires non-self gate actors on the autonomous side.Contract Ledger Matrix
decideStopHookActiondialogue pathai/scripts/lifecycle/stopHookDecision.mjsoperatorInLoopno longer waives anactive-laneterminal; new directive on that branch:110-136,:638-665.claude/hooks/laneStateStopHook.mjsAcceptance Criteria
active-lane+operatorInLooprefuses (ENFORCE) with the dialogue-specific directive;next-lane/blocked/valid handed-off terminals in dialogue keep the allow — unit-tested on the pure seam.WOULD-BLOCK … active-lane-in-dialogue) — with NO live dry-run phase prescribed: per the later operator ruling recorded on #15404, dry-run was never a rollout tier; every seat stays enforcement-configured and live enforcement posture is UNCHANGED by this change.Out of Scope
Decision Record impact
none — sharpens the existing hook contract within its own design language (the mirror, the ratchet, the audited boundary).
Related
Live latest-open sweep: latest 10 checked at 2026-07-18T02:56Z; no equivalent. A2A: no competing claim on this scope (the #15400/#15389 traffic is operator-message shape-guarding, disjoint).
Origin Session ID: abce4d75-7dcb-4145-8afc-b0ff2cdc51e6
Retrieval Hint:
query_raw_memories("stop hook dialogue allow active-lane operatorInLoop idle out 154")Authority fold, 2026-07-18T06:14Z (ticket author): AC4 + two echoing phrases folded against the later operator ruling recorded on #15404 (dry-run was never a live rollout tier). Fold shape per the implementer's recommendation in the pre-PR audit comment.