Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to `bmad-loop` are documented here. The format is based on
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the project is pre-1.0,
breaking changes may land in a minor release.

## [Unreleased]

### Added

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

## [0.8.1] — 2026-07-05

### Fixed
Expand Down
61 changes: 43 additions & 18 deletions README.md

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
| Capability | What it does | Problem it addresses |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| Deterministic control loop | Story selection, retries, gates, completion checks run in plain Python | LLM-as-orchestrator is nondeterministic, hard to debug, and costs tokens for control flow |
| Dual planning pipelines | Same loop from either `sprint-status.yaml` (sprint mode, default) or a typed `stories.yaml` dispatched by folder+id (stories mode, opt-in) | Sprint boards need `bmad-sprint-planning`; a `bmad-spec` Story Breakdown has no board |
| Per-story human checkpoints | Stories-mode `spec_checkpoint` pauses to review the plan before code; `done_checkpoint` pauses after the commit; both independent, both surfaced in the TUI | Coarse run-global gates can't ask for a plan review on _this_ story only |
| Trust-nothing verification | Checks on-disk artifacts (spec status, baseline-commit match, non-empty diff, sprint sync) + runs your test/lint commands before commit | Agents claim success without working code; broken builds slip through |
| Fresh-context adversarial review | Dev and review are separate sessions; review uses 2 parallel layers (Blind Hunter / Edge Case Hunter) | Self-review anchoring bias; implementer marks own work correct |
| Hook-based transport | Coding-agent hooks write structured event files; skills write `result.json` | Brittle terminal pane-scraping |
Expand Down Expand Up @@ -105,9 +107,19 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
- Repeat mode (`--repeat` / `[sweep] repeat`): re-triages after each cycle to absorb newly generated deferred work, stopping when a cycle does nothing addressable or hits `max_cycles`.
- Sweeps are their own resumable runs (`bmad-loop resume <id>`).

### Stories mode (folder+id dispatch)

- Opt-in second story source (`[stories] source = "stories"` + `spec_folder`, or `bmad-loop run --spec <folder>`): drives the same loop off a typed `stories.yaml` (a `bmad-spec` Story Breakdown, sibling of `SPEC.md`) instead of `sprint-status.yaml`.
- Dispatches each entry by **folder + id** (`/bmad-dev-auto Spec folder: <folder>. Story id: <id>.`); the story spec lands at `<folder>/stories/<id>-<slug>.md` and is read back by a deterministic id-keyed glob — no shared board to line-edit, no result-artifact mtime-scan.
- Strictly linear schedule (list order, no `depends_on`); `done` skipped, non-terminal statuses resumed on re-dispatch, `blocked`/sentinel/ambiguous stops the run for resolve. `bmad-loop run --dry-run --spec <folder>` and `bmad-loop status` print the schedule/board (id · live disk state · checkpoint markers · title).
- Preflight content-probe: stories mode requires a `bmad-dev-auto` new enough for folder+id dispatch, or the run aborts with remediation. Sprint mode keeps working with any installed version.
- Sentinel recovery: a pre-planning-halt sentinel spec (`<id>-unresolved.md` / `<id>-ambiguous.md`) is auto-deleted with a preserved copy under the run dir on re-arm, matching the contract's delete-to-retry.

### Gates & human checkpoints

- Gate modes (`[gates].mode`): `none` (fully unattended) / `per-epic` (pause at epic boundaries, default) / `per-story-spec-approval` (pause after each spec for approval).
- Gate modes (`[gates].mode`): `none` (fully unattended) / `per-epic` (pause at epic boundaries, default) / `per-story-spec-approval` (pause after each spec for approval). Note: `per-epic` is inert in stories mode — the flat `stories.yaml` list has no epics, so the boundary never fires; use the per-story checkpoints (below) or `per-story-spec-approval` there.
- Per-story checkpoints (stories mode): independent `spec_checkpoint` (pause before code to review the plan; approve → implement, or request a replan) and `done_checkpoint` (pause after the story commits; skipped when it is the last story). Additive to `gates.mode` — a story can pause twice.
- Every mid-run pause is surfaced in the TUI: a per-run pause-kind badge, a global attention count, and a `p` viewer per stage (plan-checkpoint spec review, story-checkpoint summary card, escalation with story context, gate spec review) — all calling the same CLI code paths.
- Retrospective handling (`retrospective = never | notify | auto`) and notification on epic boundaries.

### Multi-CLI / multi-agent support
Expand Down
74 changes: 69 additions & 5 deletions docs/tui-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,12 @@ the pane recovers on the next poll once the file is readable again.

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`). 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.
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`). 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)

Expand All @@ -113,6 +116,29 @@ a status glyph:
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:

| Glyph | State | Color |
| ----- | ---------------------------------------- | -------- |
| `✓` | done | green |
| `▶` | in-progress | cyan |
| `◆` | in-review | magenta |
| `○` | ready-for-dev | cyan |
| `◦` | draft | dim |
| `·` | pending (no spec on disk yet) | dim |
| `✖` | blocked | bold 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
Expand Down Expand Up @@ -220,6 +246,7 @@ Journal kinds are styled by substring, first match wins:
| `r` | start a run (modal) |
| `s` | start a sweep (modal) |
| `e` | resume the selected paused/interrupted run (confirm modal) |
| `p` | review the selected paused run in the stage-appropriate HITL viewer |
| `R` | resolve a run paused at an escalation (interactive, then re-arm) |
| `d` | answer deferred-work decisions past sweeps left unanswered (modal walk) |
| `a` | attach to the selected run's live session or orchestrator window |
Expand All @@ -240,11 +267,15 @@ sections, `escape` goes back without saving. In any modal: `escape` cancels.

`r` opens the **start run** modal — all fields optional:

- **epic** — integer, restrict to one epic; blank = all
- **story key** — restrict to one story; blank = all
- **source** — `sprint 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
Expand Down Expand Up @@ -319,6 +350,39 @@ of the sprint. Detach (`Ctrl-b d`) to return to the dashboard, which observes th
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 gate** — reuses the spec viewer (view the finalized spec,
then **Approve & resume**), so the pre-existing sprint-mode gates inherit the same
richer surface.

`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:
Expand Down
43 changes: 43 additions & 0 deletions src/bmad_loop/adapters/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ def _artifact_dirs(self, cwd: Path) -> list[Path]:
return dirs

def _result_json(self, handle: SessionHandle, spec: SessionSpec, *, wait: bool) -> dict | None:
# Stories mode (folder+id dispatch): the story spec lives at a
# deterministic id-keyed path, so resolve it directly instead of the
# mtime-floor scan. The engine exports BMAD_LOOP_SPEC_FOLDER only for
# stories runs, so sprint/sweep runs keep the scan path below unchanged.
if spec.env.get("BMAD_LOOP_SPEC_FOLDER"):
return self._stories_result_json(handle, spec, wait=wait)
# Mirror the base _await_result poll: the skill's terminal spec may not be
# flushed to disk the instant the Stop event fires, so briefly await it when
# wait=True instead of reading once and mis-reporting a stall.
Expand All @@ -481,6 +487,43 @@ def _result_json(self, handle: SessionHandle, spec: SessionSpec, *, wait: bool)
return None
time.sleep(RESULT_POLL_S)

def _stories_result_json(
self, handle: SessionHandle, spec: SessionSpec, *, wait: bool
) -> dict | None:
"""Deterministic stories-mode read-back: resolve ``<spec-folder>/stories/
<id>-*.md`` by id (never the mtime scan) and synthesize from it.

``BMAD_LOOP_SPEC_FOLDER`` carries the project-relative (or absolute) spec
folder; rebase a relative one against ``spec.cwd`` exactly like
``_artifact_dirs`` so worktree isolation resolves inside the live checkout.
A PRESENT or SENTINEL spec synthesizes (a blocked sentinel becomes a
CRITICAL escalation → PAUSE, same as any block); a still-PENDING or
AMBIGUOUS state is not-yet-terminal → keep waiting, then None (a
result-less Stop the dev-stall grace handles).

On a plan-halt leg (``BMAD_LOOP_PLAN_HALT`` set by the engine for a
spec_checkpoint story's first dispatch) the skill HALTs at
``ready-for-dev``; pass ``plan_halt=True`` so synthesize treats that as a
successful terminal (marked ``plan_halt``) rather than died-mid-flight."""
from .. import stories

story_key = spec.env.get("BMAD_LOOP_STORY_KEY") or ""
folder = Path(spec.env["BMAD_LOOP_SPEC_FOLDER"])
base = folder if folder.is_absolute() else Path(spec.cwd) / folder
plan_halt = bool(spec.env.get("BMAD_LOOP_PLAN_HALT"))
deadline = time.monotonic() + RESULT_GRACE_S
while True:
state = stories.resolve_story_spec(base, story_key)

@augmentcode augmentcode Bot Jul 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/bmad_loop/adapters/generic.py:510: _stories_result_json() no longer uses handle.launched_ns (unlike the mtime-scan path), so an already-terminal story spec from a previous step (notably the pre-existing done spec when starting a follow-up review session) could be misread as this session’s completion even if the session produced no new output.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if state.kind in (stories.KIND_PRESENT, stories.KIND_SENTINEL) and state.path:
result = devcontract.synthesize_result(
state.path, story_key=story_key or None, plan_halt=plan_halt
)
if result.result_json is not None:
return result.result_json
if not wait or time.monotonic() >= deadline:

@augmentcode augmentcode Bot Jul 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/bmad_loop/adapters/generic.py:515: If resolve_story_spec() yields KIND_AMBIGUOUS (multiple <id>-*.md matches), this currently just waits and then returns None, which will look like a generic stall rather than an actionable “ambiguous story file” error requiring operator intervention.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

return None
time.sleep(RESULT_POLL_S)


# Back-compat alias: the adapter was ``GenericTmuxAdapter`` before tmux moved
# behind the multiplexer seam. Keeps existing imports stable.
Expand Down
Loading
Loading