TUI guide

August 19, 2026 · View on GitHub

bmad-loop tui is a live terminal dashboard for everything the orchestrator does: watching runs, launching new ones, resuming paused ones, answering sweep decisions, and editing policy. This guide covers every screen, key, and message. For the one-page summary, see the TUI section of the README.

Installation and launch

uv sync --extra tui        # adds textual + tomlkit; the core stays pyyaml-only
cd /path/to/your/bmad/project
bmad-loop tui              # or: bmad-loop tui --project /path/to/project

--project defaults to the current directory. The selected terminal-multiplexer backend — tmux by default; external backends install separately, see Terminal multiplexer backends — must be on PATH for the launch/attach keys (r s e a); pure observation works without it. (WSL counts as Linux, so tmux works there unchanged; native Windows awaits a Windows-capable backend.) bmad-loop mux shows which backend is selected and why, and the Settings editor's mux.backend (or the BMAD_LOOP_MUX_BACKEND env var) forces the choice per machine.

Terminal sizes, as measured. The modal dialogs — confirmations, the start/sweep option forms, the escalation and checkpoint viewers — are the tightest thing the TUI draws. The figures below describe those dialogs, not the dashboard behind them, which simply shows fewer rows as the window shrinks. 39 columns × 9 rows is the size at which a dialog's own chrome was measured to fit: its border, padding, title line and margins, a row of body, and the whole docked button row. EscalationModal is what sets it, and its height is width-dependent because its docked warning wraps — 9 rows at 39 columns but only 6 at 80 — so a roomier window buys height as well as width.

That is a measurement of the chrome, not of the text a dialog is handed, and it is not a size you can rely on. Titles, headers, warnings and file paths are docked outside the scrolling body, so each line they wrap to costs a row the body cannot give back, the body having floored at one row. That text comes from the caller and nothing bounds its length — a deferred-work ledger title, a spec: folder, a spec path. A long enough value therefore consumes the frame and clips the docked controls at any fixed size, so no figure here — 39 × 9, 80 × 24, or larger — is a minimum these dialogs will always meet. Bounding that text is tracked in #629, and the spec viewer's over-wide action row in #628.

What the figures do say is that these sizes were sufficient for the dialogs as measured, at the content lengths the test suite exercises. Measured at 39 columns: a deferred-work heading of about 150 characters still fits, while about 300 characters wraps to nine rows of title and pushes the close button off; the validate-findings viewer needs one extra row for a plain result and three when the document carries a long spec folder; and the spec viewer's copy path button alongside its action verbs is wider than a 39-column dialog can hold whatever the button metrics do, over and above a spec path that wraps. A standard 80 × 24 terminal was sufficient for every one of those measured examples, which is why it is the size quoted wherever the content is unbounded — as one that worked for what was measured, not as one that cannot be overrun.

Below 60 columns or 20 rows the dialogs degrade to a compact layout rather than clipping: the dialog clamps to the screen width, the action buttons shrink and tighten, and vertically the padding, the title margin and the button-row margin collapse while the buttons render one row tall instead of three. At 60 columns and 20 rows and above you get the full chrome, unchanged (#281).

Over a slow or high-latency link (SSH, Tailscale), a 60fps update stream can't drain in time and partial frames paint over old ones. Launch with bmad-loop tui --low-frame-rate to cap Textual to 15fps and disable animations (sets TEXTUAL_FPS / TEXTUAL_ANIMATIONS), or make it permanent with [tui] low_frame_rate = true in policy.toml (editable from the settings screen). An explicit TEXTUAL_FPS in the environment still wins.

Architecture: observer/launcher, never the engine

The TUI never runs an engine in-process. The two halves:

  • Launcherr, s, and e spawn detached bmad-loop processes as windows of a dedicated tmux session, bmad-loop-ctl. Windows are named run-<run-id>, sweep-<run-id>, or resume-<run-id>, run the same Python interpreter as the TUI (python -m bmad_loop.cli, immune to PATH/venv drift inside tmux), and stay open after exit showing [bmad-loop exited <code> — press enter] so you can inspect failures. Quitting or crashing the TUI does not touch them. The engine each run drives lives in a separate bmad-loop-<run-id> session; it is torn down when the run finishes (unless [adapter] cleanup_session_on_finish = false). These parked bmad-loop-ctl windows and any leftover bmad-loop-<id> sessions can be swept with c (see Cleaning up sessions). Each launch over an existing run records the id of the window it minted in the run dir (ctl-window), so attach/stop follow the run's live window even while an older same-run-id window is still parked (#482).
  • Observer — the dashboard reads only the artifacts the engine writes atomically into .bmad-loop/runs/<run-id>/: state.json, journal.jsonl, logs/<task-id>.log, ATTENTION, engine.pid. It polls the selected run every second (run list, sprint status, and the deferred-work ledger every 3 seconds) with stat-gated readers, so unchanged files are never re-parsed. Runs started from a plain shell show up identically — the TUI has no privileged channel.

Fast read-only commands (validate, dry runs) are the exception: they are captured and shown in a scrollable modal instead of spawned in tmux.

Dashboard layout

$\text{text} ┌─ \text{bmad}-\text{loop} — /\text{path}/\text{to}/\text{project} ─────────────────────────────────────────┐ │ \text{st} \text{run} \text{type} │ 20260611-091500-3\text{f2a} ▶ \text{running} │ │ ✔ 20260610-… \text{story}│ \text{started} 2026-06-11\text{T09}:15:00 \text{epic} 2 │ │ ▶ 20260611-… \text{story}│ \text{tasks} 8 \text{done} 5 \text{deferred} 1 \text{escalated} 0 │ ├──────────────────────────┤─────────────────────────────────────────────┤ │ ▼ \text{Epic} 1 · 4/4 ✓ │ \text{story} \text{phase} \text{dev} \text{review} … │ │ ▼ \text{Epic} 2 · 1/3 │ 2-3-\text{billing} \text{review}-\text{running} \times 1 \times 2 … │ │ ✓ 1-\text{auth} ├─────────────────────────────────────────────┤ │ ▶ 2-\text{search} │ \text{Journal} │ \text{Log} │ \text{Attention} │ ├──────────────────────────┤ 09:15:02 \text{session}-\text{start} \text{task\_id}=… │ │ \text{DW}-1 \text{Fix} \text{flaky} \text{retry} │ │ │ \text{DW}-2 ✓ \text{Polish} \text{help} \text{text} │ │ ├──────────────────────────┴─────────────────────────────────────────────┤ │ \text{q} \text{quit} \text{r} \text{run} \text{s} \text{sweep} \text{e} \text{resume} \text{a} \text{attach} \text{v} \text{validate} \text{g} \text{settings} … │ └─────────────────────────────────────────────────────────────────────────┘ $

…and the same layout, live:

The bmad-loop TUI dashboard, fully populated.

Left column

Three stacked panes; tab / shift+tab move focus between them, and their heights (like the sidebar width) are resizable — see Resizing panes. The sprint and deferred panes read project-level files written outside the TUI — sprint-status.yaml, which the orchestrator writes while a run is in flight and your own BMAD skill runs generate and edit outside one, and deferred-work.md — so both parse forgivingly: a missing or malformed file shows a dim placeholder instead of an error, and the pane recovers on the next poll once the file is readable again.

Run list (top)

One row per run dir under .bmad-loop/runs/, oldest first (run ids are YYYYMMDD-HHMMSS-<hex> and sort chronologically). Columns: st (status glyph, see below), run (the id), type (story or sweep), note (a colored pause-kind badge on a paused run — plan / story / spec / epic / gate / esc, or ⏹ stop when a running run has a graceful stop pending). When any run is paused awaiting a human the pane's title shows a global ⚑ N need attention count. On first load the newest run is auto-selected; arrow keys or mouse select another. A run you just launched is selected immediately, before its directory even exists.

Sprint tree (middle)

Sprint status from sprint-status.yaml as one expandable node per epic — Epic N · done/total, fully green with a once every story is done. Enter (or click) expands an epic to its stories and retrospective, each with a status glyph:

GlyphStatusColor
donegreen
in-progresscyan
reviewmagenta
ready-for-devcyan
·backlog / optional (retro)dim
?anything unrecognizeddim

Expansion state and the cursor survive the 3-second refresh — only labels are updated in place unless an epic's story set actually changes.

Stories board (middle, stories mode)

When the selected run is in stories mode (source = "stories"), the sprint tree is replaced in place by a flat stories board read from that run's stories.yaml + the id-keyed story specs on disk. One row per story: a state glyph + label, the story id, a two-slot checkpoint cell (S = spec_checkpoint, D = done_checkpoint, dim · for an unset slot), and the title. The state column reflects live disk state:

GlyphStateColor
donegreen
in-progresscyan
in-reviewmagenta
ready-for-devcyan
draftdim
·pending (no spec on disk yet)dim
blockedbold red
ambiguous / sentinel (sentinel:<kind>)bold red

Selecting a sprint-mode run swaps the sprint tree back. The board re-derives each poll so it tracks the dev sessions writing story specs.

Deferred work (bottom)

Every entry from the deferred-work.md ledger, in file order: DW-<n> plus the title, truncated to the pane width. Done entries are green with a ; open entries are color-coded by the entry's optional severity: field — critical (bold red), high (red), medium (yellow), low (dim), unspecified (plain). Arrow keys navigate; enter opens the full entry body in a scrollable modal (escape closes).

Run header (top right)

A one-glance summary of the selected run: id, [sweep] tag for sweep runs, status glyph + word, start timestamp, current epic, and a counts line — tasks N · done (green) · deferred (yellow) · escalated (red when nonzero) · tokens, where the token figure reads <weighted> tokens (<raw> raw) — the cost-weighted total first (cache reads at limits.cache_read_weight), the unweighted one in parentheses. Below the counts, an agent line names who is driving: while a session is open it reads agent <name> · <model> · <role> (the resolved adapter for the live stage — model omitted when the session ran the CLI profile's default, role is the stage dev / review / triage); when no session is open it falls back to the run's configured adapters, rebuilt from the run's policy snapshot — agents <name·model> when dev and review resolve alike, else agents dev <name·model> review <name·model>, plus a triage <name·model> on sweep runs. A run that predates adapter stamping (no rebuildable snapshot) shows no agent line at all rather than a fabricated default. Below that, situational banners:

  • ⏸ paused (<stage>) — <reason> · press e to resume — gate or escalation pause; stages are spec-approval, epic-boundary, escalation, story-gate. At the escalation stage, e only skips the escalated story — press R instead to resolve it (see "Resolving an escalation" below).
  • ⏹ graceful stop pending — will stop after the current item — a graceful stop was requested (S, or bmad-loop stop --graceful); the run finishes the in-flight story/bundle through commit (or, mid-sweep-triage, lets triage complete and starts no bundles), then finalizes and stops (resumable).
  • ✖ engine gone — run was interrupted · press e to resume — the recorded engine pid is dead.
  • ⚑ decision needed: DW-<n> — <question> / press a to attach and answer — an attended sweep is blocked on a human decision (see below).
  • ⧗ starting… waiting for the engine to write state.json — just launched; if nothing appears within 10 seconds the TUI raises a "launch may have failed" error toast.

Task table (middle right)

One row per story (or sweep bundle/triage task) in the selected run:

ColumnMeaning
storystory key, or the sweep task id
phasependingdev-runningdev-verifyreview-runningreview-verifycommittingdone; terminal alternatives deferred / escalated; sweep triage shows triage-running / triage-verify
agentthe adapter driving this story, <name·model> (·model dropped when it's the profile default). The live agent when it's working this row, else the last session that stamped an identity (#153), else - (nothing recorded)
devdev attempt counter, ×N
reviewreview cycle counter, ×N
tokenscost-weighted token total for the story — cache reads counted at limits.cache_read_weight (default 0.1), the same total the budgets judge. - until known; a real 0 (possible with the weight set to 0) is shown as 0, never -
infodefer reason, or the commit SHA (first 12 chars) once committed
rawunweighted token total, cache reads at full price — typically 5–8x the tokens column on agentic work

Tabs (bottom right)

  • Journal — every engine decision, live-tailed from journal.jsonl. Line format: HH:MM:SS <kind> field=value … (long values truncated with ). Kinds are color-coded — see the reference below. Every session lands a session-end unconditionally (status aborted when the outcome is unknowable, even after a teardown that threw); a timed-out one carries fired_at, teardown_s, and expired_clock (monotonic / wall / bothwall alone fingerprints a host suspend that froze the monotonic clock). Entries whose usage was read carry tokens (raw) and tokens_weighted (cache reads at limits.cache_read_weight); both are null if 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). The matching tasks/<id>/ dir holds the forensic breadcrumbs the adapter wrote while the session ran: session-lifecycle.jsonl (timeout-fire, budget-guard budget-tripped / over-budget-fired, kill-escalation, session-vanished (the mux no longer reported the session during the run, #489), and the #276 missing-marker forensics spec-status-transition-observed / frontmatter-unmodified-refused / contract-nudge-sent), heartbeat.json (the wait loop's proof-of-life — stale under a live session means the orchestrator itself was frozen), and resultless-stops.jsonl (each give-up Stop with its verdict: no-artifact, ambiguous-frontmatter, unmodified-since-launch — the spec's bytes were unchanged since review launch, so it is a prior done re-opened, not this session's output (#276) — or terminal-frontmatter-pending).
  • Log — the active agent session's pane output (logs/<task-id>.log), ANSI colors preserved, starting with a dim — <task-id>.log — header. The active task is the last session-start without a matching session-end (falling back to the newest log file); the tab switches automatically when the engine moves to the next session. Only the last 64 KB of a large log is read on first open.
  • Attention — the run's ATTENTION file (escalations, gate notifications). New lines after the first poll also fire a warning toast.

Selecting and copying text

The Log and Attention panes are click-drag selectable: highlight with the mouse and press ctrl+c to copy the selection, or press y to copy the whole active pane in one keystroke. The other panes (Runs, Sprint, Deferred Work, the task table, Journal) are interactive widgets and are not drag- selectable in-app.

To select text in any pane — or when the in-app copy can't reach your system clipboard — hold your terminal's bypass modifier while dragging to use the terminal's own selection instead of the TUI's: Shift on most Linux terminals and Windows Terminal, Option/Alt on iTerm2/macOS.

ctrl+c and y copy via OSC 52. Inside tmux that only reaches the system clipboard when tmux forwards it — set set -g set-clipboard on and use an OSC 52-capable outer terminal. The Shift/Option-drag native selection above is the reliable fallback when OSC 52 isn't available.

Status reference

Run status is classified from state.json plus a liveness probe:

GlyphStatusColorMeaning
runninggreennot finished, not paused, engine pid alive
pausedyellowengine is waiting at a gate or escalation — e resumes
finisheddimrun completed
interruptedbold redengine pid is dead but the run never finished — e resumes
?unknowndimliveness can't be determined, or state.json is unreadable

Liveness is local-only: engine.pid is checked with os.kill(pid, 0). A run driven on another host (shared checkout) always shows unknown, never falsely interrupted. Legacy runs without a pid file fall back to probing the per-run tmux session, which can prove alive but never dead.

Journal kinds are styled by substring, first match wins:

SubstringColorExamples
escalat, failedredpreference-escalation, review-verify-failed
done, complete, finishedgreenstory-done, run-complete
decision, deferred, boundary, truncatedyellowdecision-pending, epic-boundary
start, resumecyansession-start, run-resume
anything elsedim

Key bindings

KeyAction
rstart a run (modal)
sstart a sweep (modal)
eresume the selected paused/interrupted run (confirm modal)
preview the selected paused run in the stage-appropriate HITL viewer
Rresolve a run paused at an escalation (interactive, then re-arm)
danswer deferred-work decisions past sweeps left unanswered (modal walk)
aattach to the selected run's live session or orchestrator window
xstop the selected live run immediately (confirm modal)
Sgraceful stop: finish the in-flight item, then finalize & stop (confirm)
Ddelete the selected run's directory (confirm modal)
Aarchive the selected run to .bmad-loop/archive (confirm modal)
cclean up tmux sessions/windows for finished & stopped runs (confirm modal)
vrun bmad-loop validate, findings in a modal (d toggles detail)
gsettings editor for .bmad-loop/policy.toml
Mtoggle theme (light/dark mode)
ycopy the active Log/Attention pane to the clipboard
ctrl+wenter/leave pane resize mode (see below)
qquit (running engines are unaffected)

In the settings editor: ctrl+s saves, ctrl+e expands/collapses all sections, escape goes back without saving. In any modal: escape cancels.

Resizing panes

Every pane boundary is adjustable, by mouse or keyboard, and the sizes persist per-project to [tui] in policy.toml (re-applied on the next launch).

  • Mouse — drag any divider bar: the column split between the left sidebar and the detail pane, the bars between the stacked left-column panes (they double as the Sprint / Deferred Work section headings), and the bar between the Tasks table and the Journal/Log/Attention tabs. Bars highlight on hover.
  • Keyboard — press ctrl+w to enter resize mode (the header shows a hint): / widen or narrow the sidebar, / move the active horizontal boundary, Tab/Shift+Tab pick which horizontal boundary is active, and Esc (or Enter) leaves the mode. Outside resize mode the arrows and Tab keep their usual navigation/focus behavior.

To forget custom sizes, delete the [tui] size keys from policy.toml (or set them to 0) — the panes return to their default proportions.

Starting runs and sweeps (r / s)

r opens the start run modal — all fields optional:

  • sourcesprint mode (walks sprint-status.yaml) or stories mode (folder+id dispatch), prefilled from [stories]
  • spec folder — stories mode only: the epic spec folder holding stories.yaml + SPEC.md; feeds a live schedule preview that validates the manifest and lists the linear schedule with [spec/done] checkpoint markers and each story's live disk state
  • epic — integer, restrict to one epic (sprint mode); blank = all
  • story key — restrict to one story; a story id in stories mode; blank = all
  • max stories — stop after N stories; blank = no limit
  • dry run — print the plan, spawn nothing (output shown in a modal)

Selecting stories mode with an empty spec folder is refused with a toast; the run's preflight also validates the manifest + SPEC.md before spawning.

s opens the start sweep modal:

  • unattended (--no-prompt) — skip decision prompts, leave decisions open
  • decisions only — triage + answer decisions, run no bundles
  • max bundles — override the policy's [sweep] max_bundles; blank = policy default
  • dry run — list open ledger entries, spawn nothing

Before any real launch the TUI applies the same guard as the CLI:

  1. tmux must be on PATH.
  2. The git worktree must be clean — otherwise an error toast, no launch.
  3. If another run on this project is currently running, a confirmation modal lists it and asks before you "launch anyway" (two engines on one project may conflict).

On success a toast names the run id and the bmad-loop-ctl session, and the dashboard selects the new run, showing ⧗ starting… until state.json appears.

Resuming (e)

e acts on the selected run. It refuses runs that are already finished or whose state is unreadable. The confirmation modal shows what you are resuming:

  • paused runs: paused at <stage> — <reason> in yellow;
  • non-paused runs: run is not paused — it looks interrupted (dim);
  • and, in bold red, engine.pid is still alive — resuming would double-drive this run when the original engine still appears to be running. Heed this one: two engines driving one run dir corrupt each other's state. It can also mean the pid was recycled by another process — verify before resuming.

Confirming spawns bmad-loop resume <run-id> detached in bmad-loop-ctl, like any other launch. Resume drops any stale bmad-loop-<run-id> session a stopped or interrupted run left behind and spins up a fresh one, so the run never re-attaches to a dead session.

Cleaning up sessions (c)

c removes leftover tmux artifacts for the current project in one pass, after a confirmation modal: every bmad-loop-<run-id> agent session whose run has finished, stopped, or crashed (and any orphan whose run dir is gone), plus the parked [bmad-loop exited …] windows in bmad-loop-ctl. Live runs, the window you triggered the cleanup from, and any session or window belonging to another project are always spared, so it is safe to press at any time even with other projects' runs in flight. A toast reports how many sessions and windows were closed. The same sweep is available from a plain shell as bmad-loop cleanup (--dry-run to preview). Runs already tear their own session down on finish unless you set [adapter] cleanup_session_on_finish = false; c is for the backlog that predates that, or that the flag deliberately keeps around.

c only reaps tmux artifacts, not disk. To reclaim run-dir disk — worktrees a mid-flight stop orphaned (each holding a Unity Library/), and old run dirs — use the bmad-loop clean CLI command (see [cleanup]); it is intentionally not bound to a TUI key since it deletes/archives run history.

Resolving an escalation (R)

R is the escalation-specific counterpart to e. It is only offered for a run paused at the escalation stage (otherwise it warns and does nothing) and refuses a run whose engine is still live. A CRITICAL escalation parks its story in a terminal escalated phase that plain resume skips — R is how you get it un-stuck.

Confirming launches bmad-loop resolve <run-id> in a bmad-loop-ctl window and attaches you to it (the resolve agent is interactive). You converse with the agent — it is seeded with the escalation detail and the frozen spec — to disambiguate the spec. When it has recorded a resolution, the same window prompts re-arm <story> and resume run <id>? [y/N]; answer y and it re-arms the story (escalated → pending, spec status reset to ready-for-dev) and resumes the run in place — a clean rebuild against the corrected spec, then on through the rest of the sprint. Detach (Ctrl-b d on tmux; other backends have their own chord — herdr's is ctrl+b q) to return to the dashboard, which observes the resumed run like any other. Exiting the agent without recording a resolution leaves the story escalated and the run paused — the safe default.

Reviewing a paused run (p)

p opens the stage-appropriate HITL viewer for the selected paused run, dispatched on RunState.paused_stage. Every action calls the exact code path the CLI uses — no duplicated logic — and every viewer is a read-only presentation of artifacts the engine already wrote.

  • Plan checkpoint (spec_checkpoint, stories mode) — a read-only viewer of the planned ready-for-dev spec at its id-keyed path (shown prominently, with a copy-path action). Approve & resume resumes straight to implementation; Request replan resets the spec to draft and strips its Auto Run Result (via the same devcontract primitives the engine's repair path uses), then resumes so the next dispatch re-plans. Edit the markdown in your own editor — the TUI never edits specs.
  • Story checkpoint (done_checkpoint, stories mode) — a summary card for the just-committed story: id/title, commit subject + short hash, verification outcome, and cost-weighted + raw token totals. Continue run resumes the schedule; Stop run marks the run stopped.
  • Escalation — the escalation view enriched with story context: the story entry's title/description (from stories.yaml), the blocking condition parsed from the spec's ## Auto Run Result, and a sentinel indicator when the matched spec is a fixed-slug pre-planning-halt sentinel. Resolve launches the same interactive agent as R; Re-arm & resume (offered once the resolve agent has recorded a resolution) re-arms and resumes — deleting a sentinel with a preserved copy for a clean re-dispatch. Both refuse a still-live engine.
  • Spec-approval / epic / story gate — reuses the spec viewer (view the finalized spec, then Approve & resume), so the pre-existing sprint-mode gates inherit the same richer surface. A story gate fires before the story is recorded, so it has no spec to show; read its reason — which names the blocking entries and the remedy — in the run-header banner or the resume confirmation.

p and R overlap for an escalation (both reach Resolve); p also exposes Re-arm & resume inline once a resolution exists. Pause badges in the run list and the run header name the stage so you know which viewer p will open.

Attaching (a) and the sweep decision flow

a picks its target in this order:

  1. Decision-blocked sweep, or no live agent session → the run's orchestrator window in bmad-loop-ctl (only exists for runs launched from the TUI).
  2. Live agent session → the per-run tmux session bmad-loop-<run-id> where the coding CLI is working.
  3. Neither → a warning; there is nothing to attach to (runs started outside the TUI between sessions, finished runs).

If the TUI itself is running inside tmux, attach uses switch-client — the TUI keeps running and you switch back with your usual tmux client commands. Outside tmux, the TUI suspends, runs tmux attach, and resumes when you detach (ctrl-b d). External backends follow the same two paths with their own chords and caveats — on the herdr adapter the detach chord is ctrl+b q and one hand-back is manual; see its operator guide.

Answering a sweep decision

An attended sweep that reaches a "needs human decision" entry blocks on its own terminal prompt. The dashboard spots the decision-pending journal event and shows the ⚑ decision needed: DW-<n> banner plus a one-time warning toast. Then:

  1. Press a — with a decision pending this always targets the sweep's orchestrator window, where the prompt is waiting.
  2. Answer the prompt (build / close / keep-open, with the triage recommendation shown).
  3. Detach with ctrl-b d. (On tmux this step is belt-and-suspenders — answering already hands your terminal back. On backends without a detach verb — e.g. the herdr adapter — the hand-back cannot be automatic: press its detach chord, ctrl+b q on herdr. Either way the sweep knows it can no longer reach you and goes unattended for the rest of the run, so detaching first never leaves it waiting on a prompt you can't see.)

The banner clears on the next poll after the sweep journals anything further (the answer is recorded as a decision: line in deferred-work.md). Sweeps launched with unattended never prompt, so this flow only applies to attended sweeps.

Answering missed decisions (d)

The flow above is for a decision a live attended sweep is blocked on. For decisions an unattended sweep skipped — or an attended one you walked away from — press d. The Deferred Work pane title shows the outstanding count (Deferred Work — N to answer (d)), and d walks them one modal at a time (question, context, and each option with its effect and the triage recommendation). Each answer is durable: a close is applied immediately, and a build/keep-open is saved to .bmad-loop/decisions.json, so the next sweep acts on it (build → bundle, keep-open → recorded) without asking again. Skip a modal to leave that one for later. The same set is available on the CLI via bmad-loop decisions (--list to just view).

Validate (v)

Runs bmad-loop validate --project <project> --json in the background and renders the document in a scrollable modal: a verdict header with the per-severity counts and the queue mode, then one row per check — glyph, the stable check id, and the message. Same preflight as the CLI: config, sprint-status, git, tmux, CLI binary, hooks.

The verdict is the document's own ok, not the exit code, which cannot tell "the checks failed" from "the command broke". A check's detail — the data it knew before flattening itself into a sentence, like every detected mux backend rather than the text mode's tmux*, psmux (unavailable) — shows inline for warnings and problems, and d toggles it on for everything. When anything failed, a footer notes that the gates are chained: a policy failure leaves the later gates emitting nothing at all, so a short findings list after a failure is not a short list of problems.

If the document cannot be rendered — a schema newer than this TUI, or output that is not a document at all — v silently re-runs validate in text mode and shows the combined output in a modal titled validate — ok (or exit <code>), exactly as it did before.

Settings editor (g)

Edits .bmad-loop/policy.toml comment-preservingly (tomlkit): saving only rewrites keys you actually changed; everything else — comments, order, formatting — stays byte-identical. A missing policy file starts from the full inline-documented template. The note at the top is load-bearing: running engines snapshot policy at start — changes apply to new runs and resumes. Because a resume re-stamps, editing limits.cache_read_weight and resuming re-weights that run's whole accumulated history, not just the sessions after the resume — every total is recomputed from raw counts at the current weight.

The form is grouped by TOML section. Every section starts collapsed with a one-line description in its header, so the grown-large form scans at a glance — expand only the section you want to edit, or press ctrl+e to toggle them all open/closed at once. Unset keys show their default as a placeholder rather than a baked-in value; clearing a field deletes the key, restoring default/inherit behavior.

Section.keyTypeDefaultNotes
gates.modeselectper-epicnone / per-epic / per-story-spec-approval
gates.retrospectiveselectnotifynever / notify / auto
limits.max_review_cyclesint ≥ 13review loop bound before plateau-defer
limits.max_dev_attemptsint ≥ 12dev retry budget
limits.max_followup_reviewsint ≥ 01extra review rounds granted for a finalized pass's own follow-up before it converges + refiles instead of burning a cycle · 0 = never honor one
limits.session_timeout_minint ≥ 190per-session wall clock
limits.git_timeout_sint ≥ 1120bound on any single git subprocess; exceeding it pauses/degrades, never crashes the run — raise on a loaded host or a very large worktree
limits.teardown_grace_sint ≥ 020verified teardown: poll a killed session up to this long, then force-kill its pane pids and re-kill · 0 = single unverified best-effort kill
limits.stop_without_result_nudgesint ≥ 01nudges when a session stops without result.json
limits.dev_stall_grace_sint ≥ 0600silence grace armed at dev/review launch and re-armed by transport activity or fresh Stop/idle evidence · 0 = no launch timer, but a result-less turn end still fails fast
limits.dev_stall_nudgesint ≥ 02best-effort wake nudges per silent grace before stalling; fresh Stop/idle evidence restores this budget · 0 = stall on grace expiry
limits.dev_stall_nudges_capint ≥ 06total (never-restored) nudge bound per dev/review session — an accepted nudge does not guarantee a wake · 0 = stall on first grace expiry
limits.workflow_stall_nudges_capint ≥ 03same monotonic cap for an injected plugin-workflow session that finished its work but never wrote its completion marker · 0 = stall on first grace expiry
limits.max_tokens_per_storyint ≥ 12000000advisory cost-weighted cap on a story's cumulative spend, re-checked at every session boundary; one ATTENTION + desktop notice per story on the crossing, nothing terminated
limits.cache_read_weightfloat 0.0–1.00.1cache-read weight in every token total, budgets and displays alike; 1.0 = raw
limits.session_budget_modeselectwarnmid-session per-session budget guard: off (no sampling) / warn (one ATTENTION + breadcrumb) / enforce (wrap-up nudge, then over_budget termination → retry/defer)
limits.max_tokens_per_sessionint ≥ 14000000weighted per-session cap sampled every ~30s mid-session; healthy sessions run ~1–2.5M weighted, so the default trips only true runaways
limits.session_budget_grace_sint ≥ 0240enforce mode: wrap-up window after the nudge before over_budget · 0 = terminate at trip, no nudge
verify.commandsone per line(none)test/lint commands run before commit
verify.stream_capture_kbint ≥ 0256per-stream cap (KiB) on verifier stdout/stderr retained under the run's verify/ directory; the tail is kept and the journal records the full size plus a truncation flag · 0 = capture nothing
notify.desktopswitchondesktop notifications
notify.fileswitchonATTENTION file logging
review.enabledswitchonoff = skip the separate review session; dev pass runs its review layers inline
review.triggerselectrecommendedrecommended (run only when the dev pass flags followup_review_recommended) / always; bounded by limits.max_review_cycles
adapter.nametextclaudeCLI profile: claude / codex / gemini / custom
adapter.modeltext(CLI default)model override
adapter.extra_argsoverride switch + argsprofile defaultssee below
adapter.cleanup_session_on_finishswitchonkill the run's tmux session on finish; off keeps it
adapter.dev / .review / `.triage$\text{text} \times 2 + \text{args}\text{inherit}\text{per}-\text{stage} $name/model/extra_args` overrides
sweep.autoselectnevernever / per-epic / run-end
sweep.max_bundlesint ≥ 15bundles per sweep; triage excess truncated
sweep.max_triage_attemptsint ≥ 12triage validation retries
sweep.repeatswitchoffre-triage after each cycle, continue on new work
sweep.max_cyclesint ≥ 15cycle cap per sweep run when repeat is on
scm.isolationselectnonenone (work in place) / worktree (per-unit worktree + merge-back)
scm.branch_perselectstoryworktree mode: branch per story, or one shared branch per run (forces delete-branch off)
scm.target_branchtext(run-start branch)worktree mode: branch units merge back into (created if missing)
scm.merge_strategyselectmergeworktree mode: ff / merge / squash
scm.delete_branchswitchonworktree mode: delete the unit branch after a successful merge
scm.keep_failedswitchonkeep a failed unit's worktree + branch mounted for inspection
scm.rollback_on_failureswitchoffin-place mode (isolation = none) only: on = auto-revert a failed attempt's tracked changes + delete the untracked files this run created (its uncommitted work is lost); off = never touch the tree, pause with manual recovery. A resolved escalation's re-drive always auto-recovers regardless. Prefer isolation = worktree
scm.preserve_keepint ≥ 020attempt-preserve/* recovery branches + attempt-preserve-dirty/* snapshots kept per family at each run start, newest by committer date; the tail is deleted (0 = never prune)
scm.seed_adapter_defaultsswitchonworktree mode: seed each loaded adapter's gitignored MCP/CLI configs (.mcp.json, .claude/settings.json, .codex/config.toml…) into the worktree
scm.worktree_seedone per line(none)worktree mode: extra project-relative gitignored files to seed, on top of the adapter defaults
scm.commit_message_templatetext(built-in)story/bundle commit message; {story_key} / {run_id} / {story_title} substituted
scm.failed_diff_max_mbint ≥ 15per-file cap (MB) for untracked files in a kept-failed unit's changes.patch
scm.failed_diff_unlimitedswitchofflift the failed-diff size cap (warns when active)
cleanup.run_retentionint ≥ 010newest concluded runs bmad-loop clean keeps whole; older ones trimmed/archived (0 = keep none by count)
cleanup.retention_daysint ≥ 000 = off; else also keep runs newer than N days regardless of the count above
cleanup.trim_artifactsswitchondrop the heavy worktrees/ tree from concluded runs; the run still lists in the dashboard
cleanup.archive_oldswitchonarchive (vs hard-delete) runs past the retention window
cleanup.auto_clean_on_finishswitchonreconcile worktrees leaked by a mid-flight stop at each run/sweep start
cleanup.clean_tmpswitchonlet engine plugins clean their /tmp scratch on finish (e.g. the Unity MCP server zips)
plugins.unity.editor_modeselectsharedshared (needs scm.isolation = none) / per_worktree (needs isolation = worktree)
plugins.unity.mcpselectivanmurzakEditor MCP the plugin targets: ivanmurzak / coplaydev
plugins.unity.unity_pathtext(auto-detect)explicit Editor binary for a per_worktree launch; ignored in shared mode
plugins.unity.ready_timeout_secint ≥ 1600readiness-gate budget for the Editor + MCP to come up
plugins.unity.ready_grace_secint ≥ -1-1pre-probe delay; -1 = auto (120s cold per_worktree, 0s warm shared)
tui.low_frame_rateswitchoffcap to 15fps + disable animations (slow/SSH links); applies next launch
mux.backendtext(auto-select)force a registered terminal-multiplexer backend by name (see bmad-loop mux); machine-specific — policy.toml is gitignored; applies next invocation

(scm.max_parallel is intentionally not exposed in the editor — it stays inert, clamped to 1, until parallel fan-out is built.)

The form is registry-driven: the core sections above are described by bmad_loop/data/settings/core.toml (presentation only — defaults and options are referenced from the policy dataclasses, never duplicated), and every enabled plugin's [[settings]] are appended automatically under a collapsible section of their own, persisting to [plugins.<name>]. So the plugins.unity.* rows above only appear once unity is listed in [plugins] enabled — the opt-in layer for game projects that drive a live Editor through an MCP, off by default. A custom plugin dropped at .bmad-loop/plugins/<name>/ surfaces the same way. See Writing a bmad-loop plugin for the settings schema, Writing a Game Engine plugin, and Writing a plugin for a specific Editor MCP. The Unity plugin's editor_modescm.isolation coupling is validated on save, so an invalid combo (e.g. per_worktree with isolation = none) blocks with an error.

extra_args fields are special: the switch distinguishes "use the profile's default flags" (off — the key stays absent) from "replace them with exactly this list" (on — the input is parsed shell-style; an empty list is a valid override and is not the same as unset).

ctrl+s validates the whole document through the engine's own parser (policy.loads()) before writing; errors land in a red strip above the buttons and block the save. The write itself is atomic (temp file + os.replace).

Troubleshooting

MessageCause / fix
multiplexer backend unavailable — launch/attach disabledinstall the selected backend's binary (tmux by default — see bmad-loop mux); the dashboard still works read-only
git worktree is not clean — commit or stash firstthe launch guard; commit/stash and retry. .bmad-loop/policy.toml is exempt, so saving in the settings editor never blocks a launch
another run is live: <ids>a second engine on the same project may conflict — confirm only if you know they won't touch the same stories
launch may have failed — attach to control session bmad-loop-ctlno state.json within 10 s of launch; attach to the ctl window to read the error (the window stays open with the exit code)
no run selectede / a need a selected run — the project has no runs yet
state for run <id> is unreadablecorrupt/missing state.json; inspect the run dir
run <id> already finishedfinished runs can't be resumed
nothing to attach: no live agent session … runs started outside the TUI have nonebetween sessions there is no agent window, and shell-started runs have no ctl window; wait for the next session or attach manually
cannot suspend here — run manually: tmux attach …the terminal can't suspend the TUI; run the printed command in another terminal
engine.pid is still alive — resuming would double-drive this runthe original engine still runs (or its pid was recycled); attach and check before resuming
policy.toml is not valid TOML: …hand-edited file is syntactically broken; fix it in an editor — the settings screen needs a parseable document to start from
sprint tree shows sprint status unavailablemissing/invalid _bmad/bmm/config.yaml or sprint-status.yaml; run bmad-loop init / bmad-sprint-planning
deferred pane shows deferred ledger unavailablemissing/unreadable deferred-work.md; normal until the first session defers something
header shows state unavailablethe run dir exists but state.json is missing or never parsed; usually transient at launch

Degradation is graceful by design: a mid-write or missing file never crashes a poll — the dashboard keeps the last good state (? / unknown where it has none), and catches up on the next tick.