Skip to content

Latest commit

 

History

History
224 lines (173 loc) · 71.6 KB

File metadata and controls

224 lines (173 loc) · 71.6 KB

bmad-loop — Features & Functionality

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 matrix (feature → problem addressed)

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

Full feature list

Core orchestration loop

  • 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-planning generates 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 next ready-for-dev story; advances by epic/story.
  • Scoping flags: --epic N, --story KEY, --max-stories N, --dry-run (prints the plan, spawns nothing).

Spec + implementation (dev stage)

  • Drives the upstream dev primitive (unmodified) in a fresh tmux session — bmad-build-auto, or bmad-dev-auto on 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 syncs sprint-status and synthesizes result.json from 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 Result section 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.

Verification (trust-nothing gate)

  • 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.

Adversarial review (review stage)

  • The follow-up review is a re-invocation of bmad-dev-auto on the done spec — a fresh-context session with no anchoring bias from the implementer (BMAD-METHOD#2508 routes a done spec 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 upstream bmad-review-* skill, so all three are bmm prerequisites the bmad-loop validate preflight 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 finishes done and 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. 0 never 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, default true): set false to 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 to done — 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, default recommended): when review is enabled, decides when the follow-up pass runs. recommended runs it only when bmad-dev-auto sets followup_review_recommended on a done spec (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). always runs it on every story (pre-0.7.0 behavior).

Failure handling & resilience

  • 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 a refs/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-only line 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 as preserve_ref in status/--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-operator instead 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's operator_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 a done story 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 = false restores the old two-outcome behavior, where such a story could only be done or blocked.
  • Completing a park: bmad-loop confirm <story-key> walks the outstanding actions one at a time, writes the spec's ## Operator Confirmation audit section, advances spec and board to done, and commits the pair together with the park record's deletion. Nothing is re-driven — the agent-doable work was committed at park time; --reverify re-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). validate warns on drift in every direction (operator.registry-stale, operator.actions-malformed, operator.park-record-missing), and confirm refuses a drifted record. A park written before #356 lives in the machine-local .bmad-loop/operator-actions.json, which confirm still 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 done with the entry still pointing at it; re-running confirm finishes 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-done board is idempotent. --list, --json (resumable, confirmation_recorded) and validate (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.yaml with 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 at done or awaiting-operator is 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 a done row. Only the review prompt adds where to go instead: a story that cannot be finished without a human decision is finalized to status: blocked with 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, because blocked halts 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_result and pre_commit_gate all fire inside that same window — as its own ## Sprint board section appended after the session-gate hooks, so a plugin prompt rewrite cannot strip it, and without the blocked redirect for the same reason a dev prompt has none; stories mode carries none of it, having no board at all.
  • Typed escalations: CRITICAL pauses the run + notifies (desktop + ATTENTION file); PREFERENCE is 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 (sh reports rc 126/127; on Windows a missing tool is caught by its is not recognized message or by resolving the command's leading token, and a command naming a file cmd cannot execute — a .sh, or any extension outside PATHEXT, which cmd hands to the file association and which exits 0 without running anything — is a fault rather than a silent rc 0 pass, #302) or a session whose log matches the profile's env_fault_patterns (an API 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: claude seeds the connection signature, opencode a provider quota/rate-limit and connection pair (#323), and the other four profiles ship none. Each adapter matches them against the log named by its ENV_FAULT_LOG_SUFFIX — the tmux pane capture logs/<task-id>.log, or <task-id>.server.out (the opencode serve process's own stdout) for opencode-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 Stop or 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 or bmad-loop stop, an operator kill-session, a server crash, the host sleeping. Both are crashed, so the retry/defer reason an operator reads said only dev 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), as session_vanished on dev-decision and fix-decision either way, beside the routing each fed, on every role's session-end journal entry when it is true (the convention env_fault already uses there), and as a session-vanished breadcrumb in session-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_session maps 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 scores completed and 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 to ready-for-dev) and resumes. --no-interactive skips 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 its resolution.json; the orchestrator re-arms the spec to in-review (not ready-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.

Git worktree isolation (opt-in)

  • Off by default ([scm] isolation = "none" — work in place on the checked-out branch, byte-for-byte the prior behavior). Set isolation = "worktree" and each story (and each sweep bundle) runs in its own git worktree on a bmad-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 (story or a shared run branch; run forces delete_branch = false), and delete_branch.
  • Failed-unit forensics: a deferred/escalated unit's worktree + branch stay mounted (keep_failed, default on) and its full diff is preserved to run_dir/failed/<unit>/changes.patch; failed_diff_max_mb caps per-file untracked-file size (oversized skipped with a marker), failed_diff_unlimited lifts 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. With seed_adapter_defaults (default on) each loaded adapter's own seed_files are copied in from the main repo before the session launches, together with every non-hookless adapter's resolved hook config_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_seed adds 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 seeded settings.json keeps 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 pinned skip-worktree in the worktree's own index, so the machine-specific command never rides git add -A into 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's git 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-scoped core.excludesFile that 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 remove takes it away, and the shared, permanent .git/info/exclude is never written. An explicitly empty core.excludesFile is 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 your git add -A can 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 sets extensions.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 = true or core.worktree in the shared config) the shield is skipped instead. Lines an older bmad-loop wrote into .git/info/exclude are 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 to git ls-files -ci --exclude-standard and 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_parallel is a validated knob clamped to 1 until parallel fan-out is built. The repo_root key 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 with isolation = "worktree", which seeds from repo_root while the preflight probes project; validate reports 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.

Plugins (extensibility)

  • A first-class plugin system extends the orchestrator without touching the core loop. A plugin is a folder-drop plugin.toml manifest (under .bmad-loop/plugins/<name>/, overlaying bundled bmad_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 at post_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 persisted shared dict — no new abort path. Distribution is folder-drop now, with a documented bmad_loop.plugins entry-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/.

Game-engine projects (opt-in)

  • 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_mode is coupled to [scm] isolation because a live Editor MCP can only act on the folder its Editor has open: shared (requires isolation = "none") runs the agent in place on the project the operator's warm Editor already has open — zero relaunches, full live MCP; per_worktree (requires isolation = "worktree") gives each worktree its own managed Editor.
  • Readiness gate: before each unit, the plugin's pre_ready_gate hook blocks until the Editor + MCP report ready (Unity: wait-for-ready for IvanMurzak, connectivity check for CoplayDev); on timeout the unit is deferred with an ATTENTION notice rather than starting a session against a half-open Editor.
  • per_worktree lifecycle (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's Library with a reflink/CoW copy of the warm main Library (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's seed_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.

Resumability & state

  • 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 / TUI S) is resumable too: unlike a hard stop killed mid-item, it lets the in-flight item finish through commit and finalizes cleanly, ending as a stopped run that resume picks up at the next item.
  • All run state in .bmad-loop/runs/<run-id>/ (gitignored): state.json; journal.jsonl (every decision, including the session-synthesized-from-frontmatter catch and its spec-marker-repaired repair, #276); tasks/<id>/ (per-session prompt + result.json + breadcrumbs — session-lifecycle.jsonl records 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.json is the wait loop's proof-of-life; resultless-stops.jsonl records 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, archive and clean remove the out-of-tree counterpart along with the run dir, and clean sweeps counterparts whose run dir is already gone; an archived run's tarball therefore no longer contains events/ — 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.jsonl records session-end for every session unconditionally — even a teardown that throws still lands one (status aborted when the outcome is unknowable). A timed-out session's entry carries fired_at (wall time the deadline was declared), teardown_s (wall seconds from that fire to this entry — the teardown gap), and expired_clock (monotonic / wall / bothwall alone fingerprints a host suspend that froze the monotonic clock). Every entry whose usage was read carries tokens (raw) and tokens_weighted (cache reads at limits.cache_read_weight), keeping per-session spend reconstructible; both are null when the usage read failed, and both are absent on an aborted end. tokens_weighted is the end-of-session total — distinct from a tripped session's budget_weighted, the guard's mid-session sample at trip time.

Hook-based transport (no pane-scraping)

  • Coding-agent hooks (Stop / SessionStart / SessionEnd / PreCompact) write structured event files the orchestrator watches; skills write a machine-readable result.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 via BMAD_LOOP_EVENTS_DIR; the state root itself resolves per BMAD_LOOP_STATE_DIR (see the env-var table in the README). The relay falls back to the legacy in-tree <run-dir>/events when that variable is absent, and the orchestrator keeps polling that location too — the hook script is copied into the project by init, so an upgraded orchestrator regularly drives sessions whose relay predates the move, and without both halves every such session would stall to session_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_like refusal on the write path (#493) as belt-and-braces rather than retiring it as redundant.

Deferred-work sweeps

  • 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 a stories.yaml entry or in a story spec's frontmatter — the two are unioned): when the story commits, each declared entry flips to status: 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, and bmad-loop validate warns about all of them before the run starts. (A non-list closes_deferred in stories.yaml is 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-3 on an entry): until the entry lands, bmad-loop validate fails (deferred.hard-gate) for every actionable story a token matches and run pauses (story-gate) rather than dispatch one — a token gates a key it equals or prefixes at a key boundary (-, or a split-story suffix), so 3-2 covers 3-2-invite-link, the stories-mode id 3-2 and both halves of a 3-2a/3-2b split, but never 3-20-later. Only an explicit status: done retires 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 unmatchable 3.2./_ are legal inside a sprint slug), an empty gate: line, a gate: not lowercase at the start of a line, and prose declaring HARD GATE: (matched mid-line, since reason: prose wraps — but not straight after a quote, so a citation stays silent) on an entry with no gate: line.
  • bmad-loop sweep triages 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 is status: 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 (or d in 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 hits max_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 to in-review and the re-driven bundle session resumes review on the re-applied patch instead of re-implementing.

Stories mode (folder+id dispatch)

  • Opt-in second story source ([stories] source = "stories" + spec_folder, or bmad-loop run --spec <folder>): drives the same loop off a typed stories.yaml (a bmad-spec Story Breakdown, sibling of SPEC.md) instead of sprint-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>.md and 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); done skipped, non-terminal statuses resumed on re-dispatch, blocked/sentinel/ambiguous stops the run for resolve. bmad-loop run --dry-run --spec <folder> and bmad-loop status print 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.

Gates & human checkpoints

  • 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-epic is inert in stories mode — the flat stories.yaml list has no epics, so the boundary never fires; use the per-story checkpoints (below) or per-story-spec-approval there.
  • Per-story checkpoints (stories mode): independent spec_checkpoint (pause before code to review the plan; approve → implement, or request a replan) and done_checkpoint (pause after the story commits; skipped when it is the last story). Additive to gates.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 p viewer 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.

Multi-CLI / multi-agent support

  • 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-Windows psmux backend 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 via register_adapter and arrives through the bmad_loop.adapters entry-point group, with its selecting profile through bmad_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] backend via bmad-loop mux set → platform default → first available platform match), the process-lifecycle ProcessHost (register_process_hostterminate/force_kill/is_alive/identity), and the hook interpreter (ProcessHost.hook_interpreter()); bmad-loop validate runs 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 — the copilot binary, not the VS Code extension; agentStop turn-end, -i interactive 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, profile opencode-http, alias opencode) — one headless opencode serve per session, SSE session.idle completion 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 in logs/<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 set model as provider/model; the Unity plugin's window guards don't apply (there is no window).
  • Experimental, isolation = "none" only: antigravity (Google's agy ≥ 1.1.3) — -i interactive launch, Stop turn-end hook (flat handler in .agents/hooks.json, no SessionStart/SessionEnd), --dangerously-skip-permissions for unattended runs; usage_parser = "none" permanently — agy's transcript exposes no usage data (tokens live only in an internal SQLite/protobuf store). agy gates each workspace on an exact-path trustedWorkspaces entry and blocks on an interactive trust dialog, which --dangerously-skip-permissions does not bypass — so worktree isolation hangs (#169). Verify against your agy build with probe-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's adapter field names a kind resolved against the registry, which a co-installed package extends.
  • bmad-loop probe-adapter collects + 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 --probe for live capture. See the adapter authoring guide.

Budgeting & cost tracking

  • 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, per session_budget_modewarn (default) raises an ATTENTION + lifecycle breadcrumb only; enforce also sends a wrap-up nudge, grants session_budget_grace_s (default 240s) to finish, then terminates the session over_budget (ordinary retry→defer routing; an artifact flushed at kill time is still honored). Sampling is live-verified on claude and 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 at cache_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 a token-budget-exceeded journal entry carrying weighted, total and budget; 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 and bmad-loop status each lead with <weighted> weighted and name the raw count as (<raw> raw incl. cache reads), the TUI pairs weighted and raw columns, and session-end entries carry tokens beside tokens_weighted.
  • Token usage read from each CLI's local session transcript (per-profile usage_parser), aggregated per story (bmad-loop status).

Configuration (.bmad-loop/policy.toml)

  • 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_rate for 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. a setsid background 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.

TUI dashboard

  • 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+w keyboard resize mode; sizes persist per-project to [tui] in policy.toml and re-apply on the next launch.
  • Survives TUI exit/crash: runs launched from the TUI are detached bmad-loop processes in a dedicated bmad-loop-ctl tmux 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+e toggles all), validated with the engine's own parser, unset keys show defaults as placeholders.

tmux session management

  • Each run drives agents in a dedicated bmad-loop-<run-id> session; attach to watch live.
  • Auto-teardown on finish (cleanup_session_on_finish, disable to inspect); a hard stop always kills it, a graceful stop --graceful tears it down under the same cleanup_session_on_finish gate a normal finish uses; paused/interrupted runs keep the session for resume.
  • bmad-loop cleanup (or c in 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.
  • --json emits 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 a dry_run flag) instead of the text. ctl_windows is a three-way partition — removed (verified gone after the kill; under --dry-run it is the would-close plan), survived (still listed) and unverifiable (the liveness listing itself failed) — because killing a window is best-effort and reports nothing; a survivor is retried by the next cleanup, 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 and ctl_windows.scan_error carries the reason (text mode puts it on stderr), so a reported preflight failure is never document-shaped like "nothing to prune". (scan_error carries 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.removed keeps its older, weaker meaning: an attempted kill. Plan and outcome share one schema — same fields, same meanings, with dry_run saying 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, becomes sessions.unverifiable_pid in the document, leaving stderr empty.

Disk reclamation ([cleanup])

  • bmad-loop clean reclaims disk (distinct from cleanup, which is only tmux). It tears down git worktrees a mid-flight stop left mounted — the main accumulation source: each carries a real Unity Library/ (incl. the MCP-server build), which git worktree remove cannot reach once the engine was killed before teardown. It then trims the heavy worktrees/ tree from runs kept for history (the run still lists in the dashboard — discovery reads state.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/clean remove the run's control-plane dir under the state root too, and clean additionally 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 its state.json parsing, 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's config-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.json carries a second copy that travels with the run directory, and resume falls 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-run previews, --retain N/--hard tune the window and archive-vs-delete.
  • --json emits a stable machine-readable document per the contract below (schema-versioned; the effective retention policy, freed_bytes as a raw integer, and the paths and run ids under worktrees/trimmed/archived/deleted/protected, and state_dirs_swept as a count) instead of the text. Plan and outcome share one schema, with dry_run saying 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 becomes unverifiable_pid in the document, leaving stderr empty.
  • Prevention is automatic: every run/sweep start reconciles worktrees leaked by a prior finished run ([cleanup] auto_clean_on_finish), and the Unity plugin's post_run hook removes the IvanMurzak MCP server's downloaded /tmp/<company>/<product>/*.zip and truncates its unbounded editor log ([cleanup] clean_tmp). For recurring housekeeping of stopped runs, schedule bmad-loop clean.

Setup & install

  • bmad-loop init installs the three bmad-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 validate preflights 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 its customize.toml review layers (or from step-04-review.md on releases that name reviewers inline), so both the merged bmad-review topology and the standalone-hunter one validate, and configured layers naming an uninstalled skill are caught — plus its customize.toml.
  • The preflight also names the multiplexer selection reason wherever selection resolves (mux.selection, e.g. platform default for win32), not only when a BMAD_LOOP_MUX_BACKEND/[mux] backend choice forced it. A fallback selection is reported as a warning (its own label says no available backend matches this platform); a selection that outright failed is carried by mux.preflight, and a detection that failed by mux.backends-detected at warning — so a missing mux.selection line is normally explained by another finding (the historical unregistered-tmux fallback is the one silent exception; see the --json contract note in documents.py). On top of that, host.win32-on-wsl-path warns 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 a win32 interpreter) and neither flips validate's exit code. bmad-loop diagnose carries the same two facts in its Environment block as sys.platform and win32 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 done spec. Your standard BMAD install is never modified.

Command reference

  • bmad-loop init — install skills, hooks, policy, gitignore.
  • bmad-loop validate — preflight all prerequisites. --json instead emits a stable machine-readable document (schema-versioned; the ok verdict, the queue mode/spec_folder, per-severity counts, and every check as a flat emission-ordered finding with a stable check id, severity, human message and structured detail) 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 a warning: 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 (--clear reverts to auto, --force allows a name only registered on the target machine). Bundled backend: tmux; external backends (e.g. the herdr adapter) register via the bmad_loop.mux_backends entry-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 to mux. Unlike mux there is no global choice to persist: a kind is selected per profile by its adapter field. A profile referencing an unregistered kind, and any out-of-tree adapter/profile package that failed to load, get a warning: on stderr; validate reports the same as adapter.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 (--list to just show them). --json instead 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 derived recommended flag) 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 at awaiting-operator once you have carried out the external actions it owes: acknowledges each in turn (--yes skips the prompts), writes the spec's ## Operator Confirmation audit section, advances spec and board to done, and commits the pair. --list shows every parked story and what it owes; --reverify re-runs your [verify] commands first and blocks on failure; re-running it on an interrupted confirmation finishes that confirmation. --json emits a stable machine-readable document per the contract below — per parked story the key, actions, spec file, spec/board status, the parking run and the commit carrying the park (empty until the record is in a commit), plus derived confirmable/resumable flags, the confirmation_recorded reading behind the latter, and a human drift reason; 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. --json instead 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. --json instead emits a stable machine-readable document (schema-versioned; run state, snapshot cache_read_weight, per-story phase/attempt/review-cycle/tokens/commit/defer-reason, plus the additively-added run-level adapters — the dev/review/triage adapter the policy snapshot resolves to, null on a run predating adapter stamping — and per-story adapters_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). --json emits 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. --graceful instead 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 resumable stopped run, suppressing any pending auto-sweeps; --cancel-graceful withdraws a pending request. Delivery is a stop-request.json control 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: x hard-stops, S requests a graceful stop.
  • bmad-loop delete <run-id> — delete a run directory and its out-of-tree control-plane dir (--force stops it first if live).
  • bmad-loop archive <run-id> — compress a run into .bmad-loop/archive and remove it, control-plane dir included (--force stops it first if live). The tarball holds the run dir, so it carries no events/.
  • 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 cleanup can read, so removing it would leak the session (#419). A session tagged to another project carries its own proof and never blocks. Run cleanup first, having confirmed the session is this project's (attach): for an untagged session cleanup proves 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. clean leaves such a run untouched and reports it as protected.
  • bmad-loop cleanup — remove leftover tmux artifacts for finished/stopped runs. --json emits 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 heavy worktrees/ 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). --json emits what was reclaimed (or would be) as a stable machine-readable document per the contract below, with freed_bytes a raw integer.
  • bmad-loop tui — the interactive dashboard (--low-frame-rate for 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 --probe live 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.

Machine-readable output (--json)

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 fieldok 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.