# 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.

Source: https://getbay.io/docs/reviewers (HTML).

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.

> Keep the findings under a field of their own, such as `findings`.
> A body field that names a field of the record itself is refused.

### 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.

> In 0.1.6, a record whose push failed stays only on this machine, and the next run can lose it.
> So rerun only after a push succeeded.

## 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
```

> A talk needs a key registry, so without one the reviewer cannot send it.
> bay names the sender from `BAY_ACTOR`, so set it to the reviewer's enrolled name.
> Give `--change` the change id, because bay finds the landing by its `Change-Id`.
> An agent sends the same talk with its own agent key instead.

## 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`.

> The landing verdict never reads a site dimension.
> So a missing or rejected review never blocks or delays a landing.
> With `require_signed = true` in the profile, a dimension that rests only on unsigned records is `unknown`.

## 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.

> `bay doctor` shows a `key-custody` row on every machine.
> It judges the release, producer, executor, registry and reviewer keys that the box's Git settings name.
> It turns red with `KEY_EXPOSED` when this account can read such a key and runs agent sessions.
> Passwordless `sudo` for this account turns it red too.
> A hardware key that signs only with a touch is not red.

### 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.

> Keep review before landing where a person must approve each change first.
> bay cannot wait for a review record before a landing, and a `reviewed` dimension does not change that.

## 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.

| Code | Meaning |
|---|---|
| `EVIDENCE_WITHDRAWN` | A host from `bay host init` with a registry refused a notes push that drops or changes a record. |
| `TALK_SENDER_NOT_ACTOR` | The sender is no agent, or a reviewer without its own `review` record on that landing. |
| `UNATTRIBUTED_LANDING` | With `--to-change`: the trunk commit is no bay landing, so it has no lander. |
| `CHANGE_NOT_LANDED` | With `--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.
