Releases: bmad-code-org/bmad-loop
Release list
v0.9.0
Added
-
Active agent indicator (#153). The TUI run header and tasks table now name who is driving —
the resolved adapter·model for the live stage, or the run's configured adapters when no session is
open. Session-start journal entries and session records stamp the resolved adapter identity, and
bmad-loop status --jsongains an additive run-leveladapters(the snapshot-resolved
dev/review/triage identity,nullon a run predating stamping) and per-storyadapters_used(the
identity actually recorded per role) — both are a projection of the run's persisted policy snapshot
(re-stamped from current config on resume) and its recorded sessions, not live policy read at status
time; a run whose snapshot predates adapter stamping reportsadapters: nullrather than a
fabricated default. Schema version unchanged. -
Graceful stop (
stop --graceful, TUIS). Ask a live run to finish its in-flight item —
a story or sweep bundle through commit, or an in-progress sweep triage — then finalize cleanly
and stop as a resumablestoppedrun, instead of the hard SIGTERM stop that kills mid-item.
Delivered through astop-request.jsoncontrol file consumed at the next item boundary, so it
needs no signal and works on every platform and multiplexer backend. Pending auto-sweeps are
suppressed;--cancel-gracefulwithdraws a request, and a hard stop still wins over a pending one.
status --jsongains an additivegraceful_stop_pendingfield (schema version unchanged). -
bmad-loop validate --json(#205). A stable, schema-versioned JSON document of the
preflight: theokverdict, the queuemode/spec_folder, per-severitycounts, and every
check as a flat emission-ordered finding. Each finding carries a stablecheckid —
hooks.registered,adapter.binary,skills.base-incomplete, … — so CI can branch on a
particular failing gate instead of matching remediation prose, which is the part most likely
to be reworded.detailkeeps what each check knew before it flattened itself into a
sentence:mux.backends-detectedkeeps every detected backend row rather than the text's
tmux*, psmux (unavailable)soup, whose trailing*a consumer had to parse to learn which
backend was selected, andskills.base-incompletekeepsmissing_markersas a list rather
than a", "-joined string. A failing check emits the whole document and still exits 1 —
the nonzero code is the verdict being reported, not a failure to produce one, so the existing
bmad-loop validate || exit 1in CI is untouched.machine.pygains the clause that makes
this well-defined: parse non-empty stdout whatever the exit code, and read the verdict from
the document's ownok, which unlikercseparates "the checks failed" from "the command
broke". The text output is byte-for-byte unchanged. -
bmad-loop clean --json/bmad-loop cleanup --json(#204). Stable, schema-versioned
JSON documents (one per command — they are separate contracts) reporting what a reclaim
removed, or under--dry-runwould remove: forcleanthe worktree paths, trimmed,
archived, deleted and protected run ids, the effective retention policy, andfreed_bytes
as a raw integer — the text's~1.2MBis a rendering of that number, and formatting is the
renderer's job; forcleanupthe run ids whose sessions went, the live ids left alone, and
the ctl windows closed. Plan and outcome share one schema — same fields, same meanings, with
dry_runsaying which one you are holding — so a script can pre-flight and then compare
against what actually happened. The values are each invocation's own sample rather than a
promise the two agree:freed_bytesis re-measured, and the world can move between the
preview and the commit. The real paths are now scriptable at all — they previously discarded
the per-item data and printed only a summary line, andprotectedwas a bare count. Both
commands printed progress as they mutated, and both warned mid-loop about an unverifiable
engine pid; under--jsonthat warning becomes a document field, so stderr stays empty and
stdout stays one pure document. -
bmad-loop decisions --json(#203). A stable, schema-versioned JSON document of the
pending deferred-work decisions, so a script can select an option by policy and pre-answer
it rather than scraping the numbered text. It is strictly richer than that text, which drops
each decision'scontextand shows only key/label/effect per option — hiding theintent,
resolutionandbundle_namethat decide what the next sweep actually builds or writes.
The recommendation, a(recommended)suffix on a free-text line in the text form, becomes a
derived boolean on the option it names.--jsonimplies the listing and never prompts (the
interactive prompter reads stdin and cannot coexist with a pure document), and nothing
pending is a valid empty document with exit 0. -
bmad-loop list --json(#192). A stable, schema-versioned JSON document — one entry
per run, oldest first (short ref, run id, type, started-at, liveness-aware status, paused
stage) — replaces the text table when passed. Unparseable runs are included as status
unknown, and an empty runs dir yields a valid empty document with exit 0. -
bmad-loop status --json(#190). A stable, schema-versioned JSON document (run
id/type/source, derived status + pause fields, snapshotcache_read_weight, raw +
weighted token totals, and per-story phase/attempt/review-cycle/tokens/commit/defer
reason) replaces the text output when passed. This is the supported machine-readable
surface — the human text, whose layout #129 already changed once with no warning
path, is now explicitly best-effort. -
session-endjournal entries carrytokens_weightedbesidetokens(#129). Only the
raw scalar was persisted, and the weight cannot be backed out of it, so a session's
cost-weighted spend was unreconstructible after the fact — the weighted figure existed
only for sessions that tripped the budget guard. Every entry whose usage was read now
records both.null(never0) when the usage read failed, since untracked is not free;
both fields stay absent on anabortedend, where no read happened. Distinct from a
tripped session'sbudget_weighted, which is the guard's mid-session sample at trip time
rather than the end-of-session total. Thecache_read_weightknob also gained a
description in the TUI settings screen, where it had none. -
Mid-session token-budget guard (#158). Both adapter wait loops now sample cumulative
weighted usage every ~30s and act on crossing the new per-session cap
(limits.max_tokens_per_session, default 4M weighted) perlimits.session_budget_mode:
warn= one ATTENTION + lifecycle breadcrumb;enforce= wrap-up nudge +
limits.session_budget_grace_s(default 240s) to finish, then termination with the new
over_budgetsession status, which rides the ordinary retry→defer routing. Defaults to
warn: on upgrade, existing installs gain visibility (one ATTENTION line per over-cap
session) but no terminations — setsession_budget_mode = "enforce"to opt into the
hard bound, or"off"to silence the guard entirely. Session-end
journal entries carrybudget_weighted/budget/budget_modefor tripped sessions.
Live-verified onclaude; other transcript-reading profiles sample best-effort, and
adapters with no mid-session usage signal (usage_parser = "none", Copilot) stay inert. -
OpenCode adapter (
opencode-httpprofile, aliasopencode). Drives
OpenCode ≥ 1.18 entirely over HTTP/SSE — one headless
opencode serveper session (no tmux window), SSEsession.idleas the completion
signal with an HTTP poll fallback, per-session server password, hermetic skills, and
token usage read back over the API. Full dev/review synthesis parity via the new
_ResultFileMixin/_DevSynthesisMixinseams ingeneric.py; profiles gained a
hookless[hooks] dialect = "none"mode (no hook registration anywhere). Install the
HTTP client withpip install 'bmad-loop[opencode]'; setmodelasprovider/model.
The pinned 1.18.2 API contract is recorded in the adapter's module docstring, guarded
by a zero-token real-binary smoke test (tests/test_opencode_live.py, skipped when
the binary is absent). -
Native-Windows
psmuxmultiplexer backend (experimental). A bundled builtin that
drives runs on native Windows through psmux — a ConPTY tmux re-implementation that speaks
the tmux CLI via its ownpsmuxbinary — so tmux's session/window model and the
bmad-loop-<run-id>/bmad-loop-ctlsession names carry over unchanged. It registers for
win32and is the platform default there, selected automatically when thepsmuxand
pwshbinaries are onPATHand psmux reports newer than 3.3.6 (older releases can
force-kill a recycled PID during teardown, so they read as unavailable and selection falls
through). Native Windows stays experimental — window hosting, attach/detach mapping, and
Unity cache-path correctness are tracked in the roadmap — but the dev→review→verify→commit
loop and TUI observation run. WSL is unaffected (it is Linux and uses tmux). (#58) -
Out-of-tree multiplexer backends (
bmad_loop.mux_backendsentry points). A backend
package installed next to bmad-loop (e.g.uv tool install bmad-loop --with <adapter>) now
registers itself with no config step: before every selection, core imports each module
advertised under thebmad_loop.mux_backendsentry-point group, whose import-time
register_multiplexer(...)call makes the backend selectable exactly like a bundled one
(builtins load first, so default selection is unchanged by installing an adapter). A package
that fails to import can never break selection — the failure is r...
v0.8.1
Fixed
- A session that finished its work but crashed before the run recorded it no longer loses that work
to a restart-and-rollback. The engine now records a completed dev/review session behind a
durability barrier before running post-session hooks (usage is folded in afterward as best-effort
metadata); on resume it consumes that durably-recorded result straight into verify/decide instead of
restarting the attempt from baseline. A rawKeyboardInterruptnow records a controlled stop, and
replay preserves the attempt/cycle/baseline. (#62, closes #57) - Several narrower resume-replay edges opened by that durability work are closed. A host death in
the post-session window of the last allowed review cycle no longer drops a recorded clean review to
a defer; a reconcile early-return no longer persists a pre-reconcile dict that could silently skip a
recommended follow-up review; and triage/sweep and labeled plugin-workflow sessions no longer persist
large, never-consumed result payloads intostate.json. (#63) - A dev session is now completed only on a real Stop or window death, never on a terminal artifact
glimpsed while the agent window is still alive. The idle-tick and grace-expiry shortcuts could
return "completed" mid-turn and let the run's cleanup kill a working agent, handing the engine
half-written state. Both shortcuts are removed, liveness is re-probed immediately before the
grace-expiry stall verdict, and a re-driven spec has its stale## Auto Run Resultsection stripped
on both re-arm paths so a resolved re-drive can't be misread as its own terminal result. Result scans
are now fence-aware, so an## Auto Run Resultheading quoted inside a fenced code block is never
mistaken for a real section (nor destructively stripped). (#53, closes #48 #52) - Long journal fields in the TUI now wrap with a hanging indent instead of spilling back under the
timestamp and kind columns. Each row renders as a fixed-width grid whose fields cell folds within
its own column.
Added
- Attempt-preserve recovery refs are now bounded. With
scm.rollback_on_failureon, auto-rollback
parks a failed attempt's committed work underattempt-preserve/*and its dirty worktree snapshot
underattempt-preserve-dirty/*; nothing pruned them, so they grew unbounded. Run start now keeps
only the newestscm.preserve_keep(default 20,0= never prune) per family by committer date and
best-effort-deletes the tail — a stuck ref never wedges the ones behind it, and prune failures are
journaled but never block the run. (#50 #54, closes #32 #49)
Changed
- The adapter shell-dialect seam is now an explicit, documented contract.
new_window/
new_parked_windowfactor their shell-dialect fragments into overridable hooks (POSIX output stays
byte-identical), and thecommandparameter's semantics are pinned in the ABC docstring
(shlex-joined argv; operator handling is backend-defined). Relevant only to authors porting the
adapter to a non-POSIX backend. (#47 #60)
v0.8.0
Changed
- BREAKING: the project is renamed
bmad-auto→bmad-loop. The distribution, console script,
and CLI are nowbmad-loop; the Python package isbmad_loop(wasautomator); the BMAD module
code and marketplace plugin arebmad-loop(wasbauto); per-project state moves from
.automator/to.bmad-loop/. The GitHub repo is now
bmad-code-org/bmad-loop — old web and git URLs
redirect. Clean break: no compatibility shims. - BREAKING: renamed public identifiers. Env vars
BMAD_AUTO_*→BMAD_LOOP_*; plugin
entry-point groupbmad_auto.plugins→bmad_loop.plugins; hook relaysbmad_auto_hook.py/
bmad_auto_probe_hook.py→bmad_loop_hook.py/bmad_loop_probe_hook.py; skills
/bmad-auto-{setup,sweep,resolve}→/bmad-loop-{setup,sweep,resolve}; tmux session/window
prefixesbmad-auto-*→bmad-loop-*; worktree branchesautomator/<run-id>→
bmad-loop/<run-id>; TUI classBmadAutoApp→BmadLoopApp. Custom plugins, CLI profiles, and
policy files that reference any of these must be updated.
Added
- New
pre_commit_gateplugin workflow-injection stage. Gate workflows can bind to
pre_commit_gate, which fires unconditionally just before every commit — on the review-converged,
review-skipped, and review-budget-rescue paths alike — while the phase can still legally defer.
TEA's trace/nfr/review gates rebind to it, fixing blocking gates that were previously inert
whenever a dev session recommended no review follow-up (soon_pre_commitfail-opened on the
missing artifacts).
Fixed
- A workflow session that finishes its work but never writes a completion marker no longer
livelocks the run. Each result-less Stop used to refill the stall-nudge budget, re-nudging a
responsive-but-signal-less session untilsession_timeout_min. The engine now appends an explicit
completion contract (absolute marker path + frontmatter shape) to every workflow-session prompt,
and a newlimits.workflow_stall_nudges_cap(default 3) caps the total nudges a workflow session
may receive — degrading a still-missing marker to "stalled" in ~30 min instead of hours. Dev/review
session nudging is unchanged. - On Windows, a live engine whose process identity can't be read now shows
UNKNOWNinstead of a
falseINTERRUPTED. psutil raisesERROR_ACCESS_DENIEDfor a process in another session or
elevation, which the identity-aware liveness read had surfaced as dead — mislabeling running runs
and weakening the resume/delete guards. Liveness is now tri-state (alive/dead/unknown) and
biased away from false-dead;resume,resolve,delete,archive, and cleanup all surface and
warn onunknownwithout ever letting an unverifiable pid block cleanup forever, andresolve
gains--forceto override a squatted-pid block. - A review session that appends to the deferred-work ledger no longer leaves a sweep bundle
unclosed. The bundle ledger is reclosed after review (journaled distinctly as
sweep-bundle-reclosed), and the review prompt now states the ledger is append-only for sessions. - Worktree git-exclude patterns now anchor correctly on native Windows.
install.pynormalizes
backslashes in the per-worktree exclude paths so the ignore rules match (a no-op on POSIX).
Migration
- Reinstall the tool under its new name — uv can't rename a package in place:
uv tool uninstall bmad-auto, then
uv tool install "bmad-loop[tui] @ git+https://github.com/bmad-code-org/bmad-loop.git". - Re-run
/bmad-loop-setup(orbmad-loop initdirectly).initmigrates a project in place:
it strips the old.automator/Stop hook from each CLI's settings, removes thebmad-auto-*
skill dirs, and carries.automator/policy.tomlover to.bmad-loop/policy.toml. Setup folds the
oldbautoconfig intobmad-loopand clears the leftoverbautoconfig section, stale
BMAD Automator Skillshelp rows, and the_bmad/bauto/installer dir. - Legacy
.automator/is left in place (runs, archives, profiles, plugins) and can be deleted
or hand-moved once the migration is confirmed; stale.automator/*gitignore lines are left
untouched.
v0.7.12
Added
- The TUI dashboard now shows the cost-proportional weighted token total, with the raw total in a new
column. Thetokenscolumn and the run-header summary discount cache-read tokens by the run's
cache_read_weight— the same weighting the per-story budget enforces — so the headline number
tracks spend rather than context re-reads; the previous unweighted total moves to a newrawcolumn.
Fixed
- The dashboard no longer crashes when a background poll lands as the screen is torn down or
switched away. A poll worker delivers its refresh on the UI thread; if that arrived just as the
app quit or another screen opened, the query for the run table raisedNoMatches. The apply now
drops stale refreshes for a screen that is no longer running (while still updating one merely
backgrounded under a modal). - A failed attempt's work is preserved before an auto-rollback hard reset instead of being silently
discarded. Withscm.rollback_on_failureon (or on a resolved re-drive), a deferred or stopped
attempt's commits above baseline are now parked under anattempt-preserve/<run_id>-<head8>branch,
and its uncommitted working-tree diff — tracked edits and run-created untracked files alike — under
refs/attempt-preserve-dirty/; both are recoverable by name and survive gc. A plain rollback that
cannot create the ref refuses to reset and pauses for manual recovery rather than destroying work.
The uncommitted snapshot is scoped to this run's own changes (never a pre-existing untracked file),
commits under a synthetic identity so it works with no git user configured, and is keyed per retry
so repeated rollbacks against the same baseline no longer overwrite each other's recovery ref. - Process liveness is now identity-aware, so a reused PID no longer reads as a live run. A recycled
pid (common on Windows) used to register as a false "alive" — blocking resume of a dead run,
stranding worktree reclaim, leaking sessions, and showing dead runs as RUNNING. The pid file now
carries a process-identity token that resume, stop, and the TUI verify against; on win32 the engine
also ignores console SIGINT/SIGBREAK during a run so a ConPTY Ctrl+C broadcast can't kill it. - A story from a resolved escalation that still can't finish now re-escalates instead of being
silently deferred. When a human-resolved CRITICALblockedescalation was re-driven and the
re-drive couldn't reachstatus: done(e.g. the environment was still broken), the story used to
exhaust its dev/review budget and plateau-defer — filing an unresolved blocker as deferred work and
rolling back the implemented code. Whileresolved_redriveis latched, budget exhaustion now
re-escalates (pauses for the human) instead of deferring, and the attempt's tree is preserved. - A resumed
--epic Nrun stays scoped to its epic and no longer declares the epic "done" while
stories remain.resumerebuilt the engine without the run's--epic/--story/--max-stories,
so a scoped run silently widened to every epic; with strict file-order story selection, deferring or
finishing a story in an epic placed out of numeric order in the sprint board (e.g. one appended last)
bounced selection to an earlier-in-file epic and fired a spurious "epic N complete" boundary,
stranding the epic's remaining stories. The selector and cap are now persisted and restored on
resume, and story selection exhausts the current epic before advancing — so an epic boundary fires
only when that epic has no actionable stories left. Document-order epic execution is unchanged.
v0.7.11
Fixed
- A finalized story the review just won't stop recommending a follow-up for is now committed, not
rolled back, when the review budget runs out. Exhaustinglimits.max_review_cyclespreviously
always deferred + reverted — discarding completed, review-passing work (frontmatterstatus: done,
verify green) whose only "failure" was a never-clearingfollowup_review_recommended. The
orchestrator now commits that work and re-files the lingering recommendation as a fresh open
deferred-work entry; a story that itself came from such an entry is committed without re-filing, so
a second non-convergence reaches a human instead of looping across sweeps. Worktree-isolation runs
were unaffected — a deferred unit already keeps its worktree.
v0.7.10
Fixed
- Completed work left at the transient
in-reviewfrontmatter is no longer falsely deferred and
rolled back. Abmad-dev-autosession that dies in its step-04 Finalize tail can leave the spec
frontmatter at the transientin-reviewmarker while the## Auto Run Resultprose already says
Status: done— the same stale-frontmatter gate bug as 0.7.8 with a different value. The 0.7.8
reconcile skippedin-reviewto protect the legacybmad-auto-devreview-handoff, but that fork
is retired andin-reviewis now only ever a transient marker, so it is reconciled todone
before the gates run. Every deterministic gate still runs afterward, and afollowup_review_recommended: true
spec still triggers the follow-up review pass. Closes a re-sweep loop that re-ran and discarded the
same completed bundles (~47M tokens/cycle).
v0.7.9
Fixed
- The multiplexer seam no longer leaks raw subprocess timeouts. Every contract method now raises
MultiplexerError/TmuxErroror returns its documented sentinel instead of letting a 30 s tmux
hang escape as a rawsubprocess.TimeoutExpired. - A transient tmux hang no longer crashes a run or mis-reads a working session as dead. The
wait-loop tolerates an unknowable liveness probe — a persistent hang degrades to an honest
timeout, never a falsecrashed. - An unexpected engine exception is now recorded instead of being lost to the parked control
window. The orchestrator writes arun-crashjournal line + acrash.txttraceback, sets a
CRASHEDrun status, and tears down the orphaned agent session — rather than dying with a
traceback printed only to the pruned control pane.
v0.7.8
Fixed
- Completed
bmad-dev-autowork is no longer falsely deferred when the skill leaves the spec
frontmatterstatusstale. The skill can finalize a run in its## Auto Run Resultprose
(Status: done) yet leave the YAML frontmatter at the template defaultdraft; since every gate
reads frontmatter, the sprint/ledger sync no-op'd and the story or sweep bundle deferred — losing
tested work on rollback. The orchestrator now reconciles the frontmatter to the success status
from the terminal prose before the gates run (journaledspec-status-reconciled). It reconciles
only adoneoutcome from a non-terminal status, and every deterministic gate (worktree diff,
dw-ids, verify commands, ledger) still runs — so the bookkeeping is repaired without trusting prose
to pass a gate.
v0.7.7
Fixed
- Spec-status gates are now case- and whitespace-insensitive. A hand-edited spec whose
frontmatter carried a stray-casedDone/In-Reviewsilently failed the dev/review gate and the
story never advanced; every spec-frontmatter status read now normalizes through a single
verify.status_ofhelper. The well-formed lowercase path is unchanged. Also fixes the
manual-rollback notice, which rendered an invalidgit reset --hard the run's baseline commit
when no baseline was recorded — it now shows a<baseline_commit>placeholder. - Project-relative path guards reject
..traversal and OS-foreign absolute paths. A CLI
profile or plugin manifest could declare aconfig_path/skill_tree/seed_files/module path
that climbed out of the project with../— or, on Windows, a POSIX-absolute/etc/...that
Path.is_absolute()failed to flag — and slip past the "must be project-relative" check. The
guards now reject both, on every platform. - Persisted relative paths serialize with forward slashes. A worktree run's
spec_fileand the
resolve context'sresolution_pathwere written with the host separator, so astate.jsonor
context file produced on Windows read back with backslashes. Both now persist viaas_posix()for
a single cross-OS contract (a no-op on POSIX). - The TUI no longer shows a stale run after a same-size state rewrite. The dashboard's
stat-gated cache keyed on(mtime_ns, size), so an atomicstate.jsonrewrite of identical size
within one coarse mtime tick (e.g. WSL2 drvfs) could be served stale. The engine rewrites
atomically onto a fresh inode, so the cache signature now includesst_ino. - A dev session is no longer mis-stalled while it is actively working or legitimately waiting.
Building ondev_stall_grace_s(0.7.5), the idle-grace window now measures genuine inactivity
rather than time-since-last-Stop: any growth of the session's pane log (a long productive turn, a
streaming subagent) re-arms it, so a session that has finished implementation and is mid-review is
no longer killed and rolled back. And because bmad-auto cannot re-invoke a turn that ended to await
a background process, the grace window no longer dead-ends in a stall — on real silence the
orchestrator wakes the session with up tolimits.dev_stall_nudges(new, default 2) nudges before
giving up; a genuine Stop restores the budget, so a slow-but-cooperative session waits up to
session_timeout_minwhile a truly unresponsive one still stalls.0restores stall-on-grace-expiry.
v0.7.6
Changed
- The OS is now abstracted behind a registry of seams, so a non-tmux/native-Windows port is new
files plus one registration line each — no core edits. The terminal multiplexer is selected
through a registry (register_multiplexer, bysys.platformwith aBMAD_AUTO_MUX_BACKEND
override) rather than hardcoded inget_multiplexer(), and the tmux backend split into a reusable
BaseTmuxBackendextension point — every tmux invocation funnels through one overridable_run()
primitive — withTmuxMultiplexeras a thin POSIX leaf, so a tmux-family backend (an eventual
"psmux") overrides only the spawn primitive and the few divergent methods. - Process-lifecycle primitives moved behind a
ProcessHostseam. Politely-stop / force-kill /
liveness / PID-reuse-identity now route throughget_process_host()(registered like the
multiplexer, with aBMAD_AUTO_PROCESS_HOSToverride); aWindowsProcessHostships ready to
register. Hook registration no longer hardcodespython3— it takes the interpreter prefix from
ProcessHost.hook_interpreter()(POSIXpython3; Windowsuv run --no-project python), and
bmad-auto validateruns a platform preflight that reports the selected backend's readiness and
names the process host. Behavior on Linux/macOS/WSL is unchanged. - The bundled Unity plugin's teardown delegates pid lifecycle to
ProcessHostand its helper
scripts run under the orchestrator's own interpreter. Plugin helper scripts are spawned via
sys.executable(not a PATH-resolvedpython3), so a bundled script may importautomatorseams;
unity_teardown.pynow reaps leaked Editor/MCP processes throughget_process_host()instead of
re-implementing kill/liveness, gaining Windows behavior for free.
Added
- A consolidated porting guide maps the four OS seams (terminal
multiplexer, process lifecycle, hook interpreter, validate preflight), their registries and
test-override env vars, and exactly what a native-Windows port costs end to end. The
adapter-/plugin-authoring guides, ROADMAP, README, and FEATURES are updated to the post-registry
world.