Skip to content

0081: Restore Stop enforcement from V3 ritual evidence

Status: accepted (2026-08-18) · Scope: product tier (core/events/v3, hooks, agents)

The V3 migration removed reply bodies from the canonical event ledger. That was the right privacy boundary, but the migration also made the Stop verdict fail open for every adapter. Claude Code and Cursor stopped enforcing the end-of-turn coordination ritual even though both adapters still had safe block channels and the hooks still computed the relevant facts.

The missing evidence was small. Task declarations already produce coord.task_changed, status checks already produce coord.status_observed, and tool use already produces tool.requested. The ledger lacked only two structural facts: whether a completed reply showed the required status and session-name cues, and whether Cursor’s next turn came from Harnery’s own Stop remediation message.

Restoring transcript storage would solve the wrong problem. The verdict needs booleans, not prose. Reading the disposable heartbeat as authority would also be wrong because that cache can be deleted or rebuilt.

Keep every adapter fail-open. Rejected because it silently weakens a documented coordination guarantee and makes missed task/status rituals indistinguishable from an intentional policy choice.

Restore reply text in the ledger. Rejected because it would retain operator and model prose merely to answer three yes-or-no questions.

Use the heartbeat cache as verdict authority. Rejected because the cache is a projection, not durable evidence, and it cannot reconstruct Cursor’s cross-turn remediation window.

Make Codex enforce the ritual too. Rejected by ADR 0051. A Codex Stop continuation can replace the answer the operator was meant to receive.

Add two optional, privacy-safe fields to V3:

  • turn.started.payload.stop_remediation records that the prompt begins with Harnery’s machine remediation marker.
  • turn.completed.payload.ritual records observation-wrapped status-box and session-name results. It contains booleans only. Cursor’s Stop payload does not expose assistant text, but its earlier afterAgentResponse hook does. That hook reduces the completed reply to status-box booleans in owner-only producer state. Stop consumes the matching observation into turn.completed and clears the temporary state. The reply body never enters V3 intake, producer state, diagnostics, or the canonical ledger.

The fields are optional so the contract change is structurally additive. A current producer writes them on every applicable event. A current Claude Code terminal without observed ritual evidence fails open instead of inventing an answer.

The Stop evaluator reads the validated V3 stream through the canonical reader. It scopes evidence to the current owner and generation, then walks backward over consecutive stop_remediation turns so Cursor repairs accumulate within the last human-authored turn. coord.task_changed, coord.status_observed, and tool.requested supply the other ritual facts.

Every set-task call records coord.task_changed, even when an empty task was already clear. The command is both a state mutation and a per-turn declaration. Deduplicating the second role would make set-task "" unable to repair a conversational Cursor turn.

Claude Code keeps exit-2 enforcement. Cursor keeps its bounded followup_message enforcement and asks for every missing repair in one pass, including pasting the status box when assistant-text evidence is false. Codex remains observe-only, and workflow children remain exempt.

Claude Code and Cursor once again enforce the same end-of-turn policy they had before V3. Cursor remediation remains monotonic, so running one missing command does not discard evidence from the turn it repairs. Codex continues to record the observations without risking a destructive continuation.

agents status --end-turn now checks that its end_turn_checked observation reached the ledger before printing the status box. If the append fails, the command exits nonzero, prints no box, and tells the agent to retry. Report-only status calls still soft-fail because their observations do not gate the turn.

The ledger now makes ritual overhead auditable without storing reply content. Tool, command, and turn economics can be compared with the coordination events that caused extra work, including remediation turns.

A later Cursor audit found that running the status command was not the same as showing its output in the assistant reply. A live Cursor trace confirmed that afterAgentResponse runs before Stop, and focused lifecycle tests confirmed that its boolean is readable when Stop records the terminal. Cursor now uses the strict assistant-text observation for rule 2/3, while rule 1/3 still requires the status command itself. init --adapter cursor also installs an Always Apply rule that tells the agent to paste the status output.

A later Cursor audit found one false-zero path in that comparison. The recorder initialized its tool aggregate to zero, then reported that value as exact when no tool hook arrived. Cursor’s terminal payload does not carry an independent tool count, so silence on the hook channel cannot prove that a turn used no tools. Cursor now records turn_tool_call_count as expected_but_missing with the reason tool_channel_unattested in that case. Delivered tool hooks still produce an observed count, and the latency projection keeps unattested tool time unknown instead of treating it as zero.

A later audit localized the remaining variation to Cursor execution modes. Local Agent Chat uses the generic tool hooks and can report an exact aggregate. Cloud and private-worker conversations carry a native bc- identifier, but their hooks can begin after an exploratory read-only phase. They cannot prove whole-turn tool completeness. Cursor’s profile therefore declares tool signals conditional. A local turn with delivered hooks reports an observed count, a local turn with a missing hook remains expected_but_missing, and a bc- turn reports the aggregate as unsupported. The producer retains only the mode classification, not the prompt, command, tool input, result, or transcript.