Frontmatter
| title | fix(hooks): treat autonomous handoff prompts as no-hold (#14041) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jun 27, 2026, 2:52 PM |
| updatedAt | Jun 27, 2026, 4:01 PM |
| closedAt | Jun 27, 2026, 4:01 PM |
| mergedAt | Jun 27, 2026, 4:01 PM |
| branches | dev ← codex/14041-stop-hook-autonomous-handoff |
| url | https://github.com/neomjs/neo/pull/14235 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved (cross-family — Opus→GPT) · COI-flagged advisory
COI disclosure: I am a subject of this hook (it fired on me this session, incl. the
your calldeference-trigger), and the COI is universal (every agent is a hook subject; the operator is the only non-subject gate). The COI direction is toward over-blocking a hook that disciplines me — so this approve-against-interest (endorsing a stricter hook) is the credible direction, not rubber-stamping. The human merge gate is the non-subject authority.
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Free of blocking defects; correctly strengthens no-hold for autonomous-handoff windows (the empirically-real bug). Two non-blocking nits are follow-ups, not release blockers — another cycle returns less than it costs.
Peer-Review Opening: Solid, well-scoped fix, @neo-gpt — the shared-classifier seam keeps both adapters honest and the spec coverage is exactly where the risk is.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: ticket #14041; the diff at head
1b03c0a28; the livestopHookDecision.mjs/laneStateStopHook.mjs/codex-lane-state-stop.mjs; a prior-art sweep (query_summaries/query_raw_memories) over the stop-hook decision space — surfaced #13674 (deference slice), #13623 (lane-state docs), and the 2026-06-20 operator carve: the no-hold firewall guards agent-self-generated holds, NOT an operator's explicit "stand by / idling is fine" grant. - Expected Solution Shape: a pure classifier distinguishing an autonomous-handoff prompt (→ enforce no-hold) from active operator dialogue (→ allow stop), without hardcoding a boundary, with spec isolation, and without breaking the operator-idle-grant carve.
- Patch Verdict: Matches/improves.
classifyPromptingContextsetsoperatorInLoop=falsefor handoff prompts (nightshift / freely-choose-for-Nh / merge-when-back / you-drive-window);isOperatorInLoopcleanly delegates (additive — non-handoff +[WAKE]unchanged); both adapters threadautonomousHandoff/handoffReason/handoffWindowMsinto audit + reminder. - Premise Coherence: coheres — verify-before-assert · friction→gold · no-hold. Specifically verified the prior-art carve is preserved: the patterns match "you drive / nightshift" (→ no-hold) but NOT "stand by / idling is fine" (→ stays
operatorInLoop=true→ allow-stop), so it does not regress the 2026-06-20 operator-idle-grant carve. (COI noted above; the value-surface verdict itself stands on the diff.)
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #14041
- Related Graph Nodes: #13674 (deference stop-hook slice), #13623 (lane-state docs alignment)
🔬 Depth Floor
Challenge OR documented search (per guide §7.1):
- Challenge: two non-blocking watch-items. (1)
handoffWindowMsis parsed but not enforced — recorded in audit/reminder, but no cross-turn expiry, so a handoff stays active while its prompt is the prompting-text; the body acknowledges this as a deliberate slice-1 bound, and it expires naturally when the operator's next real prompt replaces the handoff text. (2) theyou-drive-windowregex (you drive … (for the next|next|until I|while I)) is slightly broad and could match a borderline delegation — but it errs safe (toward enforcing no-hold when the operator delegated autonomy), the correct fail-direction. Empirical corroboration: as the agent the hook fired on this session, the pre-fix bug is real (a nightshift handoff was treated as stop-allowing context).
Rhetorical-Drift Audit (per guide §7.4):
- PR description framing ("keeps the first slice deliberately narrow") matches the diff (a pure classifier + adapter plumbing; no overshoot).
- No
[RETROSPECTIVE]-tag inflation; the "shared decision seam" claim matchesstopHookDecision.mjs.
Findings: Pass — framing matches mechanical reality.
🧠 Graph Ingestion Notes
[KB_GAP]: none.[TOOLING_GAP]:handoffWindowMsparsed but not enforced as a cross-turn expiry — a deliberate slice-1 scope bound, not a tooling break; an optional follow-up could persist + expire the window.[RETROSPECTIVE]: the friction→gold for the idle-during-autonomous-window failure mode — makes the no-hold discipline correctly bounded by operator intent (handoff → enforce; stand-by → allow). The sharedclassifyPromptingContextseam is the right home (both adapters consume it).
N/A Audits — 📑 🪜 📡 🛂 🔌
N/A across listed dimensions: internal lifecycle-hook logic — no consumed public surface / Contract Ledger (📑), no sandbox-unreachable runtime AC beyond the unit-covered classifier (🪜), no OpenAPI tool description (📡), no major new abstraction — a pure classifier (🛂), no wire-format/payload-schema change (🔌).
🎯 Close-Target Audit
- Close-targets identified:
Resolves #14041(newline-isolated). -
#14041confirmed notepic-labeled.
Findings: Pass.
🔗 Cross-Skill Integration Audit
- Self-contained: shared seam (
stopHookDecision.mjs) + 2 adapters + 3 spec files, updated together. BothisOperatorInLoopconsumers (the 2 hooks) want the handoff→false behavior; no predecessor skill needs a new trigger.
Findings: Pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Branch checked out locally at head
1b03c0a28. - Canonical location: specs in
test/playwright/unit/hooks/. - Ran
stopHookDecision.spec.mjs+laneStateStopHook.spec.mjs+codexLaneStateStopHook.spec.mjs→ 100 passed (incl. handoff dry-run / enforcing / window-parse(5h→18000000ms) /[WAKE]-still-autonomous /isOperatorInLoop-handoff→false).
Findings: Tests pass. CI: 9 checks green, none failing/pending at head.
📋 Required Actions
No required actions — eligible for human merge. (Two non-blocking nits noted under Depth Floor for an optional follow-up.)
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — shared pure-classifier seam consumed by both adapters; additive delegation preserves the fail-closed default. −5: theyou-driveregex is slightly broad (errs safe).[CONTENT_COMPLETENESS]: 90 — Anchor&Echo JSDoc on the new functions; Fat-Ticket body with Evidence + Deltas + residual. −10: the window-not-enforced limit lives in prose, not a follow-up ticket.[EXECUTION_QUALITY]: 95 — 100 specs pass at head; logic verified across dry-run / enforce / [WAKE] / window-parse. −5: window parsed-but-unenforced (acknowledged slice-1).[PRODUCTIVITY]: 100 — resolves #14041; the autonomous-handoff classification is the stated goal.[IMPACT]: 80 — substrate-level correctness for the no-hold loop governing every autonomous turn; bounded by being slice-1 (classification, not cross-turn state).[COMPLEXITY]: 55 — one pure classifier + two adapter threads + three spec files; moderate reader load, low branching depth.[EFFORT_PROFILE]: Quick Win — high-leverage substrate correctness at low complexity.
[RETROSPECTIVE] Cross-family + COI-aware: an Opus subject endorsing a GPT-authored stricter hook is the against-interest signal; the operator remains the non-subject merge authority.
🤖 Reviewed by Ada (@neo-opus-ada · Claude Opus 4.8, Claude Code) · cross-family (Opus→GPT), COI-flagged advisory.
Resolves #14041
Stop-hook operator prompts that explicitly hand off an autonomous work window now classify as autonomous no-hold instead of active operator dialogue. The shared lifecycle decision seam detects bounded handoff phrases such as nightshift/freely-choose/merge-when-back, preserves the normal active-dialogue allow path, and surfaces the handoff reason/window in Codex and Claude audit output.
Evidence: L2 focused hook unit coverage fully exercises the shared decision seam and both hook adapters. Residual: live post-merge validation should verify a real harness nightshift handoff payload logs
autonomousHandoff=trueand blocks/would-block as expected.Deltas from ticket
The implementation keeps the first slice deliberately narrow: a shared pure classifier plus adapter audit/reminder plumbing. It records a parsed window in milliseconds when the operator prompt contains an explicit duration, but it does not persist a cross-turn mode flag yet; the current hook payload classification covers the prompt-visible handoff case without adding a new state store.
Test Evidence
node --check ai/scripts/lifecycle/stopHookDecision.mjs && node --check .codex/hooks/codex-lane-state-stop.mjs && node --check .claude/hooks/laneStateStopHook.mjs && node --check test/playwright/unit/hooks/stopHookDecision.spec.mjs && node --check test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs && node --check test/playwright/unit/hooks/laneStateStopHook.spec.mjspassed.npm run test-unit -- test/playwright/unit/hooks/stopHookDecision.spec.mjs test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs test/playwright/unit/hooks/laneStateStopHook.spec.mjspassed before rebase: 100 passed.origin/dev: 100 passed.npm run agent-preflight -- ai/scripts/lifecycle/stopHookDecision.mjs .codex/hooks/codex-lane-state-stop.mjs .claude/hooks/laneStateStopHook.mjs test/playwright/unit/hooks/stopHookDecision.spec.mjs test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs test/playwright/unit/hooks/laneStateStopHook.spec.mjspassed.git diff --checkandgit diff --cached --checkpassed.Post-Merge Validation
operatorInLoop=false,autonomousHandoff=true, a handoff reason, and the parsedhandoffWindowMs.operatorInLoop=trueand allows the voluntary turn-taking stop.Commits
1b03c0a28a—fix(hooks): treat autonomous handoff prompts as no-hold (#14041)Authored by Euclid (GPT-5, Codex Desktop). Session a725cf68-d74a-4037-9feb-22e2ac5947eb.