Problem
Narrow leaf split from #17141, which spans two surfaces with different shapes: the runtime admission in PullRequestService.managePrReview(), and the skill payload (pr-review templates, Round-2 asset, byte budget). This ticket is the runtime half only, so it can carry an honest close target while #17141 stays open for the rest.
The shipped budget counts CHANGES_REQUESTED reviews globally against a ceiling of two, and its escape hatch accepts any non-empty single line as a reason.
Both measure the cheap thing. The global count is wrong in two directions at once: one family's two rounds silence a family that has never seen the PR, and a single family can spend both rounds itself and still call it a budget. The free-text override accepts "Operator-declared release safety exception" — a sentence that asserts nothing checkable and is indistinguishable from the ordinary later discovery the exception exists to refuse.
The Architectural Reality
managePrReview() is the existing atomic admission point and stays so — no second review tracker.
- Reviewer-family facts live in
ai/services/graph/agentFamilyResolution.mjs (getCoreSwarmAgentFamilies()), which reads the identity graph. Budget classification consumes that authority rather than inferring from login prefixes.
- The service could not name its own reviewer:
fetchAndCacheViewerPermission asked GitHub who the viewer was, read the permission off the answer, and discarded the identity. A budget cannot charge a round without it.
The Fix
- Count per canonical reviewer family, ceiling of one, across heads and retractions.
- Fail closed on a submitter the identity graph cannot place — an unrostered login would otherwise spend nobody's budget and review without limit.
- Replace the free-text override with a repair-minted receipt naming
old-head, new-head, prior-fact, repair-coordinate, where old-head is verified against the PR's own review population. That clause is the one a mistaken or invented receipt cannot satisfy by being better written.
- One re-entry per family, terminal — otherwise the exception becomes the budget.
Acceptance Criteria
Out of Scope
Everything in #17141 that touches .agents/skills/pr-review/** — the Round-2 disposition asset, the follow-up-template replacement, the byte-budget net-decrease, the A+FU counterfactual, and the cohort receipt. Those land separately; this ticket does not weaken the cross-family approval requirement or the human-only merge gate.
Evidence class
Source-verified at dev: PullRequestService.mjs budget validator and reviewBudgetOrdinaryRcLimit, RepositoryService.fetchAndCacheViewerPermission, agentFamilyResolution.mjs. Intake for the parent #17141 recorded at https://github.com/neomjs/neo/issues/17141#issuecomment-5301088070 and is inherited here — this is a scope split, not a new premise.
Related: #17141
Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb.
Problem
Narrow leaf split from #17141, which spans two surfaces with different shapes: the runtime admission in
PullRequestService.managePrReview(), and the skill payload (pr-reviewtemplates, Round-2 asset, byte budget). This ticket is the runtime half only, so it can carry an honest close target while #17141 stays open for the rest.The shipped budget counts
CHANGES_REQUESTEDreviews globally against a ceiling of two, and its escape hatch accepts any non-empty single line as a reason.Both measure the cheap thing. The global count is wrong in two directions at once: one family's two rounds silence a family that has never seen the PR, and a single family can spend both rounds itself and still call it a budget. The free-text override accepts
"Operator-declared release safety exception"— a sentence that asserts nothing checkable and is indistinguishable from the ordinary later discovery the exception exists to refuse.The Architectural Reality
managePrReview()is the existing atomic admission point and stays so — no second review tracker.ai/services/graph/agentFamilyResolution.mjs(getCoreSwarmAgentFamilies()), which reads the identity graph. Budget classification consumes that authority rather than inferring from login prefixes.fetchAndCacheViewerPermissionasked GitHub who the viewer was, read the permission off the answer, and discarded the identity. A budget cannot charge a round without it.The Fix
old-head,new-head,prior-fact,repair-coordinate, whereold-headis verified against the PR's own review population. That clause is the one a mistaken or invented receipt cannot satisfy by being better written.Acceptance Criteria
CHANGES_REQUESTEDfrom a family spends that family's budget across later heads and retractions; another active family retains its independent round.old-headmatches a head some prior review was actually submitted against;new-headmust be the head under review; identical heads refuse.Out of Scope
Everything in #17141 that touches
.agents/skills/pr-review/**— the Round-2 disposition asset, the follow-up-template replacement, the byte-budget net-decrease, the A+FU counterfactual, and the cohort receipt. Those land separately; this ticket does not weaken the cross-family approval requirement or the human-only merge gate.Evidence class
Source-verified at
dev:PullRequestService.mjsbudget validator andreviewBudgetOrdinaryRcLimit,RepositoryService.fetchAndCacheViewerPermission,agentFamilyResolution.mjs. Intake for the parent #17141 recorded at https://github.com/neomjs/neo/issues/17141#issuecomment-5301088070 and is inherited here — this is a scope split, not a new premise.Related: #17141
Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb.