bmod.toml
[skill] bmod = "bmod-method" source = "github:bmad-code-org/BMAD-METHOD/skills"
bmad-code-org/bmad-method · GitHub
Review code changes with several independent reviewers in parallel, then triage and present the findings. Use when the user says "run code review" or "review this code"
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add bmad-code-org/bmad-method --skill bmad-code-review설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
bmod.toml[skill] bmod = "bmod-method" source = "github:bmad-code-org/BMAD-METHOD/skills"
customize.toml# DO NOT EDIT -- overwritten on every update.
#
# Workflow customization surface for bmad-code-review. Mirrors the
# agent customization shape under the [workflow] namespace.
[workflow]
# --- Configurable below. Overrides merge per BMad structural rules: ---
# scalars: override wins • arrays (persistent_facts, activation_steps_*): append
# arrays-of-tables with `code`/`id`: replace matching items, append new ones.
# Steps to run before the standard activation.
# Overrides append. Use for pre-flight loads, compliance checks, etc.
activation_steps_prepend = []
# Steps to run after persistent facts load but before the workflow begins.
# Overrides append. Use for context-heavy setup that should happen
# once activation context is in place.
activation_steps_append = []
# Persistent facts the workflow keeps in mind for the whole run
# (standards, compliance constraints, stylistic guardrails).
# Distinct from the runtime memory sidecar — these are static context
# loaded on activation. Overrides append.
#
# Each entry is either:
# - a literal sentence, e.g. "All stories must include testable acceptance criteria."
# - a file reference prefixed with `file:`, e.g. "file:{project-root}/docs/standards.md"
# (glob patterns are supported; the file's contents are loaded and treated as facts).
persistent_facts = []
# Scalar: executed when the workflow reaches its final step,
# after review findings are presented and recorded. Override wins.
# Leave empty for no custom post-completion behavior.
on_complete = ""
# Review selection: "quick" runs quick_lenses, "thorough" runs thorough_lenses.
review = "thorough"
# Whether a human can answer during the run. Set false when nobody can (a
# review bot, a CI job, another workflow): the review then never asks or
# halts, and takes the default at every choice.
interactive = true
# What to do with patch findings: "apply" fixes them in this session;
# "handoff" (a no-code review) hands them to a coding agent instead.
fixes = "apply"
# What a no-code review writes for the coding agent instead of fixing the
# patch findings. Override wins; empty uses the text in step 5.
handoff_format = ""
# How that reaches the caller. Override wins; empty shows it in this chat.
handoff_delivery = ""
# Review lenses for the review step, in two sets the review selection picks
# between. `instruction` is the lens's whole execution recipe — subagents by
# default, but an override may run anything (e.g. an external reviewer via
# bash). {diff_file}, {claims_file}, {plan_file}, {ticket_file}, and
# {verbatim_intent} are substituted at run time; the first four are paths,
# and {diff_file} is the unified diff file the lens reads. `when` (optional)
# gates a lens; empty `instruction` disables it.
[[workflow.quick_lenses]]
id = "quick"
name = "Quick"
instruction = """
Launch a context-free subagent with this prompt:
Read `{ticket_file}` in full — the ticket, or the epic of a story with no file; it is empty when there is none. The change must meet this intent, not the rest of an epic: {verbatim_intent}
Then read `{plan_file}` — the plan holding the acceptance criteria and intent; empty when there is none — and the files its frontmatter `context:` lists. Read the agent instruction files (`AGENTS.md`, `CLAUDE.md`, or equivalents) at the repository root and in the directories the diff touches; those are the rules. Then read the unified diff at `{diff_file}` — it is the change under review.
Report unmet acceptance criteria, broken rules, and bugs. Each finding: the location, what goes wrong, and evidence checked against the surrounding code. Output a Markdown list of findings only — no severity, priority, or ranking.
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer.
"""
[[workflow.thorough_lenses]]
id = "blind-hunter"
name = "Blind Hunter"
instruction = """
Launch a context-free subagent with this prompt:
Conduct a review of CONTENT.
Look for what's missing, not only what's wrong.
Compute your finding floor N from the diff file's size: N = min(floor(sqrt(kB) + 1), 10), where kB is the file's size in kilobytes. State the arithmetic in one line, then find at least N issues to fix or improve.
Output a Markdown list of findings only — no severity, priority, or ranking.
If the content is empty, stop and say so.
If you have zero findings, re-check and keep thinking; do not stop with an empty list.
CONTENT: the unified diff at `{diff_file}`. Read that file — it is the content under review.
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer.
"""
[[workflow.thorough_lenses]]
id = "edge-case-hunter"
name = "Edge Case Hunter"
instruction = """
Launch a context-free subagent with this prompt:
Read `{skill-root}/review-prompts/edge-case-hunter.md` completely and follow it as your review instructions.
claims_file (leave unread until your instructions call for it): {claims_file}
Review content: the unified diff at `{diff_file}`. Read that file — it is the content under review.
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. If the instruction file is unreadable, report that exact failure and stop. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer.
"""
[[workflow.thorough_lenses]]
id = "verification-gap"
name = "Verification Gap Reviewer"
instruction = """
Launch a context-free subagent with this prompt:
Read `{skill-root}/review-prompts/verification-gap.md` completely and follow it as your review instructions.
Review content: the unified diff at `{diff_file}`. Read that file — it is the content under review.
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. If the instruction file is unreadable, report that exact failure and stop. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer.
"""
[[workflow.thorough_lenses]]
id = "intent-alignment"
name = "Intent Alignment Auditor"
when = "an intent is available: the ticket's Requirements, the plan's intent section, or the user's or conversation's own words on what the change is for"
instruction = """
Launch a subagent with this prompt:
You are an intent-alignment auditor. You have no other context about how this change was produced. Here is the verbatim intent this work started from:
{verbatim_intent}
The diff is the unified diff at `{diff_file}`. Read that file — it is the change under review.
Your task is strictly descriptive — do not prescribe additional work. Report: (1) the defensible readings of the intent, enumerated; (2) which reading this diff implements; (3) where the readings and the diff diverge — specifically, which surface the intent's expectations live at versus which surface the diff's changes and its tests exercise.
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer.
"""
references/claims-check.md# Claims Check
Final pass for the Edge Case Hunter — runs only when the message that launched you named a claims file. Read that file now, for the first time; the path tracing is finished and the claims cannot steer it retroactively.
The file holds the change's own narrative — commit messages and any stated description. The narrative is the author's testimony, not evidence: a claim repeated in a code comment is still the same claim, not confirmation. Extract each checkable claim — what the change does, what it preserves, ordering, arithmetic, and parity with existing code ("exactly as X does") — then try to falsify each one against the code you have already traced. Where your trace is not enough to decide, read the code that decides it: the compared-to function, the actual callee, the state the claim assumes.
Append one finding per falsified claim to the same JSON array, with the four standard fields plus:
- `kind`: `"claim"`
- `confidence`: `"high"`, `"medium"`, or `"low"`
For a claim finding the standard fields read as: `location` = where the code contradicts the claim; `trigger_condition` = the claim, quoted or tightly paraphrased; `guard_snippet` = what the code actually does; `potential_consequence` = what goes wrong for someone who believed the claim.
Verified claims produce nothing. Add nothing if nothing is falsified.
references/deletion-check.md# Deletion Check Secondary pass for the Edge Case Hunter — runs only when the diff removed meaningful code. Subordinate to the edge-case pass; findings are usually few or none. For each chunk of removed or replaced code (ignore pure renames and whitespace), ask: did it carry behavior or a contract that the change neither re-established nor intentionally retired? Add a finding for any resulting regression, orphaned reference, or newly-dead code. Skip anything already covered by your edge-case findings. Append each finding to the same JSON array as the edge-case findings, with the four standard fields plus: - `kind`: `"deletion"` - `confidence`: `"high"`, `"medium"`, or `"low"` — these are inferences; rate them For a deletion finding the standard fields read as: `location` = the removed item; `trigger_condition` = the behavior or contract it enforced; `guard_snippet` = where or how to re-establish it; `potential_consequence` = the regression or orphan. Add nothing if nothing qualifies.
review-prompts/edge-case-hunter.md# Edge Case Hunter Review
**Goal:** You are a pure path tracer. Never comment on whether code is good or bad; only list missing handling.
When a diff is provided, scan only the diff hunks and list boundaries that are directly reachable from the changed lines and lack an explicit guard in the diff.
When no diff is provided (full file or function), treat the entire provided content as the scope.
Ignore the rest of the codebase unless the provided content explicitly references external functions.
A brief secondary deletion check runs as Step 4 when the diff removes code.
A claims check runs as Step 5 when the launch message names a claims file.
**Inputs:**
- **content** — Content to review, or a path to read it from: diff, full file, or function
- **also_consider** (optional) — Areas to keep in mind during review alongside normal edge-case analysis
- **claims_file** (optional) — Path to the change's stated narrative. Do NOT read it before Step 5: the path tracing in Steps 2–3 must finish before the narrative is seen.
**MANDATORY: Execute steps in the Execution section IN EXACT ORDER. DO NOT skip steps or change the sequence. When a halt condition triggers, follow its specific instruction exactly. Each action within a step is a REQUIRED action to complete that step.**
**Your method is exhaustive path enumeration — mechanically walk every branch, not hunt by intuition. Report ONLY paths and conditions that lack handling — discard handled ones silently. Do NOT editorialize or add filler. Do not assign severity labels, rankings, or priority levels.**
## EXECUTION
### Step 1: Receive Content
- Take the content to review from the parent message that launched you — inline, or by reading the file it points to (never from this instruction file)
- If no content is supplied, or it is empty, unreadable, or cannot be decoded as text, return `[{"location":"N/A","trigger_condition":"Input empty or undecodable","guard_snippet":"Provide valid content to review","potential_consequence":"Review skipped — no analysis performed"}]` and stop
- Identify content type (diff, full file, or function) to determine scope rules
### Step 2: Exhaustive Path Analysis
**Walk every branching path and boundary condition within scope — report only unhandled ones.**
- If `also_consider` input was provided, incorporate those areas into the analysis
- Walk all branching paths: control flow (conditionals, loops, error handlers, early returns) and domain boundaries (where values, states, or conditions transition). Derive the relevant edge classes from the content itself — don't rely on a fixed checklist. Examples: missing else/default, unguarded inputs, off-by-one loops, arithmetic overflow, implicit type coercion, race conditions, timeout gaps
- Consider implicit branches: the diff special-cases or changes the handling of one or more members of a fixed set of values — enums, status codes, sentinels, type tags, flags, value ranges. The rest of the set is implicit branches (e.g. the diff changes the `RED` and `YELLOW` cases of a `RED`/`YELLOW`/`GREEN` enum; `GREEN` is the implicit branch)
- Consider handle lifetime: when the changed code re-checks, re-fetches, or re-validates something it already held — a handle, index, id, pointer — the re-check exists because an intervening call can invalidate it. Identify that call, what it does to the thing held, and what the changed code silently skips when the re-check fails
- For each call site the diff adds or changes — in test files as well as production code — read the callee's declaration and check the call against it: argument count, order, types, and defaults. Report any mismatch
- For each path: determine whether the content handles it
- Collect only the unhandled paths as findings — discard handled ones silently
### Step 3: Validate Completeness
- Revisit every edge class from Step 2 — e.g., missing else/default, null/empty inputs, off-by-one loops, arithmetic overflow, implicit type coercion, race conditions, timeout gaps
- Add any newly found unhandled paths to findings; discard confirmed-handled ones
### Step 4: Deletion Check
If the diff removed or replaced meaningful code (ignore pure renames and whitespace): load `references/deletion-check.md` and follow it.
### Step 5: Claims Check
If the launch message provided a `claims_file` path and the file exists and is non-empty: load `references/claims-check.md` and follow it.
### Step 6: Present Findings
Output all findings as a single JSON array following the Output Format specification exactly.
## OUTPUT FORMAT
Return ONLY a valid JSON array of objects. Each edge-case finding contains exactly these four fields:
```json
[{
"location": "file:start-end (or file:line when single line, or file:hunk when exact line unavailable)",
"trigger_condition": "one-line description (max 15 words)",
"guard_snippet": "minimal code sketch that closes the gap (single-line escaped string, no raw newlines or unescaped quotes)",
"potential_consequence": "what could actually go wrong (max 15 words)"
}]
```
No extra text, no explanations, no markdown wrapping. An empty array `[]` is valid when nothing is found. Deletion findings from Step 4 and claim findings from Step 5, if any, go in the same array with the extra fields defined in `references/deletion-check.md` and `references/claims-check.md`.
## HALT CONDITIONS
- If no content is supplied, or it is empty, unreadable, or cannot be decoded as text, return `[{"location":"N/A","trigger_condition":"Input empty or undecodable","guard_snippet":"Provide valid content to review","potential_consequence":"Review skipped — no analysis performed"}]` and stop
<reference path="references/deletion-check.md">
# Deletion Check
Secondary pass for the Edge Case Hunter — runs only when the diff removed meaningful code. Subordinate to the edge-case pass; findings are usually few or none.
For each chunk of removed or replaced code (ignore pure renames and whitespace), ask: did it carry behavior or a contract that the change neither re-established nor intentionally retired? Add a finding for any resulting regression, orphaned reference, or newly-dead code. Skip anything already covered by your edge-case findings.
Append each finding to the same JSON array as the edge-case findings, with the four standard fields plus:
- `kind`: `"deletion"`
- `confidence`: `"high"`, `"medium"`, or `"low"` — these are inferences; rate them
For a deletion finding the standard fields read as: `location` = the removed item; `trigger_condition` = the behavior or contract it enforced; `guard_snippet` = where or how to re-establish it; `potential_consequence` = the regression or orphan.
Add nothing if nothing qualifies.
</reference>
<reference path="references/claims-check.md">
# Claims Check
Final pass for the Edge Case Hunter — runs only when the message that launched you named a claims file. Read that file now, for the first time; the path tracing is finished and the claims cannot steer it retroactively.
The file holds the change's own narrative — commit messages and any stated description. The narrative is the author's testimony, not evidence: a claim repeated in a code comment is still the same claim, not confirmation. Extract each checkable claim — what the change does, what it preserves, ordering, arithmetic, and parity with existing code ("exactly as X does") — then try to falsify each one against the code you have already traced. Where your trace is not enough to decide, read the code that decides it: the compared-to function, the actual callee, the state the claim assumes.
Append one finding per falsified claim to the same JSON array, with the four standard fields plus:
- `kind`: `"claim"`
- `confidence`: `"high"`, `"medium"`, or `"low"`
For a claim finding the standard fields read as: `location` = where the code contradicts the claim; `trigger_condition` = the claim, quoted or tightly paraphrased; `guard_snippet` = what the code actually does; `potential_consequence` = what goes wrong for someone who believed the claim.
Verified claims produce nothing. Add nothing if nothing is falsified.
</reference>
## CONTENT SOURCE
"Review content:" in the message that launched you gives the content itself or a path to read it from. Read the file when it is a path; either way that is the content under review, and this instruction file never is.
review-prompts/verification-gap.md# Verification Gap Review **Goal:** Find behavior this change altered that could break later without any test failing. Do not hunt for bugs; report the ones you run into while tracing. A gap finding is a request to add or change one test. File it only when you can name a specific regression that test would catch. If no such test is worth writing, there is nothing to file. Gap shapes: 1. **Regression gap:** the changed code could regress where it is used, and no test would fail. 2. **Missing-adoption gap:** a site that should use the new behavior still handles the case its own way, and no test would catch that. 3. **Broken-verification gap:** a test looks like it covers the change but does not: it is skipped, flaky, not run normally, or too weak to see the regression. 4. **Stale-verification gap:** a test this change made wrong or moot. It may still pass. ## Evidence rules - Read a test before saying what it covers or misses. - Before saying no test exists, search the repo for the symbol under test and its imports. Expected file locations are not enough. - Say what you checked and how far you looked. Drop any finding you cannot ground. - Do not assign severity, confidence, priority, or ranking. ## Sequence ### Step 1: Screen Take each part of the change separately. Skip a part that changes no behavior: no change to return values, thrown errors, side effects callers can see, or observable state (including iteration order and emitted messages). Formatting, comments, renames, trivial getters and pass-throughs, and type-only changes are examples. Once a part is skipped, do not look at its callers or tests. Also skip parts that are not worth a test: static text such as prompts, skills, and documents; LLM output; and build, toolchain, dependency, and configuration changes that the build or CI run itself checks. Do not skip test-only parts. A deleted or weakened test is checked in Step 4; every touched test is read in Step 5. If every part is skipped, output the clean result. ### Step 2: Name the changed behavior For each remaining part, say what behavior differs from before: output, side effect, branch, error path, schema or event shape, default, validation rule, external contract. Handle each behavior separately. ### Step 3: Find where it is used Trace each changed behavior to the code that observes it: direct callers, registered entry points (routes, commands, DI), and contract consumers (schemas, events, APIs, database readers). Stop a path when a test there would catch a regression, when the consumer does not observe the change, or when the next hop is a guess (dynamic dispatch, reflection, code outside the repo). One to three hops is usually enough. With more than five similar consumers, check representative ones and expand only when a consumer uses the behavior differently. ### Step 4: Name the regression, then check the test For each consumer, name the smallest realistic regression it would see: the branch inverted, the default dropped, the field omitted, the old error code returned, the integration call skipped. This is the Demonstration. It describes what goes wrong for the consumer. "This line could be removed without a test failing" is not a regression. If there is no regression (the state is unreachable, the branch is defensive, the code repeats what another line already guarantees), drop the path. There is no finding and nowhere else to file it. A missing-adoption gap needs two things: evidence the change means the new behavior to replace the local one (PR intent, naming or docs, a replaced sibling site, deleted duplicate logic, a test defining the new rule), and the same observable contract at both sites. Without both, it is a refactor suggestion, not a finding. With both, missing coverage of the non-adoption is the gap. Read the relevant test. Would the Demonstration make an assertion fail? - Yes: the behavior is verified. No finding. - No test runs the path, or the test is skipped, flaky, or not run normally, or it runs the code without checking the changed result: report a regression gap or broken-verification gap. - No test asserts the site adopts the new behavior: report a missing-adoption gap. A test counts only when it runs normally and an assertion observes the changed output, branch, or contract. These do not count: assertions on source text; success, no-throw, or snapshot-only checks; mock or log-call checks; manual checks; tests that mock away the integration; end-to-end tests that pass through without checking the changed output; stale assertions or fixtures. For example, `expect(x ?? DEFAULT).toBe(DEFAULT)` passes when `x` is missing. Common patterns: a helper test covers the branch but caller values skip it; a payload or schema change is not checked at the consumer; migration tests only create new-format rows; a handled partial-failure path has no test; a deleted or weakened test leaves behavior unpinned (deleting a source-text assertion is not this). ### Step 5: Check the touched tests A test is wrong when what it asserts is not what the code should do after this change. A test or test comment is moot when it no longer exercises or describes anything. Report either as a stale-verification gap, saying what the test should assert instead, or that it should go. ### Step 6: Confirm Before writing a finding, re-open the tests or search results it relies on. Check that the Demonstration names a regression and no test you read would catch it, or that the stale assertion pins the outcome you say it does. Explain why the test misses the regression from what it sets up and checks. Do not report: cases the compiler or type checker enforces; behavior an integration, contract, or end-to-end test already verifies; low coverage or a missing test file by itself; untested code the change did not touch. Defects you ran into while tracing go under `Other findings`. Do not go looking for more. ## Output format One block per gap. No general advice, no severity or confidence. Triage trusts a gap finding as filed and does not re-verify it, so each block must stand on its own evidence. ```markdown ### <one-line title naming the gap> - **Changed surface:** the behavior or contract that changed, `file:line`. - **Impacted consumer or site:** named concretely with `file:line`, not "callers of this function". - **Existing test evidence:** what the relevant test asserts, with `file:line`; or the searches run and their result. For a broken-verification gap, why the apparent test does not count. For a stale-verification gap, the assertion that is wrong or moot. - **Missing verification:** the assertion or check that is absent, or how the stale assertion should read. - **Demonstration:** the regression, and why the tests you read would not fail. For a missing-adoption gap, the case the site mishandles. For a stale-verification gap, what the code should do, and why the test does not say so. - **Consequence:** what ships wrong. - **Disposition:** the test to add or change, fit to how this repo already tests. ``` If you noticed non-gap problems while tracing, append: ```markdown ## Other findings - <description only; no severity, confidence, priority, or ranking> ``` When there are no gaps and no other findings, output exactly this single line: `No verification gaps found.` ## Content source "Review content:" in the message that launched you gives the content itself or a path to read it from. Read the file when it is a path; either way that is the content under review, and this instruction file never is. If no content is supplied, or it is empty or unreadable, stop with exactly: `No verification gaps found.`
SKILL.md---
name: bmad-code-review
description: 'Review code changes with several independent reviewers in parallel, then triage and present the findings. Use when the user says "run code review" or "review this code"'
---
Run the following command exactly once without changing the current working directory.
Replace `{project-root}` with the nearest folder containing `_bmad/`, from the working directory upward.
Replace `{skill-root}` with the absolute path to this skill's directory.
If the invocation asks for a `quick` or `thorough` review, append `--set workflow.review=<value>` to the command.
If the invocation asks for a `no-code` review, append `--set workflow.fixes=handoff` to the command.
If the invocation is clearly non-interactive -- it says not to ask questions, or a bot, a CI job or another workflow is running the review -- append `--set workflow.interactive=false`. When in doubt, don't.
```bash
uv run --no-cache "{project-root}/_bmad/scripts/render_skill.py" --project-root "{project-root}" --skill "{skill-root}"
```
- The command prints the skill's workflow: follow it. `HALT: <reason>`: report the reason and stop.
- If the script does not exist, BMad is not set up in this project yet. Offer to set it up using `bmad` skill, then run the command again. If you do not have the `bmad` skill, offer to install it first.
- On any other output or failure, including `uv` being unavailable, report the command output and stop. Do not run any workflow source directly.
step-01-gather-context.md---
diff_file: '' # set at runtime: path to the diff file
claims_file: '' # set at runtime (path or empty)
plan_file: '' # set at runtime (path or empty)
ticket_file: '' # set at runtime: the story file, else the epic file, of the ticket under review (path or empty)
review_mode: '' # set at runtime: full or no-plan
---
# Step 1: Gather Context
## RULES
- The triggering prompt and the conversation before it name the review target — act on them; do not start from a blank slate.
- Writing `{diff_file}` and the claims file is the only change this step may make. Otherwise it is read-only.
{% if not workflow.interactive %}
- No human can answer during this run: never ask and never HALT for input. Wherever an instruction below would ask, take the default it names instead; where it names none, stop and report what is missing.
{% endif %}
## INSTRUCTIONS
1. **Find the review target.** Check in this order and stop at the first that identifies it:
- **Request or recent conversation.** A PR (resolve via `gh pr view`; if that fails, ask for a SHA or branch), commit, branch, commit range, staged or uncommitted changes, a provided diff or file list, or a plan file. A plan sets `plan_file`, and when its frontmatter `ticket` is set, `tickets.py find <the plan's folder> <ticket>` (command form in the next bullet) names the ticket to read the same way; a non-empty frontmatter `baseline_revision` other than `NO_VCS` makes the diff source the **plan baseline** — otherwise say so and continue, since a plan without one does not identify the diff.
- **The ticket tree.** Run `uv run {project-root}/_bmad/method/scripts/tickets.py --project-root {project-root} status`. On a non-zero exit, continue. Otherwise offer the `tickets` rows whose `state` is `review`, `<ref>` and `<title>` each, plus a choice of another target, and HALT for the user's pick. With none, or another target chosen, continue. For a picked ticket, run `tickets.py find <ref>` (same command form): set `ticket_file` to its `story_file`, else its `epic_file`, read that file in full (with `story_file` null, the entry's `description` and `verify` are in `find`'s output), and treat its `plan` as a plan file from the request.
- **Current git state.** If HEAD is not on the default branch, confirm: "I see HEAD is `<short-sha>` on `<branch>` — do you want to review this branch's changes?" If confirmed, it is a branch diff against the default branch.
- **Ask.** Go to instruction 2.
{% if not workflow.interactive %}
Skip the ticket-tree offer, take the current branch's diff against the default branch without confirming, and if HEAD is on the default branch and nothing else identifies a target, stop and report that there is nothing identified to review.
{% endif %}
2. HALT. Ask the user what to review: uncommitted changes, staged changes, a branch diff (which base), a commit range, or a provided diff or file list.
3. Write the diff to `{diff_file}`, a uniquely-named temp file outside the repository, so concurrent reviews cannot collide. A plan baseline diffs every change since `baseline_revision`, untracked files included. A branch diff runs from the merge-base (`<base>...HEAD`), so later commits on the base do not show up as reverted. For a file list, include untracked files. If the source does not resolve, HALT and ask for a valid one; if the diff is empty, HALT — there is nothing to review. The review lenses read that file; the diff text is never pasted into their prompts. Read `{diff_file}` yourself when you need the diff later in this workflow.
4. **Stage the claims file.** Collect the change's own narrative: for a plan baseline, branch diff, or commit range, the commit messages it covers (`git log <base>..<head>`, where a plan baseline's head is `HEAD`); for other sources, whatever description of the change the user or conversation supplied. Write it verbatim to another uniquely-named temp file outside the repository and set `claims_file` to its path, or `''` if there is none. Do not analyze or summarize it — it is input for one review lens, staged as a file so the other lenses never see it.
5. **Set the plan context.**
- The request or conversation explicitly says there is no plan → `review_mode` = `no-plan`, `plan_file` = `''`. Do not ask.
- `plan_file` is set → verify it is readable; `review_mode` = `full`.
{% if workflow.interactive %}
- Otherwise, ask for a plan file path, or to continue without one, and set `plan_file` and `review_mode` accordingly. Do not assume no-plan just because none was given.
{% else %}
- Otherwise → `review_mode` = `no-plan`, `plan_file` = `''`.
{% endif %}
6. If `review_mode` = `full` and `{plan_file}`'s frontmatter has a `context` list, load each referenced doc and warn about any that cannot be found. Open a file `{ticket_file}` lists in `sources:` or `companions:` only for a section the ticket cites; never re-derive requirements from it.
7. If `{diff_file}` exceeds about 3000 lines, warn the user and offer to chunk the review by file group. If they accept, agree on the first group, rebuild `{diff_file}` for it, and list the remaining groups for follow-up runs.
### CHECKPOINT
Present a summary before proceeding: diff stats (files changed, lines added/removed), `{review_mode}`, and the loaded ticket, plan, and context docs (if any).
{% if workflow.interactive %}
HALT and wait for user confirmation to proceed.
{% endif %}
## NEXT
Read fully and follow `{{ rendered("step-02-review.md") }}`
step-02-review.md---
failed_layers: '' # set at runtime: comma-separated list of lenses that failed or returned empty
---
# Step 2: Review
## RULES
- All review subagents must run at the same model capability as the current session.
- Run subagents synchronously: launch them together as blocking calls awaited in this turn — never backgrounded or detached, never ending the turn to await results.
## INSTRUCTIONS
1. The review lenses are listed below. For each lens:
- `Run only when` present and not satisfied by the current context (`{review_mode}`, `{plan_file}`, `{ticket_file}`, `{verbatim_intent}`) → skip the lens and tell the user which lens was skipped and why.
- otherwise → the lens is active.
2. Announce skipped lenses first, then launch every active lens before handling any lens's result. Try running all active lenses simultaneously: substitute the runtime placeholders (`{diff_file}`, `{claims_file}`, `{plan_file}`, `{ticket_file}`, `{verbatim_intent}`) into each lens's instruction. `{diff_file}`, `{claims_file}`, `{plan_file}`, and `{ticket_file}` are paths: substitute the absolute path and let the lens read the file — a launch prompt never carries diff text, and the child's working directory is not yours. `{verbatim_intent}` is the Description, Requirements and Acceptance Criteria sections of `{ticket_file}` when it is the story file, with the epic's Requirements lines in its `covers`; when it is the epic file of an entry with no file, the entry's `title`, `description` and `verify` from `find`, with the epic's Requirements lines in the entry's `covers`; else the intent section of `{plan_file}` if it has one, else the user's or conversation's own words on what the change is for, substituted inline as text; a request to review something is not an intent. When an instruction launches a reviewer subagent, launch that child with the prompt text after placeholder substitution; do not load the reviewer instruction file yourself. For any other customized instruction, execute it as written. When running lenses as subagents, spawn every reviewer before reading or reacting to any of their output; begin collection only once all are launched.
{% if workflow.review == "quick" %}
{{ workflow.quick_lenses }}
{% else %}
{{ workflow.thorough_lenses }}
{% endif %}
3. If a lens's instruction requires subagents and none are available, for each such lens write that lens's child prompt beside `{plan_file}`, named after it with the lens id appended (in `{{ initiative_folder }}/` when there is no plan), with every file it points to — the diff, the claims, the reviewer instruction file — replaced inline by that file's contents, and every other line left exactly as written. That session shares no filesystem with this one, so its prompt has to stand alone; this is the only place you read a reviewer instruction file yourself. Then HALT. Ask the user to run each in a separate session (ideally a different LLM) and paste back the findings. When findings are pasted, treat them as those lenses' findings and resume from this point.
4. **Lens failure handling**: If any lens fails, times out, or returns empty results, append the lens's `name` to `failed_layers` (comma-separated) and proceed with findings from the remaining lenses.
5. Collect all findings from the completed lenses, keeping track of each finding's originating lens `id`.
## NEXT
Read fully and follow `{{ rendered("step-03-triage.md") }}`
step-03-triage.md---
triage_file: '' # set at runtime: path to the triage file
---
# Step 3: Triage
The objective of triage is to collect the findings from the review step, and turn them into a unified list of things to deal with. The common triage failures are:
- forgetting about a finding
- rejecting a finding of a real defect for any reason, e.g. because it looks similar enough to a previously rejected finding -> defect ships to production
- assigning wrong severity -> a defect with real consequences misclassified as low (cosmetic) ships into production; a hard-to-fix finding with negligible consequence misclassified as medium creates accidental complexity.
## INSTRUCTIONS
1. Wait for all review lenses to finish and report their findings. Do not attempt to triage findings as they come - this leads to forgotten findings.
2. **Normalize** findings from all lenses into a unified list where each finding has:
- `id` -- `R` for "raw", followed by sequential integer (`R1`, `R2` etc)
- `source` -- the `id` of the lens that produced the finding (e.g., `blind-hunter`)
- `title` -- one-line summary
- `detail` -- full description
- `location` -- file and line reference (if available)
Do not attempt to deduplicate or group findings at this point - this breaks traceability and you don't know enough about them yet.
Write this list to `{triage_file}`, a uniquely-named temp file outside the repository, one block per finding. Later steps render the review from this file, so a decision that is not in it does not exist.
3. Classify each reported finding from the list as one of the following: [high, medium, low, false, maybe-false], by following this process:
3a. **Verify the finding's claim.**
Is a finding real? If a finding alleges (or implies) some unwanted code behavior, can that behavior actually occur at the cited file and line? Can the program ever reach the state that makes this behavior occur? Is the alleged behavior actually unwanted?
Read beyond the changed lines -- follow callers, guards upstream, etc -- try to get a definite yes or no answer.
A finding that says "this throws / crashes when X" is only real if you can show that X happens. An assert or throw on a state the program can never reach is defensive programming, not a defect. Ignoring corner cases for simplicity may be intentional.
Do not decide about finding B because it's similar to finding A you've already looked into. This shortcut only works when both findings cite the exact same behavior at the exact same file/line. When finding A alleges a non-existent behavior, while finding B points to a real defect, trying to judge B by similarity to A allows said defect to escape to production.
Do not bring into claim verification any notion of how severe, how rare, or how fixable the alleged outcome is - only whether it can happen or not.
Skip verification for any gap finding from the `verification-gap` review lens. If that lens found a verification gap, it's real. Do verify findings it reports under `Other findings` rubric though.
3b. **Assign severity to real findings**
What would a single occurrence of the bad outcome do to an end user or a developer? How tolerable is it?
Descriptions of `high`, `medium` and `low` are provided in 3c. Use your own judgement if a finding doesn't fit any of these.
Disregard any severity a lens assigned -- it lacks the context to grade. If a finding is not real, skip judging its severity, it won't matter.
A gap finding from the `verification-gap` lens is `medium`; skip grading it. Its `Other findings` are graded like everything else.
For developer-only problems (inconsistent design, eroded invariants, duplicated sources of truth etc): a vague "this is messy" is not a severity; name where it will cause trouble -- e.g., which caller will diverge, which rule will break, or how will it hurt code readability/comprehension. If you can't, think whether this is still a real finding, and consider the severity as "low".
Just like in verification, do not judge severity of finding B based on a similar, but different finding A.
Severity is about the impact of a single occurrence of the bad outcome. It must not depend on how rare the outcome may be, how hard it is to fix, or whether the defect predates the change under review.
The only distinction that really changes what happens to a finding within this process is whether or not it is `low`. `high` and `medium` only affect the sorting order in the triage report.
If you cannot decide between `medium` and `low`, pick `medium`.
3c. **Render your verdict**
- `high` -- data loss or corruption, a security exposure, a silently wrong result, or a path broken with no workaround.
- `medium` -- broken or wrong, but noticed and worked around, or limited in reach.
- `low` -- cosmetic or a minor inconvenience.
- `false` -- the finding is not real.
- `maybe-false` -- you cannot decide whether the finding is real or not. Use this only when the diff and surrounding code leave the question open.
Each verdict comes with a sentence of evidence: for `high`, `medium`, `low` the harm you named; for `false` what disproves the claim; for `maybe-false` what would settle it.
3d. Reject some findings
Reject every `false` finding.
Reject a `low` finding if it would be rarely visible to a user or developer, and the fix creates extra complexity - anything more than a direct correction or deletion.
If you have a plan file for the change under review, reject any finding whose fix is to edit the intent section thereof.
Once every finding has a verdict, update `{triage_file}` in one pass: each block gets its verdict and evidence, and a rejected finding stays in, marked rejected, with the reason.
4. Group the findings you did not reject by the root cause
Several raw review findings can say different things about what effectively is the same defect. At this point, you have learned enough to accurately deduplicate them. The idea is to turn them into a single finding per root cause, that carries the highest verdict of the group, but remembers which raw findings, from which sources it's based upon.
Two raw findings belong in one group only when the same root cause produced both. Be careful that it's same, not merely similar. Same location does not necessarily mean same root cause - there can be multiple bugs on the same line of code.
A deduplicated finding carries:
- `title` -- one line naming the root cause
- `location` -- the member findings' distinct locations
- `detail` -- summary of member findings' bad outcomes, focusing on most severe verified ones.
- `verdict` -- the highest verdict among the member findings (`high` > `medium` > `low` > `maybe-false`)
- `source` -- distinct sources of member findings joined with `+` (e.g., `blind-hunter+edge-case-hunter`)
- `members` -- the `R` ids it is built from
Order these deduplicated findings by verdict (highest first), and assign each of them with an `id`: `F` for "finding", followed by sequential number (`F1`, `F2` etc). Append them to `{triage_file}`.
None of the member raw findings can be `false` - all these were rejected.
A deduplicated finding which is a group of one should be identical to its single raw finding, except the `id`.
5. `{triage_file}` is this step's output: every raw finding with its verdict and evidence, every rejection with its reason, and the `F` findings with their members. Check that nothing is missing before moving on.
## NEXT
Read fully and follow `{{ rendered("step-04-route.md") }}`
step-04-route.md---
---
# Step 4: Route
## INSTRUCTIONS
`{triage_file}`, the triage step's output, is the input here. Work from its `F` findings.
1. **Route** each deduplicated finding: decide which should be fixed right away, and which dealt with in a different manner. Assign each of them to one of [`decision_needed`, `patch`, `defer`], as follows:
- **decision_needed** -- There is an ambiguous choice that requires human input. The code cannot be correctly patched without knowing the user's intent. You should assign `decision_needed` even when running in a non-interactive mode. Do not guess and patch, even if a human cannot answer during this session. List the options, one line each, and the one you recommend with the reason.
- **patch** -- Code issue that is fixable without human input. The correct fix is unambiguous, adds no public surface, and guards no state you did not demonstrate; otherwise `decision_needed`.
- **defer** -- Pre-existing issue not caused by the current change, real but not actionable now; or an entry whose members are all `maybe-false` and the claim, if true, would be `medium` or `high` -- record that severity marked unverified, plus what would settle it (if it would only be `low`, reject it with the same note); or any entry whose fix edits agent-context files (CLAUDE.md, AGENTS.md, rules) or a ticket file.
An entry this change caused, with a verified `high`, `medium`, or `low` member, is never `defer`, whatever its fix touches: it is `patch` or `decision_needed`. defer is only for what is not this change's problem.
Write each finding's route into its block in `{triage_file}`, with the options and recommendation for `decision_needed` and the note for `defer`.
2. If `failed_layers` is non-empty, report which lenses failed before announcing results. If zero entries remain after rejections AND `failed_layers` is non-empty, warn the user that the review may be incomplete rather than announcing a clean review.
3. If zero entries remain after triage (all rejected or none raised): state "✅ Clean review — all lenses passed." (Item 2 already warned if any review lenses failed via `failed_layers`.)
## NEXT
Read fully and follow `{{ rendered("step-05-present.md") }}`
step-05-present.md---
deferred_work_file: '{{ initiative_folder }}/deferred-work.md'
---
# Step 5: Present and Act
## RULES
- When `{plan_file}` is set, always write findings to it before offering action choices.
- Never change a plan's `status`. The build leaves it at `built`; only the user marks a ticket done.
- `{triage_file}` is the source for every finding below: its verdict, evidence, route, options, and rejection reason. Render from it, not from memory.
{% if not workflow.interactive %}
- No human can answer during this run: never ask and never HALT. The report text is the deliverable -- everything a reader must act on is in it, not only in files the run writes.
{% endif %}
## INSTRUCTIONS
### 1. Clean review shortcut
If zero findings remain after triage (all rejected or none raised): state that. When `{plan_file}` is set, still write section 2's block, with the summary line and the `Rejected` appendix, so the run is recorded.
{% if workflow.fixes == "handoff" %}
Then proceed to section 5.
{% elif workflow.interactive %}
Then proceed to section 6 (Next steps).
{% else %}
Then end.
{% endif %}
### 2. Write findings to the plan
If `{plan_file}` is set, append a block to its `## Code Review` section, creating that section at the end of the file when it is absent. The block opens with `### {date}` and the summary line from section 3, then the findings below, then the `Rejected` appendix. Write all findings in this order:
1. **`decision-needed`** findings (unchecked):
`- [ ] [Review][Decision] <Title> — <Detail> Options: <each option, one line>. Recommended: <option> — <reason>.`
2. **`patch`** findings (unchecked):
`- [ ] [Review][Patch] <Title> [<file>:<line>]`
3. **`defer`** findings (checked off, marked deferred):
`- [x] [Review][Defer] <Title> [<file>:<line>] — deferred: <pre-existing, or for maybe-false the evidence that would settle it>`
Also append each `defer` finding to `{deferred_work_file}` under a heading `## Deferred from: code review ({date})`. If `plan_file` is set, include its basename in the heading (e.g., `code review of story-digest-delivery-plan (2026-03-18)`). One bullet per finding with description.
### 3. Present summary
Announce what was written:
> **Code review complete.** <D> `decision-needed`, <P> `patch`, <W> `defer`, <R> rejected.
The `Rejected` appendix is one line per rejected finding: `false` with its refutation, `low` with why it was not worth fixing. Without `plan_file`, it ends the chat listing.
{% if not workflow.interactive %}
The listing is the review itself. List `decision-needed` entries first, then `patch`, then `defer`, then the `Rejected` appendix, highest severity first within each. Each `decision-needed` entry carries its options and the recommended one and is marked open -- nothing was decided. Each `patch` entry carries its location and the fix.
{% endif %}
If `plan_file` is set, add: `Findings written to {plan_file}.`
Otherwise add: `Findings are listed above. No plan was provided, so nothing was persisted.`
{% if workflow.interactive %}
### 4. Resolve decision-needed findings
If `decision_needed` findings exist, present each one with its detail and the options available. The user must decide — the correct fix is ambiguous without their input. Walk through each finding (or batch related ones) and get the user's call. Once resolved, each becomes a `patch`, `defer`, or is rejected.
If the user chooses to defer, ask: Quick one-line reason for deferring this item? (helps future reviews): — then append that reason to both the finding's bullet in `{plan_file}` and the `{deferred_work_file}` entry.
**HALT** — I am waiting for your numbered choice. Reply with only the number. Do not proceed until you select an option.
{% endif %}
### 5. Handle `patch` findings
{% if workflow.fixes == "handoff" %}
{{ workflow.handoff_format | default("Write one Markdown block a coding agent can fix the findings from without redoing the review. Name the base it was compared to by full SHA, and the reviewed commit the same way, or say that the change is uncommitted on top of the base. Then, for each `patch` finding (including any resolved from `decision-needed`): what is wrong and how you know; the files or symbols it touches; what the fixed code should do, as an outcome, not a recipe; and how to check the fix, including anything that has to be run to confirm the defect. If there are no `patch` findings, say so. Then, under their own headings, list the `decision-needed` findings still unresolved and the `defer` findings, each with its reason, so the agent knows they are not its to fix.", true) }}
Nothing was run in this review, so never claim a fix works or a check passed.
{{ workflow.handoff_delivery | default("Show it in this chat.", true) }}
{% elif workflow.interactive %}
If `patch` findings exist (including any resolved from section 4), HALT. Ask the user:
If `plan_file` is set, present all three options:
> **How would you like to handle the `<P>` `patch` findings?**
> 1. **Apply every patch** — fix all of them now, no per-finding confirmation. Defer and decision-needed items are not touched.
> 2. **Leave as action items** — they are already in `{plan_file}`
> 3. **Walk through each patch** — show details for each before deciding
If `plan_file` is **not** set, present only options 1 and 2 (omit "Leave as action items" — findings were not written to a file):
> **How would you like to handle the `<P>` `patch` findings?**
> 1. **Apply every patch** — fix all of them now, no per-finding confirmation. Defer and decision-needed items are not touched.
> 2. **Walk through each patch** — show details for each before deciding
**HALT** — I am waiting for your numbered choice. Reply with only the number. Do not proceed until you select an option.
- **Apply every patch**: Apply every patch finding without per-finding confirmation. Do not modify defer or decision-needed items. After all patches are applied, present a summary of changes made. If `plan_file` is set, check off the patch items in it (leave defer items as-is).
- **Leave as action items** (only when `plan_file` is set): Done — findings are already written to `{plan_file}`.
- **Walk through each patch**: Present each finding with full detail, diff context, and suggested fix. After walkthrough, re-offer the applicable options above.
**HALT** — I am waiting for your numbered choice. Do not proceed until you select an option.
{% else %}
Apply every `patch` finding without asking. Do not modify defer or decision-needed items. After all patches are applied, present a summary of changes made. If `plan_file` is set, check off the patch items in it (leave defer items as-is).
{% endif %}
{% if workflow.interactive %}
**✅ Code review actions complete**
- Decision-needed resolved: <D>
- Patches handled: <P>
- Deferred: <W>
- Rejected: <R>
### 6. Next steps
Present the user with follow-up options:
{% if workflow.fixes == "handoff" %}
> **What would you like to do next?**
> 1. **Re-run code review** — address findings and review again
> 2. **Done** — end the workflow
{% else %}
> **What would you like to do next?**
> 1. **Build the next ticket** — run `bmad-build` to pick up the next ready ticket
> 2. **Re-run code review** — address findings and review again
> 3. **Done** — end the workflow
{% endif %}
**HALT** — I am waiting for your choice. Do not proceed until the user selects an option.
{% endif %}
## On Complete
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
{{ workflow.on_complete }}
workflow.md{% if workflow.review not in ("quick", "thorough") %}
{{ halt("workflow.review must be quick or thorough, not " ~ workflow.review) }}
{% endif %}
{% if workflow.interactive is not boolean %}
{{ halt("workflow.interactive must be true or false, not " ~ workflow.interactive) }}
{% endif %}
{% if workflow.fixes not in ("apply", "handoff") %}
{{ halt("workflow.fixes must be apply or handoff, not " ~ workflow.fixes) }}
{% endif %}
# Code Review Workflow
Several independent reviewers hunt for defects in a code change; this workflow sends them the change, then verifies, triages, presents, and fixes what they found.
Subagents are an important part of this workflow. Use them wherever a step calls for them, if you can. If you need the user's permission to run them, ask once now for the whole run.
## Conventions
- When a step directs you to another file, read it fully and follow it. Load one step at a time, when it is reached.
- A step that shows a menu or checkpoint halts there and waits for the user.
{% if workflow.fixes == "handoff" %}
## No-code review
Review by inspection only. Do not modify code, run builds or tests, or invoke quality gates.
{% endif %}
## On activation
Run each of these in order before step 1 (`_None._` means skip):
{{ workflow.activation_steps_prepend }}
Hold every entry below as fact for the whole run. Entries prefixed `file:` are paths or globs under `{project-root}` to read; the rest are facts verbatim (`_None._` means none):
{{ workflow.persistent_facts }}
Then run each of these in order (`_None._` means skip):
{{ workflow.activation_steps_append }}
## First step
Read fully and follow `{{ rendered("step-01-gather-context.md") }}`.