Skip to content

Agent: review ​

Reviews a PR with the full change in context and reports ranked, evidence-anchored findings. Gather once, analyze once — a review takes minutes, not an hour.

  • Code: src/tools/reviewRead.ts (bounded pinned Git source pages), src/core/reviewHumanFindings.ts, src/core/dispatch/reviewHistoryRestore.ts (canonical page reconstruction and separate fresh validation), src/execution/workspaceAdvance.ts (bound-checkout Git prefix and advance), src/core/modelProxy/gitBindings.ts, src/channels/githubDoor.ts (model transport closure and original post-step claims), src/execution/factory.ts (physical sandbox binding); src/core/dispatch/provision.ts (logical thread and run ID forwarded to attach); src/agents/registry.ts (REVIEW_SYSTEM; resident-path variant REVIEW_SYSTEM_RESIDENT; seeded-sandbox variant REVIEW_SYSTEM_SEEDED); the PR head preflight and the attached-head guard as gates in src/core/dispatch/authorize.ts (authorizePrHead, authorizeAttachedHead; items 10–11); the reviewed-head settle, the verdict turn and the post-step in src/core/dispatch/runLoop.ts (items 5, 8, 12 and 18; the settle is settleReviewedHead and the step runReviewPostStep, both in src/core/reviewRound.ts; the turn is runVerdictTurn in src/core/verdictTurn.ts) + src/core/reviewPublication.ts + src/core/reviewPost.ts + src/core/reviewVerdict.ts + src/core/reviewHistory.ts + src/core/reviewedHead.ts + src/core/digestCoverage.ts (item 15) + src/core/headMoved.ts + src/execution/githubComments.ts; verdict tool submit_verdict in src/tools/submit.ts, src/core/reviewFindingSchema.ts and digest tool diff_digest in src/tools/diffDigest.ts, both in the readonly toolset (src/tools/toolsets.ts); src/core/coordinator/pullOwnership.ts
  • Tests: src/core/reviewHumanFindings.test.ts, src/core/dispatch/reviewHistoryRestore.test.ts, src/core/reviewWorkspace.test.ts, src/core/modelProxy/gitBindings.test.ts, src/channels/githubDoor.test.ts (closed model transport), src/execution/factory.test.ts; src/core/dispatch/provision.test.ts; src/core/reviewPublication.test.ts; src/core/reviewRound.test.ts; src/core/reviewVerdict.test.ts; src/execution/githubComments.test.ts; src/core/dispatch/runLoop.test.ts; src/core/coordinator/managedReviewOwnership.test.ts
  • Docs: The agents and their toolsets
  • Budgets: 25 min / 64k tokens (the turn guard is derived: 150, six a minute — harness-pi.md item 15) · built-in effort medium (a floor — every config layer overrides it, routing-and-config.md item 2) · toolset readonly (bash + read; read-only by convention)

Behavior ​

Workspace setup publishes the fallback reason before the attached-head gate can refuse. Fresh and refreshed cold checkouts require successful, untruncated structured command results. Failed checkout preparation stops admission before a model turn; an unavailable structured executor refuses rather than inferring command success from display text. HEAD reads and repairs retain their separate failure evidence in the run trace.

  1. Gather once. Read PR metadata, the complete diff and the changed source files in a small batch, with source pages as needed. Prefer the pinned review_read diff pages at their default size and continue with the returned hash and offsets to EOF. If the request requires run_check, save the complete read-only diff once under $TMPDIR and page the saved file with read. Do not use repeated small shell slices. A truncated receipt is partial evidence. Missing source keeps the review incomplete. Enormous PRs read risky files in full and state what was skimmed. These instructions guide the model; fewer calls and live review completion still require observation.
  2. Analyze once with everything in context: correctness bugs first, each with a concrete failure scenario; then significant design regressions. Verify candidates with targeted follow-up reads of affected callers, callees, tests, and contracts; check for evidence that disproves each candidate. Necessary verification reads have no fixed count, but every read must answer a specific question about a candidate.
  3. Report only verified, consequential findings with severity, confidence, and file:line, most-severe first. The review prompt and code-review-and-quality skill use an evidence and impact filter adapted from Codex's public review rubric, with candidate verification following the published Claude Code Review method: the issue must be introduced by this PR, supported by a concrete failure path or explicit applicable project rule, and worth the author's attention. Uncertain suspicions, pre-existing problems, cosmetic preferences, and failures already reported by automated checks are omitted. A reproducible failure under realistic input or existing state qualifies. The explicit spec, test-guard, and unit-contract checks below remain project rules. Every new finding declares kind: single|pattern; an outstanding finding may instead be re-raised with its exact scoped ID and optional new cases. submit_verdict refuses an untyped finding, a pattern without an invariant and non-empty well-formed case table, or a single carrying a matrix, while old stored findings remain readable. The tool validates the submitted shape; it cannot infer missing scenarios from a self-reported table. The reviewer must name the shared invariant and independently checkable cases (scenario → expected) across the selection and execution paths visible in the diff, not only the first counterexample. For push safety, check the source, destination, endpoint and command-composition axes (including background or piped execution) together. A re-review checks each prior row, widens an incomplete table under the same finding id, and gives a genuinely different invariant a new id; sharing a file alone proves neither. A correct change gets a plain "looks correct" — no manufactured findings.
  4. Maintains the status-card checklist: ✱ from the item's first command, ✓ only once its result is read, never a reporting step it has not done (the one rule of run-visibility.md item 2).
  5. Declares the verdict structurally via the submit_verdict tool (approve | request_changes + one-line summary + typed findings; readonly toolset only); the final message is the review's text alone — one paragraph per finding keyed by its id, no verdict line of its own, no recap of what was read or verified (REVIEW_FINAL_MESSAGE in the three review prompts), because both surfaces render the verdict and the findings from the tool call (item 5b). The final prose supplies detailed supporting evidence under For agents; the human fields carry the public explanation and keep all material conditions and evidence limits visible. The verdict is a judgement the model makes; how it reaches GitHub and the thread is not up to the model (items 5b and 6). The one exception to the mandatory call is item 10's preflight-infrastructure failure: if the REVIEW TARGET first-command HEAD check disagrees, the agent stops with no finding or verdict, and the dispatcher reports it without entering the post-step. The verdict turn (verdictTurn.ts): a prompt rule alone can be skipped — a run has written Verdict: approve in its prose and never called the tool, so its review posted as not approving — so a review of a pull request whose loop ended with no verdict is given ONE more bounded model turn asking for the call (runVerdictTurn, one more prompt on the run's own pi session, harness-pi.md item 14; clipped to VERDICT_TURN_MAX_TURNS and the review post-step's minutes carved from the lease's remainder (postStepLease), under a run.verdict_turn span, announced by a verdict_turn run note). The turn runs after the head settle (item 12; complete history that differs from the observed checkout gets no verdict-only turn, whether the model submitted or stopped without a call; missing verdicts and correctible submissions at matching heads retain the bounded correction) and before the post step; it asks for the verdict alone — the write-up stands as the review's text, the diff is not re-read, the turn's own reply is dropped — and the verdict arrives through the same onVerdict hook the loop fed. It is not run for a hard stop, for a relaunch that ended the run on its budget (harness-pi.md item 16: the replaced container was never re-attached and the budget's answer is no verdict), for a request that opted out of the GitHub post (item 6), or for a finish plan with no session to prompt (its note says so). Fail-closed is untouched: a turn that still submits nothing posts the no-verdict line as before. A finding may carry humanGated: true beside its severity — set only when the finding's remedy is a receipt no run can produce (a replay that needs a provider credential no sandbox holds, a procedure a person runs live), never for work a fix round could do: the parser round-trips exactly the literal true (anything else drops the flag and the finding stands actionable), the stored shape and the redaction keep it, the compact finding line renders it as (human-gated) and the posted body's machine marker carries the full typed finding — and ship's coordinator reads the flag, never prose, to park a round whose actionable findings all carry it as a live question. The next person's unit-thread reply, or a pull-request conversation comment from the requester's bound GitHub account, resumes a fix round briefed with both the finding and attributed answer; an adopted attempt reads only a newer comment from that same trusted account after the last marked verdict and fixes before reviewing (agent-ship.md item 9). 5a. The severity gate is the verdict parser's: an approve carrying a finding at or above the severity to address is a request_changes (reviewVerdict.ts parseVerdictInput). The severity to address is one lever for the whole loop — the level a review round has to act on, and the level ship holds its rounds to (agent-ship.md item 9) — resolved once per run by the dispatcher, most specific wins: the request's severity:<level> directive, the user's review.addressSeverity scope, the channel's, the org's review.addressSeverity (default minor; ADDRESS_SEVERITIES is the findings ladder blocking|major|minor|nit, validated at load on the org block and every scope, set per scope with config set channel|me --review.addressSeverity <level>). The level rides the run loop's context into submit_verdict and into the restored verdict of a resumed run: an approve whose findings include one whose DECLARED severity sits at or above it is parsed as request_changes, the summary appended [downgraded from approve: finding <id> (<severity>)[, …] at or above <level>, the severity to address], and the tool's ack names the same — keyed on the raw entries, so a gated finding that drops for another reason still poisons the approve, while a severity outside the ladder (fyi, none) drops with a note and never opens the gate. The model is told the same rule in the prompt and the tool text (approve means no finding at or above the level remains; nits alone never block; minor by default), but the guarantee is the parser's: since the posted body's first line is built from the parsed verdict (item 6), LGTM: is minted only when every finding sits below the level in force, so a repository's auto-approve-review-lgtm.yml can never approve over a finding the loop would have to address. A ship review child receives its instance's level as the severity: directive of its request (agent-ship.md item 5, composeChild), so it resolves to the hand-off's answer rather than the review thread's scope; the coordinator's own check of a posted approve against the level is defense in depth behind this one. 5b. Both surfaces are rendered from the typed verdict, never from the prose (buildReviewPostBody and buildReviewChannelReply in reviewVerdict.ts; the reply seam is deliverAnswer in reply.ts, fed the loop's verdict and reviewPost). The GitHub comment leads with a deterministic LGTM: or Changes requested: line and the typed issue count (no issues found, N issues, or issues not itemized); no accepted complete latest submission uses the incomplete-review warning and grants no formal verdict. Each issue is a separate bullet, most severe first, with a severity glyph and label and a file link pinned to the reviewed head. Fresh submissions carry humanFindings, exactly one per active finding ID: current problem, concrete scenario, impact, evidence basis/detail, requested change, and current-head file/line. The human block presents these fields with the problem first and the three labels in item 5c. The fields are capped at 900 Unicode code points before acceptance rather than clipped. Evidence distinguishes a code trace, isolated reproduction, deployed reproduction, or repository rule; reproduction detail names its environment. The tool refuses missing, duplicate, unknown, empty or oversized explanations. A current explanation and locator may change after a partial fix without changing history-owned finding metadata. Legacy records without this field retain their previous rendering. Canonical storage, accepted replay and redaction retain the new fields; they do not enter the machine marker or change its authority. The display preserves the complete title up to 180 Unicode code points, then clips with an ellipsis; it never guesses sentence boundaries from punctuation, which can cut off meaning after abbreviations. The original title remains below the fold. New reviewers write that title as one concise sentence in plain language; paths, spec item numbers and implementation detail belong in the location or evidence. The reviewed head is shown once. There are no visible tables or scoped finding IDs. One collapsed <details><summary>For agents</summary> holds the submitted rationale, full finding index with exact IDs and titles, pattern invariants and every case, reviewer-verified resolution evidence, and the model's text under Full review. Raw HTML in folded evidence is escaped as text so it cannot close the fold or open another one; Markdown code spans and fenced code retain their literal examples. The fold is omitted only when all of that is empty. A machine-readable <!-- switchboard:verdict {"verdict","head","findings":[{id,severity,file,title,kind?,invariant?,cases?,line?,humanGated?}]} --> marker remains last, preserving original IDs and titles and every typed field. Recovery reads only the last complete marker line, after rendered cases and review prose (an optional carried-head footer may follow): marker-shaped case text cannot authorize a human-gated answer. The GitHub limit is applied to Unicode code points before publication: only the Full review prose is clipped, at a code-point boundary with a visible note, while the summary, folded typed evidence and marker stay whole. When those fixed sections leave no room for a bounded review, publication refuses before the GitHub write so the write-up follows the Slack-only path; fixed sections exceeding the conservative 65,000-code-point ceiling also refuse. The thread reply: the token line, one bullet per finding (formatFinding), then Posted to <repo>#<n> joined to the run link (item 13) — the verbose shape; below verbose (the default — routing-and-config.md item 28, record 0066) a posted itemized verdict is ONE line, verdictCountsLine with the pull request link: LGTM, or Changes requested: 2 blockers, 1 major, 2 minor, 3 nits with only the non-zero counts, most severe first, since the full verdict, the finding lines and the prose already stand on the pull request. The write-up rides along in the thread only when it landed nowhere else — no GitHub post (Slack-only, an opt-out, a guard refusal) or findings not itemized — so the review's text is always somewhere a person reads it; with no accepted complete latest submission the reply retains the incomplete-review warning, the report and the available run link. The run record's answer event stays the model's own words. Why: the reply and the comment used to be the model's whole final message, so a nine-bullet "verified clean" tour and a "what I reviewed" paragraph reached the thread and the PR alike, and no prompt rule bounds a model's prose for long; rendering the surfaces from the typed verdict makes the length a property of the code, the same way the LGTM: token already is, and the prompt rule in item 5 only has to govern the write-up under the fold.

5c. Plain human explanations. Each new human finding leads with severity and a bold sentence naming the observable problem. The current code link follows the problem. Three short labels follow: Why it matters combines the concrete trigger and effect; Evidence names whether the claim comes from reading code, a separate test, the deployed system, or a project requirement, then gives the decisive fact and its limits; Change states the smallest needed correction. The model must use familiar words and explain any essential technical term. A code identifier cannot stand in for what the person does or what goes wrong. Exact call paths and lengthy supporting evidence belong under For agents; material conditions and uncertainty stay visible. The shared system and tool guidance ask what breaks, when it happens, why it warrants a change, what establishes it, and what correction is needed. A rule-only finding identifies the applicable requirement without inventing runtime harm. Required-field and length checks prove structure, not model comprehension or finding accuracy; those remain explicit model-output acceptance checks. The prompt-delivery test uses independent expectations for one paragraph per finding, no recap and no clean-check list; matching shared tool guidance alone does not prove those final-message instructions were delivered.

  1. Posts the review back to the PR by default: the user never has to add "and post to the PR". A deterministic dispatcher post-step (decideReviewPost in reviewPost.ts) fires whenever the resolved agent is review AND a PR was resolved (RepoContext.pr — from a PR URL or owner/name#N in the current message, else inherited from the thread; next item); the bot then posts the body item 5b renders — the verdict line, short issue bullets, and the evidence under For agents — to that PR as a comment — never an approval or a merge. This is a system-level guarantee (code, not model memory), so it holds identically on the sandbox and resident paths.

    • Deterministic verdict line (auto-approve contract): the repository's opt-in .github/workflows/auto-approve-review-lgtm.yml workflow — on only where the REVIEW_BOT_LOGIN and REVIEW_BOT_ID repository variables name the review App, inert otherwise — approves a PR when that App's review body starts with the exact token LGTM:. That token is written by code, never by the model's prose: reviewVerdict.ts buildReviewPostBody puts LGTM: <issue count> first iff the submitted verdict is approve, Changes requested: <issue count> for request_changes, and Review incomplete — findings not validated. when no complete latest submission was accepted (fail-closed). The model's text follows after a blank line, so a body can begin with "LGTM" only through an explicit approve verdict — whatever the prose says. The channel reply follows item 5b.
    • Mechanism + auth: the post runs in the bot process via the GitHub REST API as a COMMENT-event pull-request review (POST /repos/{repo}/pulls/{n}/reviews with event: "COMMENT" — the event the auto-approve workflow listens to; never APPROVE/REQUEST_CHANGES), pinned with commit_id to the PR head SHA captured at repo resolution (RepoContext.headSha, same REST call as the head ref) so the workflow's stale-review guard can refuse a verdict that predates a newer push (githubComments.ts), with the GitHub App installation token (App needs pull_requests:write) — the same REST-with-App-token path repo/PR resolution uses, never a gh shell-out and never from inside the sandbox/resident (AGENTS.md invariant 5). The original fenced run records exact pending publication before POST. A positive native receipt and durable accepted result certify a posted review; a definite native refusal records no write. Lost, unreadable or mismatched native responses remain uncertain and cannot authorize a retry or claim that no review posted.
    • The agent never self-posts — enforced at the token, not just the prompt: REVIEW_SYSTEM/REVIEW_SYSTEM_RESIDENT instruct the model to just produce the review and NOT run gh pr comment or any comment-creating call — but the prompt is the weak guard. The strong guard is least-privilege: the readonly toolset's sandbox gets a read-scoped GitHub App installation token (contents, pull_requests, issues, actions, checks, metadata — all read, the exact set in execution.md item 5 — githubApp.ts resolveGithubToken("read"), wired in factory.ts githubEnvs by toolset). So even though the review sandbox has gh + the credential helper, it can gh pr view/gh pr diff, clone a private repo and read its CI (gh pr checks, gh run view --log) but physically cannot comment, review, push or re-run a workflow — closing the double-post and prompt-injection (untrusted diff) hole at the token. The full (coding) toolset keeps a write-scoped token; the bot-process post (above) uses its own write token, so the deterministic post is unaffected. There is exactly one comment, and it comes from the bot.
    • Opt-out: an explicit "don't post" / "slack only" (or post:off) in the request suppresses the GitHub post; the review still replies in Slack.
    • Re-reviews inherit the thread's PR, fail-closed: a rereview(thread) reply names no PR, so resolveRepoContext (repoContext.ts) falls back to the thread's PR — the last user turn referencing a PR of the resolved repo — and re-fetches it now. The inherited PR is used only if the fetch succeeds, the PR is open, and it yields a well-formed head SHA to pin the review to; a closed/merged PR, a failed fetch, or a malformed SHA all leave pr unset (Slack-only) rather than post a verdict to a stale PR or an unpinned verdict a newer push could inherit. A PR named in the current message always wins; redirecting the thread to another repo by a strong signal (URL / owner/name#N) drops the PR — a bare owner/name-shaped token (a file path such as docs/reference/specs/memory.md, prose) never does (see docs/reference/specs/resident-repos.md item 29); the thread's ref is never rebound from an inherited PR (inheritance serves the post-step only).
    • A Slack-only verdict is never silent: when the thread's bound PR could not be posted to — closed/merged, or its head could not be verified (failed fetch, malformed SHA, unknown state; RepoContext.prUnpostable) — or the native post is definitively refused, the dispatcher replies in the thread ℹ️ Review not posted to <repo>#<n>: <reason> — this verdict is Slack-only. in addition to the [review-post] log line, so an approve in Slack is never mistaken for an approve on GitHub. An unconfirmed native response instead says I could not confirm whether the review posted and retains an uncertain receipt; it never claims Slack-only or emits a definitive review_not_posted event. Two cases stay log-only: a review with no PR anywhere (pasted code) — nothing was expected to be posted — and an explicit opt-out ("slack only", "don't post"), which wins over an unpostable bound PR: the user already knows, and the log says opted out.
    • Only for PR reviews, never silently: a review with no resolved PR (pasted code, or a repo mention with no PR) posts nowhere — no crash, just the Slack reply — and the dispatcher logs [review-post] <thread> skipped: no PR resolved|opted out so a missing GitHub post is diagnosable from logs.
  2. Resident-path variant (resident-repos.md §31): in a resident repo environment the dispatcher swaps in REVIEW_SYSTEM_RESIDENT via RunOptions.system — same gather-once discipline, but against the ready worktree (already on the branch under review, deps installed; no cloning) using git directly, since gh is not in the resident image.

  3. Reviewed-head guard — a review is posted to a PR only if it is a review OF that PR's head (reviewedHead.ts): immediately before the write, the post-step re-reads the pull request's open head; a merged, closed or unreadable pull request has no postable head, so the verdict is recorded as Slack-only and no review lands after terminal state. With the resident's per-thread bindings, pool users and exec routing all holding, an agent can still fetch and check out another PR's branch because the PR under review references it, review that tree, call submit_verdict approve, and have the deterministic post-step put LGTM: on the wrong PR — which the org's auto-approve workflow then approves on wrong evidence. Every other layer does its job; this one asks "is this review of this PR?". (a) after the model finishes and before the workspace is released (post-release a resident re-attach would show the ref's current tip, not what was reviewed), the dispatcher runs git rev-parse HEAD in the run's workspace itself — the observed head, authoritative; (b) submit_verdict requires head (the agent's own git rev-parse HEAD) — the reported head, consulted only when nothing was observed (the cold sandbox's cwd is the workspace root, not the clone); (c) checkReviewedHead({expected: RepoContext.headSha, observed, reported}) must pass or the post is skipped with a thread note that names the source (workspace-observed or model-reported), prints both usable SHA values in full and identifies their first differing hex position — never two identical seven-character prefixes — before saying the verdict is Slack-only. Fail-closed on every unknown: no headSha at resolution ("PR head unknown"), no observed and no reported head ("reviewed head unknown"), and a matching reported head never rescues a mismatching observed one. A posted review is therefore always pinned (commit_id = the verified head) — the unpinned post path is gone. Both review prompts now say: review the PR's own head, never fetch/check out another branch or PR even when referenced; if the change depends on unmerged work, say so as a finding. Upstream of this guard, the resident attaches at the resolved head itself (resident-repos.md item 51: the bot passes RepoContext.headSha to /attach and the resident fetches a mirror whose ref tip lags it), so a mirror that has not yet fetched a push is not what the guard catches — the guard stays the backstop for an agent that strays, not the outcome of an ordinary re-review.

  4. The agent is told its target — deterministically, from resolved facts (reviewTarget.ts; the upstream complement of item 8, which is the backstop): the dispatcher already resolves repo, pr, head branch (ref), head commit (headSha) and now the base branch (RepoContext.baseRef, from the same GET /pulls/{n} call, validated as a ref) before the model turn — and until this item used them only after the run (to post and to guard) while the model got a Slack sentence with a URL and a prompt saying the worktree was "typically" the branch under review. Now a review run with a resolved PR gets a REVIEW TARGET block appended to its system prompt on both paths: repository, PR number + URL, head branch, head commit, base branch (an unresolved head branch/commit is named as unknown, never invented; an unknown base renders as "the repository's default branch" and the diff target falls back to origin/HEAD). Resident path: "the worktree is already at that head; FIRST command git rev-parse HEAD, which must equal the head commit — if not, STOP, report the two heads as an infrastructure failure, submit no finding or verdict, fetch/check out nothing"; origin/<base> is already in the clone, so the diff is against it and git fetch is not run (the resident read-only attach removes the ability anyway). Cold sandbox path: Switchboard provisions the checkout at the resolved PR head before the model starts, then the same HEAD check; the agent neither clones nor checks out a ref. Both: pass that commit as head to submit_verdict. The resident review prompt no longer hedges ("typically the branch under review" is gone — the block states the branch) and no longer instructs a fetch. Coding runs and PR-less reviews get no block — their prompts are byte-identical to before. Because the block and the item 8 guard read the same RepoContext, they can disagree only when the agent strays — and then the guard refuses the post.

  5. The workspace is named and checked on every backend before the run — the agent has nothing to go looking for: item 9's "FIRST command git rev-parse HEAD" assumed the model would run it where its shell starts. Left to find the worktree itself, an agent whose shell starts elsewhere cds around until it lands in the resident's own warm default-branch checkout, runs git rev-parse HEAD there, reports that HEAD as a mismatch and submits request_changes — with the worktree attached at the PR head all along. Two fixes, both from facts the bot already holds. (a) The attach answer's workspace rides into the prompt: ResidentBinding.workspace (from POST /attach, resident-repos.md item 16) → ExecutorSelection.binding → the resident preamble ("Your shell starts in the worktree <path> on every bash call …") and the REVIEW TARGET block, which now pins the first command "from the current directory, no cd" and states that any other checkout on the host — the resident's default-branch checkout included — is NOT the PR; an attach answer without the path names no path (nothing invented) and keeps the no-cd rule. (b) The dispatcher attaches and proves the pull request's checkout, never the unit's or thread's branch: whenever a unit or direct run reviews a pull request, the attach binds to that pull request's head branch and expected commit, even when the unit's own contract branch or an older sticky resident binding names another ref. The guard runs git rev-parse HEAD through the attached executor and compares that observed checkout HEAD with the head commit being reviewed; neither ResidentBinding.sha nor Executor.moveTo().sha is proof of what is checked out. A seeded sandbox uses its named checkout; an unseeded cold backend first initializes a clean workspace with the exact bound Git door origin, fetches origin/HEAD, the resolved base and refs/pull/<n>/head with the run's read identity, checks out the resolved PR-head SHA, and only then runs git rev-parse HEAD there. Equal (≥7-hex prefix match, sameCommit) → the block says Switchboard verified it before the run. When they differ, one current-head read may adopt a raced push only when the workspace is already at that current head; otherwise the dispatcher reprovisions the workspace at the expected reviewed head and checks the resulting HEAD once. Only a failed reprovision or second mismatch refuses: card 🔀 … not started (workspace head mismatch), ONE reply quoting the full workspace and expected SHAs (or the unreadable source), without asserting why they differ or calling the condition a bug, the workspace released, no provider call. This setup outcome is infrastructure, never a finding or verdict, and the post-step is unreachable, so nothing is posted to GitHub. Review-only: a coding run attached elsewhere still runs (it branches off whatever it has).

  6. Head moved mid-run — the thread is told (headMoved.ts): a review is posted pinned to the head it examined (item 8), and the org's auto-approve workflow skips a review whose commit_id is not the PR head — so a push that lands while the run is in flight can never get unreviewed code approved. That protection is silent: the thread sees "review posted" while GitHub shows the review against an outdated commit and nothing auto-approves. After a successful post the dispatcher fetches the PR head once more (CoreDeps.fetchPrHead, default currentPrHeadSha — one REST GET, best-effort) and, when it is not the reviewed commit, replies ℹ️ <repo>#<n> moved during the run: reviewed <x>, head is now <y>. The review was posted pinned to <x> and will not auto-approve — re-request to review <y>. The post itself is unchanged (still pinned to the reviewed head — the review IS of that commit). Unknown current head (fetch failed, malformed) → no note, never a false alarm; nothing posted (guard refused, opt-out, hard stop) → no fetch. Distinct from item 8: the guard is about what the agent reviewed; this is about what happened to the PR afterwards.

  7. Unknown head — the run is not started: a re-review reading "re-review: rebuilt on main after the base landed…" binds ref: "main" from the prose; a resolver that skips the PR head fetch whenever a ref is bound then leaves RepoContext.headSha undefined, the attach carries no sha for the resident to refresh to, the worktree stays at the previous head, the agent spends its turns establishing that the new commit is not in its checkout and writes a request_changes "cannot review" verdict, and item 8 refuses the post ("PR head unknown at resolution time"). Two fixes. (a) A PR named in the message is always resolved (resident-repos.md §29): the head fetch runs whenever the current message names a PR of the resolved repo, whatever ref phrasing sits beside it; the PR's head branch is the ref for that message (prose "on X" — even on branch X — cannot redirect a PR review and binds nothing when the fetch fails). The head sha is the head branch's ref tip, not only the PR object's head.sha: after a force-push GitHub's pull-request object (head.sha, commits) lags the branch ref — observed live for minutes while the new commit was already fetchable by sha — and a review attached at the lagging head.sha reviews a head nobody asked about and refuses with a mismatch. So every PR-head read (prHead behind resolveRepoContext and currentPrHeadSha; fetchPullRequestFacts for ship) also reads GET /repos/{repo}/git/ref/heads/{branch} when the head lives on the base repo and prefers the ref's tip when the two disagree, logging both shas ([pr-head] <repo>#<n>: GitHub's PR object reports head X while refs/heads/<branch> is at Y — using the ref); an unreadable ref, or one that does not point at a commit object, keeps the PR object's sha, and a cross-fork head (no ref on the base repo) never asks. (b) A review with an unknown head is refused before any attach or model turn: review agent + a PR in the message with no headSha, or an inherited PR whose head was unreachable → 🔀 Review of <repo>#<n> not started: I could not resolve the PR's current head from GitHub … Re-send the request in a moment…, card · not started (PR head unknown), [review] … not started: PR head unknown log. Everything downstream of an unknown head is a guaranteed refusal, so nothing is spent on it. The gate and the post-step decide "is this verdict meant to be posted" by ONE predicate, reviewPostIntended in reviewPost.ts (review agent + no opt-out) — a review-like agent or a new opt-out phrasing changes both at once. An explicit "slack only" opt-out (item 6) is exempt — an unpinned Slack-only verdict is what was asked for; a closed inherited PR still runs Slack-only with the ℹ️ Review not posted … the PR is closed note (item 8) — its head is known, only the post target is gone.

  8. Review approval stays with the commit examined. Canonical review keeps its saved publication binding. An explicit standalone head pin also stays fixed. Neither can adopt or re-review another head without a new authorized request. Unpinned standalone review checks the acknowledged checkout before release and compares it with the current PR head. If the checkout already moved, only a verdict and complete review history for that exact head can be kept. Otherwise the old verdict and history are void. If the PR moved after the review, compare lists provide context only. Matching commit messages and file paths do not prove unchanged contents and never carry approval to another commit. With an existing session, the same run checkpoints its producer, advances the bound workspace, confirms the exact new HEAD and performs one fresh review under its original lease. Failed movement or changed checkout binding prevents that review. The new verdict needs current-head history. Without a session or usable comparison context, only the original reviewed head can be posted; its stale status cannot approve the new head. A second move remains subject to publication checks. Legacy carry metadata cannot change a new post's target or add a carried-review footer. Existing historical records remain readable. See reviewRound.ts, headMoved.ts and workspaceAdvance.ts.

  9. The verdict reply carries its run link — projection only: a review run's channel reply (the verdict message, rendered from the verdict per item 5b) ends with a [Live run](<url>) line — joined by · to the Posted to <repo>#<n> note when the post landed — appended at the reply seam when PUBLIC_BASE_URL is set — the verdict is what gets scanned in the review loop, and the status card that already carries the link scrolls away. Appended at the reply seam only, in standard Markdown (each adapter renders its own dialect — Slack a <url|Live run> hyperlink): the run record's answer event and the GitHub post body stay link-free (the run record is the source of truth; channels project from it). No PUBLIC_BASE_URL → the same report and incomplete-review warning when applicable, without a run link; non-review agents get no link on a successful answer (it is not a verdict, and their card carries it) — but ANY run's failure reply (the outer catch's ⚠️ <error> line) ends with the same [Live run](<url>) line when a run started and the URL exists, so a failed run's transcript is one click from the thread instead of a card that scrolled away.

  10. Spec contradiction check — the diff is read against the specs it touches, and only those (specs-coverage.md): after the diff and the changed files are in context and before the verdict, both prompt variants (REVIEW_SPEC_CHECK, one text shared by REVIEW_SYSTEM and REVIEW_SYSTEM_RESIDENT) list the specs the change touches — npm run --silent specs:coverage -- --changed origin/<base>...HEAD when the repository has that script, otherwise, or whenever that command fails (a cold checkout with no dependencies installed — the agent never installs or builds to make it run), by matching the changed paths against each spec's - **Code**: / - **Tests**: header lines — and read those specs, never the whole docs/reference/specs/ tree, so the agent's context stays small and its judgement sharp. For each touched spec the agent judges whether the diff contradicts a numbered behavior statement or a validation criterion; a contradiction is a finding of severity minor or higher with a short plain-language title naming the false promise; put the spec file, numbered item and code/spec discrepancy in the evidence, so under the review loop's default severity the spec is fixed in the same PR or the round does not close. A spec updated in the same diff to match the code is not a finding. A repository with no docs/reference/specs/ skips the step silently. The verdict contract (item 5) is unchanged: the check adds findings, not a new verdict path.

  11. Digest-coverage guard — a verdict is posted only when the digest the agent oriented with covered the PR's whole change (digestCoverage.ts): item 8 proves which commit was reviewed and says nothing about how much of its change was read. A review agent handed a digest that undersold the diff — the tool had distilled the first 120k characters of a unified diff, the Executor's output cap, and counted 13 of a 41-file PR — trusted it and approved (distilled-diffs.md item 4 is the fix at the tool; this is the guard). (a) RepoContext.prSize — the PR's changed_files / additions / deletions from the same GET /pulls/{n} call that resolves the head, explicit or inherited PR alike — is carried only while the PR object describes the head being reviewed: after a force-push the object lags the ref the sha was taken from (item 11a), and its size then describes the old head, so it is dropped rather than compared. (b) Every diff_digest call reports its totals to the run (ToolContext.onDigest, canonical full-range coverage survives later incremental inspection of the same immutable base/head, kept on the ledger row like the verdict). (c) After the item 8 head guard passes, checkDigestCoverage({ digest, pr: prSize, head, baseRef }) must pass or the post is skipped with the thread note ℹ️ Review not posted to <repo>#<n>: digest covered N of M files (+a/−d against the PR's +A/−D) — the review may not have read the whole change — this verdict is Slack-only. and the [review-post] … skipped: log line; a digest that could not state its totals (its own listing was cut) is refused the same way, naming the reason. A recorded incremental range, mismatched head, or different PR base ref is refused even when totals are large. Saved coverage without a valid current range requires a fresh digest after restart. A complete legacy digest can be rebuilt from its saved base in the original checkout only when the original repository, PR, base ref, head and verdict remain bound, the observed checkout head agrees, and a fresh canonical range has exactly the saved totals. Other legacy or malformed coverage retains the refusal. Within a matching canonical range, under-coverage refuses: fewer files than the PR by more than one plus 5 %, or fewer changed lines by more than 50 plus 25 % — tolerances for what legitimately differs between git's listing and GitHub's object (rename detection, binaries) and nothing like 13 of 41. A digest that covers MORE than the PR passes: a local base lagging GitHub's (a mirror not yet fetched to the newest default branch) widens the merge-base range, and the review then read every file of the PR plus some of the base's. Nothing to compare — no digest (the agent never called the tool), or no PR size — passes too: the guard catches a digest that lied, it does not require one. (d) The REVIEW TARGET block (item 9) states the PR's size (Size (GitHub): 41 files, +2459/−579) with the read-the-rest rule, and both prompts carry the shared whole-change text (distilled-diffs.md item 8), so the agent can see a short read for what it is before the guard has to. Ship's review rounds carry the digest but no PR size (the PR's facts are read at preflight and every fix round moves them), so there the guard still refuses incomplete or mismatched recorded ranges.

  12. Test guard — a test removed or narrowed without its spec is a finding, and a test reshaped is disposed of out loud (specs-coverage.md item 6): item 14 holds behavior to the same-PR rule; this holds the proofs to it. As sub-step 3a of the spec check, both prompt variants (the same shared REVIEW_SPEC_CHECK text) run npm run --silent specs:coverage -- --changed origin/<base>...HEAD --test-guard in the same batch and read its lines in two classes. A test-guard: <file> — removed: <what> line is deterministic — the test file deleted, an it/test/describe title gone with no new title to pair with, a skip/only/todo marker (.skip(, .only(, xit(, xtest(, xdescribe(, it.todo(, test.todo() on a test the base ran, an added focused block, or an executed test moved under a skipped group — and each unallowed line is a finding of severity minor or higher with a short plain-language title naming the behavior that lost its proof whose explanation quotes the guard's line exactly as printed, so under the review loop's default severity the spec changes in the PR or the test comes back before the round closes. A test-guard: <file> — check: <what> line is heuristic — fewer expect( calls in the file, a title gone while another arrived (a rename or a split) — and the reviewer disposes of every one explicitly in the review body, never silently: "weakened" makes it a finding at minor; "refactor, verification intact" carries one clause of why. A line ending — allowed by <spec> proves only that the base owning spec changed or was deleted, not that verification survived. The reviewer checks every loss group, including allowed lines and rename/split checks, against original criteria and surviving independent proof or explicit behavior retirement. The review names replacement proof or supported retired criterion; unsupported retirement, lost required criteria, wildcard expansion hiding loss or implementation-derived expectations is a minor finding. Read deleted or renamed owners from the base/diff. test-guard ok does not waive this review. When the command fails the agent judges the same facts from the diff by hand and never installs or builds, as in item 14. The verdict contract (item 5) is unchanged: the guard adds findings, not a new verdict path.

  13. Unit contract check — the diff is judged against what the coding child was told (agent-ship.md item 13): a review round of a plan unit is handed the same typed contract as the coding child, rendered by OpenSwitchboard into its system prompt right after the REVIEW TARGET block (makeSystemComposer's contract, before the trailing skills; after the agent's prompt when there is no target), so the two children hold one object and not two readings of a tree that may differ — the coding child's tree is the PR branch, where the plan may already be edited. As sub-step 3b of the spec check, after the test guard and before the report, both prompt variants (REVIEW_UNIT_CONTRACT, one text shared by REVIEW_SYSTEM and REVIEW_SYSTEM_RESIDENT) name the block and its sub-headings in order (### First instruction, ### Unit, ### Spec rows, ### Agent rules, ### Guards) and read the unit's Test scenarios against the diff: a scenario the unit listed and the diff did not add is a finding of severity minor with a short plain-language title naming the unproven scenario — the same severity as a spec contradiction (item 14), so under the review loop's default severity the test is added or the round does not close; a spec row the block names that the diff leaves untouched is disposed of out loud as the test guard's check lines are (item 16) — one clause on why it needed no change, or a finding at minor with a short plain-language title naming the outdated promise and the exact spec item in its evidence; a guard the block names that the diff weakens is the guard's own finding. A prompt without the block — every task-string review — skips the step silently. The verdict contract (item 5) is unchanged: the check adds findings, not a new verdict path.

  14. The post is a durable original-run fact (runReviewPostStep, reviewPublication.ts, and the run loop; run-history.md items 2 and 21). Before the native POST, the exact original fenced run saves and rereads one trim-independent receipt naming its run, repository, pull request, full commit, rendered-body hash and optional typed verdict. Fresh canonical owner, stop, requester grant and audience checks precede dispatch. Only an exact positive native review receipt followed by a durable accepted result produces reviewPost: { posted: true, target, head, verdict? } and review_posted. A known guard or native refusal can produce reviewPost: { posted: false, reason } and review_not_posted. A lost response, void adapter result, unreadable receipt or lost result ACK produces reviewPost: { posted: false, uncertain: true, reason }; it produces no definitive not-posted event and authorizes no repeat POST. Replay of accepted, refused, pending or uncertain receipts never calls POST; only saved acceptance earns posted credit. Native acceptance is retained before auxiliary head reads or replies and survives event trimming, restart and finalization under the original run. Receipt metadata contains hashes and target facts, never review prose, and generic history summaries omit it. Legacy replayed typed review_posted events retain their existing projection. Only review runs record a post outcome; hard stops and non-review runs keep their existing skip behavior.

  15. On pi, the same review (harness-pi.md item 10): a review is a pi process in the same read-only worktree under the same read identity — pi's own tools less edit and write, the readonly toolset relayed to the bot, the composed prompt above as pi's system prompt with a read-only note last — and everything in this spec from the verdict on is the code it always was: submit_verdict is the tool run in the bot with the run's context, the reviewed-head settle (item 12), the post-step behind the reviewed-head guard (item 8), the digest-coverage guard (item 15), the verdict line (item 5) and the recorded fact (item 18) are the same code on the same stream; a substantive head move's single re-review turn (item 12) keeps the session for an in-place move, or starts one confirmed successor from the saved conversation after checkout replacement.

  16. Seeded-sandbox variant (execution.md item 26): in a sandbox seeded from the resident's snapshot the dispatcher swaps in REVIEW_SYSTEM_SEEDED via RunOptions.system — the same gather-once discipline against a checkout already at the PR head at /workspace/checkout (no clone), the diff read with git against the base branch already present, gh kept for the pull request's metadata; the REVIEW TARGET block's seeded branch names the checkout, forbids a second clone and pins the first command to the HEAD check there (item 9's block, its third shape).

  17. Physical sandbox isolation for read-profile reviews: a fresh review whose effective identity is read uses its durable run ID, not the conversation thread key, as the physical Cloudflare sandbox identity on both ordinary and resident-fallback paths. The logical thread key (including Ship's unit) remains the run's thread key for admission, records and resident attach. The chosen physical key is stored in the workspace binding and reused on re-attach after a restart; if Cloudflare is no longer configured, re-attach refuses rather than switching backends. A legacy sandbox binding without a physical key re-attaches using its original thread key, never a newly minted key. Missing or invalid run IDs cannot cause a fresh read-profile review to fall back to a shared sandbox; a resident snapshot seed's ownership claim uses the same physical key the Worker receives. Resident selection, PR-head pinning and read-only authorization remain unchanged.

  18. Direct review setup under a full sandbox fleet. A PR review waits on the same refused Worker request within its original admission deadline. The physical sandbox remains keyed by the one durable run ID. A setup capacity failure closes that run with sandbox_fleet_busy, no model turn, verdict or GitHub post; its reply says plainly that review never reached the PR. A Live run link appears only when that exact run remains queryable. Recovery never replays an unknown seed or checkout and rechecks the exact PR head before the first model turn.

  19. Read a complete pinned source without shell access. The read-only review_read tool reads the bound PR's full merge-base diff or a tracked file at its required head through the executor. The controller supplies the repository, PR, head and base branch; model arguments cannot override them. An explicit checkout path takes priority. Without one, a cold review reads from the executor directory that setup prepared. The reader verifies its HEAD before returning source and never falls back from an explicit path. Git reads immutable objects with external diffs and textconv disabled. Each page names the resolved base, merge base, head and source hash, carries at most 32 KiB of UTF-8 text, and returns the next byte offset and EOF. The Responses wire explicitly sets strict: false when a function tool omits it, so the first page can omit path and sourceHash. A continuation must supply the previous source hash; changed sources, split code points and offsets beyond EOF are refused. A source above 10 MiB or an incomplete transport response yields no accepted page. Current repository access is checked before and after the read. No credential, public diff URL, arbitrary shell or recorded check is exposed. The pi oversized-line response names review_read when that tool is available. This read neither counts as digest coverage nor certifies a complete review; infrastructure failures and live rollout remain separate gates.

Validation criteria ​

A standalone review can pin one full commit with agent:review review-head:<40-character SHA> <PR URL>. The pin must be in the leading control prefix, before task prose. It is one request's constraint, retained in the original run's text for restart, not a sticky thread setting. A stale, malformed, duplicate or unreadable pin refuses before workspace attachment or review execution. Matching pins cannot adopt, carry or re-review another head, and publication rechecks the current open PR head. Without a pin, standalone reviews retain their head-move behavior. An older commit named only in prose is advisory, not this deterministic admission constraint.

CriterionProof
An explicit standalone review pin refuses a startup target move before attachment, review model work, verdict follow-up or publication; malformed, empty, duplicate and unknown targets refuse too. A matching pin starts normally with operator routing on or off. The pin survives control stripping, stays out of thread stickiness, and never comes from a prose example.[unit] src/core/dispatcher.test.ts::repo/ref resolution + resident prompt selection::a pinned startup review refuses a head that moved before admission; matching and unpinned reviews still start, src/core/dispatch/authorize.test.ts::authorizePrHead — the PR head preflight::an explicit review head rejects malformed, duplicate, missing and changed targets, but allows its exact head, src/directives.test.ts::reviewHeadOf — explicit request prefix::retains one authored pin through directive stripping and ignores prose examples, src/core/resumeLaunch.test.ts::the pure pieces::restores a standalone review pin from the original recorded request, src/core/dispatch/operator.test.ts::the operator is one loop with typed tools::a review pin preserves later formal controls and the authored request through routing
A standalone pin refuses attachment adoption and checkout drift, skips head-move re-review and carry, and posts only while the current open PR still has its exact requested head. Unpinned reviews keep their existing behavior.[unit] src/core/dispatch/authorize.test.ts::authorizeAttachedHead — every review workspace is at the PR head before the first model turn::a pinned review refuses attachment adoption but verifies its unchanged checkout, src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::an explicit standalone pin prevents adopting or re-reviewing a moved head and refuses a changed checkout, src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::a pinned standalone review posts only while the remote still holds its requested head

| A confirmation or free-text answer retains the effective authored review pin and preset in durable setup custody and in-process restart. A replacement harness checks a readable, full matching checkout SHA before any review model re-entry; the root trace records reviewPinnedHead. | [unit] src/core/dispatcher.test.ts::the operator behind routing.operator (record 0057; routing-and-config item 29)::on: confirmed and joined review pins survive durable setup and refuse attachment adoption…, src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::a %s review rejects an unverified…, ::the worktree refused by name… | | A pinned standalone review can publish an unchanged fork PR after a fresh read of that fork's head ref. Moved, missing, unreadable and deleted fork targets refuse; ordinary fact reads keep their existing fork policy. | [unit] src/execution/githubPulls.test.ts::githubPulls::fetchPullRequestFacts (ship entry checks)::an opted-in fork head verifies its own ref and reports moves, missing refs and unreadable targets, src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::a pinned standalone review posts only while the remote still holds its requested head |

Follow-up reviews use the conventional finding lifecycle documented in Claude Code Review: verify previous findings against the new code and explicitly close fixed or dismissed findings. Switchboard stores those closures in its existing typed verdict (resolutions, using findingId, fixed|declined, and an evidence note). GitHub review IDs scope legacy finding IDs so separate rounds' F1 entries cannot collide. Every PR review must read complete GitHub review and conversation history through github_pull_get with includeReviewHistory: true before submitting a verdict at that snapshot's head. Large history is returned in ordered pages from the same fingerprint and head; every page must be delivered before submission, and source changes restart the read. Each outstanding finding must appear exactly once, re-raised under its scoped ID or explicitly resolved. Prior cases have controller-assigned stable IDs (C1, C2, …); legacy cases receive IDs in their recorded order. Re-raising preserves original metadata and cases in code, and only appends new cases. Every prior case requires a caseAssessments entry with its finding ID, case ID, open|fixed|declined outcome and non-empty evidence at the verdict head. Missing, unknown, duplicate or unsupported assessments refuse the submission. No open case permits closure; a fixed finding requires at least one fixed case, while a declined finding requires every case to be declined. An accepted receipt carries the complete canonical verdict so finish replay preserves original cases and assessments without a new history read. Before a resumed model turn on either harness, the shared controller reconstructs the original ordered history pages from canonical tool results and controller receipts, then separately checks current access, the authoritative head and history fingerprint through fresh GitHub reads. Matching complete history restores the snapshot; matching partial history restores only its original bounded next-page cursor. Changed, missing, trimmed, inaccessible or metadata-absent history remains incomplete and receives refresh guidance. A prepared receipt without a recorded response gives no page credit. Fresh validation is acknowledged in existing run state as an audit result; the next restart must validate again. The existing required-head guard is saved with its target in the same run state before head-move model work, and a failed acknowledgement prevents advancement. An active resume invalidates a cached verdict without proven complete current history; accepted-verdict finish replay is separate. Only current outstandingFindings IDs require outcomes; closed historical findings are not re-raised or resolved under their old IDs. Missing history, partial reads, ambiguous markers, missing coverage and invalid resolutions refuse the submission. A bound verdict must supply a usable parsed commit head equal to the complete snapshot's head; a malformed or missing head cannot bypass binding. Before a substantive re-review or adoption without matching current-head evidence, the dispatcher invalidates complete and partial old history and pins the shared context to the newly required reviewed head. Submission must match that required head as well as a fresh complete snapshot; the same binding remains in force for the later verdict-only follow-up. A valid old SHA matching old history cannot pass, and a worktree observation at publication cannot rescue it; the independent publication head guard remains unchanged. Unbound verdict parsing remains separate. History sanitizes text leaves and prose separately so Cookie headers cannot consume the typed marker's JSON envelope, scoped identities, case tables or closures. Both published and redacted marker JSON escape angle brackets so neither --> nor --!> can terminate the envelope early. History reads prefer a positively read open same-repository branch tip over lagging PR metadata, with the same fallback, fork and frozen closed-head policy as dispatch. Omission and author claims never prove closure. Only native Bot reviews contribute typed findings; human review text remains context. Historical closures apply only to findings from the same native GitHub review author. The reviewer checks every prior invariant case and the changed paths for regressions; the record validates coverage, not the truth of the model's evidence. New consequential defects remain reportable. This does not guarantee decreasing finding counts or exhaustive first-pass detection; comparative detection quality remains an evaluation gap.

| Criterion | Proof |

Native ratings: complete current-format changes-requested reviews invite a review-body reaction before the agent fold. LGTM reviews omit the invitation, including approvals with nits. Native acceptance retains validated optional identity and system-authored cohort metadata for all complete current-format reviews; legacy missing identity keeps prior acceptance behavior[unit] src/core/reviewVerdict.test.ts::review verdict → post body::omits the feedback invitation from LGTM reviews and keeps it on changes requested; src/core/reviewRound.test.ts::runReviewPostStep durable original publication::retains feedback metadata and native identity on acceptance, then replays without a second publication; src/execution/githubComments.test.ts::postReviewComment::retains validated native feedback identity without changing legacy write acceptance
Source reads use the controller-pinned review head and acknowledged checkout[unit] src/core/dispatch/runLoop.test.ts::runLoop checkout-bound tools::binds source reads to the resolved review head and acknowledged checkout
Pinned full diff and oversized source line reconstruct exactly across bounded UTF-8 pages, including EOF[unit] src/tools/reviewRead.test.ts::review_read::reconstructs a private full diff and oversized source line through bounded UTF8 byte pages
A cold review without a recorded checkout path reads from its executor directory and still rejects the wrong head. An explicit checkout takes priority and never falls back to that directory.[unit] src/tools/reviewRead.test.ts::review_read::reads the pinned source from the cold executor directory with a…, ::uses an explicit checkout instead of the executor directory and never falls back from it
The Responses wire leaves first-page arguments optional; continuations require the returned source hash and diff reads reject file paths[unit] src/tools/reviewRead.test.ts::review_read::keeps first-page inputs optional on the Responses wire and enforces pinned continuations
Invalid offsets, source movement, denied access, foreign paths, transport truncation and oversized sources refuse pages[unit] src/tools/reviewRead.test.ts::review_read::refuses…, ::rejects unsupported transport and access revoked while reading without publishing a page, ::pins the original byte source across base movement and rejects larger sources
The actual pi reader names the available bounded reader for an oversized source line[unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::directs an oversized review source to the available byte reader instead of bash
Live private review consumes the full pinned source using the deployed reader with no shell or public diff URL[gap] Requires a deployed containing build and an authorized disposable review; local real-Git proof is not live acceptance.
A Review reads and advances the acknowledged attach/seed/cold checkout instead of assuming the transport cwd. Initial head re-provisioning and post-verdict settlement use the same validated live binding getter; a moved head is re-reviewed only after exact workspace confirmation. Initial reprovision probes the fresh acknowledged path. After a supported harness relaunch, settlement reads the current re-attached selection, never the dispatch round retained for release. A changed directory during an existing harness review is held before another prompt until a separate verified harness transition exists. Legacy implicit-cwd callers and native Resident attachment remain compatible. Dirty/untracked/ignored/unpublished work, stops, truncation, concurrent HEAD changes, missing or malformed supplied binding and unconfirmed head remain refusals, never directory inference, cleanup or replay.[unit] src/core/reviewWorkspace.test.ts::review settlement in a bound non-root checkout::*; src/execution/workspaceAdvance.test.ts::executor workspace advance::*; src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::posts a relaunched review from its new acknowledged checkout instead of the original round; src/core/dispatcher.test.ts::resident admission fixture command boundary::answers exact acknowledged checkout HEAD reads and refuses foreign paths or other commands
A canonical child reviews only its saved publication repo, PR, ref, base and head through startup, resume and final settlement. A substantive move cannot open another turn and a rebase cannot carry a verdict to a different head; the existing remote post guard must still permit the exact saved target. Standalone reviews retain their head adoption and re-review behavior.src/core/dispatch/authorize.test.ts::authorizePrHead — the PR head preflight::*, src/core/dispatch/authorize.test.ts::authorizeAttachedHead — every review workspace is at the PR head before the first model turn::a resumed coordinator review still verifies the saved head before its next turn, src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::a canonical review never follows or carries a*, src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::a canonical review cannot post a carried verdict at another saved head
Follow-up history preserves reused legacy IDs and every case; omission never closes a finding, and only explicit same-author resolutions close it. The last complete marker must match the native reviewed head.[unit] src/core/reviewHistory.test.ts::review finding history::*
Legacy markers with missing findings or empty negative findings remain readable context, with no closure authority. Later valid findings survive fresh reads and canonical restart restoration. Malformed findings, changed finding identities, duplicate case identities, ambiguous final markers and mismatched native heads still refuse the read and cannot be normalized away by redaction. New submissions still require complete findings.[unit] src/core/reviewHistory.test.ts::review finding history::reads legacy incomplete markers…, ::refuses malformed historical findings…, ::refuses changed finding identities…, ::refuses a malformed final marker…; src/tools/github.test.ts::github_* reads::delivers legacy incomplete history…; src/core/dispatch/reviewHistoryRestore.test.ts::review history restoration from canonical results::restores legacy incomplete history…
Complete history at a different head from the observed checkout blocks the verdict-only turn, with or without a submission. Both an initial rebase and a second move during re-review end incomplete; matching-head missing verdicts, malformed findings and incorrectly reported heads retain one correction. An incomplete report is not carried across a rebase and names only its review target.[unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a missing verdict is corrected only while the checkout matches history…, ::a substantive head move mid-review on pi preserves…; src/core/reviewVerdict.test.ts::review verdict → post body::no verdict → fail-closed…
A re-review assesses every prior case by stable ID with an outcome and evidence at the reviewed head. The tool preserves original case text and IDs, appends new cases, and records assessments in publication and accepted transcript replay. An oversized receipt refuses recording; an incomplete receipt cannot recover the canonical verdict. Missing, unknown or duplicate assessments, empty evidence and outcomes inconsistent with a closure refuse the whole submission.[unit] src/tools/submit.test.ts::submit_verdict tool::re-raises prior cases by ID with evidence, preserving text and identity through publication and accepted replay, ::refuses missing, unknown, duplicate and unsupported case assessments atomically before accepting a closure
Scoped references retain history-owned severity, location, title, kind, invariant and human-gated policy before approval, publication and replay. Conflicting metadata is refused for both single and pattern findings. Case additions to a prior single finding are refused rather than silently discarded. New cases cannot duplicate preserved rows or sibling additions after text normalization; copied resolved cases cannot keep a finding actionable. Legacy history remains readable.[unit] src/tools/submit.test.ts::submit_verdict tool::rejects changes to controller-owned…, ::rejects copied or duplicate new cases instead of keeping a resolved finding actionable
Closures survive verdict recovery, redaction and prose clipping in the visible review and its final marker.[unit] src/core/reviewVerdict.test.ts::review finding resolutions::*
Every review prompt requires prior-case verification and still reports new consequential defects.[unit] src/agents/registry.test.ts::review prompts: structured findings through submit_verdict (agent-ship item 6)::all review paths require GitHub history and verification of every previous case before closure
Current-head adoption cannot promote an old-head approval: stale evidence is invalidated, the current head is pinned for re-review and later verdict turns, and only an already accepted current-head verdict with matching complete history survives.[unit] src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::current-head adoption…
A bound PR verdict matches the complete history snapshot and the substantive re-review's required head, with one outcome per prior finding. Old complete/partial history is invalidated before movement; even a valid old SHA matching a later stale read is refused in both re-review and verdict-only follow-up. Unbound behavior and the independent publication guard remain unchanged.[unit] src/tools/submit.test.ts::submit_verdict tool::requires a complete exact-head history read and explicit prior-finding coverage for a bound PR review, ::rejects an unusable reviewed head…, ::rejects the valid old SHA…, ::forwards a valid verdict to the context and acknowledges it; src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::invalidates complete and partial old history…; src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a substantive head move mid-review on pi preserves…
Private installed specialist reviews retain acknowledged history and file results through actual dispatch, owner adoption and both harness resumes, then submit and publish at the exact head. Ordinary readers, changed actor/target, revoked access, altered inputs, duplicate calls and owner transfer refuse reuse.[unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::resumes an acknowledged private review…; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::restores canonical history before resumed…`
A resumed model turn reconstructs original controller-bound canonical pages without external reads, then separately validates current access, authoritative head and history fingerprint before restoring complete or partial credit. Prepared, missing, trimmed, legacy, foreign or inaccessible evidence grants no completion; native payload ranges exclude forged PR prose, and reduced page budgets restart partial reads.[unit] src/core/dispatch/reviewHistoryRestore.test.ts::review history restoration from canonical results::*
Both harness entry points receive proven canonical history and the original required-head pin before model work. Pin and validation writes require acknowledgement; owner fencing prevents model admission. Changed same-head history cannot resurrect a cached approval. Finish-phase accepted-verdict replay skips history reconstruction and fingerprint refresh; current publication source-access checks remain in force.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::restores canonical history before resumed…, ::a review resumed with its answer in hand runs its post-steps…`
Standalone head movement acknowledges the existing required-head pin in durable run state before advancing the workspace or prompting a re-review; a failed acknowledgement retains the old pin and admits no new model work.[unit] src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::requires an acknowledged durable head pin…
A checkout replacement checkpoints the completed turn and confirms its producer ending before movement. Re-review starts one successor from the saved conversation at the new head, with the original policy, owner and remaining budget. In-place moves keep their existing session.[unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a substantive head move mid-review…
A checkout replacement carries both the consumed inbox cursor and its deferred exceptions into the saved successor request. If its owner pauses before another mirror checkpoint, reclaim still offers the deferred user input and does not replay consumed provider controls.[unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::keeps deferred input across a replacement checkpoint pause and successor reclaim
Published and redacted verdict markers escape JSON angle brackets, preserving parseable typed findings without HTML comment terminator injection.[unit] src/core/reviewHistory.test.ts::review finding history::escapes JSON angle brackets…
3: all review prompts apply the same high-signal filter before submit_verdict, load the local review skill when available, and omit uncertain or low-impact observations while retaining realistic state-dependent failures[unit] src/agents/registry.test.ts::review prompts: structured findings through submit_verdict (agent-ship item 6)::all review paths filter uncertain and low-impact observations before submitting findings
22: direct review capacity failure retains the advertised run, says no review reached the PR and records no model turn[unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::keeps a direct PR review's advertised run when fleet capacity prevents setup
21: fresh read-profile review Cloudflare sandboxes use distinct durable run IDs instead of the coding predecessor's or each other's thread key, on cold and resident-fallback paths; the key survives binding round-trip and re-attach, while a legacy binding keeps the thread key and invalid fresh IDs fail closed; logical thread, resident target, head and read identity do not change[unit] src/execution/factory.test.ts::review sandbox isolation::*; src/core/dispatch/provision.test.ts::attachWorkspace — the workspace attach and the ask-once refusal::a review forwards its logical thread, durable run ID and PR head without changing its Ship unit
3: a pattern finding names one invariant and independently checkable cases across source, destination, endpoint and compound execution; the tool rejects missing or malformed tables but the reviewer judges semantic coverage; later review preserves the id for a widening, checks each prior case and gives a different invariant a new id[unit] src/agents/registry.test.ts::review prompts: structured findings through submit_verdict (agent-ship item 6)::both review prompts require invariant case coverage across push selection and command composition, and preserve widened IDs; src/core/reviewVerdict.test.ts::invariant case tables in review findings::*; src/tools/submit.test.ts::submit_verdict tool::findings (agent-ship item 6)::requires a new finding to declare single or pattern, and rejects pattern findings without a well-formed case table before recording; src/core/ship/reviewChild.test.ts::buildShipReviewTurn — the review round's one user turn (item 5)::a re-review carries the previous invariant and every case into the next review turn; [agent] Review a PR with an explicit push source, configured destination/URL and backgrounded push and compare the submitted table with each path.
Budgets and toolset as specified; the turn guard derived from the wall clock[unit] src/agents/registry.test.ts::review: readonly toolset, 25 min, ::the turn cap is a runaway guard derived from the wall clock…::*; the budget mechanics are harness-pi.md item 15's rows.
Resident variant: ready worktree, git-based gather, no clone/gh instructions; fallback prompt unchanged[unit] src/agents/registry.test.ts::resident prompt variants; selection wiring in src/core/dispatcher.test.ts::repo/ref resolution + resident prompt selection ….
Seeded variant (item 20): the seeded checkout named, no clone, gh kept for metadata, the head rule intact; the REVIEW TARGET block's seeded branch[unit] src/agents/registry.test.ts::seeded prompt variants::*; src/core/reviewTarget.test.ts::reviewTargetBlock — the seeded sandbox::*
Real PR review lands within budget with verdict-first output[agent] @switchboard agent:review <PR URL> on a real PR (~<2k changed lines). Expect: status checklist, completion well under 25 min, the thread reply is the verdict line, one bullet per finding (severity, id, file:line, title) and a Posted to … line with the run link — nothing else; the finding explanations sit on GitHub under Full review.
Findings are real (spot-check)[agent] For the top finding, open the cited file:line and confirm the described failure scenario is coherent with the code. A fabricated citation is a critical failure.
Verdict token is deterministic: LGTM: first iff a complete latest submission accepted approve; request_changes/no verdict never start with LGTM; summary collapsed to one line[unit] src/core/reviewVerdict.test.ts; src/core/dispatcher.test.ts::review post-step… (approve → LGTM: body + commitId pin; request_changes with "LGTM" in prose → Changes requested: body; no verdict → fail-closed line; coding agent cannot emit a verdict). [agent] agent:review <PR URL> on a clean PR → the GitHub review body's first line is LGTM: … and an installation that has turned on the auto-approve workflow approves it; on a PR with a planted blocker → first line Changes requested: …, no approval.
5c: public submission and rendering lead with the user problem, show three plain labels and preserve evidence kinds, proof limits and original finding metadata[unit] src/core/reviewHumanFindings.test.ts::human review explanations::*
5c: actual model output meets the same clarity bar with the agent fold closed, including normal failures, rare consequential failures, rule violations and candidates that should be dropped[gap] Run the existing read-only review harness on fixed representative changes under an authorized model/effort. Check the rendered outputs for the trigger, effect, evidence limit and required correction. Human-authored renderer fixtures alone do not satisfy this criterion.
5b: current human explanations cover every active finding and survive canonical parsing/redaction; visible code links use the current locator while the marker and agent evidence retain original metadata; invalid explanation coverage refuses fresh submission[unit] src/core/reviewHumanFindings.test.ts::human review explanations::*
5b: legacy human summary bounds reviewer-authored sentences without cutting abbreviations, has severity and a pinned code link; all exact IDs, original titles, rationale, pattern cases and resolution evidence are under one fold; HTML cannot escape it and code examples remain literal; the final marker and severity gate survive Unicode clipping; oversized fixed evidence refuses publication[unit] src/core/reviewVerdict.test.ts::review verdict → post body::*, src/execution/githubComments.test.ts::postReviewComment::keeps the typed verdict marker…, ::fails closed before the GitHub write…, ::fails closed through real publication…
5b: the thread reply is rendered from the verdict — the token line, one bullet per finding, Posted to <repo>#<n> · [Live run]; the write-up rides along only when nothing was posted or findings were not itemized; no accepted verdict → the incomplete-review warning, retained report and available run link; the answer event stays the model's words[unit] src/core/reviewVerdict.test.ts::review verdict → channel reply (item 5b: Slack gets the verdict and the findings, the prose only when it lands nowhere else)::*; src/core/dispatcher.test.ts::review post-step::the severity gate (agent-review item 5a)::item 5b: *; ::reviewed-head guard::the workspace HEAD is not the PR head → no post, the thread is told both shas
5b: below verbose a posted itemized verdict's reply is one line — LGTM or the non-zero counts with the pull request link — while verbose, an unposted verdict and an unitemized one keep the full render[unit] src/core/reviewVerdict.test.ts::verdictCountsLine + the quiet channel reply — one line in the user's words (record 0066)::*
5b: the three review prompts carry the final-message rule (one paragraph per finding, no recap, no verified-clean list) and none asks the model to lead with a verdict[unit] src/agents/registry.test.ts::review final message: the write-up is the review's text alone (item 5b)::*
5b: live — a review of a real PR: the thread reply is the verdict line, the finding bullets and one Posted to … line; the GitHub comment opens with the token line and short severity-marked issue bullets, every location link resolves at the pinned head, and all evidence and the write-up are folded under For agents[agent] @switchboard please review <PR URL> — compare the thread reply and the GitHub comment against the shape above; a thread reply carrying a "what I reviewed" or "verified clean" section is a failure.
5a: the severity gate at the parser — by default an approve over a major or a minor finding is a request_changes naming the finding, its severity and the level; every level × every severity yields LGTM: exactly when the finding sits below the level; the gate keys on the raw declared severity; a severity off the ladder drops and never opens it; request_changes is untouched[unit] src/core/reviewVerdict.test.ts::review verdict → post body::findings (agent-ship item 6)::the severity gate — an approve is held to the severity to address (default minor)::*
5a: the gate at the tool — the context's level is the one the parser holds to (the default without one), the sink receives the downgraded verdict, the ack names the finding and the level, and the tool text states the rule[unit] src/tools/submit.test.ts::submit_verdict tool::the severity gate (agent-review item 5a)::*
5a: end to end — the dispatcher resolves directive > user > channel > org and the posted body starts with Changes requested: over a gated finding (default over a major; a channel's nit over a nit) and with LGTM: below it (a nit by default; a minor under severity:major)[unit] src/core/dispatcher.test.ts::review post-step::the severity gate (agent-review item 5a)::*
5a: both review prompts state the rule — approve means no finding at or above the severity to address, minor by default, nits alone never block, an approve over one is downgraded[unit] src/agents/registry.test.ts::review prompts: structured findings through submit_verdict (agent-ship item 6)::both review prompts state the severity gate…
submit_verdict tool: readonly toolset only, validates the enum, forwards to the dispatcher; head required in the schema and forwarded (7–40 hex, lowercased; malformed → dropped, verdict still recorded)[unit] src/tools/submit.test.ts::submit_verdict tool, src/core/reviewVerdict.test.ts::parseVerdictInput carries a well-formed reported head…
5: the human-gated flag and full typed finding round-trip through the verdict marker; anything but literal true drops the flag and stays actionable; a person's PR comment in the marked verdict's GitHub timestamp second or later is recovered as its answer[unit] src/core/reviewVerdict.test.ts::review verdict → post body::findings (agent-ship item 6)::humanGated round-trips…; src/channels/adminCoordinator.test.ts::pr-check entry — the unit-start's resume facts beside the listing (agent-ship item 10, issue 1689)::an adopted pull request accepts a person's answer posted later in the human-gated verdict's GitHub timestamp second
5: the shared verdict instruction requires exactly one call after a review, with one preflight-infrastructure exception for a REVIEW TARGET first-command HEAD mismatch: no finding, verdict or GitHub post; the verdict turn's follow-up names the pull request, what a missing verdict posts as, the tool with every field and the two prohibitions; the turn publishes the verdict_turn note and prompts the session once under the clipped budget and the caller's span, the verdict reaching the hook and the return value; a turn that submits nothing or that pi refuses reports nothing and never throws; without a session (a finish plan) it asks nothing and says so[unit] src/agents/registry.test.ts::review prompts: structured findings through submit_verdict (agent-ship item 6)::all review prompts exempt a first-command head mismatch from the mandatory verdict call because it is preflight infrastructure, never a finding or GitHub post; src/core/verdictTurn.test.ts::verdictFollowUp — the user turn the model is given::*, src/core/verdictTurn.test.ts::runVerdictTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::*
5: end to end — a review whose loop ended without submit_verdict gets one verdict turn; the verdict the turn submits leads the posted body (LGTM:) over the loop's own write-up, the turn's reply is dropped, the note and the tool call are on the record; a run that still has no accepted complete submission posts an incomplete-review warning with its report; a Slack-only opt-out is not nudged; a stop before any substantive review tool gets no verdict turn even when it lands during reviewed-head settlement; an optional bounded harness stop summary produces no verdict or publication, while a stop after checkout and a real verdict retains local findings without starting a new GitHub publication; an already pending or uncertain publication keeps its original result[unit] src/core/dispatcher.test.ts::review post-step::a review whose loop ended without submit_verdict gets ONE verdict turn; the verdict it submits leads the posted body, the write-up stays the review's text, src/core/dispatcher.test.ts::review post-step::a review of a resolved PR posts the review back to the PR by default, src/core/dispatcher.test.ts::review post-step::an unknown head with an explicit 'slack only' opt-out still runs — the user asked for an unpinned, Slack-only verdict, src/core/dispatcher.test.ts::review post-step::a soft stop before review activity replies only that it stopped and runs no review tail: pending completion…, src/core/dispatcher.test.ts::review post-step::a soft stop during a substantive head fetch aborts settlement before notifications, movement, follow-up, or publication, src/core/dispatcher.test.ts::review post-step::a soft stop after checkout and real findings keeps the findings-so-far review behavior
REVIEW TARGET block (item 9): present on a resident review with a resolved PR (resident variant, carries the RepoContext head sha), present on a cold sandbox review whose checkout Switchboard already provisioned, absent for coding runs and PR-less reviews; text is pure/deterministic, unresolved head branch/commit named as unknown (base falls back to the default branch / origin/HEAD), both variants forbid clone/fetch/checkout movement[unit] src/core/reviewTarget.test.ts (5); src/core/dispatcher.test.ts::repo/ref resolution + resident prompt selection …::a resident review of a resolved PR gets the REVIEW TARGET block…, ::a sandbox-path review of a resolved PR gets the sandbox variant…, ::no REVIEW TARGET block for a coding run on a PR, nor for a review with no resolved PR. [agent] post-deploy: the first resident review's Slack card shows the agent's first command as git rev-parse HEAD and its verdict head equals the PR head.
Worktree named, attach checked (item 10): REVIEW TARGET names the worktree path, forbids cd/filesystem search, pins the first command to the current directory; no path invented when the attach lacks one; verifiedAtAttach sentence only on the resident path with matching shas[unit] src/core/reviewTarget.test.ts::resident path with the worktree path…, ::resident path without a worktree path…, ::verifiedAtAttach…; src/execution/resident.test.ts::records the attach result's ref@sha as the thread binding… (+ workspace), ::a 200 attach answer without a string \workspace` still binds…; src/execution/factory.test.ts::warm probe → ResidentExecutor… (binding` on the selection, unset on the per-thread path).
Workspace-head mismatch never reaches the model on any backend (item 10, 12d): every PR review attaches on the PR head branch even when a unit or sticky binding names another ref; the checked-out workspace HEAD is read through the executor before and after recovery and compared only with the reviewed head (binding and move metadata are never proof); a raced current head is adopted, otherwise one reprovision at the expected head is rechecked; only a failed reprovision or second mismatch is not started, quoting both full SHAs without an unproved cause, releasing the workspace before any provider call; coding and resumed runs are unaffected[unit] src/core/dispatcher.test.ts::repo/ref resolution + resident prompt selection …::a genuine push during workspace preparation is reprovisioned at the reviewed head and the review runs, ::a unit reviewing an adopted pull request binds its attach to the pull request branch instead of its own, ::a failed reprovision refuses with the two heads and no cause the guard did not establish; supporting seams: src/core/dispatch/authorize.test.ts::authorizeAttachedHead — every review workspace is at the PR head before the first model turn::a resident binding SHA cannot verify a checkout whose observed HEAD differs, ::resident moveTo metadata cannot verify a retry whose observed HEAD is still mismatched, ::a successful retry publishes the observed checkout HEAD instead of moveTo metadata, src/core/reviewRound.test.ts::guardAttachedHead (before any model call)::*
Review checkout stop (item 10): the cold checkout uses the five-minute command budget and carries the run's hard-stop signal. A hard stop during checkout ends setup without retry, refusal, or a model turn[unit] src/core/dispatch/authorize.test.ts::authorizeAttachedHead — every review workspace is at the PR head before the first model turn::a hard stop during cold review checkout ends setup without a retry or refusal, src/core/dispatcher.test.ts::executor provisioning by agent resources::a hard stop during PR review checkout closes the run before any model turn
An ownership fence during the attached-head gate aborts local work without closing the shared card or run row as an operator stop; a still-owned hard stop closes the card before a model turn[unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a review fenced during … leaves the card and row to the new owner, src/core/dispatcher.test.ts::executor provisioning by agent resources::a hard stop during PR review checkout closes the run before any model turn
A hard stop during resident reprovision skips the retry HEAD probe and ends the gate without a refusal[unit] src/core/dispatch/authorize.test.ts::authorizeAttachedHead — every review workspace is at the PR head before the first model turn::a hard stop during resident reprovision skips the retry HEAD probe
RepoContext.baseRef from base.ref (explicit and inherited PR), validated as a ref, unset when malformed/missing[unit] src/core/repoContext.test.ts::PR base branch for the review target (3)
Resident review prompt: no "typically the branch under review" hedge, no git fetch instruction, diffs against origin/<base>[unit] src/agents/registry.test.ts::review resident: no 'typically the branch under review' hedge, no git fetch instruction
Closed pull request refused before admission (item 11): merged → one line with the pull request, merge timestamp and nothing to review; closed-unmerged → parallel closed line without a merge time; neither claims admission, opens a card/workspace, registers a run, calls a provider or reaches the review post; the resolver carries the normalized merge timestamp[unit] src/core/dispatcher.test.ts::review post-step::a merged PR ends before admission with one nothing-to-review line and no run machinery, src/core/dispatcher.test.ts::review post-step::a closed-unmerged PR ends before admission with one closed line and no merge time or run machinery, src/core/repoContext.test.ts::resolveRepoContext: PR-source flags for ship::a merged PR URL in a plain ask yields no ref hint — the PR's facts (pr, headSha) still ride as context (issue 1860), src/core/repoContext.test.ts::resolveRepoContext: PR-source flags for ship::a closed-unmerged PR contributes no ref hint either…
Unknown head refused up front (item 11): review + PR in the message with no headSha → not started, named reply (acme/api#42, "head", "re-send"), no makeExecutor, no model turn, nothing posted; inherited PR unreachable → same; a legacy injected context without the closed-state fact retains the Slack-only fallback; "slack only" opt-out with unknown head → still runs, nothing posted[unit] src/core/dispatcher.test.ts::a review of a PR whose head could not be resolved is not started…, ::a review whose INHERITED PR head is unreachable is not started the same way, ::a legacy inherited-PR context without the resolved closed-state fact still runs Slack-only — only an unknown head refuses, ::an unreachable bound PR never reaches the post-step…, ::an unknown head with an explicit 'slack only' opt-out still runs…, ::reviewed-head guard::the PR head is unknown … the run is not started at all…; one shared predicate: src/core/reviewPost.test.ts::reviewPostIntended::*
PR named in the message → head always fetched; the PR's branch is the ref, prose on main / on branch X beside it never redirects or becomes a fallback; on fetch failure the ref and head stay unknown (item 11a)[unit] src/core/repoContext.test.ts::a PR URL plus a prose 'on main' in the same message…, ::an explicit 'on branch X' beside a PR URL does not redirect…, ::a PR URL whose head fetch fails does not take a prose ref beside it as a fallback
Reviewed-head guard (item 8): observed HEAD ≠ PR head (resolved AND current, item 12c) → no post + thread note naming workspace-observed, both full SHAs and their first differing hex; a reported mismatch names model-reported; observed remains authoritative; unknown stays fail-closed; a ship review re-reads the PR's open head immediately before posting, so a merge or close records a Slack-only verdict, while a standalone review keeps the established one post-publication head read; no two accepted-looking prefixes can hide the divergence[unit] src/core/reviewedHead.test.ts::checkReviewedHead::*; src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::a standalone review with a verified head posts exactly as before…, ::a pull request merged or closed while a ship review runs…; src/core/dispatcher.test.ts::review post-step …::reviewed-head guard
Post is a COMMENT-event PR review pinned to the head SHA (always — an unverifiable head means no post, item 8); head SHA resolved (also for cross-fork PRs) and dropped when malformed; the real publication path posts a fitted body with its typed marker and refuses an oversized fixed verdict before fetch[unit] src/execution/githubComments.test.ts, src/core/repoContext.test.ts::PR head SHA for review pinning
The head sha is the head branch's ref tip (item 11a): a same-repo PR reads git/ref/heads/<branch> after the PR object and prefers the ref tip over a lagging head.sha, logging both; equal tips → no log; an unreadable ref (404, network) or a non-commit object → the PR object's sha; a cross-fork head asks for no ref; ship's facts read the same way[unit] src/core/repoContext.test.ts::the head branch's ref tip wins over a lagging PR head.sha…, ::ref tip equal to head.sha → that sha, no log line…, ::a cross-fork PR still carries headSha even though its ref is not bound — and asks for no ref tip…; src/execution/githubPulls.test.ts::a same-repo head reads the head ref's tip and prefers it over a lagging head.sha…, ::a ref that does not point at a commit object…
Posts review to GitHub by default for PR reviews (opt-out available)[unit] src/core/reviewPost.test.ts (decision + opt-out parse), src/core/dispatcher.test.ts::review post-step … (posts on a resolved PR, suppressed on opt-out, no-op with no PR, non-review never posts, a lost native response retains uncertainty), src/core/repoContext.test.ts (PR number carried on RepoContext.pr; skip is logged with its reason). [agent] agent:review <PR URL> with NO "post" phrasing → a review comment appears on the PR authored by the App (requires App pull_requests:write). Opt-out: add "don't post" / "slack only" → no comment appears, Slack still gets the review.
Re-review in a PR thread (no PR in the message) posts to the thread's PR pinned to the head fetched now; a closed or merged PR ends at pre-admission with the one nothing-to-review line; failed fetch / malformed SHA → no run; repo redirect BY URL → no post; a bare path/slug token in the reply (e.g. docs/reference/specs/memory.md) never rebinds the repo or drops the PR; explicit PR wins; ref not rebound[unit] src/core/repoContext.test.ts::…re-review follow-ups inherit the thread's PR (fail-closed)… (incl. ::a bare slug-shaped token never rebinds a thread bound by URL — the PR is still inherited …); src/core/dispatcher.test.ts::review post-step::a merged PR ends before admission with one nothing-to-review line and no run machinery, ::a closed-unmerged PR ends before admission with one closed line and no merge time or run machinery, ::a lost native post response stays uncertain while the dispatch completes and the thread keeps the review. [agent] In a thread whose first message was agent:review <PR URL>, push a new commit and reply <@bot> re-review with no URL → a second COMMENT review appears on the PR with commit_id = the new head; merge the PR and reply <@bot> re-review → one merged-at line and no run or GitHub review. Also reply with a bare file path (e.g. docs/reference/specs/memory.md) and re-review → the post still lands on the thread's PR.
Head-moved note (item 10): text names both 7-char shas, the pinned post, the no-auto-approve consequence, and the re-request action; same commit (full/7-char/case) or unknown current head → no note[unit] src/core/headMoved.test.ts
Head-moved note (item 10): PR head differs and the move is unclassifiable → review still posted pinned to the reviewed head, fetch asked for the posted PR (at detection and after the post), note replied; unchanged head → no note; fetch undefined/throws → no note; guard refused the post → no note[unit] src/core/dispatcher.test.ts::review post-step …::head-moved note (item 10)
Head metadata classification is advisory: matching count, messages and file set gives matching-metadata; changed metadata gives substantive with added/removed subjects. Neither result proves unchanged contents[unit] src/core/headMoved.test.ts::classifyHeadMove…
Head-change notices name both heads and the new review. Matching metadata states that contents still need review; the follow-up requires a new verdict at the confirmed workspace head[unit] src/core/headMoved.test.ts::item 12 head-change notices::*; src/core/headMoved.test.ts::rereviewFollowUp…
Compare fetch (item 12): GET /repos/{repo}/compare/{base}...{sha} → commits (oldest first, full message) + files + filesTruncated at 300; base URL-encoded; non-2xx / network failure / malformed commit / malformed base or sha → undefined, no partial list[unit] src/core/repoContext.test.ts::prCommitsSince…
Executor.moveTo (item 12): the resident executor re-attaches with the new sha (body carries sha, refHint, readonly), answers the sha the worktree is at, keeps it for later attaches; a refused re-attach throws; the round's hard stop rides in (MoveOptions.signal) and ends a wake wait the move's transient refusal began[unit] src/execution/resident.test.ts::ResidentExecutor.moveTo, src/core/dispatcher.test.ts::review post-step::head moved during the run (item 12)::the worktree move carries the run's hard stop…
Matching commit metadata still requires a fresh review at the new head. The existing run advances and posts only the new verdict. No carry footer or carried-review note is emitted[unit] src/core/dispatcher.test.ts::review post-step::head moved during the run (item 12)::matching commit metadata still requires a fresh review at the new head; src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::matching commit metadata cannot carry approval…; src/core/reviewRound.test.ts::runReviewPostStep…::*
Substantive move (item 12b): moveTo(new head); second review turn in the same run whose last two messages are the first review (assistant) and the follow-up (user, names both heads and the new commits, "already moved your worktree"); the existing system stays with the session and the follow-up names the new head; head_moved run note on the stream; card label head moved → <y>; 🔀 note replied; posted once, pinned to the new head, with the SECOND verdict and answer; only the second answer is the run's answer event and Slack reply[unit] ::substantive move → the same run re-reviews…
Substantive move (item 12b, harness-pi item 14): the second turn continues the saved conversation, on the existing session for an in-place move or one confirmed successor for checkout replacement — the worktree moved first, the follow-up text as the appended user turn, the turn's own verdict capture, the head re-probed after; the system prompt stays the session's; a prompt pi refuses fails the run; an attachment ownership or binding acknowledgment refusal from either probe or move prevents continuation; a finish plan with no session keeps the verdict for the head it reviewed and says so; in-place movement uses one pi process and two prompts; checkout replacement confirms the original ending and starts one successor with the persisted post-turn clock; the new verdict is posted only at the new head[unit] src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::*, src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a substantive head move mid-review on pi preserves…
Substantive move with missing, refused or unconfirmed native advance: the original run ends failed before another model turn, verdict nudge or publication; a rendered SHA from a failed/truncated HEAD read cannot authorize continuation[unit] src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::*; src/core/dispatcher.test.ts::review post-step …::head moved during the run …::substantive move on an executor without moveTo…, ::worktree move fails and durable review ends failed…
Unclassifiable move → item 10 behaviour (pinned to the reviewed head, re-request note, one turn, no moveTo); moved AGAIN after the re-review → one re-review only, pinned to the re-reviewed head, re-request note for the newest[unit] ::compare unavailable…, ::the head moves AGAIN after the re-review…
Reviewed = current ≠ resolved (item 12a) → posted pinned to the current head, LGTM: intact, no refusal, no note; hard stop → no head fetch, no compare, no moveTo, nothing posted[unit] ::reviewed head ≠ resolved head but = the PR's CURRENT head…, ::a hard-stopped review never classifies, moves or re-reviews
Live head changes, including matching metadata, require a fresh same-run review at the confirmed new checkout. A pinned target remains fixed; a later head cannot inherit the old approval[agent] Pending live proof: push changed contents with the same commit message and paths during an authorized scratch review; observe the new model turn, exact checkout, invalidated old proof and current-head publication.
Verdict reply ends with [Live run](…) (item 13); the answer event and the GitHub post body stay link-free; no link on non-review answers or with PUBLIC_BASE_URL unset[unit] src/core/dispatcher.test.ts::closed-card checklist and review verdict run link (red-verified)
A failed run's error reply carries the run link (item 13)[unit] src/core/dispatcher.test.ts::closed-card checklist and review verdict run link::a failed run's error reply carries the run link — the transcript is one click from the thread
Spec contradiction check (item 14): both review prompts carry the step after the analysis pass and before the verdict, name docs/reference/specs/, the specs:coverage command and its Code/Tests header fallback (also taken when the command fails, never installing or building), read only the touched specs, file a contradiction as a finding at minor or higher naming the false promise with the exact spec item in evidence, exempt a spec updated in the same diff, and skip silently without specs[unit] src/agents/registry.test.ts::review prompts: the spec contradiction check (agent-review item 14)::*
A PR that changes a covered behavior without updating its spec receives a spec-contradiction finding (item 14)[agent] On a throwaway branch of this repository, change one behavior a spec's numbered item states (e.g. flip a default that a ## Behavior item names) without touching the spec, open a PR, and post agent:review <PR URL>. Expect: the review's findings include one naming the false promise and citing that spec item in its evidence at minor or higher, and the run page shows the agent read that spec and no other. Then update the spec in the PR and re-review: the finding is gone.
Digest-coverage guard (item 15): a digest covering fewer files than the PR → no post, thread note digest covered N of M files with both sides, outcome carries the reason; an incomplete digest → refused naming its reason; a matching digest posts pinned to the head; no digest or no PR size → posts; the head guard is asked first[unit] src/core/digestCoverage.test.ts::checkDigestCoverage::*; src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::digest-coverage guard (item 15)::*
RepoContext.prSize from changed_files/additions/deletions (explicit and inherited PR); unset on a missing or non-integer field; dropped when the PR object lags the head ref[unit] src/core/repoContext.test.ts::PR size for the digest-coverage guard::*
REVIEW TARGET states the PR's size with the read-the-rest rule when known, nothing about size when unknown[unit] src/core/reviewTarget.test.ts::reviewTargetBlock::states the PR's size from GitHub with the read-the-rest rule when known; says nothing about size when unknown
Live (item 15): the first review after deploy of a multi-commit PR states a digest whose file count equals the PR's on GitHub and posts its verdict[agent] human-gated: agent:review <PR URL> on an open PR of several commits; the run page's diff_digest result reads N files changed with N = the PR's changed_files, the REVIEW TARGET block in the run's system prompt carries Size (GitHub): N files, and the review lands on the PR with no digest covered note in the thread. Receipts on the tracker issue.
Test guard (item 16): both review prompts run specs:coverage --test-guard over the PR's range as sub-step 3a, name the skip markers, file a removed: line as a finding at minor or higher naming the lost proof and quoting the line, dispose of every check: line as weakened (a minor finding) or a refactor with one clause of why, review allowed loss groups against original criteria and surviving independent proof or supported retirement; a green guard does not waive semantic review, judge by hand when the command fails, and carry the same text in both variants[unit] src/agents/registry.test.ts::review prompts: the test guard (agent-review item 16)::*
A PR that deletes a test without touching its spec receives a test-removed finding; the same PR with the covering spec changed does not (item 16)[agent] On a throwaway branch of this repository, delete one it( from a test file a spec's Tests header names, without touching the spec, open a PR, and post agent:review <PR URL>. Expect: the review's findings include one naming the behavior that lost its proof at minor or higher, quoting the test-guard: line. Then preserve an independent replacement proof or document supported behavior retirement and change the owning spec in the PR and re-review: the finding is gone.
Review quality (item 3): reduce unsupported or low-value comments while retaining useful minor and major defects; a clean PR needs no finding[gap] Comparative review evaluation has not run. Use the same PR heads with the previous and revised policies; label actionable findings, false or low-value comments, and missed known defects, and record runtime and tokens. Include minor defects, clean changes, existing guards, and state-dependent bugs as described in decision 0088.
Unit contract check (item 17): both review prompts carry sub-step 3b after the test guard and before the report, naming the block after the REVIEW TARGET block and every sub-heading in the contract module's order, the listed-scenario finding at minor titled by the scenario as a spec contradiction's severity, the out-loud disposition of an untouched named row, the guard's own finding, and the silent skip without a block — byte-identical across the sandbox and resident variants[unit] src/agents/registry.test.ts::review prompts: the unit contract check (agent-review item 17)::*
The post as a recorded fact (item 18): a landed post records the pull request, exact reviewed commit and verdict; review_posted repeats those facts. Legacy carry metadata cannot change the commit. A guard refusal or definitive native rejection records review_not_posted with its reason. An uncertain native outcome records no definitive not-posted note. A missing pull request records a skip without a target. Non-review and hard-stopped rounds publish nothing. Without the publish seam, the returned outcome is unchanged[unit] src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::the post as a recorded fact (item 18)::*
The record carries it (item 18): a review run's finish record carries reviewPost and the review_posted event beside its verdict and reviewed head; the finish record is written AFTER the post (the start tombstone before it); a post the reviewed-head guard refused is recorded as a skip with its reason beside a review_not_posted note[unit] src/core/dispatcher.test.ts::review post-step::head-moved note (item 10)::the review run's record carries the submitted verdict (findings included) and the reviewed head, ::the record is written AFTER the post…, ::a post the reviewed-head guard refused is recorded as a skip with its reason…
The fact read back (item 18): a review resumed with its answer in hand posts its restored verdict once. Exact original-run canonical acceptance supplies replay credit even without a display event, skips settle and another native POST. A display event alone supplies no credit or permission to repost.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::a review resumed with its answer in hand runs its post-steps…, ::a review with canonical original-run acceptance skips settle and native POST even without a display event
The system composer places a contract block right after the REVIEW TARGET block and before the trailing skills (after the agent prompt with no target); a review child the plan runner spawns with a contract carries the same rendered block the coding child gets, there (agent-ship.md item 13); a run without one carries none[unit] src/core/reviewRound.test.ts::makeSystemComposer (head-pinned composition with an explicit AgentDef)::a unit contract block sits right after the REVIEW TARGET block…
On pi (item 19): the review runs under the read identity with the relayed verdict posted as LGTM: behind the reviewed-head guard and review_posted/reviewPost on the record[unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::*; the verdict path's equality: src/core/harness/pi/relay.test.ts::runRelayedTool — the verdict path::*
A re-review in a thread whose coding run recorded dispositions sees them by finding id — that run's record rendered as data in the prompt's thread-artifacts block, with the pull request it edited, before the REVIEW TARGET (session-log.md item 9)[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a review follow-up in a thread whose coding run recorded dispositions is handed them by finding id…, src/core/dispatch/threadArtifacts.test.ts::threadArtifacts — the thread's typed artifacts since the previous run::renders every artifact kind a record carries…

Native workspace advancement replaces the model checkout fallback. Re-review instructions describe the already confirmed checkout and cannot ask the model to fetch or move it. The former “worktree not moved” instruction test is retired; the native executor contract proves exact movement and preservation. Proofs: [unit] src/core/headMoved.test.ts::rereviewFollowUp (the second model turn's instruction)::worktree moved by Switchboard: names both heads, lists both commit sets, forbids fetching, demands a fresh verdict with the new head; src/execution/workspaceAdvance.test.ts::executor workspace advance::fetches the requested commit and confirms a real checkout before returning.

CriterionProof
Canonical review rejects fresh repository/ref/base drift or missing exact-head receipt immediately before posting.[unit] src/core/reviewRound.test.ts::runReviewPostStep (explicit AgentDef decides the post)::a canonical review refuses fresh…
Canonical replay accepts only the saved repository, PR and full head; a replaced workspace is checked before model re-entry.[unit] src/core/dispatch/runLoop.test.ts::a canonical finish rejects…, src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::a %s review rejects an unverified…
The provider-visible finding schema discriminates single issues from patterns: new single findings exclude matrix fields; new pattern findings require a non-empty invariant and well-formed non-empty cases. Prior findings are referenced in priorFindings by exact issued scoped ID with optional new cases; their original kind, invariant and case identities remain bound to history. Every fresh finding alternative requires kind, severity, file and title; id-only references cannot enter findings. The runtime and load harness import the same fresh finding schema. Any malformed new finding or duplicate/normalized finding ID refuses the whole submission even without PR history; a rejected submission retains prior facts as history but cannot present them as the current verdict. Legacy stored findings remain readable.[unit] src/tools/submit.test.ts::submit_verdict tool::rejects contradictory finding variants…, ::refuses malformed findings atomically…; src/load/piExtension.test.ts::*; src/core/reviewVerdict.test.ts::invariant case tables in review findings::*; src/core/harness/toolRecovery.test.ts::run-owned tool recovery::an id-only verdict gets one correction through relay and native receipts: …
CriterionProof
Original review publication awaits exact run/PR/full commit/body-hash admission before native POST. Stop or grant changes refuse before dispatch; undefined/malformed reads do not mean absent. A saved definite native refusal tells the thread that the verdict is Slack-only, including replay without another POST. Native void, lost response or result ACK remains uncertain, never claims Slack-only publication and never permits another POST; acceptance commits before auxiliary awaits.[unit] src/core/reviewRound.test.ts::runReviewPostStep durable original publication::awaits exact original pending admission before a native review POST, ::a stop or changed grant after pending ACK refuses before native dispatch, ::a definite native refusal and its saved replay notify the thread without reposting, ::a failed refusal-result ACK stays uncertain and cannot claim a Slack-only verdict, ::a void native adapter response stays uncertain and never authorizes repost, ::a lost native response remains uncertain and never reposts or declares not-posted, ::a lost accepted-result ACK keeps pending and prevents another native review, ::commits native acceptance before an auxiliary head lookup and keeps its credit if that lookup fails; src/core/dispatcher.test.ts::review post-step::a definite native post refusal keeps the review and tells the thread that its verdict is Slack-only
Native review acceptance requires an exact positive review ID, state, PR, full commit and rendered body; definite 4xx refusal differs from 408, 5xx, unreadable or mismatched response uncertainty. The stored post decoder/redactor retains typed uncertainty.[unit] src/execution/githubComments.test.ts::postReviewComment::*; src/core/reviewVerdict.test.ts::the stored review post — isReviewPostShape and its redaction::preserves uncertainty through stored decoding and redaction instead of claiming a definitive skip
Review publication bounds local waits by the smaller of the review post-step allowance and the remaining run lease. A stalled journal read or native response returns an unknown outcome and records the blocked stage. Late replies cannot start another POST or turn pending publication into acceptance. Confirmed acceptance survives an unavailable auxiliary head read. The event lane releases stalled admission reads; an already issued canonical write retains the store transport's bound and unknown-write custody.[unit] src/core/reviewRound.test.ts::runReviewPostStep durable original publication::ends a stalled journal read within the remaining lease and cannot post after its late reply, ::retains pending native publication at the deadline and never repeats the original POST, ::keeps committed acceptance when the auxiliary head lookup stalls past the lease, ::does not publish after the run lease has already expired; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::finishes an unknown review when the event-lane admission read stalls and ignores its late result; src/execution/githubComments.test.ts::postReviewComment::cannot issue a native review when token mint resolves after cancellation`
Unexpected review publication errors record the failed stage and a fixed error class without exception names or private text. A failed diagnostic sink preserves the unknown outcome and emits the same safe log.[unit] src/core/reviewRound.test.ts::runReviewPostStep durable original publication::records the stage and safe error class for an unexpected %s failure, ::records an unexpected preflight error without exposing exception text, ::keeps the unknown outcome and logs its safe diagnostic when the event sink throws
Original canonical owner changes refuse publication. A hosted ledger key still binds the actual conversation through immutable run metadata; accepted publication survives terminal finalization.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::admits exact original run review publication durably before native POST and records native acceptance, ::refuses review native POST when the original run owner changes after pending admission, ::a hosted review retains its canonical conversation owner while the ledger uses a host key
CriterionProof
Model transport closure revokes discovery and push authorization for both existing-PR and first-branch runs. Only the same original durable recorder can admit a trusted native post-step; late model begin ACKs and revoked authority cannot borrow that permission.[unit] src/core/modelProxy/gitBindings.test.ts::model publication closure and original post-step claims::seals model writes while retaining the exact original recorder for a trusted post-step: PR=%s; ::refuses a model write whose durable begin ACK arrives after model closure; ::never turns an authorization block or missing recorder into post-step permission; src/channels/githubDoor.test.ts::GitHub run-bearer door::closed model transport refuses discovery and push without borrowing authorization: PR=%s
An uncertain terminal review retains its original publication receipt without reserving PR mutation ownership. A confirmed stop after pending admission refuses before native dispatch; an untracked review does not invent uncertainty or native permission.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::an uncertain terminal review retains its receipt without reserving PR mutation ownership; ::a soft stop after durable pending admission records confirmed refusal without native dispatch; ::a fresh untracked review refuses before native dispatch without inventing uncertainty
CriterionProof
An uncertain stored review is not a confirmed skip. The consumer reads native approval patiently and credits only the same bot at the exact reviewed head; moved-head or unavailable reads cannot supply positive posted credit. The matching native approval can complete the existing unit as merge-ready after its checks, without rewriting the original uncertain publication receipt.[unit] src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::an uncertain stored review uses the native exact-head approval before the unit becomes merge-ready
CriterionProof
A replayed display review event cannot replace absent, pending or uncertain original canonical publication evidence. Losing an original tracked owner remains unknown without not-posted evidence. A confirmed prewrite stop during pending admission refuses before any write; it differs from a lost acknowledgement.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::a replayed review event grants no posted credit or repost permission with canonical receipt %s; ::a fresh review that loses its original ledger owner stays uncertain without not-posted evidence; ::a soft stop inside pending admission refuses before any review write
A failed prewrite permission check rereads the original receipt: lost ownership remains uncertain without not-posted evidence; freshly confirmed absence permits a definite no-write refusal. Neither case issues native POST.[unit] src/core/reviewRound.test.ts::runReviewPostStep durable original publication::owner disappearance between absent receipt and prewrite check stays unknown without not-posted evidence; ::a failed prewrite check with freshly confirmed receipt absence is a definite no-write refusal
CriterionProof
Restarted attaching standalone reviews may adopt a verified current PR head only with an actual canonical-null reservation ACK; original owner/start/generation and repository/ref/PR fences remain. Allocated and unverified heads refuse before promotion.[unit] src/core/dispatch/run.test.ts::original admission identity at promotion::re-reserves a reclaimed original with exact fences: %s

| A proposal without an explicit findings array, or a request for changes without validated findings, remains incomplete. A valid approval with an accepted empty findings array remains complete. Rejected entries, including a dropped Major finding, cannot produce a complete empty report. Rendering preserves the original report and diagnostic rationale, emits no approval or negative-verdict marker, and says the review is incomplete. Transcript replay retains historical accepted facts but cannot restore them as the current verdict after a later rejected or uncertain submission; a fully valid correction can replace the incomplete attempt. Redaction preserves rejection diagnostics. | [unit] src/core/reviewVerdict.test.ts::incomplete review output::*, ::review validation preserves incomplete evidence::*, ::review submission rejection and correction::*, ::missing review output remains incomplete::*, ::review restart cannot promote a rejected latest submission::* |

CriterionProof
Each submitted review keeps its raw proposal and acceptance or refusal only in the original private run state. A refused pending or accepted checkpoint cannot announce a verdict. Public incomplete reports retain authorized prose and a closed validation-stage reason without raw proposal diagnostics.[unit] src/tools/submit.test.ts::review submission acceptance state::*, src/core/reviewVerdict.test.ts::private review rejection projects a safe incomplete report::*
Missing required evidence leaves a review incomplete, with no invented finding or verdict. An empty request for changes is still refused and explains how to stop; a complete approval with no findings remains valid. The review and verdict follow-up prompts permit this stop.[unit] src/tools/submit.test.ts::submit_verdict tool::explains how to stop an incomplete review without inventing a finding or recording a verdict; [gap] Live model compliance with incomplete-review guidance is not qualified here.
Fresh findings use local F1, F2 identities; prior findings use exact controller-issued references in a separate field. Unknown current-run references and an empty negative retry remain incomplete; valid corrected findings may be accepted within the original allowance. Prior metadata, case identities and current-head coverage remain authoritative.[unit] src/tools/submit.test.ts::submit_verdict tool::keeps fresh findings separate from explicitly re-raised canonical prior references, ::re-raises prior cases by ID with evidence, preserving text and identity through publication and accepted replay, src/core/reviewVerdict.test.ts::three reported Minor findings remain incomplete after serialization refusal::*
Finish replay cannot promote a cached approval over the latest rejected or uncertain submission. A lost reply can recover an acknowledged acceptance only for the same original call and proposal. Head changes still require a complete accepted current-head verdict; otherwise the original report remains incomplete.[unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::a latest …, ::a lost review reply restores only its acknowledged original call: …`; existing head-change proofs under item 10

| Current acceptance comes from the original run's private checkpoint. Legacy transcript acceptance also requires matching admitted-call and successful-result events from that same run, followed by the existing complete current-head history checks. Seeded receipts from another run and receipts invalidated by changed history cannot restore approval. | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a fresh same-head review cannot inherit a seeded approval from another run, ::a resume with the answer in hand (the \finish` plan)::restores canonical history before resumed …, ::keeps a %s answer behind its final context checkpoint (persistent failure: %s)` |

| A verdict-only follow-up consumes the latest acknowledged submission state. A later rejected or pending proposal clears an earlier approval from the local capture and original run. An incomplete flag removes stale verdict authority from every channel verbosity, published marker, pending/accepted publication journal and result. | [unit] src/core/verdictTurn.test.ts::runVerdictTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::a later rejected submission clears an earlier approval captured in the verdict turn, src/core/reviewVerdict.test.ts::incomplete submission cannot publish stale approval credit::*, src/core/reviewRound.test.ts::runReviewPostStep durable original publication::an incomplete latest submission gives neither the publication journal nor the result approval credit, src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a verdict follow-up cannot restore approval after its latest submission is rejected | | Initial checkout failures retain their exit status and redacted evidence before any model turn. | [unit] src/core/dispatch/authorize.test.ts::authorizeAttachedHead…::refuses a failed initial checkout using its exit status and retains redacted command evidence |

Managed publication obligation ​

A managed review's canonical pending or uncertain native publication keeps the original run's obligation attached to its unit, even after unit settlement. Managed admission cannot create another review to bypass it. Original effect observation remains permitted. Exact maintenance replay can settle its saved accepted push/spawn without repeating native calls or settling the review POST. The receipt's original POST commit is authoritative; a checkout head, carried review head or later native head does not settle it. Known accepted or refused publication releases that obligation. Independent explicitly requested reviews retain their separate intent and per-run journal.

CriterionProof
Pending and uncertain original publication denies a new managed review while original effect settlement succeeds. Historical carried-head outcomes remain readable and release their original obligation without a new POST or a new carry decision.[unit] src/core/coordinator/managedReviewOwnership.test.ts::managed review publication ownership::*; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::retains the original managed review's … receipt across settlement and index upgrade