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/agents/registry.ts (REVIEW_SYSTEM; resident-path variant REVIEW_SYSTEM_RESIDENT); 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 in src/core/dispatch/runLoop.ts (items 8 + 12) and the post-step in src/core/dispatch/reply.ts (afterReply) + src/core/reviewPost.ts + src/core/reviewVerdict.ts + src/core/reviewedHead.ts + src/core/digestCoverage.ts (item 15) + src/core/headMoved.ts + src/execution/githubComments.ts; verdict tool submit_verdict and digest tool diff_digest in src/tools/workspace.ts
  • Docs: The agents and their toolsets
  • Budgets: 30 turns (backstop) / 25 min / 64k tokens · 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

  1. Gather once (2–4 batched calls): PR meta + full diff + full current contents of changed files; enormous PRs print risky files in full and say what was skimmed.

  2. Analyze once with everything in context: correctness bugs first, each with a concrete failure scenario; then design/simplification notes. At most 2–3 targeted follow-up reads.

  3. Report everything found with severity, confidence, and file:line, most-severe first. A correct change gets a plain "looks correct" — no manufactured findings.

  4. Maintains the status-card checklist; never pre-marks reporting steps.

  5. Leads the final message with a one-line verdict, and declares the verdict structurally via the submit_verdict tool (approve | request_changes + one-line summary; readonly toolset only). The verdict is a judgement the model makes; how it reaches GitHub is not up to the model (next item).

  6. 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 agent's final review text 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: <summary> first iff the submitted verdict is approve, Changes requested: <summary> for request_changes, and the explicit line No verdict submitted — not approving. when the tool was never called (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 Slack reply is the model's text unchanged.
    • 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). Best-effort: a post failure is logged but never fails the run (the review already landed in Slack).
    • 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 post itself fails, 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. 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.
  7. 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.

  8. Reviewed-head guard — a review is posted to a PR only if it is a review OF that PR's head (reviewedHead.ts): 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 the thread note ℹ️ Review not posted to <repo>#<n>: reviewed head <x> is not the PR head <y> — this 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.

  9. 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 mismatch as your only finding, request_changes, 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). Sandbox path: clone + gh pr checkout <n>, then the same HEAD check. 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.

  10. The worktree is named and the attach is checked 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 compares the attached sha with the PR head itself, before any model turn: equal (≥7-hex prefix match, sameCommit) → the block says "Switchboard attached this worktree at that commit and verified it before this run" (the model-side STOP rule stays as the backstop for drift after attach); well-formed and different → the branch moved between resolution and attach (a push/force-push racing the request — item 8 would refuse the post anyway), so the run is not started: card 🔀 … not started (branch moved), ONE reply naming both short shas and asking to re-send, the read-only worktree released (/detach force), no provider call; malformed/absent attach sha → unverified, the run proceeds exactly as before. Review-only: a coding run attached elsewhere still runs (it branches off whatever it has). The resident needs no change — its binding, exec cwd (cd <worktree> && …) and attach answer are already correct in that failure.

  11. 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.

  12. 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; it is only a fallback 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.

  13. Head moved during the run — carried across a rebase, re-reviewed otherwise; the current head is consulted before either guard refuses (headMoved.ts): when the head moves mid-run by a rebase onto main, item 10 alone would post the review pinned to the old head with the re-request note, and the re-request would spend a second full run reaching the same verdict — items 8 and 10 are tuned for the unsafe case (a stray review, unreviewed code) and would treat every move as one. So, after the model finishes and before anything is posted, the dispatcher fetches the PR head once and compares it with the head the agent reviewed (observed, else reported — the guard's own authority order). (a) Reviewed = current ≠ resolved (a mid-run re-attach after an eviction landed on the ref's newer tip): the review IS of the PR's head — adopted, posted pinned to it, no refusal. (b) Reviewed = resolved ≠ current — the PR moved under the review: the move is classified from GitHub's compare lists (GET /repos/{repo}/compare/{base}...{sha} for the reviewed and the current head, CoreDeps.fetchPrCommits, default prCommitsSince; parallel, best-effort). Rebase: same number of commits, same messages in order, same set of touched files (300-file cap on either side → messages decide) — a rebase onto main, conflict resolutions included — the review applies unchanged: posted pinned to the new head with the footer _Reviewed at <x>; the head moved to <y> during the review — a rebase of the same N commits — so this review is posted against <y>._ and the thread note ℹ️ <repo>#<n> moved during the run: reviewed <x>, head is now <y> — a rebase of the same N commits (same messages, same files). The review applies unchanged and was posted pinned to <y>. (an LGTM: auto-approves as usual). Substantive (new/dropped/reordered/reworded commits, or same messages reaching new files — the cheap guard against a message-preserving --amend): the same run re-reviews at the new head — no new run, no re-request. In order: a run_note of kind head_moved on the run stream, the card label gains · head moved → <y>, the thread gets 🔀 <repo>#<n> moved during the run: reviewed <x>, head is now <y> — B → A commits (+ “subject”, − “subject”, …). Re-reviewing at <y> before posting.; the worktree is moved to the new head where the executor can (Executor.moveTo, resident: one more /attach carrying the new sha — the item 51 fetch-on-attach recreates the tree at the ref's tip); the system prompt is recomposed with the new REVIEW TARGET head (verified when the move landed there); the earlier verdict is voided; the conversation continues with the first review as an assistant turn and a follow-up user turn (rereviewFollowUp: both heads, both commit lists by subject, where the worktree stands — or, with no moveTo / a refused move, git fetch origin <y> && git checkout <y> — and a fresh submit_verdict with head = <y>); the run's answer event and the Slack reply are the re-review's. The post is then pinned to the new head after the ordinary item 8 guard. Unclassifiable (no base branch, compare failed for either side) → item 10 exactly as before. A head that moves again after the re-review gets item 10's note, never a third turn. Hard stop → none of this. (c) Reviewed ≠ both → the agent strayed: item 8 refuses as before, having consulted the current head first. (d) Before the run (item 10's attach check): attached ≠ resolved now also asks GitHub once — the resident's attach fetched the mirror to the ref's tip, so a push that raced the request leaves the worktree at the PR's head NOW; attached = current → the run reviews the attached head (RepoContext adopts it, the block says the attach was verified) instead of refusing and asking for a re-send; anything else (unknown current, or a second move) → not started (branch moved) as before. Bounds: the re-review turn runs under the agent's budgets again (worst case one more review's worth of turns/time); the follow-up carries the first review's text, not its tool transcript (the model re-reads what it needs); the superseded first review is not in the run record (the note is).

  14. The verdict reply carries its run link — projection only: a review run's channel reply (the verdict message) ends with a [Live run](<url>) line appended by the dispatcher 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 bare answer, as before; non-review agents get no link (their answers are not verdicts, and their card carries it).

  15. 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 titled Spec contradiction — <spec file> item <n>: <what the code now does vs what the spec says>, 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.

  16. 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, the last call wins, kept on the ledger row like the verdict). (c) After the item 8 head guard passes, checkDigestCoverage({ digest, pr: prSize }) 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. Only 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 refuses only an incomplete digest.

Validation criteria

CriterionProof
Budgets and toolset as specified[unit] src/agents/registry.test.ts::review: readonly toolset, 30-turn backstop, 25 min; budget mechanics proven in src/runner.test.ts.
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 ….
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, one-line verdict first, findings with file:line + severity + confidence.
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 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.
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/workspace.test.ts::submit_verdict tool, src/core/reviewVerdict.test.ts::parseVerdictInput carries a well-formed reported head…
REVIEW TARGET block (item 9): present on a resident review with a resolved PR (resident variant, carries the RepoContext head sha), present on a sandbox review (clone + gh pr checkout <n> variant), 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), resident variant forbids git fetch[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).
Attach-head mismatch never reaches the model (item 10, 12d): attached sha = PR head → run with the verified block + path in the resident preamble; attached ≠ PR head → the current head is asked once; = attached → the run reviews the attached head (block names it, verified); unknown/other → not started (branch moved) card, one reply with both short shas + "re-send", /detach {force:true}, zero provider calls, nothing posted; coding runs unaffected[unit] src/core/dispatcher.test.ts::…::a resident review attached AT the PR head…, ::a resident review attached at ANOTHER commit than the PR head is not started…, ::a resident review attached at a commit that IS the PR's current head (moved since resolution) runs…, ::a coding run attached at a commit other than the PR head still runs…. [agent] post-deploy: agent:review <PR URL> on an open PR in a fresh thread — the run page's first bash call is git rev-parse HEAD with no cd, and the review posts to the PR (no "workspace mismatch" verdict).
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
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; inherited PR closed → still runs Slack-only; "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 review whose inherited PR is CLOSED still runs…, ::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; on fetch failure the prose ref is the fallback and the head stays 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, with a prose ref beside it…
Reviewed-head guard (item 8): observed HEAD ≠ PR head (resolved AND current, item 12c) → no post + thread note with both shas + [review-post] … skipped: reviewed head … log, the current head asked once before refusing; observed HEAD read BEFORE release; no headSha → no post ("PR head unknown"); no git in cwd → reported head decides (match posts pinned, mismatch/absent → no post); matching reported never overrides mismatching observed; non-PR reviews never probe[unit] src/core/reviewedHead.test.ts; src/core/dispatcher.test.ts::review post-step …::reviewed-head guard (8 cases). [agent] In a PR thread, ask for a review while the PR body references another PR whose branch has newer commits → the GitHub review appears on the PR pinned to ITS head, or — if the agent strayed — no GitHub review and the ℹ️ note in Slack naming the two heads.
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[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, post failure swallowed), 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; closed PR / failed fetch / malformed SHA → no post, said in the thread (ℹ️ Review not posted to …); 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::a bound PR that could not be posted to … is said out loud…, ::a post failure is swallowed … (thread note asserted). [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 → Slack reply plus the ℹ️ Review not posted to … the PR is closed note, no new 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. The skipped: log line is not capturable via wrangler tail (container stdout) — the evidence is the absent GitHub review plus the unchanged review count on the 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-move classification (item 12): same count + same messages in order + same file set → rebase (file order irrelevant; whitespace-trimmed messages; truncated file list on either side → messages decide); new/dropped/reordered/reworded commits or a new file set → substantive with added/removed subjects (first lines)[unit] src/core/headMoved.test.ts::classifyHeadMove… (7)
Item 12 texts: carried note + posted footer name both 7-char shas, the commit count, the new pin; re-review note names both shas, B → A commits, ± subjects capped at 4 (+N more), "same messages, different files" for an amend; follow-up names both heads, lists both commit sets by subject only, moved-worktree vs fetch+checkout variants, demands a fresh submit_verdict with the new head[unit] src/core/headMoved.test.ts::item 12 thread notes…, ::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[unit] src/execution/resident.test.ts::ResidentExecutor.moveTo
Rebase-only move (item 12b): ONE model turn, no moveTo, both compare sides asked with the PR base, post pinned to the NEW head with the carried footer after the verdict body, carried note in the thread, no re-request note[unit] src/core/dispatcher.test.ts::…::head moved during the run (item 12)::rebase-only move…
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"); second turn's system names the new head and not the old; 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 without moveTo / with a refused moveTo: follow-up says git fetch origin <y> && git checkout <y>; the run continues; a model that did not check out (observed HEAD still old, verdict reports new) is refused by the item 8 guard[unit] ::substantive move on an executor without moveTo…, ::worktree move fails (resident refuses)…
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 (item 12): a review whose PR is rebased mid-run (same commits) posts once, pinned to the new head, with the footer; the thread shows the ℹ️ carried note and an installation that has turned on the auto-approve workflow approves on LGTM:. A review whose PR gets a new commit mid-run shows the 🔀 note + head moved → on the card, one more model turn on the run page, and a single GitHub review pinned to the new head[agent] human-gated: push during a live review on a scratch PR; receipts on the tracker issue.
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)
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 titled by spec file and item, 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 titled Spec contradiction — <that spec> item <n>: … 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.
Quality bar[agent] A review of one of this repo's own PRs catches ≥1 real blocker or verifies root causes against vendored SDK source — that's the bar to hold.