# bmad-plus uat

> Reference for bmad-plus uat (alias recette): lint, build, serve, import, read, gate and order human acceptance recipes, with flags, exit codes and sample output.

Canonical: https://bmad-plus.rochetta.fr/docs/reference/cli-uat/
Language: en
BMAD+ version: 0.23.1
Documentation reviewed: 2026-10-19
Author: Laurent Rochetta
License: MIT

BMAD+ documentation › [Reference](https://bmad-plus.rochetta.fr/docs/reference/) › bmad-plus uat

`bmad-plus uat` (alias `bmad-plus recette`) handles human acceptance recipes. A recipe, called a recette in French, is a JSON spec of steps that a person plays on screen. The command lints the spec, builds one self-contained HTML page for the tester, stores the tester's answers as a JSON run, lists the failures and derives a gate verdict from that run. Use it when a delivery must be seen working by a person and not only by tests. Sentinel drives it for you; you need the command to script the process, to wire a CI step or to check what the agent claims.

> **Note · What the gate does and does not prove** The gate establishes that a run is finished, current, fully answered and triaged. It never establishes that the tester looked at the right place, so every verdict is labelled human-observed. A gate that says passed can still sit on top of failures: they only have to be classified, each with a decision.

<a id="synopsis"></a>
## Synopsis

**In your project terminal**

```sh
npx bmad-plus uat <action> [target] [options]
npx bmad-plus recette <action> [target] [options]
```

`action` is one of the seven actions below. `target` is a recipe id (the command looks for `<dir>/specs/<id>.json`, then for any spec in that folder whose `id` equals it) or the path of a `.json` spec file. `order` takes no target.

**The seven actions**

| Action | What it does | Writes |
| --- | --- | --- |
| `lint` | Validates the spec: schema, unknown keys, markup, step and expectation ids, witnesses, the 15-step / 30-minute budget. With `--src`, checks that every quoted on-screen label exists in a source file. | nothing |
| `build` | Lints first and refuses on any error, then writes the tester page. | `<dir>/pages/uat-<id>.html` |
| `serve` | Serves the built page on 127.0.0.1 and stores every answer the page sends. Runs until you press Ctrl+C. | `<dir>/results/<id>/<runId>.json` |
| `import` | Stores a results JSON exported from the page (file or standard input). | `<dir>/results/<id>/<runId>.json` |
| `read` | Lists every run and each failed or blocked expectation with its text and the tester note. | nothing |
| `gate` | Derives the verdict: passed, failed, awaiting or stale. `--emit-check` also writes a self-contained verifier. | `<dir>/checks/gate-<id>.cjs` (only with `--emit-check`) |
| `order` | Computes the play order of all recipes in `<dir>/specs` from their declared `after` and shared witnesses. | `<dir>/ORDER.md` |

**Options**

| Flag | Argument | Default | What it does |
| --- | --- | --- | --- |
| `-d, --directory` | `<path>` | current directory | Project directory. |
| `--dir` | `<path>` | `_bmad-output/uat` | Recipe folder inside the project: `specs/`, `pages/`, `results/`, `triage/`, `checks/`. |
| `--src` | `<path>` | none | Source folder scanned for on-screen labels (lint, build). Repeatable. |
| `--language` | `<code>` | see below | Page language, as a code (`fr`), a name (`Français`) or a locale tag. |
| `--input` | `<file>` | none | import only: results file to store; `-` reads standard input. Required for `import`. |
| `--port` | `<number>` | `4173` | serve only: port, bound to 127.0.0.1 only. |
| `--emit-check` | none | off | gate only: also write the Nexus verifier `checks/gate-<id>.cjs`. |
| `--json` | none | off | Machine-readable output, with `schemaVersion: 1`. |

Page language, in order of priority: the `language` field of the recipe, then `--language`, then the `communication_language` read from `_bmad/config.yaml`, then English. The page always embeds the ten installer languages (en, fr, es, de, pt-br, ru, zh, he, ja, it) and the tester can switch on the page.

<a id="reads-writes"></a>
## What it reads and writes

- Reads: the specs in `<dir>/specs` (refused above 4 MiB or when the file is a symbolic link), `_bmad/config.yaml` (build only, for `communication_language`), the results in `<dir>/results/<id>/` and the triage file `<dir>/triage/<id>.json`.
- With `--src`: reads source files with these extensions only: ts, tsx, js, jsx, mjs, cjs, vue, svelte, html, htm, php, py, rb, java, kt, cs, go, json, yaml, yml, md. It skips symbolic links, dot folders, `node_modules`, `.git`, `dist`, `build`, `coverage`, `.next`, `.nuxt`, `vendor`, `__pycache__`, `.venv`, `venv`, `target` and `out`, ignores files above 512 KiB and stops after 20000 files.
- Writes: only the files named in the table above. Credential-like values pasted in a tester note are replaced by `[REDACTED]` before a run reaches the disk (`import` and `serve`); the run then records how many replacements were made in `redactions`.
- Network: none, except `serve`, which listens on 127.0.0.1 only. The built page itself asks nothing of any other origin. A tester on another machine uses the page as a file or as an artifact and sends back the JSON for `import`.
- `serve` answers only to the host names `127.0.0.1:<port>` and `localhost:<port>` (any other Host header gets HTTP 403), refuses a request body above 4 MiB, and answers HTTP 409 when an older copy of a run would replace a newer one.

<a id="gate-rules"></a>
## How the gate decides

The gate takes the most recently finished run of the recipe. If there is none, the status is awaiting. If that run answered another revision of the spec (its `specSha256` differs from the current spec), the status is stale. Otherwise the status is failed as long as at least one reason below applies, and passed when none does.

- An expectation was never answered (optional steps are skipped).
- A failed or blocked expectation has no entry in `<dir>/triage/<id>.json`, or its class is `undecided`, or its decision is not one of `fix`, `amend-spec`, `remeasure`, `accept-risk`, `ask-tester`.
- A decision is `accept-risk` without `decidedBy`.
- An expectation is blocked and its decision is not `accept-risk`: nothing was observed, so replay it or have someone accept the risk by name.
- A step with `writes: true` passed, but the triage has no `writeChecks` entry with `verified: true` and an `evidence` text: a tick on a writing step is an observation, the read-only proof settles it.

**Triage vocabulary**

| Class | Meaning | Usual decision |
| --- | --- | --- |
| `product` | The screen differs from what the code was meant to render. | `fix` |
| `recipe` | The screen is right, the expectation described it wrongly. | `amend-spec` |
| `data` | The witness is no longer in the assumed state. | `remeasure` |
| `undecided` | No note, or not attributable: ask the tester. Keeps the gate red. | `ask-tester` |

<a id="exit-codes"></a>
## Exit codes

**Exit code by action**

| Action | 0 | 1 | 2 | 3 |
| --- | --- | --- | --- | --- |
| `lint` | no error | at least one error, or any failure | - | - |
| `build` | page written | lint error, template lost a guarantee, or any failure | - | - |
| `import` | run stored | any failure | - | - |
| `read` | runs read, nothing failed | a failed or blocked expectation, or any failure | no run yet (awaiting) | newest run answered another revision (stale) |
| `gate` | passed | failed (or any failure) | awaiting | stale |
| `order` | order written | a cycle, no spec in the folder, or any failure | - | - |

A failure is any thrown error: unknown action, missing target, spec not found, unreadable file, missing `--input`. It prints `uat: <message>` on standard error, or `{"schemaVersion":1,"status":"error","message":...}` with `--json`, and exits 1. A release script that must tell a failed gate (1) from a gate that could not run uses the project gate `uat-release-gate.mjs`, which adds exit 4 for the second case.

<a id="real-output"></a>
## Sample output

The first block was replayed with the example spec that ships in the `bmad-plus-uat` skill; the `read` and `gate` blocks are composed from the message templates of the command, flagged as simulated, and reuse the fixture of `tests/unit/uat-cli.test.js`. File counts, sizes and hashes depend on your project.

**Lint then build a recipe**

```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/app/_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 lint example-1.0.0 --src src
example-1.0.0: 3 steps, 5 expectations, 8 on-screen labels
  error   step 0-setup: do: the label "Folders" is in no source file — copy it from the component, accents included or not
  error   step 1-nominal: where: the label "Technical" is in no source file — copy it from the component, accents included or not
# Four more errors of the same kind follow and the command exits 1: here `src` is an empty folder, so none of the spec’s labels is found. `--src` must point at the folders that really hold your screens’ labels.
```

Output captured from a real run. Source: Replayed with the 0.23.0 package and the example spec of the bmad-plus-uat skill, in a throwaway project; the project path is replaced by /work/app

**Read a run, then ask for the gate** (Simulated)

```text
$ npx bmad-plus uat read demo-1.0.0

■ Demo 1.0.0 — Sarah — 2026-09-16 08:00, finished
  1 passed · 1 failed · 0 blocked · 0 skipped · 0 to do   (_bmad-output/uat/results/demo-1.0.0/20260916-sarah-aaaa.json)
  ✗ 1-confirm/a (failed) — Confirm
      expectation: The line reads OPCO confirmed.
      seen instead: the line still reads OPCO to confirm
# Exit code 1: a failure exists.
$ npx bmad-plus uat gate demo-1.0.0
demo-1.0.0: failed (run 20260916-sarah-aaaa by Sarah)
  - 1-confirm/a is failed and not triaged
# Exit code 1. Once the failure is classified in triage/demo-1.0.0.json (class product, decision fix), the same command prints “demo-1.0.0: passed ...” and exits 0.
```

Illustrative transcript, not captured from a real run. Source: tools/cli/commands/uat.js (read and gate templates); tests/unit/uat-cli.test.js (fixture demo-1.0.0, run 20260916-sarah-aaaa)

<a id="errors"></a>
## Errors and what to do

**Messages printed by uat**

| Message | Cause | What to do |
| --- | --- | --- |
| `uat: an id or a spec file is required` | No target given to an action that needs one. | Pass the recipe id or the path of the `.json` spec. |
| `uat: no spec with the id "<id>" in <dir>/specs` | No file in the specs folder carries that `id`. | Check `--dir` and `--directory`; the match is on the `id` field, not only the file name. |
| `step <id>: ...` or `unknown key at spec level: "<key>" ...` (listed as `error`) | A typo or a missing field in the spec: unknown keys are refused, not ignored. | Fix the spec and run `lint` again. |
| `the label "<text>" is in no source file` | A quoted on-screen label is not found in the folders given with `--src`. | Copy the label from the component, accents included or not. Without `--src` this check does not run. |
| `<id>: not built, N error(s)` | `build` runs the lint and refuses to write the page on any error. | Fix the listed errors. |
| `the page template ... lost a guarantee — not built` | The page template no longer carries one of the 17 guarantees. | Reinstall or update BMAD+; do not edit the template by hand. |
| `no page at <file> — run "bmad-plus uat build <id>" first` | `serve` needs a built page. | Run `build`, then `serve` again. |
| `--input <file> or --input - is required` | `import` without `--input`. | Pass the exported JSON or `-` for standard input. |
| `these results answer <a>, not <b>` | The imported run belongs to another recipe. | Import it with the id it answers. |
| `unsupported results schema: ...` or `results without runId` | The file is not a run exported by the page. | Export again from the page, or use the artifact export. |
| `<id>: no run yet` (exit 2) | No JSON in `<dir>/results/<id>/`. | Serve or send the page, then `import` the answers. |
| `unknown action "<x>" (lint, build, serve, import, read, gate, order)` | Mistyped action. | Use one of the seven actions. |

<a id="examples"></a>
## Examples

<a id="example-cli-uat-write-and-build"></a>
### Have Sentinel write and build the recipe

A version is ready for the test environment. You want a page a non-technical colleague can play.

**In your AI assistant**

```text
Sentinel, write the acceptance recipe for this delivery on our test environment. Lint it against src, build the page and tell me which steps write for real.
```

**What you should get** Sentinel writes the spec to `_bmad-output/uat/specs/<id>.json`, runs `bmad-plus uat lint <id> --src src` and fixes what it reports, then runs `bmad-plus uat build <id>`, which writes `_bmad-output/uat/pages/uat-<id>.html` and lists the page guarantees. It hands you the link or the file, the duration, and the steps marked `writes: true`, each with its read-only `verify` check. If the CLI is not available it says so instead of claiming the page was built.

<a id="example-cli-uat-read-and-gate"></a>
### Read the run, triage, ask for the gate

The tester has finished the page and sent back the JSON.

**In your AI assistant**

```text
Sentinel, import the results file results.json for recipe demo-1.0.0, read the run, classify every failure and tell me whether the gate passes.
```

**What you should get** Sentinel runs `uat import demo-1.0.0 --input results.json`, then `uat read demo-1.0.0`, which prints each failed or blocked expectation with its text and the tester note. It writes a class and a decision for each one into `_bmad-output/uat/triage/demo-1.0.0.json`, asks you when a note does not explain the failure (class `undecided` keeps the gate red), and finally reports `uat gate demo-1.0.0` with the status and the exit code (0 passed, 1 failed, 2 awaiting, 3 stale). It labels the evidence human-observed.

**Wire the gate into a release script (project gate copied from templates/)**

```sh
node scripts/uat-release-gate.mjs || exit 1
# deploy only below this line
```

The skill ships two project gates in `.agents/skills/bmad-plus-uat/templates/`: `uat-spec-present.mjs` fails when no recipe names the version in `package.json`, and `uat-release-gate.mjs` runs `uat gate` with the CLI installed in the project (exit 0 passed, 1 failed, 2 no finished run, 3 another revision, 4 the gate could not run, which is not a pass).

**See also**

- [CLI overview](https://bmad-plus.rochetta.fr/docs/reference/cli-overview/)
- [nexus: durable runs that can use the gate as a check](https://bmad-plus.rochetta.fr/docs/reference/cli-nexus/)
- [review: the same evidence idea for code review](https://bmad-plus.rochetta.fr/docs/reference/cli-review/)
- [The uat block of config.yaml](https://bmad-plus.rochetta.fr/docs/reference/config-yaml/)
- [Troubleshooting](https://bmad-plus.rochetta.fr/docs/faq/troubleshooting/)

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

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