promptAndParse already sends the corrective in-session and falls back to a fresh session only as a last resort, handling the snapshot-message-baseline trap the section warned about. The section was stale — marked ✅, kept the rationale as a historical note. Co-authored-by: brentvatne <[email protected]>
@expo/code-review-cli
A config-driven, multi-agent AI code reviewer. Specialist agents review a diff in
parallel; a coordinator consolidates their findings into one structured review.
The same engine runs locally (advisory) and in CI (posts one PR comment). The CLI
is the engine — each repo supplies its own agents and settings under
.expo-code-review/, so behavior is configured per-repo, not baked in.
Status: experimental. Comment-only and non-blocking — it never blocks a merge and never auto-approves. See
ROADMAP.md.
Inspired in part by Cloudflare's How we built our AI code review bot.
flowchart TD
SRC["Source<br/>local git · GitHub PR (gh)"] --> FILTER["Noise filter<br/>drop lockfiles · generated · binary"]
FILTER --> CHUNK["Chunk<br/>by changed lines (large diffs only)"]
CHUNK --> AGENTS["Agents (parallel)<br/>each .md in agents/ · read·grep·glob·list"]
CHUNK --> XCUT["Cross-cutting pass<br/>multi-file issues (large diffs)"]
AGENTS --> COORD["Coordinator<br/>dedupe · re-judge · decide"]
XCUT --> COORD
COORD --> VERIFY["Verify<br/>quote-ground · adversarially verify criticals"]
VERIFY --> REPORT["Reporter<br/>one PR comment (CI) · terminal (local)"]
Usage
Run via npx @expo/code-review-cli <command> (or the ecr / expo-code-review
binary once installed).
Reviewing a PR (--pr/ci) needs the GitHub CLI — brew install gh && gh auth login.
Everything else the reviewer needs (including the opencode runtime) ships with the
package.
First-time setup
Scaffold, add credentials, verify.
# Scaffold .expo-code-review/ + a CI workflow (--no-workflow to skip)
npx @expo/code-review-cli init
Then give it model credentials. Recommended: a Claude Pro/Max subscription — the
scaffolded config uses OAuth by default, so just mint a token and export it under the
env var your config.jsonc's auth.tokenEnv names:
# Mint a Claude Pro/Max token (prints an sk-ant-oat… token)
claude setup-token
# Export it under the env var your config.jsonc's auth.tokenEnv names
export ANTHROPIC_OAUTH_API_KEY=sk-ant-oat...
# Check env, config, and credentials
npx @expo/code-review-cli doctor
Prefer an Anthropic API key, or OpenAI/GPT or another provider? See Other providers & auth modes at the bottom.
Reviewing (already configured)
# Review working-tree changes; prints here, posts nothing
ecr review
# Review a GitHub PR by number (preview only)
ecr review --pr 4057
# …and post it as the PR comment
ecr review --pr 4057 --post
Options (most to least common):
| Flag | What it does |
|---|---|
--pr <n> |
Review GitHub PR #n by number (diff fetched via gh, no checkout); not combinable with --base/--head/--staged. |
--post |
With --pr, also post the result as the PR comment (needs gh auth). Omit to preview only; re-run with --post to publish. |
--staged |
Review only staged changes (index vs HEAD; not combinable with --base/--head). |
--base <ref> |
Base ref to diff against (default: merge-base with the default branch). |
--head <ref> |
Head ref to diff (default: working tree, incl. uncommitted changes). |
--agents <a,b> |
Run only these agents (comma-separated ids); default: all. |
--route |
Let an LLM router pick the relevant agents from the diff. |
--repo <owner/repo> |
Repo for --pr (default: inferred from the current checkout). |
--json |
Emit machine-readable JSON on stdout. |
--no-fail |
Always exit 0 (otherwise a request_changes decision exits non-zero). |
-h, --help |
Show help. |
--pr uses the PR's diff (authoritative) and checks the PR head out into a
throwaway worktree so the agents' surrounding-source reads and the verifier see the
PR's versions of files — no manual gh pr checkout needed, and your working tree is
left untouched. (If that materialization can't run — e.g. not a git checkout — it
falls back to reading the current working directory.)
In CI it runs automatically from the scaffolded workflows — by label or a /review
comment (see CI usage). From Claude Code (or another agent), add a slash command
that runs it; eas-cli's
/expo-review
is a ready example to adapt.
Command reference
| Command | What it does |
|---|---|
ecr init [--no-workflow] [--force] |
Scaffold .expo-code-review/ (config, agents, prompts) + a CI workflow. |
ecr review [options] |
Review local changes and print an advisory review (default command). |
ecr ci |
Review the current GitHub PR and post/update a comment. For GitHub Actions. |
ecr doctor |
Check environment, config, and model credentials. |
(When developing this repo itself, use bun run src/cli.ts <command>.)
How it works
- Source — local git (working tree, staged, or a ref range) or a GitHub PR
(diff + metadata fetched over the
ghAPI). - Noise filter — drops lockfiles, generated bundles/maps, snapshots, files
matching the repo's
additionalIgnores, and binary files (no textual diff to review). Filtered files are recorded, not silently dropped. - Chunking — small PRs run in a single pass; large PRs are split into chunks bounded by changed lines, plus one combined cross-cutting pass that looks for issues spanning multiple changed files across every agent's concern.
- Agents — every
.mdfile in.expo-code-review/agents/is an agent. They run in parallel with read-only repo tools (read/grep/glob/list). - Coordinator — a single pass that dedupes, re-judges severity, and produces
the final
{ decision, findings, summary }. - Verify — quote-grounds every finding against the real file and adversarially verifies criticals, so a confident-but-wrong finding doesn't ship.
- Reporter — posts/updates a single fingerprinted PR comment (CI), or prints a grouped summary (local). Findings below the configured severity floor are suppressed.
Built on the OpenCode SDK, which spawns the model provider and applies Anthropic prompt caching automatically.
Configuration — .expo-code-review/
.expo-code-review/
config.jsonc # model, policy, noise, auth, break-glass, comment tag
shared.md # instructions prepended to every agent (optional)
coordinator.md # the consolidation prompt (required)
agents/
correctness.md # each .md here is an agent (id = filename)
security.md
consistency.md
shared.md and coordinator.md are reserved names; every other .md in
agents/ becomes an agent. Per-agent overrides go in each file's frontmatter:
---
description: One line the router uses to decide relevance.
alwaysRun: true # run even when the router would skip this agent
model: anthropic/claude-sonnet-5 # override the default model
temperature: 0.1
---
# Agent instructions in Markdown…
For a real-world example, see eas-cli's
.expo-code-review/
— correctness/security/consistency agents, Opus for security + the coordinator, and
per-repo noise.additionalIgnores.
config.jsonc (JSONC — comments + trailing commas supported):
{
"model": "anthropic/claude-sonnet-5", // default model for the specialists
"policy": { "includeSuggestions": false }, // suppress suggestion-severity findings
"chunk": { "maxChangedLines": 1000, "maxFiles": 20, "concurrency": 6 },
"noise": { "additionalIgnores": ["packages/*/build/**"] },
"review": { "trigger": "all", // which PRs `ecr ci` reviews: "all"
"label": "ai-review", // (default, except ai-review:skip) or
"skipLabel": "ai-review:skip" }, // "label" (only labeled PRs)
"breakGlass": { "marker": "/skip-review" }, // PR body marker that skips the review
"commentTag": "expo-ai-code-reviewer", // hidden tag used to find/update the comment
"auth": { "mode": "oauth", "provider": "anthropic",
"tokenEnv": "ANTHROPIC_OAUTH_API_KEY" }
}
Model selection
Precedence: REVIEWER_MODEL env (global override) → per-file frontmatter
model: → config.jsonc model (the default). So a repo can run a mixed
setup, and a developer can override everything locally.
- Specialist agents (correctness/security/consistency) benefit from a
reasoning-tier model — Sonnet is the quality/speed sweet spot (default for
correctness/consistency). Opus finds more but is slower and more
rate-limited, so scope it to the highest-stakes agent: security runs on Opus
(set in
security.mdfrontmatter), the rest on Sonnet. - The coordinator makes the final call (dedupe / re-judge / decide) — worth a
strong model; set it in
coordinator.mdfrontmatter. - If latency/timeouts dominate on big PRs, moving the specialists to a faster model is the most direct lever (a real recall tradeoff — measure it).
There is no automatic cross-provider "equivalent" fallback — that would silently change which model reviewed your code. Use an explicit override instead.
Reliability — never hangs, never silently drops work
- Per-task time caps — chunk passes 15 min; cross-cutting 25 min; coordinator
10 min. A global passes budget (32 min) bounds all passes incl. the subdivision
waves, fitting inside the CI job's
timeout-minutes(60). - Tool-call cap — a pass that makes too many
read/grepcalls without finishing is wandering, not converging; hitting the cap trips the soft landing. - Soft landing on timeout — at either cap, the run is interrupted and the agent is asked to return the findings it already has, rather than discarding its work.
- Subdivide-on-timeout — a pass that times out with nothing to show has its chunk split in half and the halves re-reviewed (recursively, down to a single file), then a fast no-tools fallback over the inlined diff. Only a genuinely un-reducible pass reports a coverage gap — and it is always reported, never silent.
- Parse failures are retried (same session, then once in a bounded fresh session) — separate from the timeout path.
- Transient API errors are retried (bounded backoff on 429/5xx/network) — distinct from both the timeout path (abandon) and the parse path; a one-off blip no longer drops an entire pass.
- Auth failures surface once, and fail fast —
ecrchecks the configured provider's credential at startup and stops with one clear message if it's missing (rather than failing every pass); a credential rejected mid-run (401/403) collapses into a single actionable coverage note pointing atauth.tokenEnv/REVIEWER_MODEL. - A failed run never reads as "Approve" — all passes fail → "could not complete"; some fail → never a clean approve, and coverage-reduced.
- The coordinator can't sink the run — if consolidation fails, findings are merged deterministically and still posted.
- Coverage notes — passes that timed out/failed are listed (routine noise filtering is not flagged — it's expected and stays in the run log).
- CI always gets a terminal state — on any failure the PR gets a "didn't run" comment, not a stuck reaction and silence.
CI usage
ecr init --with-workflow scaffolds a pull_request workflow. Split along a clean
line: comments = one-shot actions, labels = persistent configuration.
- command workflow — one-shot
/reviewcomments (maintainers):/review(router picks agents),/review all,/review correctness security. Never changes configuration. - auto workflow — continuous review. Which PRs get reviewed is set in
config.jsonc→review.trigger:"all"(default) reviews every PR except those labeledai-review:skip;"label"reviews only PRs labeledai-review(orai-review:<agent>to scope agents).ecr ciself-gates on this policy; the workflow'sif:is an optional coarse gate layered on top.ai-review:skipalways wins and is write-gated, so a contributor can't opt their own PR out. - dismiss workflow —
/dismiss <id> [… -- reason]//undismiss <id>(maintainers). Each finding shows a short`id:…`. Dismissal is a display filter only — the reviewer still analyzes everything, and acritical/secretsfinding can never be hidden. (An inlineexpo-code-review-ignorecomment on/above a line does the same, with the same critical/secrets carve-out.)
These workflows are comment-only (they never fail the PR's checks). The engine runs
as the published package via npx, so no PR-controlled code is built.
Run logs
Each run appends a JSON line to .expo-code-review/.runs/reviews.jsonl with the
inputs, decision, finding count, duration, per-agent cost, and aggregate token
usage (incl. prompt-cache read/write counts) — for auditing and measuring
cost/latency/cache reuse over time. The same totals are printed as a one-line
summary to the terminal / CI job log at the end of each run, so cache reuse is
visible even in CI (where the run log is ephemeral).
Other providers & auth modes
The recommended setup is a Claude Pro/Max subscription (OAuth) — see Usage above.
Alternatives, all set in config.auth (credentials come from OpenCode):
- Anthropic API key — set
auth.modeto"api-key"and pointtokenEnvat the env var holding the key (e.g.ANTHROPIC_API_KEY); it's sent asx-api-key. Omit theauthblock entirely to fall back to OpenCode's own login /ANTHROPIC_API_KEY. - OAuth (Pro/Max) —
tokenEnvholds ansk-ant-oat…token fromclaude setup-token(not an x-api-key); it's written to an isolated OpenCodeauth.jsonas a bearer credential, using the native subscription path. - OpenAI / GPT, or another provider — the current path is the
REVIEWER_MODELenv override:opencode auth loginonce (pick the provider), then run with e.g.REVIEWER_MODEL=openai/gpt-5.4-mini-fast. It overrides every agent's model and uses your OpenCode login, so noauthblock is needed. (First-class per-provider config — Anthropic/OpenAI/others inconfig.jsonc, and mixing them per agent — is on the roadmap.)
There is no shared fallback key; if a run fails for lack of credentials, authenticate
a provider in OpenCode. ecr doctor diagnoses setup.