For BMAD users who have run bmad-sprint-planning and have a sprint-status.yaml full of ready-for-dev stories. This is what the tool actually does and the problem each capability addresses.
See README.md for the narrative overview and setup-guide.md for installation.
| Capability | What it does | Problem it addresses |
|---|---|---|
| Deterministic control loop | Story selection, retries, gates, completion checks run in plain Python | LLM-as-orchestrator is nondeterministic, hard to debug, and costs tokens for control flow |
| Dual planning pipelines | Same loop from either sprint-status.yaml (sprint mode, default) or a typed stories.yaml dispatched by folder+id (stories mode, opt-in) |
Sprint boards need bmad-sprint-planning; a bmad-spec Story Breakdown has no board |
| Per-story human checkpoints | Stories-mode spec_checkpoint pauses to review the plan before code; done_checkpoint pauses after the commit; both independent, both surfaced in the TUI |
Coarse run-global gates can't ask for a plan review on this story only |
| Trust-nothing verification | Checks on-disk artifacts (spec status, baseline-commit match, non-empty diff, sprint sync) + runs your test/lint commands before commit | Agents claim success without working code; broken builds slip through |
| Fresh-context adversarial review | Dev and review are separate sessions; review uses 4 parallel layers (Blind Hunter / Edge Case Hunter / Verification Gap / Intent Alignment) | Self-review anchoring bias; implementer marks own work correct |
| Hook-based transport | Coding-agent hooks write structured event files; skills write result.json |
Brittle terminal pane-scraping |
| Resumable state machine | Every run is on-disk state, resumable after gate/escalation/crash | Long unattended runs lost to interruptions |
| Plateau-defer | Stuck stories are skipped, stashed, and the run continues | One unconvergeable story blocking a whole sprint |
Human-gated stories (awaiting-operator) |
A story owing external actions only a human can take commits its agent-doable work, records what is owed, and parks; bmad-loop confirm completes it later |
Work needing a domain purchase or a DNS record had no honest terminal state — done hid it behind a green board, blocked halted the run |
| Typed escalations + resolve workflow | CRITICAL pauses + notifies; interactive resolve agent re-arms the story | Ambiguous specs silently producing wrong code |
| Deferred-work sweeps | Triages an append-only ledger against real code, bundles + executes | Split-off goals and review findings get lost |
| Multi-CLI adapter + profiles | Generic driver runs claude/codex/gemini/copilot/antigravity/opencode; per-stage overrides; TOML profiles; transport + process-lifecycle + hook-interpreter behind a pluggable OS-seam registry (tmux + experimental native-Windows psmux bundled; external backends via entry points) | Vendor lock-in; no way to mix models per stage; future non-tmux/Windows transport |
| Cost-weighted token budgets | Mid-session per-session guard (warn/enforce with a wrap-up grace, sampled every ~30s) plus the advisory per-story cap; both count cache reads at ~0.1x; every display leads with the weighted total and names both units |
A runaway-but-busy session was bounded only by the wall clock; naive token caps misjudge real cost (cache reads dominate) |
| Non-invasive skill forks | Drives its own bmad-loop-* skill forks; reads sprint-status.yaml only |
Modifying a user's standard BMAD install |
| Read-only TUI + launcher | Live dashboard over run-dir artifacts; launches detached runs | No visibility into what an unattended run is doing |
| Git worktree isolation (opt-in) | Each unit runs in its own worktree/branch (seeded with the adapters' gitignored MCP/CLI configs), merging back into the target locally; failed units kept for inspection | A long unattended run mutating the working tree you're actively using |
- Automated per-story pipeline:
dev → verify → review → verify → commit, end-to-end, no human in the loop. - Deterministic control flow in plain Python — story selection, retry budgets, gate checks, and completion checks are code, not an LLM session.
- Reads
sprint-status.yamlas the single source of truth (owned by BMAD skills; orchestrator only reads it); selects the nextready-for-devstory; advances by epic/story. - Scoping flags:
--epic N,--story KEY,--max-stories N,--dry-run(prints the plan, spawns nothing).
- Drives the upstream
bmad-dev-autoskill (unmodified) in a fresh tmux session: it plans a 1.5–4k-token spec, auto-approves it, implements, and self-finalizes the spec; the orchestrator syncssprint-statusand synthesizesresult.jsonfrom the spec the skill leaves on disk. - Deterministic missing-marker catch + repair (#276): the review HALT intermittently finalizes a spec's frontmatter to a terminal
status:without appending the## Auto Run Resultsection the harvest scan keys on, which once livelocked a finished story to a DEFER-drop (#224). Four harness-side mechanisms make catch + fix deterministic without ever mutating the launch frontmatter (load-bearing skill routing): a launch-state content-hash snapshot the fallback refuses to synthesize from when the candidate is byte-identical (adonespec merely re-opened for review — in every mode, killing the dead-window false positive); mid-session status-transition observation (a heartbeat-tick sighting of the spec moving off its launch status proves a later terminal frontmatter is this session's write, so synthesis needs one sighting, not two); artifact repair that appends the owed marker onto the spec on synthesis so it re-enters the normal scan; and — gated bylimits.dev_contract_nudge(default on) — one targeted contract nudge per session asking the skill to append the section itself. Frontmatter synthesis stays the backstop for a session that never complies. - Spec-only contract between stages — review consumes the frozen spec, not the dev session's context.
- After each session, checks on-disk artifacts before proceeding: spec frontmatter status, independent baseline-commit match (an LLM-lie detector), non-empty diff, sprint-status sync.
- Runs your commands (
[verify].commands, e.g.pytest -q,ruff check .) — a broken build never reaches review or commit.
- The follow-up review is a re-invocation of
bmad-dev-autoon thedonespec — a fresh-context session with no anchoring bias from the implementer (BMAD-METHOD#2508 routes adonespec to a fresh step-04 review pass), so there is no separate review skill. - Parallel adversarial layers resolved from the skill's
customize.toml(defaults: Adversarial-General, Edge-Case-Hunter, Verification-Gap — the third added by BMAD-METHOD#2550 — and the inline Intent Alignment Auditor added by #2560) → verify findings against code → triage → auto-apply patches → log → defer ambiguity → commit. The first three layers each invoke an upstreambmad-review-*skill, so all three are bmm prerequisites thebmad-loop validatepreflight checks for; the intent-alignment layer is an inline prompt and needs no extra skill. - Bounded review loop (
limits.max_review_cycles, default 3 cycles); done when the pass finishesdoneand no longer recommends a follow-up. A second guard,limits.max_followup_reviews(default 1), damps the structurally non-convergent case: a finalized pass that keeps recommending its own follow-up is honored only this many times, after which the round converges (verify + commit) and the lingering recommendation is re-filed to the deferred-work ledger instead of burning cycles to the hard cap.0never honors a pass's own recommendation. (Upstream BMAD-METHOD#2580 has since made the flag convergent by construction — a severity-weighted score over the pass's patched findings rather than a judgment — so the damping guard is now belt-and-suspenders; it stays as the orchestrator-side bound, which #2580 explicitly leaves to the driver.) - Optional (
[review].enabled, defaulttrue): setfalseto skip the follow-up review session. The dev pass's own inline review (same layers, in-context) is then the only review and it finalizes the story todone— one session per story instead of two. Verify commands still gate the commit. Applies to story runs and deferred-work sweeps alike. - Trigger (
[review].trigger, defaultrecommended): when review is enabled, decides when the follow-up pass runs.recommendedruns it only whenbmad-dev-autosetsfollowup_review_recommendedon adonespec (it self-reviews inline and computes the flag from a severity-weighted score over the final pass's patched findings — flag introduced by BMAD-METHOD#2505, scoring by #2580).alwaysruns it on every story (pre-0.7.0 behavior).
- Bounded dev retries (default 2): verify-failures keep the tree and feed the failing output to the next session via
--feedback; other failures roll back to baseline. - An auto-rollback parks the attempt before it resets — commits above baseline on an
attempt-preserve/*branch, the uncommitted tree (tracked edits + run-created untracked files) on arefs/attempt-preserve-dirty/*snapshot — and refuses the reset if it could not (#340): the run pauses with rescue instructions naming the tree, rather than discarding work the safety net failed to capture. A resolved re-drive is pause-free by contract, so there it journals and proceeds.scm.preserve_keep(default 20) bounds retention of both ref families. - Plateau-defer: when review won't converge, the story is skipped, the spec stashed into the run dir, deferred-work preserved, the run continues. The defer notification names where the attempt survives — in place, the recovery ref the rollback parked it on plus the
git merge --ff-onlyline that restores it, flagged as commits-only when the uncommitted snapshot could not be captured (only reachable on a re-drive since #340 — a plain rollback refuses the reset instead); isolated, the kept-failed unit branch plus any earlier rolled-back attempt's ref (named, not offered as a merge — it is not fast-forwardable there). That recovery ref is projected aspreserve_refinstatusand--json; the unit branch never is (#333). When the recovery itself pauses the run (rollback OFF, or a preserve failure refusing the reset), the defer record still lands before the pause — its notice then points at the ACTION REQUIRED manual-recovery notice instead of naming a ref (#342). - Stories owing human-only external actions park at
awaiting-operatorinstead of lying (#335). A story whose acceptance criteria include something no agent can do — buy a domain, publish a DNS record, grant an API key — finishes and commits everything an agent can, records what is owed in its spec'soperator_actions:frontmatter, and parks. The board advances toawaiting-operator(a forward move; nothing regresses), the run continues to the next story, and nothing is rolled back — a park is a success that commits, so there is no stash and no recovery ref. A park clears every deterministic gate adonestory clears (the spec/board pair, the project's verify commands, a non-empty action list) and skips only the review loop, which has nothing in the diff to converge on; a park declaring nothing readable is refused and repaired rather than committed. Parking is notify-only — non-blocking by design, so unlike an escalation it never halts the run.[operator] enabled = falserestores the old two-outcome behavior, where such a story could only bedone(a green board hiding outstanding work) orblocked(halting the whole run). - Completing a park:
bmad-loop confirm <story-key>walks you through the outstanding actions one at a time, then writes the spec's## Operator Confirmationaudit section, advances the spec and board todone, and commits the pair together with the park record's deletion. Nothing is re-driven — the agent-doable work was committed at park time, and re-running a session would redo finished work while the actions stayed outside the repo;--reverifyre-runs the project's[verify]commands first (for the case that matters: the external action may have changed what the tests see) and a failure blocks the confirmation. Each park is recorded in a committed per-story file under.bmad-loop/operator/, written inside the story's commit window so it rides the park's own commit — through the worktree merge-back, under everyscm.merge_strategy— to every clone the commit reaches: a teammate, a fresh clone, or CI can confirm a story parked elsewhere. (The pre-#356 machine-local index.bmad-loop/operator-actions.jsonis still read and pruned on confirm, never written, so an in-flight park from an older version stays confirmable where it was written.) The committed truth is the spec and the board; the record is howconfirmfinds them.validatewarns on drift in every direction —operator.registry-stale,operator.actions-malformed, andoperator.park-record-missingfor a board parked with no record, which is no longer the fresh-clone norm but evidence of a failed record write, a pre-upgrade park, a checkout that lacks the branch carrying the park commit (pull it), or a deleted record — andconfirmrefuses a drifted record rather than flipping a board on the record's word alone. - A confirmation is resumable. Every write is checked — the spec is read back from disk rather than trusted, so a story is never declared done over a write that did not land — and the park record is dropped last, so a failure part-way leaves the story findable. Interrupted between the spec writes and the board write, what survives is a signed-off spec at
donewith the entry still pointing at it; re-runningconfirmfinishes that rather than refusing it as stale, advancing the board, dropping the entry and committing, with no second prompt and no second audit section (the section on disk is the acknowledgment, and it is fence-aware, so an example quoted inside a code block never counts as one). It resumes equally from a board a human already fixed by hand, since that is what the failure message asks for.--list,--json(resumable,confirmation_recorded) andvalidate(operator.confirm-interrupted) all name the state rather than calling it stale — the remedy inverts, so the check id does too. - Typed escalations:
CRITICALpauses the run + notifies (desktop +ATTENTIONfile);PREFERENCEis journaled and continues. - Environment faults pause without burning budget (#194): a session whose coding CLI never reached the API — a deterministic verify command whose environment is broken (
shreportsrc 126/127;cmdhas no such convention, so on Windows a missing tool is caught by itsis not recognizedmessage or by resolving the command's leading token, and a command naming a filecmdcannot execute — a.sh, anything outsidePATHEXT— is a fault rather than the silentrc 0pass it used to be, #302), or a dev/review/fix/workflow/sweep session whose pane log matches the profile'senv_fault_patterns(anAPI Error … Connection refused-class transport failure that idled out the session clock) — pauses the run with the matched evidence instead of charging the attempt/cycle and deferring the story as if its code were broken. Re-arm (or a sweep's escalation-resume) restores the budget, so a resume after the outage clears re-drives from a clean slate. Patterns are per-profile (seeded only forclaude; extend/disable via a project profile overlay). - CRITICAL resolution:
bmad-loop resolve <run-id>opens an interactive resolve agent seeded with the escalation + frozen spec; you disambiguate, it re-arms the story (escalated → pending, spec reset toready-for-dev) and resumes.--no-interactiveskips to re-arm if you fixed the spec yourself. - Intent-gap patch-restore (BMAD-METHOD#2564): when review halts on an
intent gap,bmad-dev-autosaves the attempted change as a patch file (in the implementation-artifacts folder, referenced from the halt output) before reverting the tree. If the attempted reading turns out to be correct, the resolve agent adds"restore_patch": "<path>"to itsresolution.json; the orchestrator then re-arms the spec toin-review(notready-for-dev) and re-applies the patch onto the baseline after every reset, so the re-driven session resumes review on the restored diff instead of re-implementing from scratch. A hand-drivenbmad-loop resolve --no-interactive --restore-patch <path>does the same. A patch that fails to apply escalates rather than dispatching a session onto a half-restored tree. Deferred-work sweep bundles (below) get the same recovery — an escalated bundle re-arms toin-reviewand the re-driven bundle session resumes review on the re-applied patch.
- Off by default (
[scm] isolation = "none"— work in place on the checked-out branch, byte-for-byte the prior behavior). Setisolation = "worktree"and each story (and each sweep bundle) runs in its owngit worktreeon abmad-loop/<run_id>[/<story>]branch cut from the target branch, then merges back locally — the main checkout stays free while a run is in flight. - Merge knobs:
merge_strategy(ff/merge/squash),target_branch(default = branch checked out at run start; created if missing — a detached HEAD or unborn repo pauses the run instead of merging onto an unreferenced commit),branch_per(storyor a sharedrunbranch;runforcesdelete_branch = false), anddelete_branch. - Failed-unit forensics: a deferred/escalated unit's worktree + branch stay mounted (
keep_failed, default on) and its full diff is preserved torun_dir/failed/<unit>/changes.patch;failed_diff_max_mbcaps per-file untracked-file size (oversized skipped with a marker),failed_diff_unlimitedlifts the cap. - Config seeding: a worktree checks out tracked files only, so a project's gitignored MCP/CLI configs (
.mcp.json,.claude/settings.json,.codex/config.toml,.gemini/settings.json) would be missing — an isolated session couldn't reach its MCP server. Withseed_adapter_defaults(default on) each loaded adapter's ownseed_filesare copied in from the main repo before the session launches;worktree_seedadds extra paths. Copy-when-absent at file granularity — a directory entry whose destination already exists (a worktree checkout carries its tracked children) still seeds the children that are missing — seeded before the hook-merge (a seededsettings.jsonkeeps its content and just gains the Stop hook), and shielded from the unit'sgit add -A. - Run state never moves into a worktree —
.bmad-loop/always lives in the main repo; spec paths are persisted relative to the worktree so a kept-failed run stays portable. - Merge-back is serialized;
max_parallelis a validated knob clamped to1until parallel fan-out is built. Therepo_rootkey in_bmad/bmm/config.yaml(defaults to the project dir) decouples where git/code work happens from where run state lives (monorepos). commit_message_template({story_key}/{run_id}substituted) customizes story/bundle commit messages.
- A first-class plugin system extends the orchestrator without touching the core loop. A plugin is a folder-drop
plugin.tomlmanifest (under.bmad-loop/plugins/<name>/, overlaying bundledbmad_loop/data/plugins/<name>/) that can: observe / veto / mutate the run at every lifecycle stage via a hook bus; contribute settings that render in the settings TUI and persist to[plugins.<name>]; and inject its own workflow sessions atpost_dev_phase/post_review_result. - Two trust tiers: a data-only / declarative plugin (settings +
[hooks.<stage>]shell commands) runs on discovery; a plugin that ships an in-process[python]module is never imported unless listed in[plugins] enabled— dropping a folder in never runs code. Every hook (subprocess or Python) is failure-isolated: a raise is caught, journalled, and disables that instance for the run — never crashes it. - Veto maps onto the engine's existing control flow (
skip/defer/pause), and mutation is confined to a per-stage whitelist (proposed_prompt,proposed_commit_message, …) plus a persistedshareddict — no new abort path. Distribution is folder-drop now, with a documentedbmad_loop.pluginsentry-point seam for pip-installed plugins later. - See Writing a bmad-loop plugin for the manifest, settings, hook, stage, trust, and workflow reference, plus a worked walkthrough; a complete example ships under
examples/plugins/guardrails/.
- A niche engine layer — built on the plugin system — for projects whose dev/sweep cycle drives a live engine Editor via an Editor MCP (Unity bundled as
bmad_loop/data/plugins/unity/; Godot/Unreal later). Off by default; enable with[plugins] enabled = ["unity"]+ a[plugins.unity]table. (The legacy[engine]policy block still loads, folded onto[plugins.unity]with a deprecation warning; project-local overrides now live under.bmad-loop/plugins/<name>/.) editor_modeis coupled to[scm] isolationbecause a live Editor MCP can only act on the folder its Editor has open:shared(requiresisolation = "none") runs the agent in place on the project the operator's warm Editor already has open — zero relaunches, full live MCP;per_worktree(requiresisolation = "worktree") gives each worktree its own managed Editor.- Readiness gate: before each unit, the plugin's
pre_ready_gatehook blocks until the Editor + MCP report ready (Unity:wait-for-readyfor IvanMurzak, connectivity check for CoplayDev); on timeout the unit is deferred with anATTENTIONnotice rather than starting a session against a half-open Editor. per_worktreelifecycle (Unity/IvanMurzak): a setup hook launches the worktree's own Editor (MCP port auto-derived from the worktree path, so it self-isolates from the operator's main Editor), writes the worktree.mcp.json, and primes the worktree'sLibrarywith a reflink/CoW copy of the warm mainLibrary(so Unity reimports incrementally instead of a cold full reimport that crashes the import workers; deep-copy then symlinked-empty-cache fallbacks off-CoW); the readiness gate then waits for it; a teardown hook quits the Editor on completion and on pause/escalation. The MCP-generated skill tree (gitignored) is copied into each worktree via the plugin'sseed_globs; a setup failure defers the unit instead of running it against no Editor.- The Unity plugin's settings are editable in the TUI under its plugin section. To target another engine or a different Editor MCP, see Writing a Game Engine plugin (now built on the general plugin system) and Writing a plugin for a specific Editor MCP.
- Every run is a resumable on-disk state machine:
bmad-loop resume <run-id>continues from a gate, escalation, or interruption. - A graceful stop (
stop --graceful/ TUIS) is resumable too: unlike a hard stop killed mid-item, it lets the in-flight item finish through commit and finalizes cleanly, ending as astoppedrun thatresumepicks up at the next item. - All run state in
.bmad-loop/runs/<run-id>/(gitignored):state.json,journal.jsonl(every decision — including thesession-synthesized-from-frontmattercatch and itsspec-marker-repairedmarker repair, #276),events/(hook signals),tasks/<id>/(per-session prompt +result.json+ diagnostic breadcrumbs —session-lifecycle.jsonlrecords timeout-fire, budget-guard trips/terminations (budget-tripped/over-budget-fired), transport-failure classification (env-fault-classified, #194), and the #276 missing-marker forensics (spec-status-transition-observed,frontmatter-unmodified-refused,contract-nudge-sent),heartbeat.jsonis the wait loop's proof-of-life,resultless-stops.jsonlrecords give-up Stops with a verdict —no-artifact,ambiguous-frontmatter,unmodified-since-launch(bytes unchanged since launch, #276), orterminal-frontmatter-pending),logs/,deferred/,resolve/,ATTENTION. journal.jsonlrecordssession-endfor every session unconditionally — even a teardown that throws still lands one (statusabortedwhen the outcome is unknowable). A timed-out session's entry carriesfired_at(wall time the deadline was declared),teardown_s(wall seconds from that fire to this entry — the teardown gap), andexpired_clock(monotonic/wall/both—wallalone fingerprints a host suspend that froze the monotonic clock). Every entry whose usage was read carriestokens(raw) andtokens_weighted(cache reads atlimits.cache_read_weight), keeping per-session spend reconstructible; both arenullwhen the usage read failed, and both are absent on anabortedend.tokens_weightedis the end-of-session total — distinct from a tripped session'sbudget_weighted, the guard's mid-session sample at trip time.
- Coding-agent hooks (
Stop/SessionStart/SessionEnd/PreCompact) write structured event files the orchestrator watches; skills write a machine-readableresult.json.
- Skills accumulate an append-only ledger (
deferred-work.md,DW-<n>entries): split-off goals, pre-existing findings, "needs human decision" items. - Story-declared closure (
closes_deferred: [DW-5, DW-6], human-authored on astories.yamlentry or in a story spec's frontmatter — the two are unioned): when the story commits, the orchestrator flips each declared entry tostatus: done <date>+resolution: resolved by story <id>— the same annotation a sweep bundle writes — so the ledger stops being one-way (filed automatically, marked resolved by hand). Written at the commit boundary, after verification/review/checkpoints and just before the squash, so an in-repo ledger carries the annotation in the story's own commit and a story that fails, is rejected by review, or escalates closes nothing. Declared, never inferred from a diff; re-read at the commit so a declaration edited after implementation still counts; idempotent across a resume; an unknown id, an unreadable entry status, or a non-list declaration in a story spec is journaled, never fatal (a non-list declaration instories.yamlis a manifest schema error like any other, caught before the run).bmad-loop validatewarns about all of those before the run starts, in both queue modes. An artifact dir configured outside the repo is shared across worktrees and cannot be committed — the annotation is written all the same and journaled (deferred-close-external-ledger). bmad-loop sweeptriages every open entry against the actual code (ledger statuses treated as unreliable) → partition: already-resolved (auto-closed with evidence) / bundles / blocked / skip / decisions.- Bundles run the full pipeline (dev
--dw-bundle→ review → verify → commit); the review gate checks every bundle entry isstatus: done. - Interactive decision walkthrough (build / close / keep-open per option, with a recommendation); answers written back as
decision:lines. Unattended runs leave decisions open. - Answer skipped/missed decisions out of band with
bmad-loop decisions(ordin the TUI): reconstructed from past triage output, saved to.bmad-loop/decisions.json, and consumed by the next sweep with no re-prompt (build → bundle, close → closed, keep-open → recorded). - Auto-sweep at epic boundaries or run-end (
[sweep] auto); a failed/paused child sweep never interrupts the parent run. - Repeat mode (
--repeat/[sweep] repeat): re-triages after each cycle to absorb newly generated deferred work, stopping when a cycle does nothing addressable or hitsmax_cycles. - Sweeps are their own resumable runs (
bmad-loop resume <id>). An escalated bundle resolves like a story escalation, including intent-gap patch-restore:bmad-loop resolve <id> --restore-patch <path>re-arms the bundle spec toin-reviewand the re-driven bundle session resumes review on the re-applied patch instead of re-implementing.
- Opt-in second story source (
[stories] source = "stories"+spec_folder, orbmad-loop run --spec <folder>): drives the same loop off a typedstories.yaml(abmad-specStory Breakdown, sibling ofSPEC.md) instead ofsprint-status.yaml. - Dispatches each entry by folder + id (
/bmad-dev-auto Spec folder: <folder>. Story id: <id>.); the story spec lands at<folder>/stories/<id>-<slug>.mdand is read back by a deterministic id-keyed glob — no shared board to line-edit, no result-artifact mtime-scan. - Strictly linear schedule (list order, no
depends_on);doneskipped, non-terminal statuses resumed on re-dispatch,blocked/sentinel/ambiguous stops the run for resolve.bmad-loop run --dry-run --spec <folder>andbmad-loop statusprint the schedule/board (id · live disk state · checkpoint markers · title). - Preflight content-probe: stories mode requires a
bmad-dev-autonew enough for folder+id dispatch, or the run aborts with remediation. Sprint mode keeps working with any installed version. - Sentinel recovery: a pre-planning-halt sentinel spec (
<id>-unresolved.md/<id>-ambiguous.md) is auto-deleted with a preserved copy under the run dir on re-arm, matching the contract's delete-to-retry.
- Gate modes (
[gates].mode):none(fully unattended) /per-epic(pause at epic boundaries, default) /per-story-spec-approval(pause after each spec for approval). Note:per-epicis inert in stories mode — the flatstories.yamllist has no epics, so the boundary never fires; use the per-story checkpoints (below) orper-story-spec-approvalthere. - Per-story checkpoints (stories mode): independent
spec_checkpoint(pause before code to review the plan; approve → implement, or request a replan) anddone_checkpoint(pause after the story commits; skipped when it is the last story). Additive togates.mode— a story can pause twice. - Every mid-run pause is surfaced in the TUI: a per-run pause-kind badge, a global attention count, and a
pviewer per stage (plan-checkpoint spec review, story-checkpoint summary card, escalation with story context, gate spec review) — all calling the same CLI code paths. - Retrospective handling (
retrospective = never | notify | auto) and notification on epic boundaries.
- Generic adapter drives any CLI fitting the injection + hook-signal transport; CLI specifics live in declarative TOML profiles. Two independent axes: the CLI (
CodingCLIAdapter+ profile) and the terminal transport (TerminalMultiplexer) — tmux ships bundled (with an experimental native-Windowspsmuxbackend alongside it), and external backends (e.g. the herdr adapter) co-install as packages that self-register (how), behind a pluggable seam that lets a new backend slot in without touching the engine (see the adapter authoring guide). - The OS is abstracted by a registry of seams, each selecting an implementation by platform (with a test-override env var) and extended by a single registration line: the terminal multiplexer (
register_multiplexer, with availability-aware selection: env var → persisted[mux] backendviabmad-loop mux set→ platform default → first available platform match), the process-lifecycleProcessHost(register_process_host—terminate/force_kill/is_alive/identity), and the hook interpreter (ProcessHost.hook_interpreter());bmad-loop validateruns a platform preflight over them. Porting to a new OS is new files + registrations, no core edits — see Porting bmad-loop to a new OS. - Supported, E2E-verified:
claude(reference),codex(≥ 0.139),gemini(≥ 0.46),copilot(GitHub Copilot CLI ≥ 2026-02 — thecopilotbinary, not the VS Code extension;agentStopturn-end,-iinteractive launch,--allow-all-tools; pin a capable model — the free GPT-5 mini default is unreliable for multi-step skills). - Supported, E2E-verified over HTTP/SSE (no tmux window):
opencode(OpenCode ≥ 1.18, profileopencode-http, aliasopencode) — one headlessopencode serveper session, SSEsession.idlecompletion with an HTTP poll fallback, per-session server password, token usage read back over the API. Hookless ([hooks] dialect = "none", no hook registration). With no pane to replay, the run logs split three ways: a curated readable transcript inlogs/<task-id>.log(agent/user prose, tool calls, slash commands, file edits, permission asks/replies, errors), the server's own stdout in<task-id>.server.out, and a structured SSE trace in<task-id>.sse.jsonl. Install the extra (pip install 'bmad-loop[opencode]'), auth once globally (opencode auth login), and setmodelasprovider/model; the Unity plugin's window guards don't apply (there is no window). - Experimental,
isolation = "none"only:antigravity(Google'sagy≥ 1.1.3) —-iinteractive launch,Stopturn-end hook (flat handler in.agents/hooks.json, no SessionStart/SessionEnd),--dangerously-skip-permissionsfor unattended runs;usage_parser = "none"permanently — agy's transcript exposes no usage data (tokens live only in an internal SQLite/protobuf store).agygates each workspace on an exact-pathtrustedWorkspacesentry and blocks on an interactive trust dialog, which--dangerously-skip-permissionsdoes not bypass — so worktree isolation hangs (#169). Verify against youragybuild withprobe-adapter antigravity. - Per-stage CLI/model overrides: run dev on one CLI/model, review on another (
[adapter.dev],[adapter.review],[adapter.triage]). - Add a CLI without touching Python: drop a TOML profile in
.bmad-loop/profiles/<name>.toml(binary, prompt template, bypass flags, hook dialect, native→canonical event map). bmad-loop probe-adaptercollects + sanitizes the data needed to finalize/add a profile (hook payload shape, transcript location/format, token schema): a zero-launch scan by default, opt-in--probefor live capture. See the adapter authoring guide.
- Mid-session per-session token budget (
max_tokens_per_session, default 4M weighted): both adapter wait loops sample cumulative usage on the ~30s heartbeat cadence and trip once on crossing the cap, persession_budget_mode—warn(default) raises an ATTENTION + lifecycle breadcrumb only;enforceadditionally sends a wrap-up nudge, grantssession_budget_grace_s(default 240s) to finish, then terminates the sessionover_budget(ordinary retry→defer routing; an artifact flushed at kill time is still honored). Mid-session sampling is live-verified onclaude; other transcript-reading profiles sample the same cumulative files best-effort (early transcript-path delivery and mid-turn flush behavior are unverified vendor behavior), the wrap-up nudge into a busy pane is best-effort everywhere (the termination is the guarantee, the nudge a courtesy), and adapters with no mid-session usage signal (usage_parser = "none", Copilot's shutdown-only flush) leave the guard inert. - Per-story token budget (
max_tokens_per_story, default 2M weighted, advisory) using the same cost-weighted total — cache reads counted atcache_read_weight(default 0.1, matching ~0.1x vendor billing). The story's cumulative spend is re-checked at every session boundary, so an overrun surfaces while the story is still running and regardless of how it ends — a story that defers or escalates is judged like one that commits. The first crossing raises one ATTENTION + desktop notice (story token budget exceeded: <key>) and atoken-budget-exceededjournal entry carryingweighted,totalandbudget; the warning is latched per story (and persisted, so a resume does not re-notify) and nothing is terminated — advisory means the story runs on. Every operator-facing total leads with that weighted figure and labels both units — the run-finished summary andbmad-loop statusread<weighted> weighted tokens (<raw> raw incl. cache reads), the TUI pairs a weightedtokenscolumn with arawone, andsession-endjournal entries carrytokensbesidetokens_weighted. - Token usage read from each CLI's local session transcript (per-profile
usage_parser), aggregated per story (bmad-loop status).
- Single policy file written by
init, stamped into the run at every engine start —run,sweep,resume— so it always describes the policy that process enforces (applies to new runs and resumes; editable live from the TUI). - Sections:
[gates],[limits],[verify],[notify],[review],[adapter](+ per-stage),[sweep],[scm](worktree isolation + merge-back),[cleanup](run-dir retention + disk reclamation),[plugins](trust allowlist + per-plugin[plugins.<name>]config — e.g. the opt-in game-engine layer via[plugins.unity], off by default),[tui](low_frame_ratefor slow/SSH links; persisted dashboard pane sizes). - Tunable limits:
max_review_cycles,max_dev_attempts,max_followup_reviews,session_timeout_min,git_timeout_s,teardown_grace_s(one shared budget bounding the verified window kill and the follow-on reap of any straggler descendant the session detached — e.g. asetsidbackground writer — combined; whatever remains after the window dies is what the straggler reap gets, before the worktree is merged and removed),stop_without_result_nudges,dev_stall_grace_s,dev_stall_nudges,dev_stall_nudges_cap,workflow_stall_nudges_cap,max_tokens_per_story.
- Read-only observer + launcher (
bmad-loop tui): runs table, expandable sprint tree (epics → stories/retro), severity-colored deferred-work ledger, per-story phase table (phase · agent · dev attempts · review cycles · tokens · commit/defer), a run header naming the live-or-configured active agent, tabs tailing journal / pane log /ATTENTION. - Launch & manage from keys: start run/sweep (
r/s), resume (e), resolve escalation (R), answer missed decisions (d), attach (a), cleanup (c), validate (v), settings editor (g), theme/mode toggle (M), quit (q). - Resizable panes: every boundary is drag-adjustable by mouse (the divider bars double as the Sprint / Deferred Work section headings) or a
ctrl+wkeyboard resize mode; sizes persist per-project to[tui]inpolicy.tomland re-apply on the next launch. - Survives TUI exit/crash: runs launched from the TUI are detached
bmad-loopprocesses in a dedicatedbmad-loop-ctltmux session; the dashboard watches purely via run-dir artifacts, so shell-started runs appear identically. - Comment-preserving policy editor (
g): grouped form, sections collapsed by default with one-line descriptions (ctrl+etoggles all), validated with the engine's own parser, unset keys show defaults as placeholders.
- Each run drives agents in a dedicated
bmad-loop-<run-id>session;attachto watch live. - Auto-teardown on finish (
cleanup_session_on_finish, disable to inspect); a hardstopalways kills it, a gracefulstop --gracefultears it down under the samecleanup_session_on_finishgate a normal finish uses; paused/interrupted runs keep the session forresume. bmad-loop cleanup(orcin the TUI) sweeps leftover sessions/windows for finished/stopped/orphaned runs of the current project; live runs, and anything belonging to another project, are never touched.--jsonemits a stable machine-readable document per the contract below (schema-versioned; the run ids whose sessions were removed, the live ids left alone, the ctl windows closed, and adry_runflag) instead of the text. Plan and outcome share one schema — same fields, same meanings, withdry_runsaying which one you are holding — so a script can pre-flight a sweep and compare it against what actually happened. (Values are each invocation's own sample, not a promise the two agree: a live session can die between the preview and the real run.) The unverifiable-pid warning, which text mode writes to stderr, becomessessions.unverifiable_pidin the document, leaving stderr empty.
bmad-loop cleanreclaims disk (distinct fromcleanup, which is only tmux). It tears down git worktrees a mid-flight stop left mounted — the main accumulation source: each carries a real UnityLibrary/(incl. the MCP-server build), whichgit worktree removecannot reach once the engine was killed before teardown. It then trims the heavyworktrees/tree from runs kept for history (the run still lists in the dashboard — discovery readsstate.json, not the worktree), and archives or deletes runs past the retention window.- Safe by construction: only finished or stopped runs are touched; running, unknown-host, paused and interrupted (resumable) runs are never reclaimed.
--keep <run-id>protects a specific run (e.g. a finished one whose Editor is still live),--dry-runpreviews,--retain N/--hardtune the window and archive-vs-delete. --jsonemits a stable machine-readable document per the contract below (schema-versioned; the effective retention policy,freed_bytesas a raw integer, and the worktree paths and run ids underworktrees/trimmed/archived/deleted/protected) instead of the text. Plan and outcome share one schema — same fields, same meanings, withdry_runsaying which one you are holding — so a script can pre-flight a reclaim and compare it against what actually happened. (Values are each invocation's own sample, not a promise the two agree:freed_bytesis re-measured, and a run that was still in progress at preview time can be reclaimable by the time you commit.) It names every item the text only counts (protected) or renders (~1.2MBis a formatting offreed_bytes), and the unverifiable-pid warning text mode writes to stderr becomesunverifiable_pidin the document, leaving stderr empty.- Prevention is automatic: every
run/sweepstart reconciles worktrees leaked by a prior finished run ([cleanup] auto_clean_on_finish), and the Unity plugin'spost_runhook removes the IvanMurzak MCP server's downloaded/tmp/<company>/<product>/*.zipand truncates its unbounded editor log ([cleanup] clean_tmp). For recurring housekeeping of stopped runs, schedulebmad-loop clean.
bmad-loop initinstalls the threebmad-loop-*skills (bmad-loop-setup,bmad-loop-resolve,bmad-loop-sweep, into.claude/skills/and/or.agents/skills/), the hook relay,.bmad-loop/policy.toml, and a gitignore covering the runs dir, plugin caches, and policy.toml itself (per-machine config). Flags:--cli(repeatable),--no-skills,--force-skills.bmad-loop validatepreflights every prerequisite: BMAD config, sprint-status, git, the selected terminal-multiplexer backend (listing all detected when more than one is registered), CLI binary, hook registration, and the review skills the installedbmad-dev-autoactually invokes — derived from itscustomize.tomlreview layers (or fromstep-04-review.mdon releases that name reviewers inline), so both the mergedbmad-reviewtopology and the standalone-hunter one validate, and configured layers naming an uninstalled skill are caught — plus itscustomize.toml.- Non-invasive: drives the upstream
bmad-dev-autoskill unmodified — there is no fork to keep in sync — and review is just a re-invocation of it on thedonespec. Your standard BMAD install is never modified.
bmad-loop init— install skills, hooks, policy, gitignore.bmad-loop validate— preflight all prerequisites.--jsoninstead emits a stable machine-readable document (schema-versioned; theokverdict, the queuemode/spec_folder, per-severitycounts, and every check as a flat emission-ordered finding with a stablecheckid,severity, humanmessageand structureddetail) per the contract below; a failing check still emits the whole document, at exit 1 — the nonzero code is the verdict, not a failure to produce one.bmad-loop mux— list registered terminal-multiplexer backends (platform · availability · version · which is selected and why);mux set <name>persists a machine-scoped choice into policy.toml (--clearreverts to auto,--forceallows a name only registered on the target machine). Bundled backend:tmux; external backends (e.g. the herdr adapter) register via thebmad_loop.mux_backendsentry-point group — see Terminal multiplexer backends.bmad-loop run— drive the dev → review → verify → commit loop.bmad-loop sweep— triage + execute open deferred-work entries.bmad-loop resume <run-id>— continue a paused/interrupted run.bmad-loop resolve <run-id>— resolve a CRITICAL escalation, then re-arm + resume (--story,--no-interactive,--restore-patch <path>for intent-gap patch-restore,--resume/--no-resume).bmad-loop decisions— answer deferred-work decisions past sweeps left unanswered (--listto just show them).--jsoninstead emits a stable machine-readable document (schema-versioned; per decision the id, question, context, recommendation and every option's key/label/effect/intent/resolution/bundle-name plus a derivedrecommendedflag) per the contract below; it implies the listing and never prompts, and nothing pending yields a valid empty document.bmad-loop confirm <story-key>— complete a story parked atawaiting-operatoronce you have carried out the external actions it owes: acknowledges each action in turn (--yesskips the prompts), writes the spec's## Operator Confirmationaudit section, advances spec and board todone, and commits the pair.--listshows every parked story and what each owes without confirming any (marking one whose confirmation was interrupted as already signed off, not as unconfirmable);--reverifyre-runs the project's[verify]commands first and blocks the confirmation if they fail. Re-running it on an interrupted confirmation finishes that confirmation instead of refusing it.--jsoninstead emits a stable machine-readable document (schema-versioned; per parked story the key, actions, spec file, spec/board status, the parking run and the commit that carries the park — derived from the record's own git history, empty when the record is not yet in any commit — plus the derivedconfirmableandresumableflags, theconfirmation_recordedreading behind the latter, and a humandriftreason) per the contract below; it implies the listing and never prompts, and nothing parked yields a valid empty document.bmad-loop list(ls) — list every run/sweep with its short ref, type, and status.--jsoninstead emits a stable machine-readable document (schema-versioned; one entry per run, oldest first: short ref, run id, type, started-at, liveness-aware status, paused stage) per the contract below; an empty runs dir yields a valid empty document.bmad-loop status [<run-id>]— run + sprint summary with per-story token totals, cost-weighted with the raw count alongside.--jsoninstead emits a stable machine-readable document (schema-versioned; run state, snapshotcache_read_weight, per-story phase/attempt/review-cycle/tokens/commit/defer-reason, plus the additively-added run-leveladapters— the dev/review/triage adapter the policy snapshot resolves to,nullon a run predating adapter stamping — and per-storyadapters_used, the adapter identity actually recorded per role) per the contract below — the supported surface for scripts; the text output is best-effort.bmad-loop diagnose [<run-id>](diag) — emit a sanitized diagnostic dump of a run/sweep to hand maintainers (histograms, counts, env, file sizes — no code/spec/prompts/paths/PII); a stray pseudonymized identifier is auto-substituted with its alias (disclosed in the report), while PII/secret hits still refuse to emit; defaults to the latest run (--all,--out,--max-journal-entries).--jsonemits the same dump as a stable machine-readable document per the contract below instead of the markdown report.bmad-loop attach [<run-id>]— tmux-attach to a run's live agent session.bmad-loop stop <run-id>— stop a live run. The default is a hard stop: SIGTERM the engine mid-item and kill its agent session.--gracefulinstead requests a graceful stop — the engine finishes the in-flight item (a story through commit, a sweep bundle through commit, or an in-progress sweep triage — after which no bundles start), then finalizes cleanly and stops as a resumablestoppedrun, suppressing any pending auto-sweeps;--cancel-gracefulwithdraws a pending request. Delivery is astop-request.jsoncontrol file consumed at the next item boundary (no signal, so it works on every platform and multiplexer backend); a hard stop always supersedes a pending graceful one. The TUI surfaces the same pair:xhard-stops,Srequests a graceful stop.bmad-loop delete <run-id>— delete a run directory (--forcestops it first if live).bmad-loop archive <run-id>— compress a run into.bmad-loop/archiveand remove it (--forcestops it first if live).bmad-loop cleanup— remove leftover tmux artifacts for finished/stopped runs.--jsonemits the sessions and ctl windows removed (or, with--dry-run, that would be) as a stable machine-readable document per the contract below.bmad-loop clean— reclaim disk from concluded runs per[cleanup]: tear down worktrees a mid-flight stop orphaned, trim heavyworktrees/from runs kept for history, archive/delete past the retention window (--dry-run,--keep,--retain N,--hard).--jsonemits what was reclaimed (or would be) as a stable machine-readable document per the contract below, withfreed_bytesa raw integer.bmad-loop tui— the interactive dashboard (--low-frame-ratefor slow/SSH links).bmad-loop probe-adapter <cli>(collect-adapter-data) — collect + sanitize adapter-finalization data for a CLI profile; default zero-launch scan, opt-in--probelive capture.- Every command takes
--project <dir>(default: current directory). Any<run-id>accepts a partial — the tail after the last-, shortened to any unique prefix.
A command's --json mode emits exactly one JSON object on stdout and nothing else — no trailers, no fenced blocks. Every document carries an inline integer schema_version owned by that command; evolution is additive-only, and anything breaking bumps the version. Errors never produce a partial or error document: the message goes to stderr, stdout stays empty, and the exit code is nonzero — so stdout is always either one complete valid document or empty. An error means the command could not do its job; a command whose job is to report a verdict is a different thing, and exits nonzero to carry the answer while still owing you the document (validate --json is the case). So the rule is positive rather than inferred from the exit status: parse non-empty stdout whatever the exit code, and take the verdict from the document's own field — ok on validate, which unlike rc separates "the checks failed" from "the command broke". The contract is codified in src/bmad_loop/machine.py and holds for every command that takes the flag — currently status, list, decisions, confirm, validate, clean, cleanup, diagnose and probe-adapter — with no exception (#195). On diagnose and probe-adapter, whose default output is a human-readable report, --json replaces that report rather than appending to it; with --out FILE the document goes to the file, stdout stays empty, and the confirmation goes to stderr.