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; your BMAD install is never modified, and sprint-status.yaml is the one board it writes while a run is in flight — the sessions it dispatches never do |
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.
- Owns
sprint-status.yaml, the single source of truth:bmad-sprint-planninggenerates it, and while a run is in flight the orchestrator is its sole writer (sprintstatus.advance— idempotent, never-regress) while the dev and review sessions it dispatches are told never to write or revert it; your own BMAD skill runs still edit the board outside a run. 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 dev primitive (unmodified) in a fresh tmux session —
bmad-build-auto, orbmad-dev-autoon pre-rename releases, resolved from disk per skill tree and invoked under the name it resolves to, so either era runs with no config edit (the legacy name is accepted only when marker-complete, which refuses the forwarding shim the rename leaves behind): 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 into a DEFER-drop (#224). Four mechanisms catch and fix that without ever mutating the launch frontmatter (load-bearing skill routing): a launch-state content hash — a candidate still byte-identical to the spec as it stood at launch is provably untouched by this session, so synthesis is refused — mid-session observation of the spec leaving its launch status (which outranks the hash), artifact repair that appends the owed marker, and one contract nudge per session (limits.dev_contract_nudge, default on). - 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, and the run continues. The defer notification names where the attempt survives — in place, the recovery ref plus the
git merge --ff-onlyline that restores it (flagged commits-only when the uncommitted snapshot could not be captured); isolated, the kept-failed unit branch plus any earlier attempt's ref, named rather than offered as a merge. That ref is projected aspreserve_refinstatus/--json; the unit branch never is (#333). When the recovery itself pauses the run, the defer record still lands first, pointing at the manual-recovery notice instead of a ref (#342). - Stories owing human-only external actions park at
awaiting-operatorinstead of lying (#335). A story owing something no agent can do (buy a domain, publish a DNS record, grant an API key) commits everything an agent can, records what is owed in its spec'soperator_actions:frontmatter, and parks. The board moves forward, the run continues, and nothing is rolled back — a park is a success that commits, so there is no stash and no recovery ref. It clears every deterministic gate adonestory clears (spec/board pair, your verify commands, a non-empty action list) and skips only the review loop. Parking is notify-only and never halts the run;[operator] enabled = falserestores the old two-outcome behavior, where such a story could only bedoneorblocked. - Completing a park:
bmad-loop confirm <story-key>walks the outstanding actions one at a time, writes the spec's## Operator Confirmationaudit section, advances 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;--reverifyre-runs your[verify]commands first and a failure blocks the confirmation. Each park is 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 merge-back to every clone — a teammate, a fresh clone or CI can confirm a story parked elsewhere (#356).validatewarns on drift in every direction (operator.registry-stale,operator.actions-malformed,operator.park-record-missing), andconfirmrefuses a drifted record. A park written before #356 lives in the machine-local.bmad-loop/operator-actions.json, whichconfirmstill reads and prunes but nothing writes anymore — so an in-flight park from an older version stays confirmable on the machine that wrote it. - A confirmation is resumable. Every write is checked — the spec is read back from disk, 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, with no second prompt and no second audit section (the section on disk is the acknowledgment, and the check is fence-aware). It resumes equally from a board a human fixed by hand, which is what the failure message asks for — advancing an already-doneboard is idempotent.--list,--json(resumable,confirmation_recorded) andvalidate(operator.confirm-interrupted) name that state rather than calling it stale. - Dispatched sessions are told the sprint board is orchestrator-owned (#437) — the sibling of the park contract above, injected into the prompt the same way. The board advances as soon as dev verifies, but the story's single commit lands only after the review loop, so a session dispatched in between opens on an uncommitted, unattributed change to
sprint-status.yamlwith nothing in the repo naming its author (one read it as a spec violation, reverted it, and tripped the sign-off-regression gate on a story both sessions agreed was finished). Story dev prompts and the review prompts of sprint and sweep runs carry the same prohibition: never write the board, never revert it, and a row atdoneorawaiting-operatoris the orchestrator's own bookkeeping — not a defect to fix, and not proof that the work is verified, deliberately, since the row is written before the deterministic dev verification runs and a repair session opens on a red tree under adonerow. Only the review prompt adds where to go instead: a story that cannot be finished without a human decision is finalized tostatus: blockedwith a reason — the one hand-back that both withholds the commit and reaches a human, where any other non-terminal status just burns the review budget onto a defer that rolls the work back. A dev prompt gets no such invitation, becauseblockedhalts the whole run — the exact failure park exists to avoid — and a dev session that cannot finish already has park. A deferred-work bundle's dev prompt carries nothing (a bundle has no board row) while a bundle's review prompt does, since a sweep runs inside a project whose board exists and is just as revertible; every injected plugin-workflow session carries the prohibition too —post_dev_phase,post_review_resultandpre_commit_gateall fire inside that same window — as its own## Sprint boardsection appended after the session-gate hooks, so a plugin prompt rewrite cannot strip it, and without theblockedredirect for the same reason a dev prompt has none; stories mode carries none of it, having no board at all. - 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 verify command whose environment is broken (
shreports rc126/127; 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, or any extension outsidePATHEXT, which cmd hands to the file association and which exits0without running anything — is a fault rather than a silent rc0pass, #302) or a session whose log matches the profile'senv_fault_patterns(anAPI Error … Connection refused-class transport failure, or a provider quota/usage-limit refusal, that idled out the session clock) — pauses the run with the matched evidence instead of charging the attempt and deferring the story as if its code were broken. Re-arm restores the budget. Patterns are per-profile:claudeseeds the connection signature,opencodea provider quota/rate-limit and connection pair (#323), and the other four profiles ship none. Each adapter matches them against the log named by itsENV_FAULT_LOG_SUFFIX— the tmux pane capturelogs/<task-id>.log, or<task-id>.server.out(theopencode serveprocess's own stdout) foropencode-http, never that adapter's model-written transcript. A pattern is only sound against a log the model cannot write to, which is why the pane-capture profiles anchor an error token plus a cause on the same line and seed no bare quota patterns. Extend or disable them in a project profile overlay. - A session the multiplexer lost says so (#489). Sessions complete on a hook
Stopor on window death, and a window is gone whether the CLI exited or something destroyed the whole mux session out from under the run — an external reaper, a concurrent prune orbmad-loop stop, an operatorkill-session, a server crash, the host sleeping. Both arecrashed, so the retry/defer reason an operator reads said onlydev session crashed— pointing at the agent when the host was at fault. The crash verdict now asks whether the session still exists and, when it does not, says so in the reason (… session crashed: the multiplexer no longer reports the session, so the window's disappearance is not evidence the CLI exited), assession_vanishedondev-decisionandfix-decisioneither way, beside the routing each fed, on every role'ssession-endjournal entry when it is true (the conventionenv_faultalready uses there), and as asession-vanishedbreadcrumb insession-lifecycle.jsonl. The repair path carries it the same way: when fix attempts are exhausted the defer names the lost session instead of blaming the tree for repairs that never ran. The wording states what the evidence withdraws, not what it proves:has_sessionmaps every nonzero backend result to False, so a negative lookup is "the backend did not confirm it" rather than proof the session is gone — enough to stop an operator reading window death as a CLI exit, not enough to name a destroyer. It composes with an environment-fault pause instead of being swallowed by it. A session reaped after flushing its result still scorescompletedand is not diagnosed — it produced something. Diagnosis only — the routing is unchanged, and a retry re-creates the session. - 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, the dev primitive saves the attempted change as a patch file (referenced from the halt output) before reverting the tree. If that reading turns out to be correct, the resolve agent adds"restore_patch": "<path>"to itsresolution.json; the orchestrator re-arms the spec toin-review(notready-for-dev) and re-applies the patch after every reset, so the re-driven session resumes review on the restored diff instead of re-implementing.bmad-loop resolve --no-interactive --restore-patch <path>does the same by hand. A patch that fails to apply escalates rather than dispatching onto a half-restored tree. Sweep bundles get the same recovery.
- 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, together with every non-hookless adapter's resolved hookconfig_path— which is gitignored for codex, so without it the project's own hook configuration never reached the worktree and the session ran against a file holding bmad-loop's relay registrations alone (#471). Seeding does not decide whether that relay hook registers — the hook-merge writes it either way.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, but its relay entry is replaced rather than kept — the seeded copy names the main repo's$CLAUDE_PROJECT_DIR-relative relay, which resolves to the worktree, where no relay exists; left in place it would read as already registered and the session would stall silently, #352; a tracked config gets the same rewrite pinnedskip-worktreein the worktree's own index, so the machine-specific command never ridesgit add -Ainto a story commit — while pinned the config is orchestrator-owned, so a story's own edit to it stays session-local), and shielded from the unit'sgit add -A— in a private exclude scoped to that worktree alone (see below), never repo-wide. - The git-add shield is scoped to the worktree and expires with it (#384). Provisioned tool files (skill trees, hook config, seeded configs) go into a private
.git/worktrees/<id>/info/exclude, activated by a worktree-scopedcore.excludesFilethat shadows rather than concatenates with your own — so yours is copied into the private one byte for byte, the shield covers that unit only,git worktree removetakes it away, and the shared, permanent.git/info/excludeis never written. An explicitly emptycore.excludesFileis honored literally — no excludes file at all — rather than read as unset, so git's XDG fallback is not consulted and there is nothing to copy. It then proves it applies or stands down: bmad-loop asks git which excludes file it actually resolves, and anything else — ambient command-scope config (git -c,GIT_CONFIG_PARAMETERS,GIT_CONFIG_COUNT), an unreadable excludes file, an unanswerable probe — skips the shield with a journaled and notified reason. Two runs against one repository serialize on an exclusive lock, leaving a zero-length.git/bmad-loop-shield.lock— never in the working tree, so nothing yourgit add -Acan see; on Windows the wait gives up after ~10s and the shield is skipped naming the lock. Caveats: it needs git 2.20 or newer (older git skips the shield, and the repo-format flag is deliberately not written), and enabling it setsextensions.worktreeConfig— a permanent repo-format flag, rolled back wherever it could be left set without a working shield, but surviving in two cases the reason distinguishes: a sibling worktree still depends on it, or the rollback could not be made at all. Where it cannot be set safely at all (core.bare = trueorcore.worktreein the shared config) the shield is skipped instead. Lines an older bmad-loop wrote into.git/info/excludeare not removed for you — delete them by hand. A path your project tracks gets no pattern: git applies ignore rules only to untracked paths, so the pattern would shield nothing while making the file read as tracked-and-ignored togit ls-files -ci --exclude-standardand to repo-hygiene gates built on it (#392). A tracked directory keeps its pattern, since that one does hide new children — its tracked children still report as ignored, which no pattern shape avoids. If git cannot say whether a path is tracked, the pattern is kept and the reason is journaled. - 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) — but it is mutually exclusive withisolation = "worktree", which seeds fromrepo_rootwhile the preflight probesproject;validatereports the pair and the run/sweep/resume preflight refuses it (#414; #443 lifts the restriction). commit_message_template({story_key}/{run_id}/{story_title}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-repairedrepair, #276);tasks/<id>/(per-session prompt +result.json+ breadcrumbs —session-lifecycle.jsonlrecords timeout fires, budget-guard trips (budget-tripped/over-budget-fired), transport-failure classification (env-fault-classified, #194), a mux session lost under the run (session-vanished, #489) and the #276 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,terminal-frontmatter-pending);logs/;deferred/;resolve/;ATTENTION;ctl-window(the control-session window id the last TUI launch minted, so attach/stop follow the live window, #482). - One piece deliberately lives outside that directory: the hook-event channel (#494) is at
<state root>/<project>/<run-id>/events/under the user-scoped state root (BMAD_LOOP_STATE_DIR, see the transport section below and the README's env-var table), not<run-dir>/events/. The orchestrator still polls the legacy in-tree location, so a project whose installed relay predates the move keeps completing its sessions.delete,archiveandcleanremove the out-of-tree counterpart along with the run dir, andcleansweeps counterparts whose run dir is already gone; an archived run's tarball therefore no longer containsevents/— those files are transient completion signals, consumed while the run was live, and everything an archive is read for later is in the run dir. 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. - The event channel lives outside the project tree (#494), at
<state root>/<project>/<run-id>/events/— a branch switch, a worktree mount or a rollback must not be able to take a live run's control plane away. Each session is told where to write viaBMAD_LOOP_EVENTS_DIR; the state root itself resolves perBMAD_LOOP_STATE_DIR(see the env-var table in the README). The relay falls back to the legacy in-tree<run-dir>/eventswhen that variable is absent, and the orchestrator keeps polling that location too — the hook script is copied into the project byinit, so an upgraded orchestrator regularly drives sessions whose relay predates the move, and without both halves every such session would stall tosession_timeout_min. - What the move is and is not worth: it eliminates the whole in-tree redirect class — nothing an agent writes inside the project can any longer point the completion channel somewhere else — and it is a hard boundary for a sandboxed session, which cannot reach outside the tree at all. It is not a boundary against a session running with permissions bypassed: that session is told the directory by
BMAD_LOOP_EVENTS_DIR, so it can reach it by construction. This is why the relay keeps its own_is_link_likerefusal on the write path (#493) as belt-and-braces rather than retiring it as redundant.
- 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, each declared entry flips tostatus: done <date>+resolution: resolved by story <id>— the annotation a sweep bundle writes — so the ledger stops being one-way. Written at the commit boundary, 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, andbmad-loop validatewarns about all of them before the run starts. (A non-listcloses_deferredinstories.yamlis different: the manifest is a schema the parser owns, so it is refused outright, before the run.) An artifact dir outside the repo cannot be committed — the annotation is written anyway and journaled (deferred-close-external-ledger). - Hard gates (
gate: 3-2, 3-3on an entry): until the entry lands,bmad-loop validatefails (deferred.hard-gate) for every actionable story a token matches andrunpauses (story-gate) rather than dispatch one — a token gates a key it equals or prefixes at a key boundary (-, or a split-story suffix), so3-2covers3-2-invite-link, the stories-mode id3-2and both halves of a3-2a/3-2bsplit, but never3-20-later. Only an explicitstatus: doneretires a gate; an unreadable status (opne, or no status line) still gates. The dispatch pause precedes the story's own run record, so closing the entry and resuming runs it; sweeps are exempt, since a sweep is what closes the gating entry. The only deferred check that gates rather than advises; cleared by closing the entry or dropping the token. A warning (deferred.hard-gate-unstructured) covers the four gates nothing can enforce: a token that cannot name a story key (3-2 3-3, or an unmatchable3.2—./_are legal inside a sprint slug), an emptygate:line, agate:not lowercase at the start of a line, and prose declaringHARD GATE:(matched mid-line, sincereason:prose wraps — but not straight after a quote, so a citation stays silent) on an entry with nogate:line. 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-build-auto Spec folder: <folder>. Story id: <id>., spelled with whichever primitive name resolves on disk); 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 dev primitive new 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. The CLI axis has the same seam: a new adapter class registers viaregister_adapterand arrives through thebmad_loop.adaptersentry-point group, with its selecting profile throughbmad_loop.profiles— so an out-of-tree adapter family needs no core edit either (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). A CLI that needs its own adapter class still needs Python — but not a core edit: the profile'sadapterfield names a kind resolved against the registry, which a co-installed package extends. 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 and trip once on crossing, persession_budget_mode—warn(default) raises an ATTENTION + lifecycle breadcrumb only;enforcealso 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). Sampling is live-verified onclaudeand best-effort on other transcript-reading profiles (two independent unknowns there: whether the CLI delivers the transcript path early — until a hook event carries it the guard is inert — and whether it flushes usage mid-turn); the nudge into a busy pane is best-effort everywhere (the termination is the guarantee), 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) on the same cost-weighted total — cache reads counted atcache_read_weight(default 0.1, matching ~0.1x vendor billing). 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. 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, and nothing is terminated. Every operator-facing total leads with the weighted figure and labels both units — the run-finished summary andbmad-loop statuseach lead with<weighted> weightedand name the raw count as(<raw> raw incl. cache reads), the TUI pairs weighted and raw columns, andsession-endentries 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.ctl_windowsis a three-way partition —removed(verified gone after the kill; under--dry-runit is the would-close plan),survived(still listed) andunverifiable(the liveness listing itself failed) — because killing a window is best-effort and reports nothing; a survivor is retried by the nextcleanup, and text mode marks the stdout count and names both non-removed arms on stderr rather than counting them as removed. If the candidate scan reports a failure, no window was chosen or killed: the arms are empty andctl_windows.scan_errorcarries the reason (text mode puts it on stderr), so a reported preflight failure is never document-shaped like "nothing to prune". (scan_errorcarries the failures the scan can see; the multiplexer seam deliberately degrades a listing transport fault to an empty listing, the same documented ceiling the post-kill verdict accepts.) Exit stays 0 either way — the verdict is the text/document, not the code.sessions.removedkeeps its older, weaker meaning: an attempted kill. 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.- It also collects the out-of-tree half of a run. Removing a run dir no longer removes everything the run owns (#494), so
delete/archive/cleanremove the run's control-plane dir under the state root too, andcleanadditionally sweeps this project's orphans there — subtrees whose run dir is gone, from a hand-removed run or a delete that predates this. The sweep keys on the run directory existing, not on itsstate.jsonparsing, so a corrupt run an operator is trying to recover keeps its control plane; a trimmed run keeps its own for the same reason (it is still resumable). Their bytes are not in the reclaim estimate — a state dir holds consumed event files, the run'sconfig-digest(#498), and little else. Known limit: the state root is keyed by the project's resolved path, so a project that is deleted, moved or renamed leaves its old subtree unsweepable — after a move the project keys somewhere new, and no project can name the old key. A move does not cost the run its config-change baseline, though:state.jsoncarries a second copy that travels with the run directory, andresumefalls back to it exactly when the out-of-tree file is out of reach (#498). - 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 paths and run ids underworktrees/trimmed/archived/deleted/protected, andstate_dirs_sweptas a count) instead of the text. Plan and outcome share one schema, withdry_runsaying which one you are holding, so a script can pre-flight a reclaim and compare it against what happened — though values are each invocation's own sample, not a promise the two agree. It names every item the text only counts or renders, 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 installed dev primitive actually invokes (reporting which name it resolved) — 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.- The preflight also names the multiplexer selection reason wherever selection resolves (
mux.selection, e.g.platform default for win32), not only when aBMAD_LOOP_MUX_BACKEND/[mux] backendchoice forced it. Afallbackselection is reported as a warning (its own label says no available backend matches this platform); a selection that outright failed is carried bymux.preflight, and a detection that failed bymux.backends-detectedat warning — so a missingmux.selectionline is normally explained by another finding (the historical unregistered-tmux fallback is the one silent exception; see the--jsoncontract note indocuments.py). On top of that,host.win32-on-wsl-pathwarns when a native-Windows interpreter is working on a\\wsl.localhost\...project (#332 — see multiplexer-backends.md for why WSL can hand a bash prompt the Windows build). Both are diagnostics only: neither changes which backend is selected (psmux is correct for awin32interpreter) and neither flips validate's exit code.bmad-loop diagnosecarries the same two facts in its Environment block assys.platformandwin32 on WSL distro path(yes/no). - Non-invasive: drives the upstream dev primitive unmodified — there is no fork to keep in sync — and review is just a re-invocation of it on the
donespec. 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; a backend whose binary is present but crashed the version probe gets awarning:on stderr carrying the probe's own failure, since the-in the VERSION column cannot tell that apart from a binary that reports no version);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 adapters— list registered coding-CLI adapter kinds (name · builtin/external · whether the family drives a multiplexer · which profiles select it), the CLI axis's counterpart tomux. Unlikemuxthere is no global choice to persist: a kind is selected per profile by itsadapterfield. A profile referencing an unregistered kind, and any out-of-tree adapter/profile package that failed to load, get awarning:on stderr;validatereports the same asadapter.kind/adapter.external/adapter.external-profile.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 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 it owes;--reverifyre-runs your[verify]commands first and blocks on failure; re-running it on an interrupted confirmation finishes that confirmation.--jsonemits a stable machine-readable document per the contract below — per parked story the key, actions, spec file, spec/board status, the parking run and thecommitcarrying the park (empty until the record is in a commit), plus derivedconfirmable/resumableflags, theconfirmation_recordedreading behind the latter, and a humandriftreason; 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 and its out-of-tree control-plane dir (--forcestops it first if live).bmad-loop archive <run-id>— compress a run into.bmad-loop/archiveand remove it, control-plane dir included (--forcestops it first if live). The tarball holds the run dir, so it carries noevents/.- Removal refuses while a matching agent session is live that the project cannot prove is another one's, even when the engine is dead: for an untagged session the run dir is the last ownership proof
cleanupcan read, so removing it would leak the session (#419). A session tagged to another project carries its own proof and never blocks. Runcleanupfirst, having confirmed the session is this project's (attach): for an untagged sessioncleanupproves ownership by that same run dir, so two projects sharing a run id can prune each other's. Or pass--force, which removes anyway and kills nothing.cleanleaves such a run untouched and reports it as protected. 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, and sweep orphaned run control-plane dirs from the out-of-tree state root (--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, anything breaking bumps it. Errors never produce a partial or error document: the message goes to stderr, stdout stays empty, and the exit code is nonzero. A command reporting a verdict is different: it exits nonzero to carry the answer while still emitting its document (validate --json). So the rule is positive: 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". Codified in src/bmad_loop/machine.py, it holds for every command taking the flag — status, list, decisions, confirm, validate, clean, cleanup, diagnose, probe-adapter (#195). On diagnose and probe-adapter, --json replaces the human report rather than appending; with --out FILE the document goes to the file, stdout stays empty, and the confirmation goes to stderr.