# Code review and human acceptance

> How review scope, anchor and gate make a code review checkable, the built-in rules, and how a person plays an acceptance recipe that is read, triaged and gated. Why a release tag needs one.

Canonical: https://bmad-plus.rochetta.fr/docs/concepts/code-review-and-acceptance/
Language: en
BMAD+ version: 0.23.1
Documentation reviewed: 2026-10-19
Author: Laurent Rochetta
License: MIT

BMAD+ documentation › [Concepts](https://bmad-plus.rochetta.fr/docs/concepts/) › Code review and human acceptance

Two kinds of proof sit on top of automated tests. A checkable code review shows what was reviewed, where each finding is and why the review stopped. A human acceptance run (called a recette in French, the word the tool uses for the page) shows that a person saw the chain work on screen. Both are evidence you can read afterwards, and both refuse to say "all good" when the evidence is incomplete. This page is for reviewers, team leads and anyone who decides whether a version ships.

<a id="review-why"></a>
## Why a review needs evidence

A review written in prose cannot be told apart from a review that read twelve files out of forty and found nothing. BMAD+ keeps the review itself with the host agent (it calls no model) and adds the three things only code can guarantee.

1. What must be reviewed is computed, sealed with a hash and listed, with a named reason for every file left out.
2. Where a finding is comes from a verbatim quote of the code, located by the CLI. A model’s line number is never trusted.
3. The verdict is derived from coverage (every selected file accounted for), never from the number of findings.

<a id="review-flow"></a>
## The review flow: scope, anchor, gate

The identifier you pass (`demo-1` below) names a folder, `_bmad-output/review/<id>/`, where all the evidence lives. The terminal shows a real run on a two-commit Git project: the second commit rewrote a function with an off-by-one loop and also committed a `.env` file.

**review scope, rules, then a gate that says incomplete**

```text
$ npx bmad-plus review scope demo-1 --base HEAD~1 --effort high
/work/demo/app4/_bmad-output/review/demo-1/scope.json
1 file(s) to review, 4 changed lines, 1 unit(s) — scope d29789f5ea88
  excluded 1 × secret
checklist: /work/demo/app4/_bmad-output/review/demo-1/checklist.md
plan: high effort, 3 pass(es), refutation pass
Every selected file must end in coverage.json as completed, failed or waived (with a reason).
$ npx bmad-plus review rules src/cart.js
rule set 77b96cb6389d (built-in)
src/cart.js gets 2 rule(s):
  general                  code         builtin  **
  javascript-typescript    code         builtin  **/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx} **/*.{vue,svelte}
$ npx bmad-plus review gate demo-1
demo-1: incomplete — coverage 0/1 completed (0 waived), 0 open finding(s), 0 refuted
  - no coverage.json: which files were reviewed is not established
  - no findings.json: write it even when there is nothing to report
  - 1 selected file(s) never accounted for: src/cart.js
# Exit code 2. The `.env` file was excluded as a secret before any pattern could bring it back.
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-18

Now the host agent reviews. It reads `scope.json` and `checklist.md` (the review rules that apply to the selected files, each once, with the files it covers) and writes two files: `findings.json` and `coverage.json`. A finding quotes the code; the file is bound to the scope by its hash.

**findings.json (one confirmed finding; the scope hash is shortened)**

```json
{
  "schema": "bmad-plus/review-findings/1",
  "scopeSha256": "d29789f5ea88...",
  "findings": [
    {
      "id": "F1",
      "path": "src/cart.js",
      "existing_code": "for (let i = 0; i <= items.length; i++) sum += items[i].price;",
      "content": "The loop runs one step past the end: items[items.length] is undefined, so reading .price throws.",
      "category": "correctness",
      "severity": "high",
      "confidence": "high",
      "disposition": "confirmed",
      "evidence": "With items = [{price: 1}], i reaches 1 and items[1] is undefined, so items[1].price throws.",
      "trigger": "Any non-empty cart.",
      "consequence": "total() throws a TypeError."
    }
  ]
}
```

**coverage.json (every selected file must appear)**

```json
{
  "schema": "bmad-plus/review-coverage/1",
  "scopeSha256": "d29789f5ea88...",
  "items": [ { "path": "src/cart.js", "outcome": "completed" } ]
}
```

**anchor, then gate with a CI check file**

```text
$ npx bmad-plus review anchor demo-1
/work/demo/app4/_bmad-output/review/demo-1/findings.anchored.json — 1 located, 0 ambiguous, 0 unlocated
$ npx bmad-plus review gate demo-1 --emit-check _bmad-output/review/demo-1/check.json
demo-1: findings — coverage 1/1 completed (0 waived), 1 open finding(s), 0 refuted
  check: /work/demo/app4/_bmad-output/review/demo-1/check.json
# Exit code 1. In the first attempt the finding had no evidence field: the gate said "incomplete" with the reason "a confirmed finding needs evidence" until it was added.
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-18

<a id="review-commands"></a>
### Commands and exit codes

**bmad-plus review**

| Command | What it does | Exit |
| --- | --- | --- |
| `review scope ID` | Resolves the refs, diffs from the merge base, selects files, writes `scope.json` and `checklist.md`. Options: `--base REF` (default `HEAD~1`, or `HEAD` with `--workspace`), `--head REF` (default `HEAD`), `--workspace` (working tree including untracked files), `--include GLOB`, `--exclude GLOB` (repeatable), `--effort low\|medium\|high`. | 0; 3 on error |
| `review anchor ID` | Validates `findings.json`, locates every `existing_code` quote in the head version and writes `findings.anchored.json`. Credentials in quoted text become `[REDACTED]`. | 0 all located; 1 invalid or not located |
| `review gate ID` | Validates findings and coverage against the scope hash and derives the verdict. `--emit-check FILE.json` also writes a CI check document. | 0 clean; 1 findings; 2 incomplete; 3 error |
| `review continue ID` | Resumes an interrupted review on the same sealed scope and writes `continue.json` with what is still owed. | 0; 3 if the code, the rules or a selected file moved |
| `review compare ID --since EARLIER` | Sorts the findings of two reviews of the same work into new, persisting, resolved, refuted and not reviewed. | 0; 3 on error |
| `review rules [PATH]` | Lists the effective rules, or those that apply to one path. | 0; 3 on error |

<a id="review-selection"></a>
### What is selected and what is left out

Every file left out carries a reason: `secret` (`.env*`, keys, certificates, `credentials/`, `secrets/`, `.npmrc`, `.netrc`; checked first, no include pattern brings them back), `binary`, `deleted`, `excluded-by-pattern`, `generated-or-vendored` (lockfiles, `node_modules`, `dist`, `build`, minified files; an include pattern can bring these back) and `not-included`. Selected files are cut into ordered units by directory, at most 8 files and 400 changed lines each, so parallel reviewers can share the work deterministically. The review folder itself is never part of the change.

`--effort` fixes the depth before reading: `low` is one pass; `medium` (the default) is two passes and a refutation pass; `high` is three passes and a refutation pass. A change of 150 lines or more is surveyed before it is read, and a change of 400 lines or more with several units is reviewed in parallel units. The effort is sealed in the scope identity.

<a id="review-verdicts"></a>
### How the verdict is derived

**Dispositions of review gate**

| Verdict | When | Exit |
| --- | --- | --- |
| `incomplete` | Coverage or findings are missing or invalid; they answer another scope; a selected file is unaccounted for or failed; the run record says the review stopped before the end or abandoned a unit; an open finding is not anchored. | 2 |
| `findings` | The scope is fully covered and at least one finding is open (confirmed or unresolved). | 1 |
| `clean` | The scope is fully covered and no finding is open. It means "no open finding within a fully covered scope"; it is not a proof that the code is correct, and the command says so. | 0 |

- Findings use closed vocabularies: severity `critical`, `high`, `medium` or `low`; confidence `high`, `medium` or `low`; disposition `confirmed`, `refuted` or `unresolved`; ten categories (correctness, security, data-loss, concurrency, performance and others). An unknown value is an error, never coerced. A refuted finding keeps its quoted ground; a confirmed one keeps its evidence.
- Anchoring compares whole consecutive lines after normalising whitespace. One match is located, several are ambiguous, none is unlocated. An open finding that is not located keeps the gate `incomplete`: requote a longer exact excerpt.
- A review may record why it stopped (`coverage.json` field `run.stop`: completed, budget, time-limit, failure-streak, interrupted). Everything but `completed` keeps the gate `incomplete`, even if every file is listed: a stopped review is never clean. Three failed attempts in a row on the same unit abandon it for this review.
- `review continue` refuses (exit 3, status `moved`) when the commit, the rules or a selected file changed; the answer is then a new scope and `review compare`. In `compare`, a finding is identified by its path, category and normalised quote, never by line or id, and a finding that disappeared counts as resolved only if the later review completed its file; otherwise it is "not reviewed".
- Redaction floor: before `findings.anchored.json` is written, credential-like values in the quoted fields (private-key blocks, URL credentials, authorization headers, bearer tokens, values of credential-named keys, well-known token shapes) become `[REDACTED]`. It is a floor, not a scanner: a secret of no recognisable shape can pass.

<a id="review-rules"></a>
## The built-in rules

A rule is a Markdown document bound to path patterns; `checklist.md` lists every rule that applies to at least one selected path, once, with the files it covers. A rule is a prompt to look, not a finding. A rule can name a `group` (a family) so that parallel reviewers take one unit per group.

**The eight rules shipped with BMAD+**

| Rule | Group | Applies to |
| --- | --- | --- |
| `general` | code | Every file: callers, removed behaviour, boundaries, errors, trust, tests. |
| `javascript-typescript` | code | JS, TS, JSX, TSX, Vue and Svelte files. |
| `python` | code | Python files. |
| `sql-and-migrations` | data | SQL and migration files. |
| `shell` | code | Shell scripts. |
| `ci-workflows` | delivery | CI workflow files. |
| `containers` | delivery | Container build files. |
| `configuration` | delivery | Configuration files. |

A project adds, replaces or disables rules in `_bmad/review-rules.yaml` (schema `bmad-plus/review-rules/1`): a rule is `{id, title, globs, doc}` with a Markdown document inside `_bmad/` of at most 64 KB, and `disable: [id]` removes one. Unknown keys, duplicate ids, a document outside `_bmad/` and disabling an unknown rule are refused, never ignored. The rule set is hashed into the scope, so a findings file answers exactly one rule set. With the Shield pack installed, six compliance rules join: `shield-access-control`, `shield-personal-data`, `shield-cryptography`, `shield-logging`, `shield-ai-integrations` and `shield-change-and-supply-chain`. Each names the controls it relates to, such as `ISO27001:A.8.5`, `SOC2:CC6.1`, `GDPR:Art.32` or `NIS2:Art.21(2)(d)`.

<a id="review-ci"></a>
### Gating pull requests in CI

Two copy-ready GitHub Actions workflows live in `docs/recipes/ci-review/`. `review-scope.yml` seals the scope and publishes the checklist in the job summary and as an artifact (no agent, no model). `review-gate.yml` seals the scope, has the host agent installed and signed in on a self-hosted runner write `findings.json` and `coverage.json`, checks that nothing but those two files changed in the review folder and nothing outside it in the checkout, then anchors and gates; the job fails on an incomplete review or open findings. Both run only for pull requests from the same repository, with read-only permissions and everything pinned. `gate --emit-check` writes a document whose `github` block has the shape of a check run, with at most 50 annotations on anchored lines (`critical` and `high` as failure, `medium` as warning, `low` as notice).

<a id="example-review-sealed-scope"></a>
### Review uncommitted work in one folder

Use when the change is not committed yet or only one folder matters. `--base HEAD --workspace` compares the working tree, untracked files included, with the last commit, and `--include` narrows the scope.

**In your AI assistant**

```text
Sentinel, review my uncommitted work in src/billing only. Seal the scope with bmad-plus review scope billing-1 --base HEAD --workspace --include "src/billing/**" --effort medium and apply the rules in the checklist. Quote the code of every finding word for word, account for every selected file in coverage.json, run review anchor and review gate, and tell me exactly which files were left out and why. If the gate says incomplete, say what is missing instead of calling it clean.
```

**What you should get** `_bmad-output/review/billing-1/` holds `scope.json` and `checklist.md`, then `findings.json`, `findings.anchored.json` and `coverage.json` once the review is written. The report gives the verdict (`incomplete`, `findings` or `clean`) with the coverage figure, lists each open finding with its quoted code, and names the files left out: a `.env` next to the code is excluded as `secret`, a file outside `src/billing` as `not-included`.

<a id="acceptance"></a>
## Human acceptance: the recipe

Automated tests prove that the pieces behave. A recipe proves that a person saw the chain work on screen: writer, reader, screen, gesture. The Core skill `bmad-plus-uat` (driven by Sentinel; commands `uat`, alias `recette`) turns a delivery into a self-contained page a non-technical tester plays step by step. Their ticks become a JSON run that the agent reads, triages and gates on.

1. **Write the spec** A JSON file `_bmad-output/uat/specs/<product>-<version>.json` (schema `bmad-plus/uat-spec/2`). One real sequence of steps, each with `where`, `do` and `expect`; one click per `do` line, one verifiable fact per expectation; on-screen labels word for word, copied from the code; a witness (a thing the test relies on) with the read-only query that proves its state; `writes: true` plus a `verify` check on every step that persists something.
2. **Lint and build** `uat lint` checks labels against your source files, the markup, the budget (15 steps and 30 minutes by default) and the witnesses. `uat build` writes the page, which carries the spec’s SHA-256 so that a run always says which revision it answered.

   ```sh
   npx bmad-plus uat lint ID --src src
   npx bmad-plus uat build ID
   ```
3. **Deliver the page** Locally, `uat serve` listens on 127.0.0.1 only and writes the answers straight into the project at every tick. Elsewhere, send the HTML file; the tester saves the JSON and you load it with `uat import`. In Claude Code or claude.ai the page can be published as an artifact whose database keeps the answers. Say plainly where the answers live, and which steps write for real.

   ```sh
   npx bmad-plus uat serve ID --port 4173
   ```
4. **Read the run** `uat read` prints, per run, the tester, the dates, the counts of passed, failed, blocked, skipped and to do, stale or unsigned warnings, and each failure with its expectation text and the tester’s note.

   ```sh
   npx bmad-plus uat read ID
   ```
5. **Triage, confirm writes, gate** Classify every failed or blocked expectation in `triage/<id>.json`, run the `verify` of each passed writing step read-only and record it, then run `uat gate`.

   ```sh
   npx bmad-plus uat gate ID
   ```

**lint, build, then a gate with no run yet (the shipped example spec)**

```text
$ npx bmad-plus uat lint example-1.0.0
example-1.0.0: 3 steps, 5 expectations, 8 on-screen labels
  no error
$ npx bmad-plus uat build example-1.0.0
/work/demo/app4/_bmad-output/uat/pages/uat-example-1.0.0.html — 3 steps, 83 KiB, spec a4a4850b284d
opens in English; the tester can switch to any of the 10 languages on the page
carries the 17 page guarantees: hidden-before-start, no-answer-before-run, verified-local-write, honest-save-status, save-on-every-change, restore-before-capabilities, newer-copy-wins, revision-guard, other-tab-notice, unreadable-draft-kept, progress-accessible, storage-explained, finished-is-not-accepted, unload-guard, utf8-and-escaped-diacritics, export-is-the-run, no-third-party-requests
$ npx bmad-plus uat gate example-1.0.0
example-1.0.0: awaiting
  - no run yet
# Exit code 2: awaiting. The spec file is the example shipped in the Core uat skill (templates/example-uat-spec.json).
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-18

<a id="uat-gate-and-triage"></a>
### Gate verdicts, triage classes and verify kinds

**uat read and uat gate exit codes**

| Code | `uat gate` verdict | `uat read` |
| --- | --- | --- |
| 0 | `passed`: the run is finished, current, complete and triaged. | No failed step. |
| 1 | `failed`. | At least one failure. |
| 2 | `awaiting`: no finished run yet. | No run. |
| 3 | `stale`: the run answered another revision of the spec. | Stale. |

**Triage: classify every failure before fixing anything**

| Class | Sign | What follows |
| --- | --- | --- |
| `product` | The screen differs from what the code was meant to render. | A fix task on the story, and a replay step in the next version’s recipe. |
| `recipe` | The screen shows the intended result; the expectation described it wrongly. | Amend the spec, rebuild (new fingerprint), replay the affected steps, and say no product change was needed. |
| `data` | The witness is no longer in the assumed state. | Re-run the witness query, reset or rename it, replay. |
| `undecided` | No note, or not attributable. | Ask the tester what they saw; never guess. The gate stays red. |

A decision is recorded for each triaged failure: `fix`, `amend-spec`, `remeasure`, `accept-risk` (with a named owner) or `ask-tester`. `verify` checks have a kind: `sql`, `http`, `command` or `manual`. A tick on a writing step is not a write: its `verify` is run read-only and recorded in `writeChecks`, because a step ticked nine times out of nine once turned out to have written nothing. The gate states what it establishes: "human-observed: complete, current and triaged". It does not establish that the tester looked at the right place.

<a id="uat-config"></a>
### The uat settings

**The uat block of _bmad/config.yaml**

| Key | Default | Meaning |
| --- | --- | --- |
| `mode` | `advisory` | `advisory` builds and offers the page, never blocks. `gate` makes the delivery wait for a passing gate. `off` produces no recipe and the delivery report says so. A missing block means `advisory`. |
| `granularity` | `delivery` | One recipe per delivery. |
| `environment` | name and url empty | The test environment the recipe targets (asked at install). |
| `page_budget` | 15 steps, 30 minutes | Beyond it, split into ordered pages. |
| `dir` | `_bmad-output/uat` | Where specs, pages, results and triage live. |

<a id="why-a-tag-needs-a-run"></a>
## Why a release tag needs an accepted run

BMAD+ applies the rule to itself. On 2026-09-16 a command (`studio list`) was found broken in a real installation, and had been for weeks. The test suite was green, the install contract passed and CI was green. What none of them did was install the product and use it the way a person does. Since 0.17.0, a version does not reach npm until a person installed the candidate in an empty folder, walked the recipe page, and every failure was classified. The 0.23.0 recipe was played in an empty project on 2026-10-18: 28 of 28 expectations passed, with each writing step confirmed read-only. A first run of that recipe had exposed an uninstall defect that left the copied design skills behind; it was fixed before the tag. The runs themselves stay on the maintainer’s machine, because they carry local paths; the changelog records the defect it found.

- In the BMAD+ repository, `npm run uat:spec` fails when the version in `package.json` has no recipe, and `npm run uat:gate` fails unless the latest run is finished, answers the current revision, leaves nothing unanswered, has every failure classified with a decision and has every passed writing step confirmed read-only. A pre-push hook enforces both on a `v*` tag push.
- A blocked expectation needs a replay or an explicit `accept-risk` decision by a named owner. A recipe ticked just to make the gate pass is worse than no recipe: if a step cannot be played, the answer is "blocked" with the reason.
- Your project can do the same with two scripts shipped in the uat skill (`templates/`): `uat-spec-present.mjs` (a version with no recipe is refused, before the push) and `uat-release-gate.mjs` (exit 0 passed, 1 failed, 2 no finished run, 3 other revision, 4 the gate could not run, which is not a pass). BMAD+ installs no lifecycle hook, so these scripts are the layer an instruction or an agent cannot skip; wire them where they bite.

<a id="example-acceptance-build-recipe"></a>
### Prepare an acceptance page for a booking form on staging

Use when a version reaches a staging site and a non-technical colleague must confirm it on screen. Prefer disposable test data: the step that creates a booking writes for real.

**In your AI assistant**

```text
Sentinel, write the recette for the booking form we just delivered to staging. The tester is someone from the front desk, not a developer: one step per gesture, one verifiable fact per expectation, on-screen labels copied from the code. For the step that creates a booking, add the read-only query you will run afterwards to prove that the row exists. Run uat lint with --src src, then uat build, and show me the guarantees the build lists. Tell me the file or link, how long it takes, which steps write, and where the answers will be stored.
```

**What you should get** A spec in `_bmad-output/uat/specs/`, a built page in `_bmad-output/uat/pages/uat-<id>.html`, the lint line ("N steps, M expectations, K on-screen labels checked against F source files"), a `verify` query on the booking step, the list of page guarantees, and a plain statement of where the answers live. The page is not handed over if lint or build fails.

<a id="example-acceptance-read-and-triage"></a>
### Read a finished run and classify every failure

Use when the tester has finished or stopped. The result file is in `_bmad-output/uat/results/<id>/`, or imported with `uat import`.

**In your AI assistant**

```text
Sentinel, read the latest acceptance run with uat read. Classify every failed or blocked expectation as product, recipe, data or undecided in the triage file and ask me about any you cannot attribute. Run the verify check of each passed writing step read-only and record the result. Then run uat gate and report its verdict, the tester, and what stays open. Do not mark any unperformed step as passed.
```

**What you should get** A triage file with one class and one decision per failure, `writeChecks` entries for the writing steps, and a gate line such as `<id>: passed (run <runId> by <tester>)` with exit code 0, or `failed`, `awaiting` or `stale` with the reasons. An undecided failure keeps the gate red until the tester answers.

**See also**

- [Reference: bmad-plus review](https://bmad-plus.rochetta.fr/docs/reference/cli-review/)
- [Reference: bmad-plus uat](https://bmad-plus.rochetta.fr/docs/reference/cli-uat/)
- [Reference: bmad-plus assurance](https://bmad-plus.rochetta.fr/docs/reference/cli-assurance/)
- [Nexus and verification before acceptance](https://bmad-plus.rochetta.fr/docs/concepts/nexus-orchestration/)
- [Reference: config.yaml](https://bmad-plus.rochetta.fr/docs/reference/config-yaml/)

[BMAD+ documentation](https://bmad-plus.rochetta.fr/docs/) · [Concepts](https://bmad-plus.rochetta.fr/docs/concepts/)

[Source on GitHub](https://github.com/lrochetta/BMAD-PLUS)
