Skip to content

Latest commit

 

History

History
213 lines (162 loc) · 57.1 KB

File metadata and controls

213 lines (162 loc) · 57.1 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; reads sprint-status.yaml only Modifying a user's standard BMAD install
Read-only TUI + launcher Live dashboard over run-dir artifacts; launches detached runs No visibility into what an unattended run is doing
Git worktree isolation (opt-in) Each unit runs in its own worktree/branch (seeded with the adapters' gitignored MCP/CLI configs), merging back into the target locally; failed units kept for inspection A long unattended run mutating the working tree you're actively using

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.
  • Reads sprint-status.yaml as the single source of truth (owned by BMAD skills; orchestrator only reads it); 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 bmad-dev-auto skill (unmodified) in a fresh tmux session: it plans a 1.5–4k-token spec, auto-approves it, implements, and self-finalizes the spec; the orchestrator 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 to a DEFER-drop (#224). Four harness-side mechanisms make catch + fix deterministic without ever mutating the launch frontmatter (load-bearing skill routing): a launch-state content-hash snapshot the fallback refuses to synthesize from when the candidate is byte-identical (a done spec merely re-opened for review — in every mode, killing the dead-window false positive); mid-session status-transition observation (a heartbeat-tick sighting of the spec moving off its launch status proves a later terminal frontmatter is this session's write, so synthesis needs one sighting, not two); artifact repair that appends the owed marker onto the spec on synthesis so it re-enters the normal scan; and — gated by limits.dev_contract_nudge (default on) — one targeted contract nudge per session asking the skill to append the section itself. Frontmatter synthesis stays the backstop for a session that never complies.
  • Spec-only contract between stages — review consumes the frozen spec, not the dev session's context.

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, the run continues. The defer notification names where the attempt survives — in place, the recovery ref the rollback parked it on plus the git merge --ff-only line that restores it, flagged as commits-only when the uncommitted snapshot could not be captured (only reachable on a re-drive since #340 — a plain rollback refuses the reset instead); isolated, the kept-failed unit branch plus any earlier rolled-back attempt's ref (named, not offered as a merge — it is not fast-forwardable there). That recovery ref is projected as preserve_ref in status and --json; the unit branch never is (#333). When the recovery itself pauses the run (rollback OFF, or a preserve failure refusing the reset), the defer record still lands before the pause — its notice then points at the ACTION REQUIRED manual-recovery notice instead of naming a ref (#342).
  • Stories owing human-only external actions park at awaiting-operator instead of lying (#335). A story whose acceptance criteria include something no agent can do — buy a domain, publish a DNS record, grant an API key — finishes and commits everything an agent can, records what is owed in its spec's operator_actions: frontmatter, and parks. The board advances to awaiting-operator (a forward move; nothing regresses), the run continues to the next story, and nothing is rolled back — a park is a success that commits, so there is no stash and no recovery ref. A park clears every deterministic gate a done story clears (the spec/board pair, the project's verify commands, a non-empty action list) and skips only the review loop, which has nothing in the diff to converge on; a park declaring nothing readable is refused and repaired rather than committed. Parking is notify-only — non-blocking by design, so unlike an escalation it never halts the run. [operator] enabled = false restores the old two-outcome behavior, where such a story could only be done (a green board hiding outstanding work) or blocked (halting the whole run).
  • Completing a park: bmad-loop confirm <story-key> walks you through the outstanding actions one at a time, then writes the spec's ## Operator Confirmation audit section, advances the 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, and re-running a session would redo finished work while the actions stayed outside the repo; --reverify re-runs the project's [verify] commands first (for the case that matters: the external action may have changed what the tests see) and a failure blocks the confirmation. Each park is recorded in a committed per-story file under .bmad-loop/operator/, written inside the story's commit window so it rides the park's own commit — through the worktree merge-back, under every scm.merge_strategy — to every clone the commit reaches: a teammate, a fresh clone, or CI can confirm a story parked elsewhere. (The pre-#356 machine-local index .bmad-loop/operator-actions.json is still read and pruned on confirm, never written, so an in-flight park from an older version stays confirmable where it was written.) The committed truth is the spec and the board; the record is how confirm finds them. validate warns on drift in every direction — operator.registry-stale, operator.actions-malformed, and operator.park-record-missing for a board parked with no record, which is no longer the fresh-clone norm but evidence of a failed record write, a pre-upgrade park, a checkout that lacks the branch carrying the park commit (pull it), or a deleted record — and confirm refuses a drifted record rather than flipping a board on the record's word alone.
  • A confirmation is resumable. Every write is checked — the spec is read back from disk rather than trusted, so a story is never declared done over a write that did not land — and the park record is dropped last, so a failure part-way leaves the story findable. Interrupted between the spec writes and the board write, what survives is a signed-off spec at done with the entry still pointing at it; re-running confirm finishes that rather than refusing it as stale, advancing the board, dropping the entry and committing, with no second prompt and no second audit section (the section on disk is the acknowledgment, and it is fence-aware, so an example quoted inside a code block never counts as one). It resumes equally from a board a human already fixed by hand, since that is what the failure message asks for. --list, --json (resumable, confirmation_recorded) and validate (operator.confirm-interrupted) all name the state rather than calling it stale — the remedy inverts, so the check id does too.
  • 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 deterministic verify command whose environment is broken (sh reports rc 126/127; cmd has no such convention, so 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, anything outside PATHEXT — is a fault rather than the silent rc 0 pass it used to be, #302), or a dev/review/fix/workflow/sweep session whose pane log matches the profile's env_fault_patterns (an API Error … Connection refused-class transport failure that idled out the session clock) — pauses the run with the matched evidence instead of charging the attempt/cycle and deferring the story as if its code were broken. Re-arm (or a sweep's escalation-resume) restores the budget, so a resume after the outage clears re-drives from a clean slate. Patterns are per-profile (seeded only for claude; extend/disable via a project profile overlay).
  • CRITICAL resolution: bmad-loop resolve <run-id> opens an interactive resolve agent seeded with the escalation + frozen spec; you disambiguate, it re-arms the story (escalated → pending, spec reset 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, bmad-dev-auto saves the attempted change as a patch file (in the implementation-artifacts folder, referenced from the halt output) before reverting the tree. If the attempted reading turns out to be correct, the resolve agent adds "restore_patch": "<path>" to its resolution.json; the orchestrator then re-arms the spec to in-review (not ready-for-dev) and re-applies the patch onto the baseline after every reset, so the re-driven session resumes review on the restored diff instead of re-implementing from scratch. A hand-driven bmad-loop resolve --no-interactive --restore-patch <path> does the same. A patch that fails to apply escalates rather than dispatching a session onto a half-restored tree. Deferred-work sweep bundles (below) get the same recovery — an escalated bundle re-arms to in-review and the re-driven bundle session resumes review on the re-applied patch.

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; 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 and just gains the Stop hook), and shielded from the unit's git add -A.
  • Run state never moves into a worktree — .bmad-loop/ always lives in the main repo; spec paths are persisted relative to the worktree so a kept-failed run stays portable.
  • Merge-back is serialized; max_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).
  • commit_message_template ({story_key} / {run_id} 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 marker repair, #276), events/ (hook signals), tasks/<id>/ (per-session prompt + result.json + diagnostic breadcrumbs — session-lifecycle.jsonl records timeout-fire, budget-guard trips/terminations (budget-tripped / over-budget-fired), transport-failure classification (env-fault-classified, #194), and the #276 missing-marker forensics (spec-status-transition-observed, frontmatter-unmodified-refused, contract-nudge-sent), heartbeat.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 (bytes unchanged since launch, #276), or terminal-frontmatter-pending), logs/, deferred/, resolve/, ATTENTION.
  • 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.

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, the orchestrator flips each declared entry to status: done <date> + resolution: resolved by story <id> — the same annotation a sweep bundle writes — so the ledger stops being one-way (filed automatically, marked resolved by hand). Written at the commit boundary, after verification/review/checkpoints and just before the squash, so an in-repo ledger carries the annotation in the story's own commit and a story that fails, is rejected by review, or escalates closes nothing. Declared, never inferred from a diff; re-read at the commit so a declaration edited after implementation still counts; idempotent across a resume; an unknown id, an unreadable entry status, or a non-list declaration in a story spec is journaled, never fatal (a non-list declaration in stories.yaml is a manifest schema error like any other, caught before the run). bmad-loop validate warns about all of those before the run starts, in both queue modes. An artifact dir configured outside the repo is shared across worktrees and cannot be committed — the annotation is written all the same and journaled (deferred-close-external-ledger).
  • bmad-loop 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-dev-auto Spec folder: <folder>. Story id: <id>.); 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 bmad-dev-auto 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 (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).
  • 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 cadence and trip once on crossing the cap, per session_budget_modewarn (default) raises an ATTENTION + lifecycle breadcrumb only; enforce additionally 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). Mid-session sampling is live-verified on claude; other transcript-reading profiles sample the same cumulative files best-effort (early transcript-path delivery and mid-turn flush behavior are unverified vendor behavior), the wrap-up nudge into a busy pane is best-effort everywhere (the termination is the guarantee, the nudge a courtesy), and adapters with no mid-session usage signal (usage_parser = "none", Copilot's shutdown-only flush) leave the guard inert.
  • Per-story token budget (max_tokens_per_story, default 2M weighted, advisory) using the same cost-weighted total — cache reads counted at cache_read_weight (default 0.1, matching ~0.1x vendor billing). The story's cumulative spend is re-checked at every session boundary, so an overrun surfaces while the story is still running and regardless of how it ends — a story that defers or escalates is judged like one that commits. The first crossing raises one ATTENTION + desktop notice (story token budget exceeded: <key>) and a token-budget-exceeded journal entry carrying weighted, total and budget; the warning is latched per story (and persisted, so a resume does not re-notify) and nothing is terminated — advisory means the story runs on. Every operator-facing total leads with that weighted figure and labels both units — the run-finished summary and bmad-loop status read <weighted> weighted tokens (<raw> raw incl. cache reads), the TUI pairs a weighted tokens column with a raw one, and session-end journal 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. 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.
  • 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 worktree paths and run ids under worktrees/trimmed/archived/deleted/protected) instead of the text. 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 reclaim and compare it against what actually happened. (Values are each invocation's own sample, not a promise the two agree: freed_bytes is re-measured, and a run that was still in progress at preview time can be reclaimable by the time you commit.) It names every item the text only counts (protected) or renders (~1.2MB is a formatting of freed_bytes), 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 bmad-dev-auto actually invokes — 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.
  • Non-invasive: drives the upstream bmad-dev-auto skill 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); 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 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 action 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 each owes without confirming any (marking one whose confirmation was interrupted as already signed off, not as unconfirmable); --reverify re-runs the project's [verify] commands first and blocks the confirmation if they fail. Re-running it on an interrupted confirmation finishes that confirmation instead of refusing it. --json instead emits a stable machine-readable document (schema-versioned; per parked story the key, actions, spec file, spec/board status, the parking run and the commit that carries the park — derived from the record's own git history, empty when the record is not yet in any commit — plus the derived confirmable and resumable flags, the confirmation_recorded reading behind the latter, and a human drift reason) per the contract below; it implies the listing and never prompts, and nothing parked yields a valid empty document.
  • bmad-loop list (ls) — list every run/sweep with its short ref, type, and status. --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 (--force stops it first if live).
  • bmad-loop archive <run-id> — compress a run into .bmad-loop/archive and remove it (--force stops it first if live).
  • 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 (--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, and anything breaking bumps the version. Errors never produce a partial or error document: the message goes to stderr, stdout stays empty, and the exit code is nonzero — so stdout is always either one complete valid document or empty. An error means the command could not do its job; a command whose job is to report a verdict is a different thing, and exits nonzero to carry the answer while still owing you the document (validate --json is the case). So the rule is positive rather than inferred from the exit status: parse non-empty stdout whatever the exit code, and take the verdict from the document's own fieldok on validate, which unlike rc separates "the checks failed" from "the command broke". The contract is codified in src/bmad_loop/machine.py and holds for every command that takes the flag — currently status, list, decisions, confirm, validate, clean, cleanup, diagnose and probe-adapter — with no exception (#195). On diagnose and probe-adapter, whose default output is a human-readable report, --json replaces that report rather than appending to it; with --out FILE the document goes to the file, stdout stays empty, and the confirmation goes to stderr.