Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 22:15
7255174

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 --json gains an additive run-level adapters (the snapshot-resolved
    dev/review/triage identity, null on a run predating stamping) and per-story adapters_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 reports adapters: null rather than a
    fabricated default. Schema version unchanged.

  • Graceful stop (stop --graceful, TUI S). 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 resumable stopped run, instead of the hard SIGTERM stop that kills mid-item.
    Delivered through a stop-request.json control 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-graceful withdraws a request, and a hard stop still wins over a pending one.
    status --json gains an additive graceful_stop_pending field (schema version unchanged).

  • bmad-loop validate --json (#205). A stable, schema-versioned JSON document of the
    preflight: the ok verdict, the queue mode/spec_folder, per-severity counts, and every
    check as a flat emission-ordered finding. Each finding carries a stable check id
    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. detail keeps what each check knew before it flattened itself into a
    sentence: mux.backends-detected keeps 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, and skills.base-incomplete keeps missing_markers as 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 1 in CI is untouched. machine.py gains the clause that makes
    this well-defined: parse non-empty stdout whatever the exit code, and read the verdict from
    the document's own ok, which unlike rc separates "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-run would remove: for clean the worktree paths, trimmed,
    archived, deleted and protected run ids, the effective retention policy, and freed_bytes
    as a raw integer — the text's ~1.2MB is a rendering of that number, and formatting is the
    renderer's job; for cleanup the 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_run saying 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_bytes is 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, and protected was a bare count. Both
    commands printed progress as they mutated, and both warned mid-loop about an unverifiable
    engine pid; under --json that 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's context and shows only key/label/effect per option — hiding the intent,
    resolution and bundle_name that 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. --json implies 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, snapshot cache_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-end journal entries carry tokens_weighted beside tokens (#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 (never 0) when the usage read failed, since untracked is not free;
    both fields stay absent on an aborted end, where no read happened. Distinct from a
    tripped session's budget_weighted, which is the guard's mid-session sample at trip time
    rather than the end-of-session total. The cache_read_weight knob 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) per limits.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_budget session 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 — set session_budget_mode = "enforce" to opt into the
    hard bound, or "off" to silence the guard entirely. Session-end
    journal entries carry budget_weighted/budget/budget_mode for tripped sessions.
    Live-verified on claude; other transcript-reading profiles sample best-effort, and
    adapters with no mid-session usage signal (usage_parser = "none", Copilot) stay inert.

  • OpenCode adapter (opencode-http profile, alias opencode). Drives
    OpenCode ≥ 1.18 entirely over HTTP/SSE — one headless
    opencode serve per session (no tmux window), SSE session.idle as 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/_DevSynthesisMixin seams in generic.py; profiles gained a
    hookless [hooks] dialect = "none" mode (no hook registration anywhere). Install the
    HTTP client with pip install 'bmad-loop[opencode]'; set model as provider/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 psmux multiplexer 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 own psmux binary — so tmux's session/window model and the
    bmad-loop-<run-id>/bmad-loop-ctl session names carry over unchanged. It registers for
    win32 and is the platform default there, selected automatically when the psmux and
    pwsh binaries are on PATH and 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_backends entry 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 the bmad_loop.mux_backends entry-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 recorded and surfaced as a
    warning: line by bmad-loop mux and a note in the validate preflight
    (external_backend_errors()).

  • Unity modal-dialog guards ([plugins.unity]). A chronically-dirty Unity scene raises modal
    Editor dialogs ("scene changed on disk", "save changes before closing") that freeze the MCP
    dispatch loop and stall the whole run. The bundled Unity plugin now defends in depth: it seeds an
    editor-only SceneAutoSaveGuard into the project (install_scene_guard, default on), quiesces
    the Editor around a failed-attempt rollback so git reset --hard can't leave a stale scene open
    (quiesce_on_rollback, default on), and appends the scene-save discipline (from a shipped
    unity_facts.md) to every dev/review prompt so the agent saves at the boundaries that would
    otherwise trip a modal. As a last-resort observability net, an opt-in detect-only probe
    (dialog_probe, default off) watches — via xdotool, X11/Linux only — for those dialogs and
    reports any it sees (a JSONL record, an ATTENTION line, and a best-effort notify-send); it
    never clicks or keys anything, no-ops where there is no X display, and self-reaps when the engine
    exits (dialog_probe_interval_sec, dialog_probe_notify).

  • Follow-up-review damping (limits.max_followup_reviews, default 1). Bounds how many extra
    review rounds a story is granted solely because a completed round finalized status: done yet
    still set followup_review_recommended: true. Once spent, the next such round force-converges —
    verify, then re-file the lingering recommendation to the deferred-work ledger, then commit —
    instead of burning cycles up to max_review_cycles. This damps the structurally non-convergent
    case where every review pass patches findings and therefore recommends another pass. The damped
    converge is the expected steady state and stays quiet (no ATTENTION); only the re-review cap (a
    story that itself originated from a review-budget-followup entry and still won't converge) still
    notifies. Verify-repair rounds, non-terminal rounds, and PAUSE/DEFER/RETRY never spend the
    grant; 0 never honors a pass's own recommendation. runs.rearm_escalation resets the counter so
    a human-resolved re-drive gets a fresh budget.

  • Resizable dashboard panes. Every pane boundary is now adjustable by mouse-drag (divider
    bars, which also carry the Sprint / Deferred Work headings) or a keyboard resize mode (ctrl+w,
    then / for the sidebar, / for the active horizontal split, Tab to pick it, Esc to
    exit). Sizes persist per-project to a new [tui] section in policy.toml and re-apply on the
    next launch; untouched projects keep the previous fixed proportions.

  • Dev-choosable multiplexer backend selection (#87). get_multiplexer() now resolves by
    precedence — BMAD_LOOP_MUX_BACKEND env var → the new machine-scoped [mux] backend key in
    policy.toml → the platform default (win32: psmux, elsewhere: tmux) when installed → the first
    registered backend that matches the platform and is available() — so two same-platform
    backends (psmux / tmux-windows) no longer collide by registration order. Forced names are trusted
    (no availability gate) and fail loudly when unregistered, naming the policy file. New
    bmad-loop mux lists registered backends (platform / available / version / selected + why);
    bmad-loop mux set <name> persists the choice (--clear reverts to auto, --force allows a
    name that only registers on the target machine); no interactive prompts anywhere. validate's
    preflight lists all detected backends when more than one is registered and notes an env/policy
    forced selection. A tmux-less POSIX host still selects TmuxMultiplexer and reports it
    unavailable, exactly as before.

  • Seam-canonical window targets. The =session[:window] target grammar is now owned by the
    TerminalMultiplexer seam instead of living as hand-assembled tmux syntax in core: a new
    concrete target(session, window=None) encoder (overridable per backend, tmux inherits the
    default and passes it straight through) and a module-level parse_target() decoder that
    native-id backends reuse instead of re-deriving the grammar (the out-of-tree herdr
    adapter's _parse_target delegates to it). runs.py/tui/launch.py/tui/app.py format every
    target via target() (new runs.session_target / launch.ctl_target helpers) — output is
    byte-identical, so no backend or operator behavior changes; the contract is documented in the
    adapter authoring guide's new "Window targets" section.

  • Herdr multiplexer backend — shipped out-of-tree. A complete non-tmux-family
    TerminalMultiplexer backend for herdr's cross-platform
    workspace/tab/pane model was developed in-tree (engine run path #136, TUI-launch surface
    #137) and extracted before ever shipping in a release to
    bmad-loop-adapter-herdr, where it
    co-installs with bmad-loop and registers through the bmad_loop.mux_backends entry-point
    discovery above. Core bundles only tmux; herdr's capabilities, remaining degradations, and
    operator notes live in the adapter repo's docs.

  • Stories mode — a second planning pipeline that drives the loop off a typed stories.yaml (folder+id dispatch) instead of sprint-status.yaml. Opt in with [stories] source = "stories" + spec_folder, or per run with bmad-loop run --spec <folder> (overrides policy); --story then filters by story id. Each entry dispatches by folder + id — the dev skill creates-or-resumes the story spec at <folder>/stories/<id>-<slug>.md and the orchestrator reads that id-keyed path back deterministically (no shared board to line-edit, no result-artifact mtime-scan). Strictly linear schedule (list order, no depends_on); bmad-loop run --dry-run --spec <folder> and bmad-loop status print the board (id · live disk state · checkpoint markers · title). Sprint mode is unchanged and remains the default. Requires a bmad-dev-auto new enough for folder+id dispatch — the run preflight checks and remediates.

  • Per-story human checkpoints (stories mode). Independent spec_checkpoint (pause before code to review the plan — dev halts at ready-for-dev; approve to implement, or request a replan that resets the spec to draft) and done_checkpoint (pause after the story commits, skipped when it is the last story); both additive to gates.mode. A blocked story escalates + resolves as in sprint mode, with a pre-planning-halt sentinel auto-deleted (a copy preserved under the run dir) on re-arm.

  • TUI human-in-the-loop surface for stories mode. The sprint tree is replaced by a stories board (id · live disk state · spec/done checkpoint markers · title) when a stories-mode run is selected; paused runs carry a per-run pause-kind badge and the run list shows a global ⚑ N need attention count; p opens the stage-appropriate viewer — plan-checkpoint spec review (Approve & resume / Request replan), story-checkpoint summary card (Continue / Stop), escalation with story context (Resolve / Re-arm & resume), and a gate spec viewer that the existing spec-approval/epic pauses reuse. The start-run modal gains a source select + spec-folder field with a live schedule preview. Every TUI action calls the same code paths as the CLI.

  • Intent-gap patch-restore recovery. When review halts on an intent gap, bmad-dev-auto
    now saves the attempted change as a patch before reverting (BMAD-METHOD#2564). If the attempted
    reading was correct, bmad-loop resolve re-arms the spec to in-review and re-applies the patch
    onto baseline after every reset, so the re-driven session resumes review on the restored diff
    instead of re-implementing. New --restore-patch <path> flag for the --no-interactive path; a
    patch that fails to apply escalates instead of running on a half-restored tree (a resolve session
    that committed over the patched lines triggers exactly this — re-resolve without a restore).
    Restore is rejected up front for worktree-isolation runs and for stories-mode pre-planning
    sentinels, and the latched patch file itself never counts as proof-of-work. Deferred-work
    sweep bundles 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 (#75).

  • Preflight covers the inline review layers. bmad-loop validate (and run-start) now require the
    three upstream review-hunter skills bmad-dev-auto's step-04 invokes — bmad-review-adversarial-general,
    bmad-review-edge-case-hunter, and bmad-review-verification-gap (new in BMAD-METHOD#2550) — plus a
    customize.toml in bmad-dev-auto (its review-layer config, BMAD-METHOD#2535/#2550). A pre-July bmm
    install missing any is reported with remediation before a run stalls.

Changed

  • The TUI's validate modal (v) renders validate --json instead of the text output (#210).
    One row per check — glyph, stable check id, message — with the verdict taken from the
    document's ok rather than the exit code, which cannot tell "the checks failed" from "the
    command broke". A check's detail is now reachable at all: inline for warnings and problems,
    and d toggles it on for everything, so mux.backends-detected expands to a row per backend
    instead of the text's tmux*, psmux (unavailable). A failure adds a footer noting the gates
    are chained — the later gates emit nothing after one fails, so a short list is not a short
    list of problems. An unrenderable document (a newer schema, unparseable stdout) re-runs
    validate in text mode and shows the old modal unchanged.

  • validate reports a failed external mux backend as a warning, not a note (#210). The
    mux.external-backend finding has always read as a failure — "external mux backend 'x'
    failed to load: …" — while carrying severity ok, so it counted as a passing check; it is
    now warning, matching what bmad-loop mux has always printed for the same condition. It
    stays below problem deliberately: selection degrades past a broken external, so the verdict
    and exit code are unchanged. On an affected host validate --json's counts shift by one
    (warning +1, ok −1) while ok and rc do not; the schema version is deliberately
    unchanged, since the document contracts each check id, not a given check's outcome. The
    text line gains the doubled ok: warning: prefix that render() preserves by design.

  • BREAKING: probe-adapter now runs diagnose's egress leak self-check, and captured hook
    payloads ship as a schema instead of scrubbed values (#199).
    The rendered report re-scans
    itself before emitting (the guard moved to sanitize.guard, one audited implementation for
    both commands): an email / secret / home path / username in the final bytes makes the command
    refuse to emit — message on stderr, empty stdout, exit ≠ 0, no --out file — and a stray
    occurrence of the pseudonymized project directory name is repaired to its alias and disclosed.
    Each captured event now reports dotted key paths with leaf types (tool_input.command:str),
    never payload values, so the --json document's schema_version bumps to 2
    (captured_events[].payload removed, payload_schema added; payload_keys stays, now
    identifier-gated). Collection hardening rides along: transcript-location components that embed
    the username are redacted, the project dir name is aliased in locations, a home-rooted
    --binary hint renders ~-relative, and a credential-shaped dict key can no longer surface
    in the token key paths.

  • BREAKING: diagnose --json and probe-adapter --json now emit a pure JSON document
    (#195).
    Both used to print their human-readable report with a fenced ```json block
    appended, so a consumer had to scrape the fence out of prose. --json now emits the
    document instead of the report — stdout parses whole, and every human-facing line (ok:
    trailers, the leak-backstop warning, the unknown profile notice) moves to stderr. With
    --out FILE the document goes to the file, stdout stays empty and the confirmation goes to
    stderr; no file written in JSON mode carries markdown fences any more. That file is held to
    the same standard as the stream — it is validated and newline-terminated identically, so
    --json --out FILE and --json > FILE produce byte-identical files. The text mode (no
    --json) is unchanged. diagnostics.SCHEMA_VERSION deliberately stays at 1 — it versions the
    document, and only the packaging changed — while the probe document gains a schema_version
    of 1 alongside its existing version key, which still holds the probed CLI's --version
    output. Scripts that split on ```json must switch to parsing stdout directly; the break is
    in the flag's output shape, not in either payload. Two
    incidental fixes ride along: diagnose --json no longer renders the markdown report it was
    about to discard, which was double-counting every leak-backstop repair in the warning, and
    the probe document is now sort_keys-stable so two probes of the same CLI diff cleanly.

  • Machine-output (--json) contract codified in machine.py. The pure-document conventions
    from #190 — one JSON object on stdout, inline schema_version, additive-only evolution,
    errors → stderr with empty stdout — now live in one module with shared emit/add_json_flag
    helpers; status --json uses them (output byte-identical) and the duplicated token-total math
    folded into run_token_totals. All four --json commands share the contract (#195); --json
    adoption on more commands is tracked in #196.

  • Backend-neutral naming for the seam-backed helpers and operator messages. The multiplexer
    seam has non-tmux backends now, so the helpers that wrap it drop their legacy tmux names —
    launch.tmux_availablemux_available, app._tmux_missing_mux_missing,
    runs.tmux_sessionsmux_sessions (internal, no deprecation aliases) — and the operator-facing
    strings stop naming tmux when they mean the selected backend: launch errors say
    multiplexer new-session/new-window failed and multiplexer backend unavailable (binary not on PATH), the TUI notifies multiplexer backend unavailable — launch/attach disabled and
    launched (control session bmad-loop-ctl), and the "attach to … bmad-loop-ctl" hints say
    control session. The TUI-guide troubleshooting table matches. Behavior is unchanged.

  • Docs: multiplexer backend guide (docs/multiplexer-backends.md). The user-facing docs no
    longer claim tmux is the only multiplexer backend. The new page covers backend selection
    (bmad-loop mux / mux set) and how external backends are installed and discovered;
    backend-specific operator guidance (what changes from your seat on herdr, its degradations)
    moved out with the extraction and lives in each adapter repo's docs. README, setup guide,
    TUI guide, and FEATURES name the mechanism and link the page.

  • Docs: followup_review_recommended is now scored upstream. BMAD-METHOD#2580 replaced the
    skill's convergence-prone significance judgment with a severity-weighted score over patched
    findings and added a fourth default review layer (Intent Alignment Auditor, #2560). README,
    FEATURES, TUI guide, the [review].enabled setting description, and the engine's damping
    comments now describe the scored flag; limits.max_followup_reviews is unchanged and remains
    the orchestrator-side bound.

  • bmad-loop init now gitignores .bmad-loop/policy.toml. Policy is per-machine-per-repo —
    it carries the machine-specific [mux] backend choice (and the TUI settings editor rewrites
    it), so it must not travel to teammates on other machines or OSes. A .gitignore entry does not
    untrack an already-committed file: existing repos run git rm --cached .bmad-loop/policy.toml
    once (the local copy is kept; init prints this hint when it detects a tracked policy).
    bmad-loop's own worktree-clean preflight already exempted policy.toml — this additionally stops
    inner dev sessions and plain git status from reading a policy edit as a dirty tree.

  • The patch-restore seam is now one validator, one path normalizer, and one exclusion site.
    runs.validate_restore_latch holds every latch precondition (sentinel wedge, spec-less escalation,
    worktree isolation) — the worktree check lived only in the CLI, so rearm_escalation called
    programmatically could latch a patch the re-drive can never honor; it now rejects it too.
    verify.resolve_restore_path replaces four copies of the maybe-relative→absolute join, and the
    shared verify gate derives the restore-patch proof-of-work exclusion from the task instead of
    threading it in from three call sites. The resolve context's restore_supported signal is now the
    validator's verdict too, so the agent never negotiates a restore for a sentinel-wedged or spec-less
    escalation either. Otherwise behavior-neutral. (closes #91)

  • Test helper fidelity. make_engine seeds the launching scope (max_stories, story_filter,
    epic_filter) on RunState like cmd_run does, so resume tests no longer silently ran uncapped;
    the three _escalated_run fixtures collapse into one parameterized conftest builder. (closes #84)

Fixed

  • Locale-stable rollback (#236). Git subprocesses now run with LC_ALL=C, so safe_rollback's
    benign "pathspec did not match" no-op is no longer misread as a hard failure under a localized git
    (e.g. LANG=it_IT.UTF-8) — which had turned a resolvable re-drive into a rollback pause. Forced at
    the single _run_git spawn point, so every git message the orchestrator inspects stays English.

  • The parked-window return target is now backend-composed (#221). An interactive attach
    recorded the client's origin as a bare pane id (%N) and replayed it as switch-client -t %N
    from inside the control session — sound under tmux's one-server model, but on psmux (one
    server per session, upstream-final per psmux/psmux#483) a bare id is session-local: at best
    unresolvable, at worst colliding with a real control-session pane and landing the client on
    the wrong one with exit 0, past the switch-client -l fallback. No single form resolves on
    every backend (tmux's window resolver rejects a pane id in the session:%N slot, and a
    native-id backend needs its own id passed through untouched), so the recording seam now asks
    the backend: TerminalMultiplexer.current_return_target() defaults to the bare native pane
    id — tmux and native-id backends behave exactly as before — and psmux overrides it to emit
    =session:%N, which releases carrying the psmux/psmux#483 fix resolve cross-server,
    degrading to the bare id only if the session probe fails. The replay sides treat the value
    as an opaque target and are unchanged.

  • A worktree_seed entry that silently copies nothing is now journaled (#230). Under
    worktree isolation provision_worktree copies a seed only when the destination is absent —
    right for a file the checkout legitimately carries, but a directory entry is skipped whole
    the moment any child is tracked, so worktree_seed = ["_bmad"] with a tracked _bmad/custom
    copies nothing at all, including the absent children that would clobber nothing. Provisioning
    is quiet by contract (it runs under the TUI), so the skip was invisible: user-authored config
    that reads as applied was a no-op. It now returns the skipped entries and the engine records a
    worktree-seed-skipped journal event; glob-expanded matches are excluded, since a plugin glob
    is expected to hit paths the checkout already carries. Behavior is otherwise unchanged —
    nothing new is copied.

  • A dry run the TUI cannot spawn opens a modal instead of taking the app down (#210). The
    run --dry-run / sweep --dry-run workers called run_captured unguarded, and
    @work(thread=True) defaults to exit_on_error=True — so an OSError from the spawn itself
    (a venv deleted out from under sys.executable, EAGAIN off a loaded process table) escaped
    the worker and killed the whole app rather than the one modal. Both workers and the validate
    degrade now share a guard that reports the reason in the modal body.

  • --json output survives a console that cannot encode it (#200). A JSON document is not
    necessarily ASCII: diagnostics.render_json serializes with ensure_ascii=False so its leak
    guard can scan values unescaped (#195, below), which lets a non-sensitive non-ASCII field —
    a localized platform.release(), say — reach stdout verbatim. Printing it to a console whose
    encoding could not carry it raised UnicodeEncodeError, in practice a legacy non-UTF-8
    Windows one. It failed safe rather than silently — the encode runs before any write, so
    stdout stayed empty instead of half-written — but diagnose --json still died on a machine
    where --out FILE would have worked. machine.emit_document now switches stdout to UTF-8
    before writing. Re-serializing the document as escaped ASCII would have been the smaller
    change and the wrong one: the leak check verified the unescaped bytes, and emitting anything
    re-derived from them is what that helper exists to prevent. --out FILE was never affected;
    it has always written encoding="utf-8".

  • Leak self-check now matches JSON-escaped values (#195). Two evasions became reachable
    the moment diagnose --json stopped also rendering the markdown report, since that raw-text
    pass was what had been catching them: json.dumps doubles backslashes, so a Windows home
    path (C:\Users\…) serialized to a form _ABS_HOME_RE did not match, and its default
    ensure_ascii=True escaped non-ASCII sensitive values to \uXXXX, hiding them from the
    pseudonymizer's stray-original check while json.loads handed the consumer back the
    original. The home-path rule now matches either separator form, and diagnostics.render_json
    serializes with ensure_ascii=False so the guard sees values as themselves. Both apply to
    the markdown path too; neither changes what a clean dump contains.

  • Resumed runs display the policy they actually enforce (#189). policy_snapshot was
    stamped only at run creation. resume reloads policy.toml and enforces it — the
    per-story budget, every SessionSpec — but left the launch-time snapshot in place, and
    every display reads the snapshot: the run summary, bmad-loop status, the TUI, and the
    policy block of the diagnose bundle, which claimed to describe the run that was
    executed. Edit limits.cache_read_weight between launch and resume and the run enforced
    at the new weight while every surface reported the old one, silently up to 10x apart at
    the legal extremes (0.0–1.0). Resume now re-stamps the whole snapshot and persists it
    before the engine starts, restoring the documented contract that policy edits apply to
    resumes. A single session-end entry could likewise carry tokens_weighted at the
    snapshot weight beside budget_weighted at the live one; the two now agree by
    construction. Run scope and mode (source, spec_folder, epic_filter, …) stay pinned
    at launch as before — a policy edit still cannot redirect a live run.
    Visible output change: a run resumed across a weight edit re-weights its whole
    history, not just post-resume sessions, since totals are recomputed from raw counts (this
    is what the budget always did). A pre-0.8.2 run with no snapshot at all gets one on its
    first resume, so it stops displaying at the hardcoded 0.1 default. run-resume journal
    entries now carry cache_read_weight, policy_changed, and cache_read_weight_was when
    it moved, keeping per-session totals written under the old weight reconstructible.

  • Run summaries and bmad-loop status report weighted tokens, with both units labeled
    (#129).
    The run-finished summary — stdout, the ATTENTION file, and the desktop
    notification all render from one place — reported the raw total, counting cache reads
    at full price, while every budget judges the cost-weighted total. On a cache-heavy run
    that overstates spend by ~6.5x, and neither figure said which unit it was. Both surfaces
    now lead with weighted and name both: <weighted> weighted tokens (<raw> raw incl. cache reads), matching the TUI, which has shown weighted since 0.7.12. bmad-loop status also
    gained a run-level tokens: line (it previously printed no run total at all).
    Visible output change: per-story status cells go from <raw>t to
    <weighted>t (<raw> raw), so the number is both differently scaled and differently
    shaped — scripts scraping that column need updating. A story with only cache reads under
    cache_read_weight = 0 correctly renders 0, not - (which means no tokens at all).
    Displayed weights come from the run's persisted policy snapshot, so every observer
    reproduces the same number from state.json alone.

  • The TUI guide's task-table reference described the pre-0.7.12 columns. It documented
    tokens as the raw total and omitted the raw column entirely; the run-header and
    journal sections were likewise silent on the weighted/raw split. Docs only.

  • diagnose leak self-check is now recoverable (#186). A stray pseudonymized
    identifier (a per-field routing gap) is repaired by substituting its alias and disclosed
    in the report and on stderr, instead of refusing to emit any dump; residual failures name
    sensitive[<ns>:<alias>] instead of an opaque index, and the local --legend file is
    written even on refusal so the operator can decode it. PII/secret/path/username hits
    still fail closed.

  • Deferred-work bundles that adopt an existing story spec pass the baseline gate (#161). A
    "follow-up review of story X" bundle is routed by bmad-dev-auto into that story's done
    spec, whose baseline_revision is the story's original dev baseline — necessarily older
    than the bundle's worktree cut, so the exact-match gate failed every such bundle after the
    session had already done its work. The bundle gate now accepts a claimed baseline that is
    an ancestor of the orchestrator-recorded one (the session diffed a superset of the
    unit's changes); diverged or unknown baselines still fail, any git fault in the probe
    reads as not-an-ancestor, and sprint/stories modes keep the exact-match requirement.

  • A failed attempt inside a unit worktree auto-recovers instead of pausing with in-place
    instructions (#161).
    The mid-drive dev retry was the only recovery path without an
    isolation guard: with rollback_on_failure = false it paused the run with manual-recovery
    instructions aimed at the operator's checkout — whose HEAD is the baseline under
    worktree isolation, while the commits sat on the unit branch, so following them literally
    did nothing and invited a destructive reset of a tree the attempt never touched. A mounted
    unit worktree is disposable: the attempt's commits are parked on attempt-preserve/ refs
    and the worktree resets regardless of the flag, which gates in-place (isolation = "none")
    recovery only. The remaining reachable pauses name their tree (git -C "<root>" …).

  • A failed worktree teardown no longer crashes the run after the merge landed (#139). When a
    process the just-ended session left running (e.g. pytest recreating .pytest_cache) makes
    git worktree remove fail with ENOTEMPTY, git still drops its admin entry, so the force=True
    retry failed with "is not a working tree" and that second GitError crashed the run. The
    teardown tail of close_unit_workspace never raises now: a failed worktree removal falls back to
    rmtree + worktree prune (the rmtree confined to the run's own worktrees dir — the path can
    arrive from persisted state), a failed branch delete is reported and swallowed, and both journal a
    worktree-teardown-degraded event — teardown is post-merge housekeeping. A failed forensic diff
    capture instead preserves the worktree + branch (they hold the only copy of a dropped unit's
    changes). discard_worktree gains the same removal fallback so a stuck dir can't block the
    resume re-mount.

  • A git call exceeding its timeout no longer crashes the whole run (#156). Every git
    subprocess the orchestrator spawns now translates subprocess.TimeoutExpired into
    GitError, so the existing degrade guards handle a slow git like any other git failure.
    The rollback gate specifically (_rollback_or_pause's dirty check — the reported crash
    path) degrades to assume-dirty: rollback OFF pauses with the manual-recovery notice and
    the worktree kept; ON / resolved re-drives still auto-recover behind their preserve
    steps. A rollback-dirty-check-failed journal entry records the fault. The bound is now
    configurable as limits.git_timeout_s (default 120).

  • Session timeouts now fire on time and leave a forensic trail (#157). A
    session_timeout_min that fired but journaled its session-end 2h19 late — with zero record
    of when the deadline was declared or why — is now timely and observable on three fronts.
    (1) wait_for_completion gains a wall-clock co-bound: a host suspend (macOS sleep)
    freezes time.monotonic(), silently extending the monotonic deadline by the nap's length;
    the wall clock keeps counting through a suspend, so it may now EXPIRE the deadline — never
    extend it (a stepped-back wall clock changes nothing, and all sub-waits stay monotonic).
    (2) The fire moment stamps the result (timeout_fired_at, timeout_expired_clock
    "wall" alone is the suspend fingerprint) and appends a timeout-fired line to
    tasks/<id>/session-lifecycle.jsonl; each wait tick tops up a throttled
    tasks/<id>/heartbeat.json whose staleness under a still-live session diagnoses a frozen
    orchestrator (the previously uninstrumented gap). The engine journals session-end
    unconditionally
    — even a teardown that throws still records the ended session (status
    aborted when the outcome is unknowable), carrying fired_at/teardown_s/expired_clock.
    (3) Teardown is now a verified kill escalation: terminate → wait → force_kill, where
    limits.teardown_grace_s bounds the liveness-wait before escalating (default 20; 0 = a
    single unverified best-effort kill) and every escalation step carries its own bound, so a
    timeout can no longer hang on an unkillable session. Covers the tmux (generic) and
    opencode-http adapters alike. A frozen
    process still cannot run this code while frozen, but recurrence is now diagnosable rather
    than silent.

  • validate and probe-adapter no longer report antigravity's hooks as unregistered
    (#159).
    The antigravity-hooks-json dialect keys .agents/hooks.json by hook-group name
    at the top level, with no "hooks" wrapper — but both readers looked up "hooks", got {},
    and reported a correctly-installed relay as missing (FAIL: bmad-loop hooks not registered for antigravity, immediately after a successful init --cli antigravity). Both now share
    one install.relay_registered() helper that resolves each dialect's container shape, so the
    two call sites can no longer drift apart. init's merge dedup keys on the narrow bmad-loop
    script markers rather than the bare bmad_loop substring, so an unrelated hook command whose
    path merely contains bmad_loop can't make init skip a registration that validate would then
    report missing.

  • The antigravity hook relay now reads agy's payload keys. agy encodes hook payloads as
    protojson — conversationId, transcriptPath, workspacePaths — while the relay only tried
    snake_case plus copilot's sessionId. Every agy event therefore recorded a null
    session_id, and cwd was never populated (agy sends no cwd, only a workspacePaths
    list). Both the relay and the probe capture hook now try agy's casing, verified against a
    live 1.1.3 turn.

  • probe-adapter antigravity finds the transcript. The shipped convention glob had the
    wrong filename — agy writes transcript_full.jsonl, not transcript.jsonl, under
    ~/.gemini/antigravity-cli/brain/<conversationId>/.system_generated/logs/. Corrected against
    a live capture. A live --probe now also prefers the transcriptPath the CLI hands the hook
    on stdin over the convention glob: the payload names this turn's file, while a glob can
    only take the newest match and may land on an unrelated session.

  • antigravity: usage_parser = "none" is now documented as permanent, not pending. A live
    capture confirmed agy's transcript carries only
    step_index/source/type/status/created_at/content/thinking — no usage block
    anywhere. agy does count tokens, but only inside conversations/<id>.db, an undocumented
    SQLite/protobuf store outside the (transcript_path) -> TokenUsage parser contract. Runs
    work; token columns stay empty.

  • antigravity: trust is exact-path (verified against agy 1.1.3). agy blocks on an
    interactive "trust this folder" dialog for any workspace not listed verbatim in
    settings.json trustedWorkspaces; a trusted parent does not cover subdirectories, and
    --dangerously-skip-permissions does not bypass it (it covers tool permissions only).
    isolation = "none" (the default) works; isolation = "worktree" hangs on every run, since
    each worktree is a fresh untrusted path — now called out in the profile and setup guide, and
    tracked in #169. Replaces the profile's previous "verify during probe" placeholder.

  • Follow-up review sessions are no longer killed on their first Stop by the dev pass's stale
    ## Auto Run Result (#160).
    The review leg re-invokes bmad-dev-auto on the finalized (done)
    spec whose dev pass left that terminal marker; the review's own entry write lifted it past the
    adapter's launch-mtime floor, so the first result-less Stop read the stale marker as this
    session's result and ended the review mid-flight (the #109 stall grace never armed). The engine
    now strips the marker before every review launch — the frontmatter done stays, so step-01
    still routes to a review pass. The review-budget exhaustion defer reason now reports the last
    pass's actual status instead of always claiming a lingering follow-up recommendation.

  • branch_per=run + keep_failed no longer poisons a multi-story run after the first kept
    failure (#138).
    The first story to end deferred under keep_failed=true left its worktree
    checked out on the single shared run branch, so every subsequent story's git worktree add
    collided ("branch already checked out") and insta-deferred with zero dev activity — one kept
    failure turned an N-story run into a 1-story run. A kept worktree under branch_per=run now
    detaches its HEAD (git checkout --detach), freeing the shared branch name for the next story
    while preserving the working tree, uncommitted changes, and the branch ref (still at the kept
    commit) for inspection; subsequent stories mount the run branch normally and get genuine
    attempts. Best effort — if the detach ever fails, the existing worktree-open-failed defer
    still surfaces the collision (no regression). The escalate-and-pause path was already safe: it
    halts the run rather than continuing, and resume frees the kept worktree before any sibling mounts.

  • Dev/review sessions can no longer livelock on their own wake nudges (#149). The idle
    wake nudge is delivered as a submitted turn, so a session that merely answers it ends in
    another result-less Stop — which refilled the nudge budget, re-armed the grace window, and
    repeated until session_timeout_min, burning a turn per cycle. Dev/review sessions now get the
    same monotonic cap injected workflow sessions already had: after limits.dev_stall_nudges_cap
    (default 6) total nudges the session is declared stalled instead (post-kill reconcile still
    rescues a finished one whose terminal artifact is on disk). The nudge text now also states that
    a prose reply cannot end the session. And each result-less Stop leaves a diagnostic breadcrumb
    (tasks/<task_id>/resultless-stops.jsonl: pending / not-terminal / stale-mtime / ambiguous /
    no-artifact / no-result-json) so why a completed-looking session read as result-less is
    answerable from the run dir.

  • Split-story keys (2-6a-…) are no longer silently skipped (#144). The sprint-status
    parser rejected story numbers carrying BMAD's split-story letter suffix, dropping exactly the
    stories that were split to be loop-tractable — invisible to run/--story/the TUI tree, and
    skipped by the epic-lift. The suffix is now a first-class Story/selector field: --story 2-6a
    (or 2.6a, or --epic 2 --story 6a) selects exactly that half, while a plain 2-6 selects the
    whole 2-6a/2-6b family in file order. run and --dry-run also print a stderr warning when
    sprint-status keys remain unparseable, instead of only journaling them.

  • Review leg repairs a finalize-tail death. A review session that died between writing its
    terminal ## Auto Run Result (Status: done) and flipping the spec frontmatter off the transient
    in-review marker left the orchestrator re-reviewing already-finished work — a burned review
    cycle. The review leg now runs the same terminal-status reconcile the dev leg does: when the prose
    says done and the frontmatter sits at a reconcilable non-terminal status, it advances the spec to
    done and re-folds the frontmatter's followup_review_recommended flag (only when present) before
    the convergence/damping gate reads it. Bookkeeping-only — every deterministic verify gate still
    runs against real on-disk/git state, so it cannot pass uncompleted work.

  • Claude sessions launch with CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 (#109). Claude Code
    could bias a dev session toward backgrounding its implementation sub-agent despite the
    bmad-dev-auto prompt ban; the session then ended its turn to await a completion notification,
    and a harness exit at that turn boundary stranded the sub-agent with the story stuck
    in-progress → manual-rollback pause. The shipped claude profile now forces subagents and bash
    to run synchronously — the behavior the skill contract already requires. Opt out with a custom
    profile in .bmad-loop/profiles/.

  • Resume no longer discards a story that already passed its pre-commit gates. A host death in
    the COMMITTING window (phase persisted before finalize_commit ran and the DONE save stamped
    commit_sha) matched no resume arm — there is no COMMITTING-keyed session record to replay — and
    fell through to resume-restart, rolling back or pausing over fully-verified work. Resume now
    finishes the commit in place: the pre_commit_gate workflows are not re-charged (the persisted
    phase is durable proof they passed), the pre_commit hook re-fires (message regeneration and
    pause veto honored), and finalize_commit's content-idempotence covers both the pre- and
    post-squash crash states. Sweep bundles get the same recovery in _recover_inflight_bundle (#115).

  • Resume no longer asks for a rollback of a completed session's committed work. A host death
    in the post-verify decision window left the task persisted at DEV_VERIFY/REVIEW_VERIFY,
    where the resume replay matcher (which only knew the *_RUNNING phases) missed the
    durably-recorded completed session and fell through to resume-restart — pausing with a
    git reset --hard <baseline> instruction that would discard the attempt's finished, possibly
    already-pushed commits. Those phases now replay the recorded result through the normal
    verify/decide pipeline, and the rollback-OFF manual-recovery notice detects commits above
    baseline and leads with saving/checking them instead of a bare reset (#100).

  • The TUI no longer crashes on a private-mode CSI sequence in an adapter log. The gemini
    CLI's startup burst includes XTMODKEYS CSI > 4 ; ? m; the marker byte sat inside the
    params, so the private-marker strip filter missed it and pyte 0.8.2 raised a TypeError
    that killed the poll worker — and the whole dashboard. The filter now matches a marker
    anywhere in the params, and any escape sequence pyte still can't parse is dropped instead
    of propagating (upstream fix exists but was never released — selectel/pyte#202) (#111).

  • An unreadable spec no longer crashes the whole run. Every spec read-back — the four verify
    gates, the reconcile/sprint/ledger bookkeeping passes, and the generic adapter's Stop poll — raced
    the dev skill's own writes, so a transient OSError (a TOCTOU truncation, a lock, an EACCES)
    escaped to engine.run() and abandoned every remaining story. Observation now degrades where
    repair still raises: verify gates return a retryable outcome naming the read fault (never a phantom
    status mismatch), bookkeeping passes skip and journal spec-read-failed, and the read-back poll
    treats it as not-yet-terminal, falling through to the existing stall/timeout → post-kill-reconcile
    ladder. Review routing re-derives followup_review_recommended from the finalized spec when a
    replayed result lacks it, so a fault that skips the reconcile re-fold can no longer silently skip
    a recommended follow-up review on resume (#97).

  • A resumed sweep re-drives its in-flight bundles by identity, not by bundle name. SweepEngine
    recovered a bundle only from inside _run_bundle, which a cycle reaches after re-deriving the key
    from the current triage plan — so a bundle re-armed by bmad-loop resolve survived a resume only
    because the cached triage.json reloaded and re-emitted the same name. Lose that cache and a fresh
    triage partitioned the ids under new names, silently orphaning the human's resolution. The sweep
    loop now opens with _finish_inflight_bundles, mirroring the base engine: every non-terminal dw*
    task is re-driven under its own persisted story_key, before the ledger is read, so its ids leave
    the open set and no fresh plan can re-bundle them. A still-escalated bundle stays terminal and
    untouched. A missing bundle intent file is regenerated from the task (the verbatim ledger entries
    become the contract; the triage prose is the only unrecoverable piece), and a bundle that survives
    to a cycle anyway is journaled + notified rather than dropped. Relatedly, a truncated or
    wrong-shaped triage.json now degrades to a fresh triage instead of crashing the whole run — the
    corrupt leg of this bug was previously unreachable. New journal events: sweep-inflight-redrive /
    -stranded, sweep-intent-regenerated. (#94)

  • Run ids are validated, so a run ref can no longer escape the runs directory. A positional ref
    (delete, stop, archive, resume, status) was recomposed into a path raw, so
    bmad-loop delete ../../x deleted any outside directory holding a state.json; the hidden
    --run-id flag on run/sweep reached a directory name, a multiplexer session name and a git ref
    unchecked. A supplied id must now match [A-Za-z0-9][A-Za-z0-9_-]* (≤ 120 chars, no reserved
    Windows device name) — rejected, never sanitized, so ids stay bijective with paths and sessions —
    and a ref that is absolute, climbs with .., or carries a separator skips the exact-match branch,
    falling through to partial matching over enumerated run dirs only. Ids recovered from the outside
    world — a bmad-loop-<id> session name, a <kind>-<id> control-session window name — pass the
    same validator before they steer a path. Partial refs unaffected (#104).

  • An abandoned patch-restore no longer smuggles its files into the corrected story's commit.
    Re-arming a story whose previous re-drive had already applied a restore patch snapshotted that
    patch's new (untracked) files as pre-existing, so every later rollback preserved them and
    finalize_commit's add -A swept the abandoned attempt into the corrected commit. The re-arm now
    parses the old latch (verify.patch_new_files) and subtracts its creations from the refreshed
    baseline snapshot — the re-drive's own reset then removes them. Best-effort: a missing or
    unreadable patch degrades to the old behavior instead of failing the resolve. Commits the
    escalated attempt left below the advanced baseline can't be reverted mechanically (the resolve
    session's own commits share that range), so they are journaled and echoed to stderr for the human
    to classify. New journal events: stale-restore-excluded / -unparseable / -commits.
    (closes #90)

  • Baseline-era untracked residue no longer vacuously satisfies the proof-of-work gate.
    has_changes_since counted every untracked file. After an intent-gap halt the saved patch is
    untracked residue under the artifact dirs every reset deliberately protects, so a from-scratch
    re-arm — which never learns the patch's path — let a re-driven session that produced nothing but a
    spec status flip pass the gate on that file's mere presence, and finalize_commit's add -A swept
    it into the story commit. The gate now subtracts the task's baseline_untracked snapshot. A None
    snapshot (a pre-upgrade run) still counts every untracked file — deliberately the opposite of
    attempt_dirty's ignore-all, because a proof-of-work gate has to fail open toward "work happened".
    (closes #88)

  • The baseline-match verify gate was dead code for generic dev sessions. The gate read the spec's
    baseline_commit and skipped itself when that key was absent — but bmad-dev-auto stamps
    baseline_revision; baseline_commit exists only in the orchestrator's synthesized result.json.
    In production the check never fired, so a spec claiming a stale or foreign baseline sailed through.
    The gate now reads either key, the idiom devcontract already used. The test fixture stamps
    baseline_revision like the real skill does, so it can no longer fabricate the key that hid this.
    (closes #89)

  • Unit keys with git-ref-illegal characters no longer break worktree runs. unit_branch_name
    built bmad-loop/<run_id>/<unit_key> from the raw ids, so a key or --run-id carrying :, ..,
    @{, a space or a trailing .lock cleared the (already-sanitized) worktree dir only to die at
    git worktree add with "is not a valid branch name". Both segments now go through a new
    platform_util.safe_ref_segment — identity for clean ids, -<hex8> digest suffix otherwise, on
    git's alphabet rather than Windows' (CON is a legal ref; a..b is a legal filename). A
    git check-ref-format oracle test pins the agreement; the attempt-preserve recovery-ref slugs
    now reuse the same sanitizer instead of their own inline one. (closes #102)

  • The deferred-artifact stash overwrites its target atomically. A story deferring a second time
    re-stashes the same spec filename over the previous one. shutil.move fell back to a non-atomic
    copy2 there — which tears the stash on a mid-copy crash and fails outright on Windows when an
    AV/indexer handle turns the rename into a sharing violation. The stash now stages a copy inside the
    destination dir and routes through platform_util.atomic_replace, inheriting its win32 retry; the
    source removal gets the same retry via a new platform_util.retrying_unlink, since Windows denies a
    delete against an open handle exactly as it denies a rename-over. (closes #101)

  • A finished session whose final Stop hook was lost no longer loses its work. A dev/review
    session that wrote its terminal spec but never delivered the Stop ended stalled — or timeout,
    when hooks were misconfigured and no event ever arrived — and the on-disk result was discarded.
    The adapter now re-reads the spec after the window is provably dead, rescuing a self-consistent
    successful terminal; every rescue still faces the full deterministic verify, and the journal records
    session-rescued-post-kill so it stays distinguishable from a live completion. (#95, closes #61)

  • A corrupt terminal artifact no longer crashes the whole run. A spec truncated mid-write (a
    multi-byte UTF-8 sequence cut in half) raised out of the read-back and past the per-task boundary,
    marking the run CRASHED and abandoning every remaining story. The read-back now degrades an
    undecodable spec to "no result yet" — the session retries or keeps its verdict — and the post-kill
    rescue additionally keeps its verdict on any read fault, so a best-effort rescue can never make
    things worse. The repair path still raises on purpose. (#95, closes #96)

  • Windows installs now pull psutil automatically — moved from the opt-in non-linux extra to a
    platform-scoped core dependency (sys_platform == 'win32'), so the TUI liveness column no longer
    shows every run as ? on a stock install. macOS keeps the non-linux extra; Linux stays dep-free.
    (#72, closes #71)

  • bmad-loop-setup no longer deletes live core BMAD config or the installer manifest. In a
    multi-module BMAD v6 project the setup scripts hardcoded core (and --also-remove _config) into
    their delete lists, destroying _bmad/core/config.yaml, per-module config, and the whole
    _bmad/_config/ manifest — breaking future npx bmad-method install upgrades. Cleanup now removes
    a directory only when it is a verified-redundant skill payload (has a SKILL.md, carries no
    config/manifest, and its skills are installed); live config dirs are protected and reported under
    directories_protected. The merge scripts read legacy config as fallback but never delete it. Same
    root cause as upstream bmad-code-org/bmad-builder#96. (closes #64)