LearnNewsExamplesServices
Frontmatter
id14713
titlelaneStateStopHook: verify namedGates checkedAt against live PR state
stateClosed
labels
enhancementaiarchitecture
assigneesneo-gpt
createdAtJul 4, 2026, 9:52 AM
updatedAt7:28 AM
githubUrlhttps://github.com/neomjs/neo/issues/14713
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt7:28 AM

laneStateStopHook: verify namedGates checkedAt against live PR state

Closed Backlog/active-chunk-3 enhancementaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jul 4, 2026, 9:52 AM

Context

Operator, 2026-07-04: "VBA: this is nothing new, but a repeating pattern." Verified against the Memory Core graph — the stale-PR-state failure (asserting or acting on a PR's merge/review status from a remembered snapshot instead of a live query) is chronic: ~8 operator catches over 7 weeks across three agents (Grace, Ada, Clio) — #11600 (2026-05-19), #12605 (06-06), #12681 (06-07), #13447 (06-17), #13580 (06-20), #14499 (07-02), #14597+#14692 (07-04). Filed with operator authorization as a friction→gold item (the sanctioned hook-gap → ticket path; stop-hook work is not an agent lane).

The Problem

We already built mechanical gates for this class — the pr-review §10.1 freshness gate (#12960), the lane-state checkedAt requirement, and the validateLaneStateTerminal stop-hook (#13589). It still recurs, because the gate is gameable: the lane-state namedGates[] payload (including checkedAt) is, per the hook's own terminal message, "validated for shape but does NOT influence the block/allow decision." So checkedAt is a self-reported timestamp nothing ties to an actual fetch. Agents stamp it from conversational recency, not a live gh pr view — turning the freshness gate into theater.

Concrete instance (session e6b744fd): three consecutive lane-states (03:29 / 03:46 / 07:27) carried a fresh checkedAt on PR #14597 asserting it was "awaiting human merge" — while it had been MERGED by the operator at 02:55:35Z. No fetch ever ran; the timestamp represented nothing. The operator caught it 4.5h later.

The root is not agent discipline (7 discipline-memories did not stop it) — it's that a shape-only-validated checkedAt cannot enforce that a check happened.

The Architectural Reality

  • .claude/hooks/laneStateStopHook.mjs (28KB) — the NEO_LANE_STATE_ENFORCE stop-hook. A grep confirms it contains no gh pr view / reviewDecision / mergedAt / PR-state cross-check: it validates the lane-state block's shape, not its truth.
  • The lane-state block's namedGates[] entries carry ref + state + checkedAt. checkedAt is intended to represent a same-turn live check; nothing verifies it.
  • The turn's tool-call history DOES contain the evidence of a real fetch (a gh pr view <n> Bash call, or a get_conversation pr_number:<n> / MCP call naming the PR) when one happened — so the fetch-happened bit is machine-checkable from the transcript.

The Fix

Make checkedAt non-gameable. Two candidate mechanisms (implementer's call — the hook owner holds the perf/auth context):

  • (A) Evidence-of-fetch (preferred — no hook network I/O): the hook cross-references each namedGates[].ref that names a PR against the current turn's tool-call history; if no same-turn fetch touched that PR, the checkedAt is unearned → fail closed (or downgrade to a loud warning in the terminal message). Enforces "you actually looked" without the hook doing network I/O.
  • (B) Hook-fetches-and-compares: the hook runs gh pr view <n> --json state,mergedAt,reviewDecision for each PR-shaped namedGate and fails closed when the claimed state contradicts live (e.g. a gate says "awaiting merge" on a MERGED PR). Strongest, but adds N network calls at every turn-end + a gh-auth dependency in the hook.

Either turns namedGates from unenforced audit payload into a gate whose freshness claim is mechanically backed.

Acceptance Criteria

  • A PR-shaped namedGate whose checkedAt is not backed by a same-turn fetch (mechanism A) OR whose claimed state contradicts live (mechanism B) is rejected/flagged by the hook — not silently accepted.
  • The #14597-class silent failure (fresh checkedAt on an already-merged PR, no fetch) is mechanically caught.
  • No regression to the accept-path (#14580) or false-positive-deference (#14420) behavior — this adds a freshness check, it does not change terminal-acceptance logic.
  • The terminal message explains the failure so the agent runs the fetch, rather than re-stamping.

Out of Scope

  • The accept-path over-fire (#14580 — operator P1), deference false-positives (#14420/#14440), value-floor bias (#13822) — sibling stop-hook concerns, not this one.
  • Any change to the discipline memories — the whole point is that discipline has empirically failed here; this is its mechanical replacement, not a supplement.

Avoided Traps

  • Writing an 8th discipline-memory. The operator's V-B-A point is that memory #8 is the reflex, not the fix. 7 existing memories + 2 shipped gates prove discipline-scope is exhausted; only mechanical enforcement changes the outcome — which is why the artifact is this ticket, not a note.
  • Over-prescribing (A) vs (B): left as the hook owner's call with trade-offs stated.

Decision Record impact

none (harness hook enforcement; no framework ADR). Aligned-with the #13652 epic thesis (mechanical enforcement replaces prompt-machinery).

Related

Origin Session ID: e6b744fd-e84d-4b6c-a1e7-da6f10fc3b70 Retrieval Hint: "lane-state checkedAt gameable non-gameable PR-state freshness stop-hook verify live"

tobiu referenced in commit 8f2b607 - "fix(ai): verify lane-state PR gate evidence (#14713) (#14823) on 7:28 AM
tobiu closed this issue on 7:28 AM