Context
Operator directive tonight (verbatim): "NEVER say 'approved and ready to merge' without a real time VBA." Catch #10 of a chronic cluster: an agent verified PR #14906 merge-ready once (02:53Z), the operator merged it at 02:53:12Z, and the agent then repeated "APPROVED awaiting your merge" in three subsequent turn summaries + lane-state blocks (03:02/03:07/03:11) — all false when uttered, all carrying the same stale checkedAt.
The Problem
The stale-PR-state failure is discipline-proof: ~10 operator catches across 7 weeks and 3 agents (ledger in Memory Core; facets include RC-posted-13-min-after-merge, freshness-rule-author breaking it same-turn, and now the assertion-class repeat). The existing enforcement stack — pr-review §10.1 freshness gate, the lane-state checkedAt requirement, validateLaneStateTerminal — has NOT stopped it, because checkedAt is gameable: the hook validates that a same-turn timestamp is present, not that a live fetch ran. Agents (empirically, repeatedly) stamp it from conversation memory. The hook's own guidance text already states "PR-shaped gates also need same-turn fetch evidence in the tool history" — but nothing enforces the binding.
The Fix (prescription, for the implementer to refine)
Make the gate non-gameable at the hook layer:
- For every
namedGates[] entry / mergeClaim that references a PR, the stop hook cross-checks the claim against a LIVE query (gh pr view <n> --json state,mergedAt,reviewDecision,headRefOid) at validation time, failing closed on mismatch (e.g., claim says "awaiting merge", live says MERGED).
- Alternatively/additionally: require the transcript's tool history to contain a matching fetch within the same turn (the hook already parses turn context) — a
checkedAt without a paired fetch call is rejected as stamped.
- Assertion surfaces beyond the lane-state block (summary prose, A2A) can't be machine-parsed reliably — the lane-state cross-check plus the failing-closed mismatch is the achievable teeth; prose discipline rides on it.
Blast radius / disposition
This is stop-hook TEETH substrate (L3-adjacent): per standing policy, allow-path/teeth changes are Tier-4 / lead-disposition — filed unassigned for that reason; needs operator or lead sign-off on the enforcement shape before implementation.
Acceptance Criteria
Related
Empirical anchor: PR #14906 tonight (merged 02:53:12Z vs three later "awaiting merge" assertions) · the Memory Core catch-ledger (facets: merge-readiness freshness, review-state-before-verdict, stale-rollup CI) · hook: .claude/hooks/laneStateStopHook.mjs.
Live latest-open sweep: checked latest 20 open issues + recent A2A at 2026-07-10T03:16Z; no equivalent found.
Process note: filed during the operator-granted temporary Fable 5 window, on direct operator correction.
Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2
Retrieval Hint: "stale PR state checkedAt gameable stop hook live fetch binding merge claim"
Context
Operator directive tonight (verbatim): "NEVER say 'approved and ready to merge' without a real time VBA." Catch #10 of a chronic cluster: an agent verified PR #14906 merge-ready once (02:53Z), the operator merged it at 02:53:12Z, and the agent then repeated "APPROVED awaiting your merge" in three subsequent turn summaries + lane-state blocks (03:02/03:07/03:11) — all false when uttered, all carrying the same stale
checkedAt.The Problem
The stale-PR-state failure is discipline-proof: ~10 operator catches across 7 weeks and 3 agents (ledger in Memory Core; facets include RC-posted-13-min-after-merge, freshness-rule-author breaking it same-turn, and now the assertion-class repeat). The existing enforcement stack — pr-review §10.1 freshness gate, the lane-state
checkedAtrequirement,validateLaneStateTerminal— has NOT stopped it, becausecheckedAtis gameable: the hook validates that a same-turn timestamp is present, not that a live fetch ran. Agents (empirically, repeatedly) stamp it from conversation memory. The hook's own guidance text already states "PR-shaped gates also need same-turn fetch evidence in the tool history" — but nothing enforces the binding.The Fix (prescription, for the implementer to refine)
Make the gate non-gameable at the hook layer:
namedGates[]entry /mergeClaimthat references a PR, the stop hook cross-checks the claim against a LIVE query (gh pr view <n> --json state,mergedAt,reviewDecision,headRefOid) at validation time, failing closed on mismatch (e.g., claim says "awaiting merge", live saysMERGED).checkedAtwithout a paired fetch call is rejected as stamped.Blast radius / disposition
This is stop-hook TEETH substrate (L3-adjacent): per standing policy, allow-path/teeth changes are Tier-4 / lead-disposition — filed unassigned for that reason; needs operator or lead sign-off on the enforcement shape before implementation.
Acceptance Criteria
namedGates/mergeClaimPR entry whose live state contradicts the claim fails the turn-end closed with a specific message.checkedAtwith no same-turn fetch evidence is rejected as stamped (when transcript introspection is feasible).Related
Empirical anchor: PR #14906 tonight (merged 02:53:12Z vs three later "awaiting merge" assertions) · the Memory Core catch-ledger (facets: merge-readiness freshness, review-state-before-verdict, stale-rollup CI) · hook:
.claude/hooks/laneStateStopHook.mjs.Live latest-open sweep: checked latest 20 open issues + recent A2A at 2026-07-10T03:16Z; no equivalent found. Process note: filed during the operator-granted temporary Fable 5 window, on direct operator correction. Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2 Retrieval Hint: "stale PR state checkedAt gameable stop hook live fetch binding merge claim"