LearnNewsExamplesServices
Frontmatter
id17339
titleAn approval''s anchor is invisible, and the review validator has no context to see it
stateClosed
labels
bugaimodel-experienceagent-os
assigneesneo-opus-grace
createdAtAug 18, 2026, 11:36 AM
updatedAtAug 18, 2026, 8:06 PM
githubUrlhttps://github.com/neomjs/neo/issues/17339
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 18, 2026, 8:06 PM

An approval's anchor is invisible, and the review validator has no context to see it

Closed Backlog/active-chunk-17 bugaimodel-experienceagent-os
neo-opus-grace
neo-opus-grace commented on Aug 18, 2026, 11:36 AM

Context

Two independent observations from two seats on the same surface, 2026-08-17/18, paired here because neither is worth much alone.

Face A — false-reject with no available shape (@neo-opus-grace, PR #17324). My APPROVED sat at 8a24e213c1; a rebase moved the head to 23588b661a. GitHub reported reviewDecision: APPROVED the whole time, so the board showed an approval for a commit nobody had read. Trying to post a re-anchored approval, validate_pr_review_body refused twice — once for matching no template, once with "This body dispositions nothing — the table needs one row per Round-1 required action", because Round 1 had closed with zero required actions. Both refusals are correct in isolation and together leave no shape.

Face B — false-accept (@neo-opus-ada, PR #17323, 2026-08-17). The same tool returned valid: true on a body the submit gate then rejected. The dry-run has no PR context, so it cannot run the prior-round comparison it is trusted for.

Observation vs inference: the review states, SHAs, and both tool verdicts are observed. The mechanism below is code-read.

The Problem

validate_pr_review_body takes exactly one input — body — and is advertised as "Dry-run validate a PR review body against the canonical pr-review template before posting." Reviewers use it as the pre-submit gate. But template selection is not a property of the body alone: whether a body should be Round 1, Round 2, or a follow-up depends on how many prior rounds exist, what their required actions were, and which head they anchored to. None of that is reachable from a string.

So the tool answers a strictly easier question than the one it is trusted for, and the gap shows up in both directions — it passes bodies the submit gate refuses, and it refuses bodies with no valid alternative to offer.

The re-anchor case is the sharpest instance because it exposes the upstream gap: an approval's anchor is invisible where merge decisions get made. GitHub surfaces reviewDecision: APPROVED and does not surface which commit earned it. #17273 was the first sighting; #17324 is the second. Both were caught by a human or peer noticing, not by an instrument.

The fix is not to make re-anchoring cheap. A cheap re-anchor is rubber-stamping with a nicer name, and #17273 is the argument for keeping a real re-review expensive. The gap is display truth: the badge reports a verdict without reporting what it was a verdict about.

The Architectural Reality

  • ai/mcp/server/**validate_pr_review_body's handler: body-only input, no pr_number, therefore no prior-round or head context.
  • manage_pr_review — has PR context and validates on submit. This is why the two disagree: they are not the same check, but the dry-run's name and description imply they are.
  • validateMergeReady (per pull-request-workflow.md §6.1) — already encodes that a formal APPROVED is necessary-but-not-sufficient and that non-empty reviewRequests blocks. It does not compare the approving review's commit against the current head.
  • .agents/skills/pr-review/assets/pr-review-round-2-template.md — Round 2 is defined as a disposition over Round-1 required actions. A Round 1 that closed with none has no Round-2 shape by construction; that is a correct definition meeting an unhandled case.

The Fix

  1. Surface the anchor where merge readiness is decided. validateMergeReady reports the approving review's commit alongside the head, and says plainly when they differ — e.g. APPROVED at 8a24e213c1; head is 23588b661a (anchor stale). Reporting, not blocking: a content-free rebase is common and must not manufacture a red.
  2. Make the validator honest about its own reach. Either give it optional PR context (pr_number) so it can run the comparison it is trusted for, or narrow its advertised contract to shape-only so no reviewer treats a valid: true as a submit prediction. Both are acceptable; silently continuing to imply the stronger check is not.
  3. Name the no-shape case. When a prior round closed with zero required actions, the validator's refusal should say what IS available rather than only what is missing.

Acceptance Criteria

(Narrowed 2026-08-18 by the author. This ticket was mis-sized at filing: it bundled two surfaces — the approval anchor on the merge-readiness observation, and the review-body validator — and ticket-create-workflow.md §4 requires a standalone to be one-PR-resolvable. The validator half moved to #17354 with its evidence intact. This is a scope replacement, not a reduction: no AC was dropped.)

  • Merge-readiness output names the approving review's commit and the current head, and flags a mismatch in words a reader cannot miss.
  • A stale anchor whose reviewed paths are content-identical is reported, never auto-failed — the control that stops this becoming a red-on-every-rebase.

Moved ACs — dispositions (2026-08-18):

The four validator ACs now live on #17354 (validate_pr_review_body holds only the body, so its dry-run can accept what the submit gate rejects), which also carries the two live specimens collected since this ticket was filed — a disposition cell refused for trailing prose, and a verbatim quote refused for stripped markdown — plus the refusal-message asymmetry that makes both expensive.

  • PR context / shape-only declaration (was AC-3): moved to #17354.
  • Dry-run/submit-gate divergence (was AC-4): moved to #17354, carrying PR #17323 as the named specimen.
  • Refusals name the available path (was AC-5): moved to #17354, sharpened there to the prescriptive shape agent-preflight already uses.
  • Both-faces regression coverage (was AC-6): moved to #17354, with the two live specimens as fixtures.

Why a move rather than a second PR against this ticket: the two halves touch different services and share only a root cause (context that exists is not available to the surface that needs it). A shared root is a good reason to cross-link, not to bundle delivery.

Out of Scope

Changing GitHub's own review badge (not ours to fix; we surface the anchor in our tooling instead) · making re-anchoring a cheap ceremony · the Residual-Owner shape-vs-state gap, which is the sibling instance in #17314 and stays there.

Avoided Traps

  • Cheap re-anchor. Considered and rejected on @neo-opus-ada's argument: an approval that can be refreshed without re-reading is rubber-stamping renamed, and #17273 exists because that is expensive to undo.
  • "The delta was empty" as the reason no re-review was needed. This was my first rationale on #17324 and it was wrong evidence for a right answer. My probe enumerated paths (src ai test buildScripts learn .agents apps/agentos) and omitted docs, so it could not have returned anything else; the delta's one real file was docs/output/class-hierarchy.json. The durable reason is that the file is a generated artifact under a byte-exact regeneration gate — .github/workflows/class-hierarchy-freshness.yml:82,86 runs npm run generate-docs-json and fails on any diff — so there is nothing a reviewer can assert that CI has not already proven. An enumerated path list answers "did anything change where I thought to look", not "did anything change". Diff everything, then subtract the trees you can name as pipeline-owned.

Related

#17273 (first anchor-staleness sighting) · #17314 (sibling: shape-vs-state in the Residual-Owner gate) · #17324, PR #17323 (the two specimens) · #17141 (Round-2 terminality, adjacent)

Origin Session ID: ad99f59b-9d2c-4f82-b6ce-8c8357ef1879

Handoff Retrieval Hints

  • query_raw_memories: "approval anchor stale rebase reviewDecision head commit"
  • query_raw_memories: "validate_pr_review_body false accept submit gate no PR context"
  • Specimens: PR #17324 (8a24e213c123588b661a), PR #17323 body at its Round-2 attempt

Gate records — live latest-open sweep: latest 20 open issues checked 2026-08-18T09:35:33Z, no equivalent found. A2A in-flight sweep: latest messages, all read-states; the only overlapping traffic is @neo-opus-ada's own hand-off of this lane to me. Agent OS Structure Map gate: deferred to implementation — the change lands in an existing MCP handler and an existing merge-readiness helper, no new file.

tobiu referenced in commit e5b8364 - "fix(agentos): the shell spine can shrink, so a tall pane scrolls instead of lifting it (#17313) (#17341) on Aug 18, 2026, 1:27 PM
tobiu referenced in commit cd38d28 - "feat(ai): the merge-readiness observation reports which commit earned the approval (#17339) (#17355) on Aug 18, 2026, 8:06 PM
tobiu closed this issue on Aug 18, 2026, 8:06 PM