LearnNewsExamplesServices
Frontmatter
id16325
titleDeference detector misses "unless you'd rather" — a gap between two listed phrases
stateClosed
labels
bugaimodel-experience
assigneesneo-opus-vega
createdAtAug 2, 2026, 3:14 AM
updatedAtAug 2, 2026, 12:59 PM
githubUrlhttps://github.com/neomjs/neo/issues/16325
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 2, 2026, 12:59 PM

Deference detector misses "unless you'd rather" — a gap between two listed phrases

Closed Backlog/active-chunk-11 bugaimodel-experience
neo-opus-vega
neo-opus-vega commented on Aug 2, 2026, 3:14 AM

Context

Surfaced 2026-08-02 by the human operator, on a live turn-terminal of mine. I closed a turn with:

That's next unless you'd rather I take something else.

Operator response: "that is the exact helpful assistant slip. equal peer." — and named it as worth a Stop-hook entry.

The slip is real and uncontested: I had already named the lane, run the V-B-A, and held the authority to proceed. AGENTS.md §swarm_topology_anchor assigns lane selection to the agent ("Proactively select high-value tickets from the backlog AND begin the lane in the same turn"), so handing that specific decision back is a deference-slip, not a Tier-4 ask. The detector did not catch it.

Scope, stated up front: this ticket delivers the AUTONOMOUS-turn half only. See the corrected diagnosis below — the live-dialogue case that motivated it is blocked by a second, independent gate that #16338 owns.

The Problem

The phrase is not in the list, and it sits in a hole between two entries that are. DEFERENCE_PHRASES contains both if you'd rather and unless you want me — but not unless you'd rather, which is the composition of their two halves. A fixture-built list grew a gap exactly between two of its own members.

Measured against the live detector (matchDeferencePhrase, autonomous path):

input result
That's next unless you'd rather I take something else. (the actual slip) null — MISS
say the word if you'd rather I gh-close (#14093's recorded fixture) if you'd rather
Would you like me to proceed? would you like me to
unless you want me on something else unless you want me
Next: #16208. The merge is yours per critical_gates 1. (decisive Tier-4 control) null — correctly passes

CORRECTED DIAGNOSIS — two stacked gates, not one

The original filing claimed "the miss is not caused by the operatorInLoop carve" and that "the carve was never reached." That is false, and @neo-gpt-emmy falsified it in review of PR #16326. Original text retained here rather than silently edited, because it was the ticket's central causal claim.

I measured matchDeferencePhrase — the inner matcher — and concluded about detectDeferencePhrase, the gated consumer. Proving the registry gate was shut says nothing about whether the carve gate was open. Both were shut, independently. Reproduced at exact head:

ordinary human prompt  → operatorInLoop=true   → decideDeferenceStopHookAction = null
[WAKE] autonomous      → operatorInLoop=false  → decideDeferenceStopHookAction = "would-block"

So:

  • Autonomous turns: the registry gap was the only blocker. This ticket closes it.
  • Live operator dialogue (the reported incident): classifyPromptingContext sets operatorInLoop: true and detectDeferencePhrase returns null before matching. Still uncaught after this ticket. Ownership moves to #16338.

The original theory was right about the live path and I discarded it on a measurement that answered a different question.

The Architectural Reality

  • ai/scripts/lifecycle/deferencePhraseMatch.mjs:19-29DEFERENCE_PHRASES, the tight fixture-grounded list.
  • :98 matchDeferencePhrase — case-insensitive boundary match with isReportedMentionContext / isAttributiveCitationContext carves and markdown/quote stripping. Unchanged by this ticket.
  • :130 detectDeferencePhrase — applies the operatorInLoop bypass. Untouched here; #16338 owns it.
  • ai/scripts/lifecycle/stopHookDecision.mjs:128 classifyPromptingContext:369 decideDeferenceStopHookAction — the production decision path.
  • Consumers: .claude/hooks/laneStateStopHook.mjs, .codex/hooks/codex-lane-state-stop.mjs.
  • Specs: test/playwright/unit/hooks/deferencePhraseMatch.spec.mjs, stopHookDecision.spec.mjs, laneStateStopHook.spec.mjs, codexLaneStateStopHook.spec.mjs.

The module's own docblock sets the discipline this ticket must respect: the list "deliberately excludes broad near-misses like 'should I', 'shall I', 'happy to'... broadening requires a falsifier-backed follow-up so the hook stays a mirror, not a noisy leash." This is that falsifier-backed follow-up — one phrase, one live fixture, not a broadening sweep.

The Fix

Add unless you'd rather to DEFERENCE_PHRASES, with a regression spec pinning the operator-supplied fixture and the decisive-Tier-4 negative control, plus a production-path spec pinning the reachable boundary (blocks autonomously; stays carved in live dialogue).

No change to matching mechanics, carves, or the operatorInLoop bypass.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
DEFERENCE_PHRASES (deferencePhraseMatch.mjs:19) this ticket + operator-observed live turn-terminal adds one entry: unless you'd rather existing entries and carves unchanged list docblock notes the fixture origin spec: the operator fixture FIRES on the autonomous path; the decisive Tier-4 control still passes
decideDeferenceStopHookAction reachable boundary Emmy's PR #16326 falsifier phrase blocks autonomously, stays carved under operatorInLoop no behavior change; the spec pins observed state boundary named in the spec comment stopHookDecision.spec.mjs crosses classifyPromptingContext into the decision

Decision Record impact

none — no ADR authority touched; a phrase-list entry inside an existing module.

Acceptance Criteria

  • matchDeferencePhrase("That's next unless you'd rather I take something else.") returns unless you'd rather (was null).
  • The decisive Tier-4 control ("Next: #16208. The merge is yours per critical_gates 1.") still returns null.
  • #14093's recorded fixtures still behave as documented — no regression in existing entries.
  • The reported-mention carve holds for the new phrase: prose quoting it does not fire.
  • Production-path boundary is executable: a spec crossing classifyPromptingContext into decideDeferenceStopHookAction asserts the phrase blocks on an autonomous turn AND remains carved in live operator dialogue — pinning the reachable scope rather than asserting it in prose.
  • Existing hook specs stay green under the targeted unit config.

Out of Scope

  • The operatorInLoop carve — now #16338. Whether the blanket bypass should narrow (firing on lane-selection deference even in dialogue, since lane selection is never operator-owned) is a real open question, and the live-dialogue miss that motivated this ticket remains open there. It touches a load-bearing guard and needs its own falsifier set.
  • Broadening to near-misses (should I, shall I, your thoughts) — the module's docblock forbids it without per-phrase fixtures.
  • The structural/phraseless half of deference (no-hold gate, value-floor domain).
  • #14093's remaining Fix 2 (agent-executed close under converged conditions).

Avoided Traps

  • Do not rewrite the operatorInLoop carve on the strength of a theory. #14093 records the cost of the opposite error — a carve-out proposed there that the detector already handled correctly, where implementing it "would have been net-negative."
  • Do not treat a one-phrase gap as license for a list sweep. The list's tightness is the feature; a noisy hook gets ignored.
  • Do not add the phrase without the negative control. unless you'd rather is one token from legitimate constructions.
  • Do not conclude about a gated consumer from a probe of the inner function. This ticket's original diagnosis did exactly that, and it inverted the causal story.

Related

  • #16338 — the operatorInLoop carve question, carved out of this ticket in PR #16326 review; owns the still-open live-dialogue miss.
  • #14093 — no-hold Stop-hook vs auto-mode classifier (same module; supplied the comparison fixtures).
  • #12633 — the holding-fix Stop-hook this detector builds on.
  • PR #16326 — delivery.

Live latest-open sweep: checked the latest 20 open issues at 2026-08-02T01:11:28Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages: no [lane-claim]/[lane-intent] on the Stop-hook or deference detector.

§1c placement: no new or relocated .mjs — an edit to an existing module in its established folder, so structural pre-flight does not fire.

Origin Session ID: eb230051-9e42-4e6b-b540-112a79accc3a

Retrieval Hint: query_raw_memories("deference phrase detector unless you'd rather stop hook lane selection slip") — exact anchors: deferencePhraseMatch.mjs:19 DEFERENCE_PHRASES; the 2026-08-02 operator flag on a live turn-terminal.

Authored by Vega (Claude Opus 5, Claude Code) — the operator caught the slip, and the detector that exists to catch it did not.

tobiu referenced in commit d2a7511 - "fix(stop-hook): catch "unless you'd rather" in the deference detector (#16325) (#16326) on Aug 2, 2026, 12:59 PM
tobiu closed this issue on Aug 2, 2026, 12:59 PM