# Code Review Agent

The Code Review Agent is a precision-first pull request reviewer. It captures a pull request, plans once, runs four specialists in parallel, has a verifier try to disprove each one's findings, consolidates the survivors in code, and posts only when asked. It's a command-line app built only on June's public packages: [`apps/code-review-agent`](https://github.com/johno/june/tree/main/apps/code-review-agent).

It's also June's runtime thesis in one application: dependencies are nesting, independent specialists render in parallel, recovery lives at branch boundaries, and the rendered tree is the execution trace.

## Problem

A person wants a second reader on a pull request: one that catches the bug they missed and otherwise stays out of the way. Typical AI review bots comment on everything until the one real finding is lost and people stop reading. This one optimizes for precision:

- **A real bug beats volume.** A finding is a concrete failure this pull request introduces, on a line it changed, checked against the surrounding code.
- **There's no quota, and no findings is a successful review.** Findings the verifier isn't sure of are held back, and a review shows at most five.
- **It stays out of everything else.** No style, naming, formatting or documentation comments, no refactors, no risks with no failure today, no checks that callers or types already guarantee, and nothing on code the pull request didn't change.

Precision alone would reward a reviewer that never says anything, so the [benchmark](#testing) measures recall beside it.

## Architecture

```text
capture the pull request          plain code, before the run
  └─ plan                         one structured step
       └─ <Parallel>              independent: they start together
            ├─ correctness  propose ─▶ verify ┐
            ├─ security     propose ─▶ verify │  each inside <Catch>,
            ├─ tests        propose ─▶ verify │  retried once from the
            └─ performance  propose ─▶ verify ┘  diffs alone if it fails
consolidate                       plain code, after the run
  └─ --post                       optional: one comment-only GitHub review
```

Every edge inside the run is a dependency, written as nesting: the specialists can't render until the plan's result exists, and a verifier can't render until its specialist has proposed something. The only independence is declared once, with `<Parallel>`: the four specialists don't depend on each other, so they run together. Capture and consolidation sit outside the run as ordinary code.

- **GitHub boundary** (`src/github.ts`): the only code that knows GitHub. It captures the pull request, reads files at the captured commits, and posts.
- **Read-only tools** (`src/tools.ts`, `src/repository-access.tsx`): `listHeadFiles`, `readHeadFile` and `readBaseFile`, AI SDK tools over a small `Repository` interface that GitHub and the benchmark's fixtures both implement, declared in each agent that reads with one capability bundle, `<RepositoryAccess>`.
- **Workflow** (`src/review.tsx`): `<CodeReview>`, the June composition, over a plain, serializable `PullRequestContext`.
- **Consolidation** (`src/findings.ts`): deterministic code that turns verified findings into the review, and records what it held back and why.
- **Saved run**: every review is saved to `<home>/runs/<owner>/<repo>/<number>/<head sha>.json`, where `<home>` is `CODE_REVIEW_AGENT_HOME` (`.code-review-agent` by default), with June's `RunResult` embedded unchanged.

The workflow never sees a URL or a token, and models never get a write tool.

### Why June

The framework's value is in the shape of `src/review.tsx`, not its imports:

- **Composition is the dependency graph.** The plan passes its result to the specialists, and each specialist passes its candidates to its verifier, through render props. There's no graph to declare beside the code.
- **`<Parallel>` is the independence.** Nothing in the app schedules branches.
- **`<Catch>` contains a branch's failure.** One specialist failing never touches another, and the retry is just another subtree.
- **`useStep` puts ordinary code on the run.** Here it records a caught failure, so consolidation and the saved run still see it.
- **The trace mirrors the source.** A saved run has the same shape as the JSX.
- **The AI SDK still owns models and tools.** Models, `tool()`, `Output.object` and `stepCountIs` are the AI SDK's; June composes the calls that use them.

## June modules used

| Import       | Why the Code Review Agent needs it                                                                                                                                    |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `june`       | The workflow: `<Agent>`, structured `<Step>`, `<Parallel>` for the specialists, `<Catch>` per branch, `useStep` to record a failure, and `run()` with an abort signal |
| `june/train` | Measuring the product objective: `evalSuite`, `evaluate` and `grader` for the precision-first benchmark                                                               |

It doesn't use `june/chat`, `june/copy` or `june/trainers`: a review is one run, with no conversation, voice or model to train.

## Execution flow

### Capture

A review starts outside June. `fetchPullRequest()` reads the pull request, pages through its changed files, checks that the head didn't move while it was reading, and asks GitHub for the merge base the patches are diffed against:

```ts
// Patches are diffed against the merge base, not the base branch's tip,
// which may have moved on since the pull request branched.
const what = `the merge base of ${name}`;
const compared = await request(
  `${repo}/compare/${pull.base.sha}...${pull.head.sha}?per_page=1`,
  what,
  options,
);
```

From [`apps/code-review-agent/src/github.ts`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/github.ts#L222-L229), lines 222–229.

Every read then happens at those two commits, so a push mid-review doesn't change what's reviewed. The captured pull request becomes the workflow's context, and its repository is where the specialists read:

```tsx
const pr = await fetchPullRequest(target, { token, signal });
const repository = githubRepository(pr, { token });
```

From [`apps/code-review-agent/src/review-pull-request.tsx`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/review-pull-request.tsx#L67-L68), lines 67–68.

### Plan, then fan out

One structured `plan` step routes the pull request: for each specialist, the changed files in its area and what to check there. Its result is the argument to its children, so the four specialists render once it's done, inside one `<Parallel>`:

```tsx
<Agent model={models.planner} instructions={planningInstructions}>
  <Step id="plan" prompt={pullRequestPrompt(pr)} output={reviewPlan}>
    {(plan) => {
      const branch = { pr, plan, repository, model: models.reviewer };
      return (
        <Parallel id="specialists">
          <Specialist kind="correctness" {...branch} />
          <Specialist kind="security" {...branch} />
          <Specialist kind="tests" {...branch} />
          <Specialist kind="performance" {...branch} />
        </Parallel>
      );
    }}
  </Step>
</Agent>
```

From [`apps/code-review-agent/src/review.tsx`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/review.tsx#L42-L56), lines 42–56.

Each specialist looks only at the files the plan gave it, and reads beyond the diff through the repository to find callers and tests or the code before the change. A specialist the plan gave nothing renders `null` and doesn't run, so a documentation change costs one model call. A test proves the fan-out from the trace: every branch's `startedAt` comes before the first branch's `endedAt`.

### Propose, then try to disprove

A specialist proposes candidate findings with structured output: a path, a changed line, the failure mode, the evidence and a severity. When there are any, its step renders a verifier `<Agent>` with its own instructions:

```tsx
<Step
  id={`${id}-candidates`}
  prompt={specialistPrompt(pr, plan, scope) + note}
  output={candidateFindings}
>
  {({ findings }) =>
    findings.length > 0 && (
      <Agent
        model={model}
        instructions={verifierInstructions(kind)}
        stopWhen={stepCountIs(MAX_STEPS)}
      >
        {repository && <RepositoryAccess repository={repository} />}
        <Step
          id={`${id}-verify`}
          prompt={verifyPrompt(pr, findings) + note}
          output={verification}
        />
      </Agent>
    )
  }
</Step>
```

From [`apps/code-review-agent/src/review.tsx`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/review.tsx#L127-L148), lines 127–148.

The verifier's job is to falsify: it looks for the guard, caller invariant, existing handling, test or same behavior in the base version that would make each finding false, keeps what survives with a confidence of `high` or `medium`, and says why it rejected the rest. This is where real noise goes away. Reviewing [johno/june#338](https://github.com/johno/june/pull/338), the Sonnet verifier rejected two of the tests specialist's four candidates with reasons grounded in code it read ("refused, because `closed` is already true at the post-steer check (line 363)").

### The rendered tree is the trace

Because each stage is a component, a run's trace has the shape of the JSX, and `test/workflow.test.tsx` asserts it:

```text
agent (planner)
└─ step plan
   └─ parallel specialists
      ├─ agent (reviewer; listHeadFiles, readHeadFile, readBaseFile)
      │  └─ step correctness-candidates   ← its tool calls
      │     └─ agent (verifier instructions)
      │        └─ step correctness-verify
      ├─ agent … step security-candidates
      ├─ agent … step tests-candidates
      └─ agent … step performance-candidates
```

Each step carries its status, result, model, usage, timing and tool calls. To see why a finding was shown or dropped, follow its `source` step in the saved run to `<kind>-candidates` and `<kind>-verify`.

### Recover from a failed branch

If a specialist or its verifier fails, its `<Catch>` fallback records the failure and runs the branch once more, from the diffs alone and without tools. If that fails too, it records the second failure and the review carries on without that specialist:

```tsx
<Catch
  fallback={(error) => (
    <>
      <Failure id={`${kind}-failure`} error={error} />
      <Catch
        fallback={(retryError) => (
          <Failure id={`${kind}-retry-failure`} error={retryError} />
        )}
      >
        <ProposeAndVerify id={`${kind}-retry`} {...branch} />
      </Catch>
    </>
  )}
>
  <ProposeAndVerify id={kind} repository={repository} {...branch} />
</Catch>
```

From [`apps/code-review-agent/src/review.tsx`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/review.tsx#L82-L97), lines 82–97.

Two choices are worth copying:

- **The retry is a new subtree, not `retry()`.** `<Catch>`'s `retry()` renders the same children again, but this attempt should differ: no repository access, and a prompt that says so. Rendering `<ProposeAndVerify id="security-retry">` gives it its own step ids, so a finding's step id says which attempt produced it.
- **The failure is recorded on purpose.** When a fallback renders, the failed steps leave both `RunResult.steps` and the trace. `Failure` is a ten-line component whose `useStep` returns the error's name and message, which keeps the failure visible to consolidation and the saved run.

The plan step isn't inside a `<Catch>`. If the provider is down, the review fails at once with the provider's message rather than producing an empty review that looks like a clean one.

### Consolidate in code

After the run, `consolidate()` turns the verified findings into the review. It's deterministic: no model merges the branches, because the parallel results are just keyed steps. It follows each branch to its retry by step id, then, in order:

1. drops findings that aren't on a line the pull request added or changed, and never moves one to a nearby line;
2. holds back findings on documentation files;
3. holds back medium-confidence findings;
4. orders the rest by severity;
5. merges repeats: the same line, or a title that names the same problem;
6. keeps the first five.

```ts
const confident = code.filter((finding) => {
  if (includeMedium || finding.confidence === "high") return true;
  suppressed.push({
    finding,
    reason: "medium-confidence",
    detail: "the verifier wasn't sure the code settles it",
  });
  return false;
});
```

From [`apps/code-review-agent/src/findings.ts`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/findings.ts#L232-L240), lines 232–240.

Everything held back lands in `suppressed` with its reason. The changed-line check and the documentation rule are product promises, so they live in code rather than in prompts a model might not follow.

### Post, only when asked

With `--post`, the saved review becomes one GitHub review with `event: "COMMENT"`, pinned to the reviewed head commit, with an inline comment per finding. It never approves or requests changes, never posts held-back findings or an empty review, and escapes HTML and @mentions, since the pull request's own content reaches the models. If GitHub refuses the post, the command fails, but the local review is already saved.

## Using it

Build June's packages, set a model key and a GitHub token, and review a pull request:

```sh
make build:packages
export ANTHROPIC_API_KEY=...          # or OPENAI_API_KEY with an openai: model
export GITHUB_TOKEN=$(gh auth token)  # GH_TOKEN works too

bun apps/code-review-agent/src/cli.ts review https://github.com/owner/repo/pull/123
bun apps/code-review-agent/src/cli.ts review owner/repo#123 --json
bun apps/code-review-agent/src/cli.ts review owner/repo#123 --post
```

A review prints each finding, then a line per specialist:

```text
acme/shop#7 at 1a2b3c4: Simplify discount lookup

1. [high] Unknown discount codes now throw
   src/discount.ts:3 (correctness)
   codes.get returns undefined for an unknown code, so entry.percent throws at checkout.
   Evidence: src/discount.ts:2-3 drops the has() check the base version had.
   Fix: Return 0 for unknown codes.

correctness 1 proposed, 1 verified, 0 rejected
security    not run: nothing in its area
tests       0 proposed, 0 verified, 0 rejected
performance not run: nothing in its area
```

By default a review is local and advisory. `--json` adds its provenance (every finding's specialist and step, what was held back and why, token usage), `--post` adds the findings as one comment-only review, and `--limit <n>` changes the five-finding cap. Ctrl-C aborts every step in flight through `run()`'s signal, and nothing is saved or posted. `benchmark` runs the precision-first benchmark with real models, and `help` lists every command and setting.

`CODE_REVIEW_AGENT_MODEL` sets the model for every role as `provider:model`, and `CODE_REVIEW_AGENT_PLANNER`, `CODE_REVIEW_AGENT_REVIEWER` and `CODE_REVIEW_AGENT_JUDGE` set one role each. Moving the whole app from Anthropic to OpenAI was one environment variable.

## Testing

Three kinds of evidence answer three different questions.

### Tests prove the architecture

The tests run the real composition with deterministic models and a fake GitHub, so they need no API keys or network:

- `test/workflow.test.tsx` and `test/verification.test.tsx` script each role with AI SDK mock models and cover the composition: one plan, specialists that really start together, tool calls on the trace, verification keeping and dropping findings, and recovery when one or both attempts fail while other branches finish.
- `test/e2e.test.ts` runs the real CLI as its own process, with [msw](https://mswjs.io) faking GitHub and the Anthropic API, so the real provider code, `run()`, consolidation, the saved run and the post all run, through clean reviews, recoveries and refusals.
- Unit tests cover consolidation, posting, saved runs, cancellation and the GitHub boundary.

### The benchmark measures the product objective

Scripted models prove contracts, not whether a review is any good. The objective is a few high-confidence findings, so that's what the eval protects. [`benchmark/`](https://github.com/johno/june/tree/main/apps/code-review-agent/benchmark) holds 17 small, hand-checked pull requests: 11 with seeded defects (a 1-based page used as an offset, a missing owner check, SQL injection, an N+1 query, a contract break visible only in an unchanged caller, and more) and 6 where the right review says nothing, including traps where a guard, a type or a caller makes suspicious-looking code safe.

Its target is the same workflow and consolidation the CLI runs, with fixture files behind the same `Repository` interface:

```tsx
const repository = memoryRepository(files);
const { steps, usage } = await run(
  <CodeReview pr={pr} repository={repository} models={models} />,
  { signal },
);
return { review: consolidate(pr, steps), steps, ...(usage && { usage }) };
```

From [`apps/code-review-agent/src/benchmark.tsx`](https://github.com/johno/june/blob/main/apps/code-review-agent/src/benchmark.tsx#L296-L301), lines 296–301.

It's scored with June's `evaluate()` and one grader per measure (precision, recall, clean-case silence, invalid locations, duplicates, findings per case and branch failures), so no single number hides the rest. Graders that don't apply return `undefined`, so precision skips silent reviews and recall skips clean cases in one suite. A finding counts toward a defect only at one of its locations, and only then does a judge decide whether it describes it.

Across five recorded runs:

- **Precision**: 92–93% of shown findings matched a seeded defect on `anthropic:claude-sonnet-5-5`, and 81–82% on `openai:gpt-5.5`.
- **Recall**: 12 of 12 seeded defects on both providers, once reviewers could list files.
- **Silence**: Sonnet said nothing on every clean case; gpt-5.5, with the final rules, on five of six.
- **Locations**: no finding in any run landed off a changed line.

### Real-model acceptance

The benchmark is small and synthetic, so the agent also reviewed merged johno/june pull requests. A 13-file refactor got no findings on both providers, and so did a behavior change with the final rules. An async, multi-file change got exactly one finding: a real cancel hang in `june/chat` that human review had missed. Sonnet and gpt-5.5 found it independently, at high confidence, with the exact lines.

Real runs also found what mocks couldn't: a reviewer needed `listHeadFiles` to find an unchanged caller, "medium" confidence had to be recalibrated before a real N+1 was shown, and the tests specialist now reports only tests a change breaks, weakens or makes misleading.

## What stayed in the app

June supplied composition, structured steps, tool loops, parallelism, recovery, traces, cancellation and `evaluate()`. Everything that makes it a code reviewer is the app's:

- **The GitHub boundary**: auth, pagination, the pinned commits and the one comment-only `POST`.
- **What the models see**: numbered patches, a character budget, and read limits of 400 lines and 500 paths.
- **The review dimensions and prompts**: four fixed specialists and what a verifier must look for.
- **Consolidation and posting policy**: the changed-line check, the documentation rule, confidence, the cap, and comment-only posts that are never empty.
- **The benchmark's cases and graders**: what counts as a match, and the judge.

## What building it taught June

Building the Code Review Agent was also a test of June's public API. [OBSERVATIONS.md](https://github.com/johno/june/blob/main/apps/code-review-agent/OBSERVATIONS.md) logs each finding, and [AUDIT.md](https://github.com/johno/june/blob/main/apps/code-review-agent/AUDIT.md) sums them up. Nothing needed a June change to be correct, so no June code changed while it was built.

- **The JSX held up as the workflow.** An imperative orchestrator would need the same structure plus the scheduling, error isolation and tracing June provides.
- **Results consumed cleanly after the run.** The saved run embeds `RunResult` unchanged, so a consolidation change can be replayed over it with no model calls.
- **Failures are too easy to lose.** A caught failure vanishes from the run, which is why `Failure` exists, and a tool that throws leaves a call with no output and no error on the trace. Both are tracked as June work.
- **The scripted test model is the third copy in the repo,** the evidence for a public test helper, and precision needs counts June can sum, not averages.

## Source and related docs

- Source: [`apps/code-review-agent`](https://github.com/johno/june/tree/main/apps/code-review-agent), with its [README](https://github.com/johno/june/blob/main/apps/code-review-agent/README.md) and [benchmark](https://github.com/johno/june/blob/main/apps/code-review-agent/benchmark/README.md).
- The concepts it demonstrates, in the order this guide used them: [Composition and data flow](https://june.dev/docs/workflows/composition.md) (plan to specialists to verifier), [Parallelism](https://june.dev/docs/workflows/parallelism.md#fan-out), [Recovery](https://june.dev/docs/workflows/recovery.md), [Custom steps](https://june.dev/docs/workflows/use-step.md) (`useStep`), [Traces and usage](https://june.dev/docs/workflows/traces.md) and [Running workflows](https://june.dev/docs/workflows/running.md#cancellation-and-timeouts) (cancellation).
- [Evals](https://june.dev/docs/training/evals.md#graders-that-dont-apply): `evaluate()`, suites and graders that skip.
- Start from [React as the workflow](https://june.dev/docs/workflows.md) and [Agent and Step](https://june.dev/docs/workflows/agent-and-step.md) if the components are new.
