Review

AI code review using guides from init / sync. Progress and diagnostics go to stderr. The result goes to stdout (human text or JSON with --json).

This page helps you pick a mode. Commands, flags, and checklists live on the linked pages below.

Which mode should I use?

Start with what you are trying to review, not the flag name.

flowchart TD start([What are you reviewing?]) start --> q1{GitHub pull request
by number?} q1 -->|yes| pr[PR review] q1 -->|no| q2{Uncommitted or local
commits in a clone?} q2 -->|not yet| clone[Clone the repo and make your changes] q2 -->|yes| q3{Use shared guides
on serve?} q3 -->|no, init locally| local[Local review] q3 -->|yes, team context| remote[Remote review]
If this sounds like you Mode Read next
You are in a clone and want feedback on your branch or working tree before you open a PR Local review OpenRouter on your machine, guides on disk
Same as local review, but the repo is already inited on serve and you skip per-laptop init/sync Remote review Shared context on the server, diff from your clone
You have a PR number and do not need to check out that branch locally PR review gh only, guides on disk, AI on your machine

PR review and remote review do not combine: --remote is only for a local diff with no PR positional arguments.

How the three modes differ

Same underlying guides from init / sync, different subject (what gets reviewed) and who keeps that context up to date.

flowchart TB subgraph context["Guide context"] disk["Per laptop: init / sync"] shared["Per repo on serve: init / sync once"] end subgraph subject["What gets reviewed"] direction LR diff["Git diff in your clone"] pr["PR metadata and patch via gh"] end diff --> localN["Local review"] diff --> remoteN["Remote review"] pr --> prN["PR review"] disk --> localN disk --> prN shared --> remoteN
Local review PR review Remote review
Typical moment Pre-push in your clone, you own local guides Open PR by number, you own local guides Pre-push using the team's shared server context
Who runs init/sync You on the laptop You on the laptop Ops on the server (you usually do not)
Guides at review time Laptop cache Laptop cache Server dashboard repo
Extra setup OpenRouter gh auth login serve, token, set --remote-host
Deep dive local-review local-pr-review remote-review

Team-wide automatic PR reviews on webhooks are not a fourth review mode. They go through serve and the GitHub App. Use PR review when you run the CLI against one PR.

Depth: --improve-matrix

Reviews share one depth control, --improve-matrix=N. 1 is the default. Each step up adds another pass that audits the draft against the complete diff and the guides, and raises the output budget so a longer answer fits. The pass preserves valid findings, corrects inaccurate ones, removes duplicates, and adds what the first pass missed. More passes cost more tokens and take longer. 2 is a reasonable step up when a change is high risk or the repository is large. The value is capped at 4. See Cost for what a pass costs.

Exit codes

Code Meaning
0 Review finished, no open blocking findings
1 Review finished, at least one blocking finding
2 Usage or precondition error (init missing, not a git repo, etc.)
3 Runtime failure or abort

What counts as blocking

reviewBlocking in config.json decides it, and the same rule is used by local, PR, remote and the automatic GitHub App reviews.

Value A finding blocks when
model (default) The model says so. A P0 always blocks, even if the model labeled it otherwise.
severity Its severity is P0 or P1. P2 and P3 never block, whatever the model said.

The default keeps the 0.4.13 behavior. severity exists because the model's own label can move between two reviews of the same code, which flipped the exit code for the same finding. With severity the same finding set always yields the same exit code, the same blocking count in --json, and the same GitHub review type.

co-maintainer config set --review-blocking=severity
co-maintainer config set --review-blocking=model      # back to the default

CO_MAINTAINER_REVIEW_BLOCKING overrides it for one environment.

JSON output

With --json, stdout is a single JSON object (schemaVersion 1). Fields include mode (local, remote, or pr), subject, summary, findings[], warnings[], usage, and durationMs. Local and remote runs also include diff metadata and codegraph state. Errors with --json are JSON on stdout as well.

Flag details per mode: Local review, Remote review, PR review.

Human output

Without --json, all three modes print the same format. Only the header names the mode, because the subject differs:

co-maintainer review · owner/repo · my-branch
4 files · +12 −3 · guide built 2026-09-15 · codegraph used

New (1)
  • src/app.ts:16-18  [P1 · blocking] the gate ignores its threshold
    A violation exactly at the threshold passes.

Still open (1)
  • src/util.ts:4  [P2 · non-blocking] the helper ignores its argument

Closed (1)
  ✓ src/old.ts:9  [P2 · non-blocking] the dead branch

Summary: 1 new · 1 open · 1 closed · 2 blocking

Findings are grouped Closed, Still open, New, each labeled with the same blocking decision the exit code uses. The header names the real guide build date (guide unknown only when no guide timestamp can be read) and the state the run actually reached: codegraph used, codegraph disabled, or codegraph unavailable: the last one when codegraph was requested but could not be prepared, which is also what --json reports.

The severity legend and the "If you'd like me to explain it in more detail, please ask." sentence are not in terminal output: a terminal has nobody to ask, and the legend is fixed noise. Both stay in the GitHub review comment, which is a different reader.

How findings are produced

The model is asked for a JSON object, not for prose. For each finding it returns the severity, whether it blocks, the path and line span, the symbol, a title, the explanation, and an optional replacement. co-maintainer renders the Markdown from that object, so a finding's boundaries are never guessed from the model's wording. The JSON schema is sent as OpenRouter's response_format when the provider accepts it. A provider that rejects the combination gets one request without the schema, and the prompt also asks for a fenced JSON block.

If the answer is not usable JSON, the request is retried once and then, if it still fails, the older Markdown parser reads it. That parser accepts the em dash or : as the heading separator, so answers written before this change (and carry-over records saved by 0.4.13) keep working. The --json output and the human output are both built from the same parsed findings, so they can never disagree.

Before any review

Check Why
init or sync on your laptop Required for local and PR review
OpenRouter for local and PR review AI runs on your machine
Remote setup for --remote Host, token, repo already inited on the server