Part 4 of My Agentic Setup. Start from part 1
About 50 PRs a week come to me for review. I don't open any of them cold.
Each PR gets read first by an agent, in its own pane. The agent can read the PR and nothing else: it can't comment, approve, push or merge. It writes a report on my machine, and I read the report before I open the diff.
I start all of it with one command:
fmreview https://github.com/acme/api/pull/123 acme/web:45 'acme/api#130'
The idea: every PR gets a review from a model on a different vendor than the one that wrote the code, the reviewer is read-only, and its report comes to me before anything is posted on GitHub.
What broke
The reviewer model didn't reach the reviewers
My main agent, OMP, has a reviewer role pinned to a model on another vendor. But the pin applies only to a subagent whose type is literally reviewer.
My review agents are Firstmate scouts, and a scout is not a reviewer. Neither are the subagents a scout starts on its own. None of them get the reviewer model by themselves.
The fix is to pin the model by hand, every time. The note in my captain's preferences:
- Dispatch every code-review task on `openai-codex/gpt-6.1-sol` at high effort,
not the default model (same as omp's `task.agentModelOverrides.reviewer` and
`FMREVIEW_MODEL`). When spawning the scout, pass
`--model openai-codex/gpt-6.1-sol --effort high` explicitly (the omp override
only auto-applies to a subagent literally typed `reviewer`, not to a
`scout`-typed spawn or its own internal fan-out subagents)
And fmreview puts the model into the message it sends, so the first mate doesn't have to remember:
# Keep FMREVIEW_MODEL/EFFORT in sync with
# task.agentModelOverrides.reviewer in ~/.omp/agent/config.yml.
: ${FMREVIEW_HARNESS:="omp"}
: ${FMREVIEW_MODEL:="openai-codex/gpt-6.1-sol"}
: ${FMREVIEW_EFFORT:="high"}
There is also an option to start omp in the reviewer role from the command line, if your omp build supports it. I leave it empty. Pinning the model is the path I verified.
# is a glob in my shell
The short way to name a PR is acme/api#123. In zsh with extendedglob on, # is a glob operator, so that word has to be quoted, or zsh tries to match files with it.
So fmreview also takes a form that needs no quotes:
case "$t" in
https://github.com/*/pull/*)
t="${t#https://github.com/}"; printf '%s#%s\n' "${t%%/pull/*}" "${t##*/pull/}" ;;
*/*\#[0-9]*) printf '%s\n' "$t" ;;
# owner/repo:123 needs no quoting under extendedglob, unlike the # form.
*/*:[0-9]*) printf '%s#%s\n' "${t%%:*}" "${t##*:}" ;;
[0-9]*) [ -n "$dr" ] && printf '%s#%s\n' "$dr" "$t"; return 0 ;;
*) return 1 ;;
esac
A bare number works too, with --repo acme/api.
herdr's split ratio is the other way around
Before fmreview I had a simpler batch launcher: one pane per PR, each running a review. The first version was for cmux, which takes a whole pane layout as JSON in one call. When I moved to herdr, there was no such call. Panes are made one herdr pane split at a time.
To get equal panes, you split off one slice at a time. herdr's --ratio is the share the original pane keeps, not the share the new pane gets. Splitting a 20-row pane with --ratio 0.75 leaves it 15 rows and gives 5 away. So to carve n equal slices, the ratio at each step is 1/n:
split_off() {
herdr pane split "$1" --direction "$2" \
--ratio "$(awk "BEGIN { printf \"%.6f\", 1 / $3 }")" --no-focus |
json_get result.pane.pane_id
}
The panes are laid out as a grid, rows first, so the PRs read left to right and top to bottom, and the panes stay close to square. Five PRs make three panes on top and two below.
A running server that looked down
The launcher checks that a herdr server is running. The obvious check is:
herdr status server | grep -q "status: running"
With set -o pipefail, this fails even when the server is up. grep -q exits on the first match and closes the pipe. herdr is still writing, gets SIGPIPE, and exits with an error. pipefail reports that error as the result of the whole pipeline.
The fix is to capture first and match after:
server_status="$(herdr status server 2>/dev/null || true)"
case "$server_status" in
*"status: running"*) ;;
*) echo "Error: no herdr server is running. Start one with: herdr" >&2; exit 1 ;;
esac
The setup
One PR: review-pr
The smallest piece is review-pr. It takes a PR URL, goes to my clone of that repo, and starts Cursor's agent CLI with the /review-pr command:
if [[ "$PR_URL" =~ github\.com/([^/]+)/([^/]+)/pull/[0-9]+ ]]; then
ORG="${BASH_REMATCH[1]}"
REPO="${BASH_REMATCH[2]}"
fi
REPO_DIR="$HOME/GitHub/$ORG/$REPO"
cd "$REPO_DIR"
exec "$(dirname "$0")/wt-agent" agent "/review-pr $PR_URL"
It needs the repo cloned at ~/GitHub/<org>/<repo>, and it stops if the folder isn't there. /review-pr is the command from the pr-review-toolkit plugin. wt-agent sets a marker in wt list while the agent runs, so I can see which worktrees have an agent in them.
The review stays in the pane. Nothing is posted.
Many PRs: one scout each
For a batch, I use fmreview. It does four things:
- Normalizes each PR to
owner/repo#number. - Creates a Firstmate home called
reviews, the same wayfmdoes, if it doesn't exist. - Writes a review charter into that home.
- Opens the home in herdr, waits for omp to start, and sends the first mate this message:
ahoy
Dispatch one SCOUT per pull request below. One scout each, in parallel, each in its own pane.
Pull requests:
- acme/api PR #123
- acme/web PR #45
Profile for every one of these scouts: harness omp, model openai-codex/gpt-6.1-sol, effort high.
Register each repo as local-only if it is not registered yet.
Every brief must carry the full contents of data/review-charter.md in its Firstmate spec.
The charter is binding: these scouts are read-only and must not comment, review, approve,
request changes, push, or merge anything on GitHub.
When a scout finishes, relay its verdict and findings to me and then STOP. Do not act on
any recommendation, do not open follow-up tasks, and do not tear the scout down until I say so.
local-only means the first mate never opens a PR for that repo. A scout writes a report, never code.
fmreview --print shows the message without sending it.
The charter
Every scout gets the same charter, word for word, in its brief. The first part is about what it may not do:
## Read-only, without exception
- NEVER post a comment, review, approval, change request, or reaction on GitHub.
- NEVER push, close, reopen, label, assign, or merge anything.
- NEVER edit files in the project clone or the worktree.
- `gh` is for READING the PR only: diff, files, checks, linked issues.
- The single deliverable is `data/<id>/report.md`. Nothing leaves the machine.
- If the review implies an action, write the recommendation in the report and
stop. The captain decides every next step.
The second part is what the report has:
- Verdict in one line, then findings ordered by severity.
- Every finding cites `path/to/file:line` from the PR's own diff.
- Separate what the diff does from what it fails to do.
- Call out failing or missing CI checks and say what they imply.
- Flag anything touching auth, tenancy boundaries, data retention, migration
ordering, or IAM as captain-escalation regardless of test status.
- End with a short list of open questions for the author.
The third part says which review tools win when they disagree. First, the repo's own review rules, when a repo has them. A repo with compliance rules, for example, gets those checked before anything generic. Then pr-review-toolkit for the mechanics: correctness, edge cases, error handling, tests for the changed lines. Then a second mechanical pass, only on what the first two didn't cover.
Each finding goes in the report once, under the tool with the highest precedence that found it. The report also says which tools were there and which weren't. If none loaded, it says so and reviews without them, instead of guessing what their checklists say.
Five passes, one of them for the spec
Inside each review there are five review passes, all on the reviewer model. Four look at the code. The fifth is a spec reviewer, and it gets less context on purpose: only the diff, the commit list, and the linked ticket. When there's no ticket, it gets the intent from the brief.
It returns three lists, and every item quotes its source line:
- asked but not built
- built but not asked for
- built wrong
The other four passes read the code the way the author meant it. This one checks the code against what was asked.
The reviewer is on a different vendor
The model that writes most of my code is Claude. The reviewer is a GPT model, so the reviewer is never the same model that wrote the code.
The profiles in my dotfiles set this per machine:
# Agent pins (task.agentModelOverrides). The pr-review-toolkit agents are the
# cheap first pass; reviewer and security-reviewer are the real gate.
reviewer = "openai-codex/gpt-6-sol:high"
security-reviewer = "anthropic/claude-sonnet-5-5:high"
code-reviewer = "@crew-light"
silent-failure-hunter = "@crew-light"
pr-test-analyzer = "@crew-light"
comment-analyzer = "@crew-light"
@crew-light points at another role, a small model. The pr-review-toolkit agents run often, so they get the cheap model. The security reviewer is Claude, so only the main reviewer is on the other vendor.
What runs today:
| Role | What it does | Work laptop | Personal laptop |
|---|---|---|---|
| default, crew | writes the code | Claude Sonnet 5.5 | Claude Sonnet 5.5 via Cursor |
| reviewer | the real gate, fmreview scouts |
GPT-6.1 Sol, high | GPT-5.6 Terra via Cursor, high |
| security-reviewer | security pass | Claude Sonnet 5.5, high | Claude Sonnet 5.5 via Cursor, high |
| pr-review-toolkit agents | cheap first pass | Claude Haiku 5.5 | Claude Haiku 5.5 via Cursor |
When my own agents open the PR
The same review runs on the other side, when my agents write the code. These rules are in the instructions every agent gets (trimmed):
- Before opening: run the review skill (the repo's own, or
`pr-review-toolkit:review-pr` otherwise) and `simplify`, and address the findings.
- Always draft (`gh pr create --draft`).
- Promote to ready only on the captain's explicit instruction for that PR. Green
CI or self-review never authorizes promoting or merging.
Humans approve and merge (squash); do not merge unless asked.
So an agent's PR has been reviewed by an agent before it exists, and a human still decides when it's ready and when it merges.
A budget for review rounds
The newest change is a limit on how many times a PR goes back and forth with a reviewer. Until late October, nothing in my setup said when to stop. Now my captain's preferences have this:
### Review budget
- Default to 3 review rounds per PR. Round 1 is a full review. Later rounds
check only the changed code and the findings that are still open.
- Extend to 5 rounds only when the change touches security, permissions, auth,
data loss, or state and concurrency, and a blocker is still open with
evidence. Also extend when the captain asks.
- Stop early when a round returns only nits.
- Keep a ledger of every finding: id, severity, and outcome (fixed, rejected
with reason, or open). Never guess usage or token numbers. Report only what
the tools show.
- Make the smallest change that satisfies a reviewer. Do not widen the scope
to fix things nobody flagged.
With the ledger, every finding ends with an outcome, and "rejected" needs a reason. A finding can't just disappear.
Try it
The companion folder has sanitized copies of review-pr and the herdr batch launcher, the PR parser, a script that prints the fmreview message, and the charter. The tests use a fake agent CLI and a fake herdr that keeps the size and place of every pane, so they check the real grid for 1 to 7 PRs, not just the calls. They also check URL quoting, PR parsing, and the read-only lines in the message.
cd 04-pr-review
make test
golem-notes-examples/04-pr-review
Who reviews your agents' code, and does the reviewer come from the same vendor as the author? Leave a comment.
This is part 4 of my series on running a real agentic dev setup. I publish each part first on Golem Notes, with the full write-up and what's coming next. Subscribe there to get the next one in your inbox: Golem Notes


Top comments (0)