Coordinate agents

Wire any reviewer to a bay trunk

Run any review tool on each landing after it lands, and keep every review out of the landing decision.

bay runs no reviewer and calls no model. Any review tool you choose can sit beside a trunk instead. It reads each landing after it lands, and answers with a review record. bay reads review records, but never combines one into a check's verdict. So a review never decides a landing, and a slow or failed reviewer never blocks one.

In bay's terms, the reviewer is an outside producer of evidence. It needs its own name, its own reviewer key and a short script that you write. bay ships no reviewer adapter, and names no review tool as its own. On this page, <reviewer-cli> stands for the tool you pick, and review-bot for its name.

When to wire a reviewer

  • Review exactly what landed: each trunk commit is one change, so a review reads one fixed range.
  • Fix forward: your team answers a bad change with a fix or bay undo, not by holding it back.
  • Tell the author: the reviewer sends its finding to the agent that landed the change.
  • Track review per change: a reviewed dimension in bay status shows whether each review arrived.
  • Measure a reviewer: compare its findings with the changes your team later undid or fixed.
  • Gate a release: stop a release while a landing since the last one has no review.

Find what to review

bay evidence missing lists the trunk commits that no review record from your reviewer covers. It reads trunk's first-parent line, oldest first, and lists each commit under detail.missing. A record covers a commit when it names that commit, or that commit's base tree and tree. A tree alone never covers a commit, so an undo of an undo still needs its own review. Add --since <rev> to list only the commits after that revision. With a registry, only a record that verifies against the reviewer file counts.

sh
bay evidence missing --kind review --producer review-bot --since v1.4.0 --json

Follow landings as a stream

bay watch --landed lists the landings after a cursor, each with its base, agent and time. Keep the detail.cursor it returns, and pass it back with --since next time. Outside a view, a first watch with no cursor lists nothing and returns a starting cursor. A commit that landed outside bay stays in the stream, with outside_bay: true. Merged-in commits off the first-parent line are not listed, and off_first_parent counts them.

sh
bay watch --landed --json --since "$cursor"

Review each landing

Each landing is one commit on trunk's first-parent line, so <commit>^..<commit> is exactly what landed. Give that range to any review tool, and read the code at that fixed commit. A landed commit never changes, so its review never goes stale. Keep the author's reasoning out of the reviewer's input, so the review stays independent. bay plays no part in this step.

sh
bay evidence missing --kind review --producer review-bot --json \
  | jq -r '.detail.missing[].commit' \
  | while read -r c; do
      <reviewer-cli> review "$c^..$c" > "review-$c.json"
    done

Record the review

bay evidence record --kind review writes one review record for each landing you name with --commit. When one review read several landings, repeat --commit, and each landing gets its own record. --verdict has no default, and --body takes a JSON object, such as the review's findings. bay signs each record whole with bay.reviewerKey, findings included, so a changed finding no longer verifies. It adds the records to refs/notes/bay/evidence/<reviewer> on each landing's tree, then pushes that ref once. Each record names the landing's commit, its tree and the tree of its first parent.

sh
bay evidence record --kind review --verdict product_failure --commit "$c" \
  --body "review-$c.json" --version reviewer-cli-2.3 --json

Choose the verdict

  • A useful convention for the verdict: pass when the review found nothing important.
  • product_failure when it found something important.
  • unknown when the review failed, timed out or gave no answer. Never write pass for no answer.

Reruns, conflicts and size

  • Same id: a record's id is the reviewer's name, the kind, the commit and a digest of --input and --version.
  • Rerun: the same content again writes nothing, and the result counts it under unchanged.
  • Conflict: a different record under one id is refused, so a changed verdict needs a new --version.
  • Size: a record over 64 KiB of JSON is refused with the limit named, and bay never cuts one.
  • All or nothing: one refused record writes nothing for any commit in the call.
  • Keys first: the command fails without bay.reviewerName and bay.reviewerKey, even on a trunk with no registry.

Route a finding

A finding about a landing goes to the agent that landed it. The reviewer sends that talk itself, signed with its reviewer key, and needs no agent key. It sends only a delivery talk with --change, about a landing it holds a review record on. bay checks that record against the registry before it sends, and the host checks it again on the push. --to-change reads the recipient from bay's own landing record, never from a commit trailer. The talk closes as answered when a fix, undo or supersede of the change lands.

sh
BAY_ACTOR=review-bot bay talk --to-change c7k2m4 "Fix the unchecked null in src/orders/parse.rs line 42" \
  --kind delivery --change c7k2m4 --follow-up 2026-10-22T09:00:00Z

Track review per change

A [[dimension]] table in .bay/policy.toml defines a completion dimension of your own, such as reviewed. It names the record kind and the producers whose records meet it. A record counts only when it names the landing commit, and with a registry, only when it verifies. A [profile.<name>] table may require the dimension, and [policy] profile in .bay/config.toml selects that profile. bay reads both at the landing's base, so a change cannot relax its own profile. bay status <change> reports the dimension under completion.dimensions, and the profile verdict beside it.

toml
[[dimension]]
name = "reviewed"
kind = "review"
producers = ["review-bot"]

[profile.release]
require = ["published", "checked", "reviewed"]

How bay judges the dimension

  • Met: every counted record about the landing says pass.
  • Rejected: a counted record says anything but pass, unknown included.
  • Answered: a later landing whose Fixes:, Reverts: or Supersedes: names the change answers a rejection.
  • Missing: no counted record about the landing exists yet.
  • A later pass never answers a rejection. Only a landing that names the change does.
  • A profile counts met and answered as met, and names every missing dimension, as in reviewed missing.

Measure the reviewer

bay status --lineage lists the Reverts:, Supersedes: and Fixes: pairs on a trunk range. Each pair names the commit, its change, the relation and the target change. Join those targets with your reviewer's records on the same landings. The command reads local refs only, so fetch trunk first.

sh
bay status --lineage --json

What the join tells you

  • Missed: an undone landing whose review said pass.
  • Caught: an undone landing whose review said product_failure.
  • Noise: a landing with a finding that nobody undid or fixed later.
  • Gap: a landing with no review record, or with unknown. Never count a gap as caught.

Keys and custody

bay.reviewerName names the reviewer, and bay.reviewerKey is the only key that signs its review records. Without a registry, bay verifies no signature, and every record in the reviewer's notes ref counts. With a registry, enrol the reviewer's key in the reviewer file, under that same name. A review record verifies only against that file, and a reviewer key never signs a check. bay refuses a registry that enrols a reviewer name as a producer or an agent too. So an author's agent key can never sign the review of its own landing.

  • Its own key: the reviewer never holds a key that signs checks, landings or releases.
  • No agent key: the reviewer's account can read no agent key, so it never acts as an agent.
  • Its own account: run the reviewer under its own OS account, apart from agent sessions.
  • Read only: the reviewer reads landings from its own clone or a bundle, and never lands code.
  • Keys out of agent reach: no account that runs agent sessions can read a signing key.

Choose who answers a check

A review record never answers a check, whatever check it names. To limit which producers answer a check, list them in [[check]] producers in the policy. A record from any other producer does not count for that check. bay reads the list at the base, so a change cannot widen it for itself.

toml
[[check]]
id = "tests"
environment = "linux-x86_64"
# ... command, adapter and the check's other fields
producers = ["ci-linux"]          # only this producer's records answer the check

What not to do

Never make a model score, a review verdict or a runtime review a required check. A model can disagree with itself on one tree, and bay then blocks the change as CHECK_FLAKY. A rebuild on a moved trunk makes a new tree, so a required review would run again each time. A reviewer that times out answers unknown, and unknown never passes, so its outage blocks landing. A fixed, repeatable test, such as a smoke test or a secret scan, belongs in a [[check]] instead. Never let a finding start bay undo by itself: a person or an agent decides between undo and a fix.

Review after landing

For a team that fixes forward, review after landing is the better default. It reads exactly what landed, costs the landing path nothing, and never loops on one change. A bad change stays on trunk until its review, so this default holds on three conditions.

  • Catch the irreversible before landing: secrets, migrations and build scripts need deterministic required checks.
  • Gate the release, not the landing: if you deploy from trunk, require a review on each landing first.
  • Review within minutes: run the reviewer soon after each landing, and measure the time to its record.

Gate a release on review

Before you tag a release, list the landings since the last release that have no review. bay evidence missing exits 0 with decision: ok even when it lists commits, so read detail.missing. Stop when that list is not empty, or when the command could not fetch trunk. Also stop when detail.off_first_parent is not 0: those commits came in through a merge. A record covers its commit whatever its verdict, so read the verdicts too if a failed review must stop you. A reviewed dimension shows the same answer for one change in bay status.

sh
#!/bin/sh
# release-gate: stop while a landing since the last release has no review record.
set -eu
last=${1:?usage: release-gate <last-release-tag>}
out=$(bay evidence missing --kind review --producer review-bot --since "$last" --json) || {
  echo "$out" >&2; exit 1
}
if ! echo "$out" | jq -e '
    (.freshness.fetched_at | startswith("not fetched") | not)
    and (.detail.missing | length == 0)
    and .detail.off_first_parent == 0' >/dev/null; then
  echo "not reviewed since $last:" >&2
  echo "$out" | jq -r '.detail.missing[].commit' >&2
  exit 1
fi

Keep review records

bay never withdraws an evidence record, and has no retention cut for evidence notes. bay evidence record only adds records, and a new --version writes a second record beside the first. On a trunk from bay host init with a registry, the host refuses a notes push that drops or changes a record. That refusal is EVIDENCE_WITHDRAWN, and the host also refuses to delete a notes ref. On any other host, a cut of notes is your own act with plain Git, never bay's. To archive, bay share <change> --bundle <file> writes that change's ref and every evidence notes ref to one bundle.

sh
bay share c7k2m4 --bundle evidence-2026-10.bundle

What a review cannot do

  • It cannot block, delay or approve a landing.
  • A reviewer key cannot sign a check, so it never counts toward one.
  • A reviewer cannot send a discovery or reliability talk.
  • A reviewer cannot talk about a landing it holds no review record on.

Refusals

The Results page lists what to do next for each one.

CodeMeaning
EVIDENCE_WITHDRAWNA host from bay host init with a registry refused a notes push that drops or changes a record.
TALK_SENDER_NOT_ACTORThe sender is no agent, or a reviewer without its own review record on that landing.
UNATTRIBUTED_LANDINGWith --to-change: the trunk commit is no bay landing, so it has no lander.
CHANGE_NOT_LANDEDWith --to-change: the change or commit is not on trunk.

Limits

  • Watch: one bay watch --landed lists at most 500 landings, and sets more when it stops early.
  • First parent: both queries list first-parent trunk commits only, and count the rest in off_first_parent.
  • Signed records: with a registry, a review record counts only when it verifies against the reviewer file.
  • Record size: one record is at most 64 KiB of JSON.