---
name: review-relay
description: >-
  Use when the user asks for a review relay or review loop, for repeated or
  multi-provider review passes on a pull request, for a PR to be reviewed until
  it is clean or merge-ready, or to address review comments and resolve review
  discussions on a PR. A request for one review pass over a diff is not a relay.
---

# Review relay

Drive the PR for the current branch to merge-readiness as a relay race: each provider runs a read-only review leg, then you verify and fix what is real before handing the new head to the next reviewer. The race ends when a full lap comes back clean. Do not merge; that is a separate, explicit request.

**The baton is the head SHA.** Every handoff passes a head that already absorbed the previous leg's findings; a provider reviewing a stale head is a dropped baton, and its leg does not count.

## Scope: the diff, and only the diff

**Every finding must be caused by this diff**: a defect these changes introduce, or a latent one they newly expose. Anchor each finding to a line the diff touched, even when the damage lands elsewhere.

Read as widely as you need to: callers, callees, tests, config, persisted formats, anything that tells you what the change breaks. That reading is how blast radius gets judged, and it is encouraged. What it is not is a review target. A bug that predates this branch and that the diff does not touch or worsen is out of scope, however real. Someone else's PR owns it.

The distinction in practice: the diff changes a function's return shape and an unchanged caller mishandles it. In scope: cite the changed line and name the caller as the consequence. That same caller was already mishandling a case the diff never touches. Out of scope: leave it.

Reviewing beyond the diff is the most common way one leg turns into an unbounded audit. Stay inside it. The pressure to leave it comes from the reviewer, and it is persuasive by construction: an out-of-scope finding arrives fully argued with a real defect attached, so agreeing feels like diligence rather than drift. Scope is yours to hold and never the reviewer's to widen; decide it here, before the first report lands.

## The lineup

One reviewer per leg, from one provider, covering all three domains at once. Fix before every handoff; a fresh pair of eyes on fresh code is the point.

With Cursor omitted, the normal relay alternates Codex and Claude indefinitely. The host-aware first leg below only decides which of those two starts. A user may override the lineup for one run; record that as runtime state and never persist it into this skill's defaults.

Set the lineup from the host (the agent that invoked this skill), classified once at loop start:

| Host | Lineup (laps) | Classified by |
| --- | --- | --- |
| Claude Code | codex → claude → cursor | default when nothing below matches |
| Codex | claude → codex → cursor | `REVIEW_LOOP_RUNNER=codex`, `CODEX_THREAD_ID`, or `CODEX_CI` |
| Cursor | codex → claude → cursor | `REVIEW_LOOP_RUNNER=cursor` or `CURSOR_AGENT` |

The host never runs the first leg: the first opinion on the diff comes from outside the agent that wrote it. Workflow wrappers that do not preserve their host's environment set `REVIEW_LOOP_RUNNER` before invoking this skill. **Cursor is opt-in**: drop it from the lineup unless the user asked for cursor reviews, leaving the alternating two-provider lap. Record the host, its evidence, and the resulting lineup in the final report.

### Reviewers review; the host fixes

Every reviewer stays read-only. After a leg, you independently verify each claim against the code, reproduce every surviving finding, and implement the fixes inline in the current worktree. Do not spawn or shell out to a fixer agent: you already hold the acceptance criteria, relay history, and blast-radius context needed to make the change coherently.

The next reviewer grades the resulting head. This keeps provider diversity where it adds value—independent review—without handing implementation to an agent that lacks the relay's full context.

## Picking each leg's model

Every leg runs at high reasoning effort on the top-intelligence model **within its own provider**; the relay's value is provider diversity, so a leg never switches provider to chase a score. That is **opus-5 for the claude leg and gpt-5.6-sol for the codex leg**. Fable-5 stays out despite topping the rubric: it drains usage limits about twice as fast, so it is explicit-request only.

Intelligence outranks everything here, because a leg run cheap costs a whole lap to discover, and cost never breaks a tie; reviewing is where the budget goes. Taste breaks ties only on a UI-heavy diff, where the defect is a bad interaction or a wrong-feeling layout that a high-intelligence, low-taste reviewer scores as working code.

Higher is better in every column, cost included: the cost score is per task actually run, so a model that burns usage limits fast scores low no matter what its per-token price says. The table is carried here rather than referenced, because it lives in `~/.agents/CLAUDE.md` and a codex or cursor host reads `AGENTS.md` instead; keep the two in sync when either changes.

| model         | cost | intelligence | taste |
| ------------- | ---- | ------------ | ----- |
| opus-5        | 6    | 9            | 9     |
| fable-5       | 2    | 9.7          | 9     |
| gpt-5.6-sol   | 9    | 9.4          | 4     |
| opus-4.8      | 4.5  | 8            | 8     |
| gpt-5.6-terra | 9.5  | 7.5          | 4     |
| gpt-5.5       | 8.5  | 7            | 4     |
| sonnet-5      | 5.5  | 6.5          | 7     |
| gpt-5.6-luna  | 10   | 5.5          | 3     |
| sonnet-4.6    | 6.5  | 3.5          | 7.5   |

Selection mechanics:

- **claude**: `model: 'opus'` for the Agent tool or Workflow `agent()`; that parameter is unversioned and takes only `opus`/`sonnet`/`fable`/`haiku`. The `claude --model` CLI flag also accepts a full versioned id, which is the only way to pin an older release like opus-4.8.
- **codex**: the top-intelligence GPT model selected by the local Codex configuration; see the runtime contract below for how the leg is launched.
- **cursor**: `--model auto`, always. This leg is billing-constrained, not rubric-selected; see below.

No GPT model scores above 4 on taste, so a UI-heavy relay gets its taste coverage from the claude leg; say so in the report rather than swapping the codex leg to a Claude model.

### Codex runtime contract

Codex gets the same rendered prompt as every other leg, at full length: no line limit, nothing summarized away. `--read-only-preamble` prepends the one line that keeps it read-only despite the sandbox flag it needs.

Treat repository decisions and recon as authoritative. Browse or re-scrape a live integration only for a concrete unresolved contradiction, because unconstrained recon can turn one leg into a duplicate investigation.

Codex produces the report at the end of its turn. Wait for the completion event rather than polling on a timer. How that event reaches you depends on the host, and a host with no terminal to attach to must not run codex interactively at all; see the host sections below. A turn that ends without a final report is a failed attempt: record it, fix the invocation, and rerun the same lineup slot. It never counts as a leg.

## Runtime state and resume

Create a stable artifact directory for the PR under the system temp directory and keep `state.md` there. Before the first leg, or when resuming a handoff, record and verify:

- PR number, base/head branches, baton SHA, host evidence, lineup, and next leg.
- The environment's posture (cooperative or adversarial, per the trigger test), written once and reused as `{{ENVIRONMENT}}` by every leg, so it is decided rather than re-litigated per reviewer.
- Acceptance-criteria sources and the open PR stack (`gh pr list --json number,headRefName,baseRefName,headRefOid`). Compare descendant diffs when deciding whether a finding is live, already fixed downstream, or superseded; a base defect that descendants inherit is still live.
- Baseline commands/results, required CI, bot opt-ins, supported bots present, unresolved thread IDs, and which enabled bot reviews target the baton SHA.
- Every attempted leg: provider, reviewed SHA, report/trace paths, outcome, and whether it counts.
- A finding ledger: stable ID, failure mode, disposition, evidence, finding provider, fixing SHA, affected files, GitHub thread/comment IDs, and induced regression if any.

On resume, compare the PR head with the recorded baton before doing work. Reuse valid completed legs and dispositions; rerun only stale, interrupted, failed, or empty-report attempts. Feed reviewers a compact resolved-finding ledger so a repeated rejection needs new evidence rather than another vote.

## Launching a leg

**Spawn the host's own provider through the host's native subagent mechanism; reach every other provider through that provider's CLI.** Native spawning keeps the leg inside the host's session (its own model selection, streaming, and cancellation), while a CLI shell-out is the only transport that crosses providers. So the claude leg is a subagent under Claude Code and a `claude -p` shell-out under Codex; the codex leg inverts that.

| Leg | From a Claude Code host | From a Codex host | From a Cursor host |
| --- | --- | --- | --- |
| claude | Agent tool, or Workflow `agent()` | `claude` CLI | `claude` CLI |
| codex | interactive `codex` in a PTY | native codex subagent, else the same PTY call | interactive `codex` in a PTY |
| cursor | `agent` CLI | `agent` CLI | `agent` CLI |

**Render before launching.** The template in `references/` is not sendable as-is: every `{{FIELD}}` must be substituted first. The script below does it: one fresh file per leg, substituting literally so prose full of `/`, `&`, and newlines survives, failing loudly on a field it cannot fill rather than sending the reviewer at a head it has to guess. Rendering by hand instead is where paraphrase creeps into a prompt that has to go whole. Its path is absolute because a leg runs with the target repo as cwd, not this skill's directory.

```bash
PROMPT="$(mktemp "${TMPDIR:-/tmp}/relay-prompt-<leg>.XXXXXX.md")"
REPORT="$(mktemp "${TMPDIR:-/tmp}/relay-<provider>-<leg>.XXXXXX.md")"

~/.claude/skills/review-relay/scripts/render-prompt.py reviewer-prompt.md \
  --field PR_URL="$PR_URL" --field HEAD_SHA="$HEAD" \
  --field DIFF_TARGET="git diff $BASE...$HEAD" \
  --field ENVIRONMENT='Cooperative: <what surrounds this code>' \
  --field ACCEPTANCE_CRITERIA=@/path/to/criteria.md \
  --field PRIOR_LEGS=@"$RELAY_LOG" \
  -o "$PROMPT"                       # --read-only-preamble for the codex leg
```

Pass the rendered file rather than an inline string; the prompt is long and full of markdown and backticks that a shell argument mangles. The native subagent path takes the same rendered text as its prompt.

```bash
# claude leg, from a non-Claude host
claude -p --model opus --permission-mode plan < "$PROMPT" > "$REPORT"

# codex leg: interactive, per the Codex runtime contract above
codex -s danger-full-access --no-alt-screen "$(<"$PROMPT")"

# cursor leg, from any host
agent -p --model auto --mode plan "$(cat "$PROMPT")" > "$REPORT"
```

The codex leg is the exception to redirecting into `$REPORT`: it runs interactively, so its report is the final response captured off the attached session, not stdout.

CLI reviewers routinely outrun a 10-minute Bash timeout; pass an explicit longer timeout, or run in the background and poll for the report file.

**Re-running the same provider** (after a stale-head review, or a second pass once this leg's fixes landed) resumes that reviewer's session rather than paying for the full prompt again, and the follow-up carries only what changed. That path has its own ordering, and getting it wrong costs a lap: [`references/resuming-a-reviewer.md`](references/resuming-a-reviewer.md). A cross-provider handoff is never a resume; a fresh process gets the whole rendered file.

Keep reviewers read-only: `--permission-mode plan` for claude, `--mode plan` for cursor. A reviewer that can write starts fixing what it finds, which strands edits outside the fix sequence in "Fixing". The codex leg is the weak point: it runs `-s danger-full-access` per its runtime contract, so nothing but the `--read-only-preamble` line stops it editing. Check `git status` after that leg.

Cursor specifics:

**Always `--model auto`.** `auto` is the only permitted value of this flag. Auto-routed requests are the included Cursor plan usage; every other model id (`gpt-5.6-sol-high`, `claude-opus-5-thinking-high`, `composer-2.5`, anything from `agent --list-models`) bills the API pool per token. Requests to run the cursor reviewer on a specific model get the same answer: run `auto`, or drop cursor from the lineup, and say which. Cost, not capability, decides this leg's model; the rubric governs the claude and codex legs only.

This path is the local `agent` CLI only. Cursor's PR-side bots are a different mechanism and never a substitute: leave `@cursor review`, `@bugbot run`, and every other PR comment trigger out of it. Bots already in the PR conversation are handled by the bot-allowlist step, which does not include Cursor.

## Running legs from a Claude Code host

A leg is only useful if the host finds out it finished. Everything below is in service of that one thing: the host must get a completion event it cannot miss, and a report it can read without decoding a terminal.

### The claude leg

Agent tool, `model: 'opus'`, backgrounded. The harness tracks the subagent and notifies you when it returns, so the next turn starts by itself. Pass the rendered prompt file's contents as the agent's prompt, and check `git status` afterwards.

**Pick an agent type with no Agent tool in it.** `Explore` is the one to reach for: its toolset is everything a reviewer needs and nothing it needs to be denied, with no Edit, no Write and no Agent. Its blurb describes a search agent, but a toolset is a capability list rather than a job description, and this is the toolset the job wants. `general-purpose` has every tool, delegation included.

That matters because a reviewer handed the Agent tool will use it. Give one a large diff and it splits the work into areas and fans out to sub-reviewers of its own, which is not a leg. A leg is one reviewer's own judgement over the whole diff, and that is what makes it comparable to the leg beside it and reproducible on a rerun; a coordinator merging area reports it never verified is a second relay hiding inside the first. Observed on leg 14 of PR #65: one spawn became six and cost 925k subagent tokens for a single leg.

Render this leg with `--read-only-preamble` as well, which says the same thing in words. Say it and enforce it both: the words are what stops a reviewer that finds another way to delegate, and the toolset is what stops the one that ignores them. Asking was the guard that already failed once.

If a fan-out happens anyway, its reports are still worth reading as raw input, the same way a bot's comments are. They are not that leg. Verify each finding yourself and rerun the leg.

### The codex leg: `codex exec`, never interactive `codex`

```bash
codex exec -s read-only --color never \
  -m gpt-5.6-sol -c model_reasoning_effort=high \
  -o "$REPORT" - < "$PROMPT" > "$LOG" 2>&1
```

Run it with Bash `run_in_background: true`. A leg on a large diff runs well past the ten-minute foreground timeout, and `codex exec` exits when the turn ends, so backgrounding it produces a real completion notification.

- `-s read-only` is the sandbox enforcing what `--read-only-preamble` only asks for. Reads and `git diff` both work; writes fail. Keep the preamble as the line that stops it trying, and keep the `git status` check.
- `-o "$REPORT"` writes the final message to a file. **That file is the report**, not stdout. Stdout is progress noise; keep it as `$LOG` for diagnosing a failed leg.
- `-` reads the prompt from stdin, so the long markdown never goes through argv.

**Interactive `codex` cannot be driven from this host, and the failure is silent.** It does not exit when the turn ends; it returns to its own prompt and waits. A watcher on the process therefore waits forever, and the host sits believing the leg is still working. Observed once at a cost of two hours: the review completed, the log stopped growing, and nothing said so. Its output is also a TUI screen-repaint stream rather than a transcript, so capturing it through `script` yields megabytes of cursor moves with the report interleaved a character at a time and the early findings already scrolled off. There is no report in there worth recovering.

### Watching a leg

The completion notification is the mechanism. Do not build a polling loop around work the harness already tracks; you will be re-invoked.

A watcher is only for a process the harness cannot see, which means one you detached yourself. When you write one, **never `pgrep -f` the command string**: the watcher's own shell carries that string in its command line, so the pattern matches the watcher, the condition never goes false, and the loop runs until it times out. Watch the pid.

### Salvaging a leg someone ran interactively

Codex writes a real transcript to `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl` however it was launched. The report is the last assistant message:

```bash
python3 -c "
import json,sys
msgs=[]
for line in open(sys.argv[1]):
    try: d=json.loads(line)
    except: continue
    p=d.get('payload',{})
    if p.get('type')=='message' and p.get('role')=='assistant':
        t=''.join(c.get('text','') for c in p.get('content',[]))
        if t.strip(): msgs.append(t)
print(msgs[-1])
" "$(ls -t ~/.codex/sessions/**/*.jsonl | head -1)" > "$REPORT"
```

Match the session file to the leg by its timestamp before trusting it. This rescues a leg already paid for; it is not the path to run one.

## Reviewer prompt

[`references/reviewer-prompt.md`](references/reviewer-prompt.md) is the complete reviewer prompt. Once rendered, it goes to the reviewer whole, never summarized or paraphrased, and nothing from this skill gets appended to it. Every leg renders its own copy, because the head, the diff target, and the log all move between legs.

| Field | Filled with |
| --- | --- |
| `{{PR_URL}}` | `url` from step 1's `gh pr view` |
| `{{HEAD_SHA}}` | the head SHA recorded in step 1 |
| `{{DIFF_TARGET}}` | the command that reproduces the diff under review, e.g. `git diff <baseRefName>...<HEAD_SHA>` |
| `{{ACCEPTANCE_CRITERIA}}` | criteria verbatim from the PR body, linked issue, or plan file, or "None stated" |
| `{{ENVIRONMENT}}` | the posture decided by the trigger test, plus one line on what surrounds this code, e.g. "Cooperative: a Tampermonkey userscript sharing a document with wplace. wplace does not know this code exists." |
| `{{PRIOR_LEGS}}` | `--prior-legs @"$RELAY_LOG"`, which inlines the log's full content and never its path, or "You are the first leg; nothing has been reviewed yet." on leg 1. Resumed sessions get only the delta, per [`references/resuming-a-reviewer.md`](references/resuming-a-reviewer.md) |

## The relay log

The baton carries a head SHA and a log. `$RELAY_LOG` is one append-only markdown file created at relay start (`mktemp "${TMPDIR:-/tmp}/relay-log.XXXXXX.md"`), holding every finding any leg has raised and what became of it. It exists so the relay stops re-litigating settled ground: without it, leg 3 spends its budget rediscovering what leg 1 already fixed, and there is nothing to dedupe against in step 3.

**You, the host, author and maintain it. Reviewers never touch it.** They are launched read-only and their reports are raw input to step 3, where you read the cited code and decide. What the log records is your verdict after that check, not the reviewer's claim: a finding is Rejected because *you* traced why it cannot 