> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Code Review

> A reviewer agent that owns each pull request: one severity-ranked comment, a managed check, folding re-reviews on every push, and an optional Slack verdict.

This example puts a standing reviewer on every pull request in a repository. The agent reads the diff in context — the surrounding code, the repo's own convention docs, recent related changes — posts exactly one severity-ranked review comment with a merge recommendation, and reports a GitHub check that can gate merges. One session owns each PR: pushes and review conversation route back to it instead of spawning a duplicate reviewer.

Use it when you want review coverage that scales with PR volume without a human in the loop for the first pass, and a check that branch protection can require.

## The problem

Human review attention is the scarcest resource in most teams. The failure mode of naive automation is noise: a fresh bot review per push, restated diffs, unranked nitpicks, and no way to tell whether the latest verdict applies to the latest head. This example is built around three constraints:

* **One current verdict per PR.** A new push supersedes the in-flight review rather than stacking a second one.
* **Read-only by construction.** The mount grants `contents: read`, and the GitHub tool surface is narrowed to reading PRs and posting one comment — no approve, no merge, no pushes, regardless of what the prompt says.
* **A check, not just a comment.** The trigger declares a managed check (`pr-review`) that the platform creates as `queued` the moment the session spawns and that concludes `failure` on its own if the agent never reports — so a dead reviewer can't leave a PR permanently blocked in limbo.

## How it works

```mermaid theme={null}
flowchart TD
    A["github.pull_request.opened /<br/>reopened / synchronize"] --> B{"Session bound to<br/>this PR?"}
    B -->|no| C["Spawn reviewer session,<br/>bound to the PR at spawn"]
    B -->|yes| D["Deliver into the owning session:<br/>fold the new head into the review"]
    C --> E["checks.begin → review →<br/>one PR comment →<br/>checks.success / failure"]
    D --> E
    F["PR comments, reviews,<br/>review comments"] -->|"bind, onUnmatched: drop"| G["Same session reads the update"]
    E --> H["Optional Slack verdict<br/>threaded in #pr-review"]
```

Three triggers drive the agent:

| Trigger           | Events                                                                                                                                          | Routing                                               | Purpose                                                                      |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
| `pr-events`       | `github.pull_request.{opened,reopened,synchronize}`                                                                                             | `bind` on `github.pull_request`, `onUnmatched: spawn` | Start a review, or fold a new head into the session that already owns the PR |
| `pr-conversation` | `github.issue_comment.{created,edited}`, `github.pull_request_review.{submitted,edited}`, `github.pull_request_review_comment.{created,edited}` | `bind` on `github.pull_request`, `onUnmatched: drop`  | Route human follow-up to the owning session; drop it if no reviewer is live  |
| `mention`         | `chat.message.mentioned`                                                                                                                        | `spawn`                                               | Optional Slack entry point for ad-hoc review requests                        |

The `bind` + `onUnmatched: spawn` combination is what makes the session own the PR: when no session is bound to the pull request yet, the router spawns one and binds it to the PR in the same creation step, so every later matching event resolves to it. See [triggers and events](/concepts/triggers-and-events) for the routing model.

## Install from the consolidated agent package

The packaged PR Review role is published in `@auto/agents`. The thin install is one file — the import carries the prompts, triggers, tools, runtime, and identity:

```yaml .auto/agents/pr-review.yaml theme={null}
name: pr-review
imports:
  - "@auto/agents@latest/pr-review.yaml"
variables:
  repoFullName: acme/widgets
  githubConnection: github-acme
```

Set `repoFullName` and `githubConnection` to your repository and GitHub connection. When [GitHub Sync](/concepts/github-sync) applies this file from the bound repository, both variables also have context defaults derived from the Sync binding, and values you declare win. Override any inherited field by declaring it in this file (triggers merge by their `name:` key), and drop inherited entries with `remove: { triggers: [...], tools: [...] }`. See [managed templates](/reference/managed-templates) and [imports and fragments](/reference/imports-and-fragments).

Merge the file to your production branch and GitHub Sync applies it.

## The full configuration

The directory below is the handwritten equivalent of the template, pinned to a concrete repository so every name lines up. Copy it if you want to own the whole definition instead of tracking the template.

```text theme={null}
.auto/
  agents/pr-review.yaml
  assets/pr-reviewer.png
  fragments/environments/agent-runtime.yaml
```

```yaml .auto/fragments/environments/agent-runtime.yaml theme={null}
harness: claude-code
environment:
  name: agent-runtime
  image:
    kind: preset
    name: node24
  resources:
    memoryMB: 8192
```

```yaml .auto/agents/pr-review.yaml theme={null}
name: pr-review
model:
  provider: anthropic
  id: claude-opus-4-8
identity:
  displayName: PR Review
  username: pr-review
  avatar:
    asset: .auto/assets/pr-reviewer.png
  description: Reviews each pull request, posts one merge recommendation, and optionally reports the verdict in #pr-review.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are the code review agent for acme/widgets.

  Read the repository's convention docs (README.md, CONTRIBUTING.md, AGENTS.md,
  CLAUDE.md, and any style guides) before judging a diff, and incorporate the
  user's documented preferences where they are current and relevant. Do not
  blindly enforce stale local-agent instructions, local-only setup notes, or
  errata. Confirm important preferences against the current repo shape and CI.

  Review posture:
  - Prioritize correctness bugs, regressions, data integrity, operational risk,
    and missing tests over style nits.
  - Prefer simple, practical code over performative functionality, security
    theater, or abstractions that only add indirection.
  - Prefer established local patterns over home-rolled machinery.
  - Look for strong type guarantees at ingress and egress, especially provider
    payloads, webhook inputs, API boundaries, environment variables, database
    rows, and tool outputs.
  - Look for real tests, especially at provider boundaries. Expect both success
    and failure cases when behavior crosses an external system.
  - Run targeted tests or typechecks when they would validate a concrete
    concern; install only the dependencies those commands need. Keep
    commands scoped to the PR.
  - Be terse. Produce exactly one PR comment a human can scan in seconds:
    - a `## Recommendation` line that is exactly `thumbs-up` or `thumbs-down`,
      immediately followed by a one-line rationale. Do not restate what the PR
      does, do not write a Summary section, and do not praise the work;
    - a `## Findings` section listing only material findings, most severe
      first, omitting the section entirely when there are none (put
      `No blocking or notable findings.` in the rationale instead). Each
      finding is one tight line, no sub-bullets:
      `P{n} · {dimension} · {file:line} — {what's wrong} → {why it matters}`
      where dimension is one of correctness, security, data-integrity,
      operational-risk, missing-tests, or idioms. No diff restatement, no
      per-file walkthroughs, no Impact/Source/Verification/Fix sub-bullets;
    - drop P3 (nits) from the comment entirely; they never gate and only add
      noise. The severity tiers that drive the recommendation:
      P0 — blocker (breaks the goal, or a severe correctness/security/
      data-integrity failure); P1 — major (a likely failure, missing critical
      handling, or a missing test for high-risk behavior); P2 — minor
      (meaningful friction, inconsistency, or weak coverage); P3 — nit (never
      posted). Thumbs-down on any unresolved P0 or P1, thumbs-down on an
      unresolved P2 unless the PR documents why it is acceptable, and never on
      a P3 alone.

  You are the one reviewer session for your pull request: updates to it route
  back to you instead of spawning another reviewer. When a message announces a
  new head — whether you are mid-review or already posted a verdict — fold it
  into your review cycle: analysis of the older head is superseded (never post
  its verdict or conclude the managed check with it), the managed check has
  been rolled onto the new head, and you re-begin the check and re-review
  against the pull request's current head. Keep exactly one current verdict
  per pull request at all times.

  When posting GitHub comments, append this hidden attribution marker with
  the environment variables expanded:

    <!-- auto:v=1 session_id=$AUTO_SESSION_ID agent=$AUTO_AGENT_NAME -->

  Slack verdict reporting is optional and uses the standard `slack` connection
  and #pr-review channel. When the chat tool is available, use mrkdwn links,
  reuse or create one top-level PR thread, and post the verdict as one brief
  reply. When the tool is unavailable, skip Slack without treating it as a
  review failure; the GitHub comment and managed check remain complete.

  Hard limits: do not edit files, push commits, approve, request changes,
  or merge.
initialPrompt: |
  Review GitHub pull request #{{github.pullRequest.number}} in
  {{github.repository.fullName}}.

  Call checks.begin with { "name": "pr-review" } before doing anything else.
  Your session is already bound to this pull request at spawn, so later PR
  comments, reviews, and pushes route back to this session without an
  explicit bind call.

  Inspect the PR metadata with the pull_request_read tool (method `get`),
  then the changes (methods `get_diff` and `get_files`). Record the head
  commit SHA you reviewed.

  The local checkout is a shallow checkout of the PR head only. Fetch other
  refs explicitly if you need them.

  Post exactly one review comment with the add_issue_comment tool, following
  the review posture and attribution marker from your instructions.

  Then conclude the check: checks.success for a thumbs-up recommendation,
  checks.failure for thumbs-down, including the reviewed SHA, the
  recommendation, and the findings that gate it (unresolved P0/P1, plus any
  P2 that drove a thumbs-down).

  When the chat tool is available, inspect #pr-review for an existing thread
  for this PR, creating one only when none exists, then post one threaded reply
  with the recommendation, gating findings or `No blocking issues found.`, a
  raw mrkdwn link to the PR comment, and the reviewed commit SHA. When the chat
  tool is unavailable, skip this Slack step.
mounts:
  - kind: git
    repository: acme/widgets
    mountPath: /workspace/repo
    ref: refs/pull/{{payload.github.pullRequest.number}}/head
    depth: 1
    auth:
      kind: githubApp
      capabilities:
        contents: read
        pullRequests: write
        issues: write
        checks: read
        actions: read
workingDirectory: /workspace/repo
tools:
  auto:
    kind: local
    implementation: auto
  github:
    kind: github
    tools:
      - pull_request_read
      - add_issue_comment
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
      optional: true
triggers:
  - name: mention
    event: chat.message.mentioned
    connection: slack
    optional: true
    where:
      $.chat.provider: slack
      $.auto.authored: false
    message: |
      {{message.author.userName}} mentioned you on Slack:

      {{message.text}}

      Channel: {{chat.channelId}}
      Thread: {{chat.threadId}}

      Reply in that thread with chat.send. If the user clearly links or names
      a PR, review it. If required context is missing, ask for the PR. Otherwise,
      briefly explain that you review pull requests for acme/widgets, post one
      PR comment, report a check, and optionally leave a short Slack verdict.
    routing:
      kind: spawn
  - name: pr-events
    events:
      - github.pull_request.opened
      - github.pull_request.reopened
      - github.pull_request.synchronize
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
    message: |
      Pull request #{{github.pullRequest.number}} in {{github.repository.fullName}} has a review-triggering
      update (action: {{github.action}}; current head {{github.pullRequest.headSha}}).

      You are the reviewer session bound to this PR, so fold this update into
      your review cycle now:
      - Analysis still in progress for an older head is superseded. Do not
        post its verdict and do not conclude the managed check with it. The
        platform has already concluded the old head's check run and queued a
        fresh `pr-review` check for the current head.
      - Call checks.begin with `{ "name": "pr-review" }` before inspecting
        anything else; completing a rolled-over check without a fresh begin
        is rejected as a stale verdict.
      - The local checkout still holds the head this session started from.
        Fetch the current head before inspecting the diff:
        `git fetch origin refs/pull/{{github.pullRequest.number}}/head` and
        check out the fetched commit.
      - Re-run your full review protocol from your initial instructions
        against the current head, including every required output for this
        entrypoint. Treat this as a repeat review when your prior review
        comment exists: summarize what changed since it and post a fresh
        review comment with add_issue_comment.
      - Conclude the check with checks.success or checks.failure for the
        current head's verdict. There must be exactly one current verdict
        for this PR.
    checks:
      - name: pr-review
        displayName: Auto PR review
        description: Auto reviews this pull request and reports whether blocking issues were found.
        instructions: |
          Call checks.begin with { "name": "pr-review" } before doing
          anything else. After posting the review comment, call
          checks.success for a thumbs-up recommendation or checks.failure
          for thumbs-down, with a summary of the gating findings (unresolved
          P0/P1, plus any P2 that drove a thumbs-down). A delivered PR update
          rolls this check onto the new head and queues it again; call
          checks.begin again before concluding that new cycle.
        beginTimeout:
          seconds: 1200
          conclusion: failure
        completeTimeout:
          seconds: 1200
          conclusion: failure
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: spawn
  - name: pr-conversation
    events:
      - github.issue_comment.created
      - github.issue_comment.edited
      - github.pull_request_review.submitted
      - github.pull_request_review.edited
      - github.pull_request_review_comment.created
      - github.pull_request_review_comment.edited
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.authored: false
      $.github.auto.externalBot: false
    message: |
      A PR conversation update arrived for acme/widgets PR #{{github.pullRequest.number}}.

      Source URLs, when present:
      - issue comment: {{github.issueComment.htmlUrl}}
      - review: {{github.review.htmlUrl}}
      - review comment: {{github.reviewComment.htmlUrl}}

      Read the update, incorporate any material reviewer or author context,
      and decide whether the pull request needs a refreshed review or a
      concrete blocker summary. Do not react to your own prior comments.
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
```

Points worth noticing before the walkthrough:

* **The mount is the PR head itself.** `ref: refs/pull/{{payload.github.pullRequest.number}}/head` with `depth: 1` checks out exactly what is under review. Mount `ref` is the one template surface where the `payload.` prefix is correct — see [mounts](/reference/mounts).
* **`contents: read` makes the reviewer physically unable to push**, while `pullRequests: write` and `issues: write` allow the review comment. GitHub credentials are minted per call by the platform and never enter the sandbox — see [the GitHub tool](/runtime/github-mcp).
* **The `github` tool is narrowed to two tools** (`pull_request_read`, `add_issue_comment`). Naming tools replaces the curated default allowlist entirely.
* **Slack is optional wiring.** Both the chat tool and the mention trigger carry `optional: true`: apply skips them while no `slack` connection exists and activates them on the next apply once one does. The GitHub review flow works either way.
* **`$.github.auto.authored: false` and `$.github.auto.externalBot: false`** keep the conversation trigger from reacting to the agent's own comments or to third-party bots.

## Walkthrough

<Steps>
  <Step title="A pull request opens">
    GitHub delivers `pull_request` with action `opened`; ingress verifies the webhook signature, normalizes it to `github.pull_request.opened`, and records it once (duplicate deliveries never re-route). The `pr-events` trigger matches its `where` filter on the repository.
  </Step>

  <Step title="Spawn, bound to the PR">
    The `bind` route looks for a session bound to this `github.pull_request` target. There is none, so `onUnmatched: spawn` creates one — and binds it to the PR in the same creation step. From now on, every matching event for this PR resolves to this session.
  </Step>

  <Step title="The check appears immediately">
    Because the trigger declares `checks:`, the platform creates the **Auto PR review** check run on the head SHA in `queued` state as part of routing — before the sandbox even boots — and arms the `beginTimeout` watchdog. If the agent never calls `checks.begin` within 1200 seconds, the check concludes `failure` on its own.
  </Step>

  <Step title="The review runs">
    The sandbox boots the `node24` environment with the PR head mounted at `/workspace/repo`. The agent calls `checks.begin` (check goes `in_progress`), reads the convention docs and the diff, optionally runs targeted tests, posts exactly one review comment with `add_issue_comment`, and concludes with `checks.success` or `checks.failure` carrying the reviewed SHA and gating findings. If Slack is connected, it threads a one-line verdict under the PR's message in `#pr-review`.
  </Step>

  <Step title="A push folds into the same session">
    The author pushes; GitHub sends `synchronize`. The `bind` route now resolves the owning session, so instead of spawning a second reviewer, the trigger `message` is delivered into it — interrupting the current turn if one is running. On the platform side the delivery supersedes the old check cycle: the old head's check run is concluded (as skipped when it never finished) and a fresh `queued` run is created on the new head. The agent re-begins the check and re-reviews the current head. One PR, one session, one current verdict.
  </Step>

  <Step title="Conversation routes back; nothing leaks after archive">
    Reviewer comments and review threads deliver into the same session through `pr-conversation`. Its `onUnmatched: drop` means that once the session is gone, stray conversation events are dropped rather than spawning a reviewer with no review context — while a fresh push (via `pr-events`, `onUnmatched: spawn`) starts a new owning session.
  </Step>
</Steps>

To make the review blocking, mark **Auto PR review** as a required status check in the repository's branch protection — the check's `displayName` is what GitHub shows.

## Variations

* **Cheaper reviews.** Swap `model.id` to `claude-sonnet-5` (any id from the curated Anthropic list works with the `claude-code` harness). Keep Opus for repositories where a missed P0 is expensive.
* **Several repositories.** Install one thin template file per repository with a distinct agent `name` (for example `pr-review-widgets`, `pr-review-api`) and per-file `variables`. Two agents cannot share a name.
* **Tune the posture, keep the machinery.** Override `systemPrompt` in your importing file to encode your team's real review bar — the trigger/check/binding machinery is inherited unchanged. Use `{ append: ... }` on `systemPrompt` to add house rules below the template's text instead of replacing it.
* **CI watchdog.** If required GitHub Actions workflows guard the repo, configure GitHub Sync's `ciWatchdog` (via the `auto.sync.enable` tool or [sync settings](/concepts/github-sync)) for workflows that support `workflow_dispatch`. This is deliberately separate from the review trigger: `github.check_run.completed` triggers are passive routing and may reference checks auto cannot safely re-dispatch.
* **React to CI results.** Add a trigger on `github.check_run.completed` with `routing: { kind: bind, target: github.pull_request }` and a `where` filter like `$.github.checkRun.conclusion: failure` to have the reviewer weigh red CI into its verdict. Check-completion deliveries defer until the session is idle instead of interrupting mid-review. See [GitHub events](/reference/events/github).
