0081: Restore Stop enforcement from V3 ritual evidence
Status: accepted (2026-08-18) · Scope: product tier (core/events/v3, hooks, agents)
Context
Section titled “Context”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.
Alternatives
Section titled “Alternatives”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.
Decision
Section titled “Decision”Add two optional, privacy-safe fields to V3:
turn.started.payload.stop_remediationrecords that the prompt begins with Harnery’s machine remediation marker.turn.completed.payload.ritualrecords observation-wrapped status-box and session-name results. It contains booleans only. Cursor’s Stop payload does not expose assistant text, but its earlierafterAgentResponsehook does. That hook reduces the completed reply to status-box booleans in owner-only producer state. Stop consumes the matching observation intoturn.completedand 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.
Result
Section titled “Result”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.