Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
48121c9
feat(engine): auto-resolve deferred-work entries a story declares via…
pbean Jul 24, 2026
f993907
feat(validate,docs): pre-flight warning for unknown closes_deferred i…
pbean Jul 24, 2026
b8ae6c0
feat(stories): declare closes_deferred on the stories.yaml entry, not…
pbean Jul 24, 2026
f1c13fd
fix(engine): close declared deferred-work at the commit boundary, not…
pbean Jul 24, 2026
c0f139d
fix(validate,stories): one shared closes_deferred reading, warned in …
pbean Jul 24, 2026
e269b2c
docs: correct the closes_deferred claims the review found overstated …
pbean Jul 24, 2026
2c81215
fix(deferredwork): preserve the ledger's mode and symlink across the …
pbean Jul 24, 2026
27c8142
fix(engine): close deferred work only for a story that durably landed…
pbean Jul 24, 2026
b8415f8
docs: closure is undone if the commit fails, and retried if it never …
pbean Jul 24, 2026
e9f9ff4
test(engine): pin the in-place external-ledger closure landing after …
pbean Jul 24, 2026
1aebc7a
fix(engine): report the ids a rollback actually un-flipped (#234 review)
pbean Jul 24, 2026
0291fe5
fix(engine): close deferred work against the spec at the commit, not …
pbean Jul 24, 2026
a84507a
fix(engine): drop a parked closure the re-drive no longer declares (#…
pbean Jul 24, 2026
5e34926
fix(platform): fsync the temp file before the replace publishes it (#…
pbean Jul 24, 2026
e52d310
test: pin the two contracts these assertions only looked like they co…
pbean Jul 24, 2026
a2e7bc9
fix(engine): arm the deferred-close rollback before the ledger write …
pbean Jul 24, 2026
8fb0aa7
fix(engine): a spec that is gone or out of the roots declares nothing…
pbean Jul 24, 2026
3decb08
fix(worktree): stamp the merge proof at the merge, not after its tail…
pbean Jul 24, 2026
54bff9f
fix(engine): an unavailable ledger location is not an answer about th…
pbean Jul 24, 2026
eec9857
fix(stories): an unreadable manifest is a StoriesError like every oth…
pbean Jul 24, 2026
2fe4066
fix(validate): report a ledger it cannot read instead of returning si…
pbean Jul 24, 2026
087fd33
style: black formatting on the new external-ledger test (#234 review)
pbean Jul 24, 2026
d6cba04
fix(deferredwork): one duplicate id, one answer — classify what the w…
pbean Jul 25, 2026
181f7b5
fix(frontmatter,engine): an unparseable spec is not a spec declaring …
pbean Jul 25, 2026
86844f6
fix(engine): a bookkeeping write must not be able to skip a checkpoin…
pbean Jul 25, 2026
ab0568b
fix(engine,verify): restore the index with the tree when a commit rol…
pbean Jul 25, 2026
ca0d68e
fix(engine): an unavailable ledger is owed only while the run can sti…
pbean Jul 25, 2026
1346617
docs: the closure is retried while the run is resumable, not forever …
pbean Jul 25, 2026
9dd7bbc
style: black + prettier on the round-6 review fixes (#234 review)
pbean Jul 25, 2026
20677c4
test: cover the run-end release on Windows too (#234 review)
pbean Jul 25, 2026
6f27142
fix(verify): an advisory repair must not raise past the failure it is…
pbean Jul 25, 2026
c7a26fa
fix(engine): a spec nobody could stat is not a spec that was withdraw…
pbean Jul 25, 2026
03add18
fix(deferredwork,engine): undo the close, not the whole ledger (#234 …
pbean Jul 25, 2026
09a2fe3
fix(engine): one read decides both whether the ledger is there and wh…
pbean Jul 25, 2026
e10aae8
fix(engine): every retry site goes through the guard, not just one of…
pbean Jul 25, 2026
f00048a
docs: the rollback is targeted, and an unreadable ledger cannot crash…
pbean Jul 25, 2026
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
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,58 @@ breaking changes may land in a minor release.

### Added

- **Stories can close deferred-work entries (#234).** The ledger was one-way: the loop reliably
_files_ `deferred-work.md` entries but never _marked_ one resolved when a later story closed it,
so a multi-epic run ended with entries satisfied epics ago still reading `open` and the retro
reconstructing by hand which story closed what. A story spec can now declare the entries its work
closes — `closes_deferred: [DW-5, DW-6]`, on its `stories.yaml` entry (stories mode) or in the
story spec's frontmatter, the two unioned — and when the story commits the
orchestrator writes the same annotation a sweep bundle writes: `status: done <date>` plus
`resolution: resolved by story <id>`. It fires in both sprint and stories mode.

The write sits at the **commit boundary** — after artifact verification, the verify commands,
every checkpoint, the review loop and the `pre_commit` workflows, and immediately before
`finalize_commit` squashes the story — so a story that fails verification, is rejected by
review, or escalates closes nothing, and an in-repo ledger still carries the annotation in the
story's own commit. A commit that then fails (a rejecting native `pre-commit` hook) or never
happens at all (a SIGTERM landing mid-commit) rolls the annotation back, so it never outlives the
commit it was written for; the rollback reverts only the entries that story flipped, so a hook
that edited the ledger before refusing the commit keeps its own edit. An artifact dir configured _outside_ the repo — including one reached
through a symlink that only looks in-repo — is shared between worktrees and cannot be committed
at all; that closure is held until the work is durably landed — after the commit in place, after
the branch merges under isolation (journaled `deferred-close-external-ledger`) — and a write that
never got to happen is retried on the next resume rather than being dropped. A ledger location
that cannot be read or written at all — a dropped mount, a broken symlink, undecodable bytes — is
journaled and retried, never read as "no such entries" and never able to fail the story or crash
the run that owns it.

Closure is declared, never inferred from a diff, and both channels are re-read at that boundary
so a declaration edited after the story was implemented is the one that counts — a withdrawn id
is not closed, and one added late is. A read that fails there falls back to the declaration
captured when the dev artifacts verified rather than to "declares nothing"
(`deferred-close-declaration-unreadable` when even that is unavailable). An id already `done` is
a silent no-op, so a resumed run re-driving the same close neither doubles the `resolution:` line
nor warns. Nothing at the commit boundary can fail a story: an id matching no ledger entry
(`deferred-close-unmatched`), a ledger entry whose `status:` reads as neither `open` nor `done`
(`deferred-close-malformed`), and a story spec declaring `closes_deferred: DW-5` where a list
belongs are journaled and dropped. The same wrong container in `stories.yaml` is a schema error
like any other manifest field of the wrong type — the manifest fails to load, before any story
runs, where `validate` has already reported it. Closes are journaled as `story-deferred-closed`.

The `stories.yaml` channel is the one that avoids hand-editing each generated spec:
`bmad-dev-auto` writes the story spec and knows nothing of the ledger, whereas the Story
Breakdown is authored while the ledger is in view. Like `spec_checkpoint` / `done_checkpoint`
it is human-authored — no upstream skill emits it yet (BMAD-METHOD#2619 proposes the schema row
and the breakdown prompt), and re-deriving `stories.yaml` drops it unless the intent is logged
in `.memlog.md`.

`bmad-loop validate` adds matching pre-flight warnings in **both** queue modes —
`deferred.closes-unknown` for an id absent from the ledger (a typo or a renumbered entry) and
`deferred.closes-malformed` for a spec declaration in an unreadable shape. Stories mode reads the
manifest plus each id-resolved spec; sprint mode reads the story specs already on disk in the
artifacts dir. Both are warnings, so neither changes the exit code; a malformed declaration in
`stories.yaml` itself is reported by `queue.stories-manifest`, which does.

- **`review.on_timeout` policy knob (#271).** A timeout-like review verdict (`timeout` /
`stalled` / `over_budget`) previously always burned a review cycle (RETRY) until
`max_review_cycles`, then deferred — even when the dev product was already finalized and
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,20 @@ Turn it on per project with `[stories] source = "stories"` + `spec_folder = "<ep
- **`spec_checkpoint`** — pause _before_ code, to review the plan. The dev session halts right after planning (`Halt after planning.`) with the spec at `ready-for-dev`; the run pauses at a **plan checkpoint**. Approve to resume straight to implementation, or request a replan (resets the spec to `draft` so the next dispatch re-plans).
- **`done_checkpoint`** — pause _after_ the story commits, to review the result before the loop moves on (skipped automatically when it is the last story).

A story may set both (it pauses twice); `gates.mode` pauses stack on top. A blocked story escalates exactly as in sprint mode — `bmad-loop resolve`, then re-arm + resume — now with the story's title/description and the blocking condition surfaced; a pre-planning-halt **sentinel** spec is auto-deleted (a copy preserved under the run dir) on re-arm for a clean re-dispatch.
An entry may also carry **`closes_deferred`** — the `DW-<n>` ledger ids that story's work closes (see [Deferred-work sweeps](#deferred-work-sweeps)):

```yaml
- id: '3-2'
title: Export digests
description: …
closes_deferred: [DW-5, DW-6]
```

Declaring it here rather than in the story spec is what lets the annotation happen without touching each generated spec: `bmad-dev-auto` generates the spec and knows nothing of the ledger, whereas the breakdown is authored while the ledger is in view. A spec that _does_ carry the field in frontmatter is honored too — the two are unioned.

Like `spec_checkpoint` and `done_checkpoint`, this is a **human-authored, caller-only field**. Breakdown time, with the ledger open, is where it belongs — but it is not a deadline: the declaration is read when the story commits, so adding one to a story spec's frontmatter mid-run is honored, and withdrawing one before the commit means it is not closed. Upstream Story Breakdown does not emit it yet ([BMAD-METHOD#2619](https://github.com/bmad-code-org/BMAD-METHOD/issues/2619)), and re-deriving `stories.yaml` rewrites the file — so record the intent in `.memlog.md` alongside the story, or the declaration is lost on the next re-derive.

A story may set both checkpoints (it pauses twice); `gates.mode` pauses stack on top. A blocked story escalates exactly as in sprint mode — `bmad-loop resolve`, then re-arm + resume — now with the story's title/description and the blocking condition surfaced; a pre-planning-halt **sentinel** spec is auto-deleted (a copy preserved under the run dir) on re-arm for a clean re-dispatch.

`bmad-loop run --dry-run --spec <folder>` prints the linear schedule (list order, checkpoint markers, live on-disk state); `bmad-loop status` shows the same stories board.

Expand Down Expand Up @@ -235,6 +248,22 @@ bmad-loop sweep [--no-prompt] [--decisions-only] [--max-bundles N] [--repeat] [-
`status: done` in the ledger.
```

**Closing entries from a story.** Sweeps are not the only way an entry gets closed. A story spec can declare the entries its work closes, in frontmatter:

```yaml
closes_deferred: [DW-5, DW-6] # DW-<n> ids this story closes
```

In stories mode the same declaration can live on the `stories.yaml` entry instead (`closes_deferred: [DW-5, DW-6]`), which is where it belongs for an unattended run — the breakdown is authored while the ledger is in view, whereas the story spec is generated later by a dev skill that knows nothing of the ledger. Both channels are unioned. Either way the field is human-authored — like `spec_checkpoint` / `done_checkpoint`, no upstream skill emits it yet, and re-deriving `stories.yaml` drops it unless the intent is logged in `.memlog.md`.

The orchestrator writes the same annotation a bundle close writes — `status: done <date>` and `resolution: resolved by story <id>` — into each declared entry. Without it the ledger is one-way: entries are filed automatically but only ever marked resolved by hand, so a multi-epic run ends with entries that were satisfied epics ago still reading `open`.

**Closure happens at the commit boundary**, after artifact verification, the verify commands, every checkpoint, the review loop and the pre-commit workflows — and just before the story's commit is squashed, so an in-repo ledger carries the annotation in that same commit. A story that fails, blocks, is rejected by review, or escalates closes nothing, and there is nothing to undo. Closure is declared, never inferred from the diff; a resume re-driving the same close is a no-op; and an id that matches no entry is journaled rather than failing the story. The declaration is re-read at that boundary, so an edit made after the story was implemented is the one that counts.

A declaration in a shape nothing can read — a bare `closes_deferred: DW-5` where a list belongs — depends on which channel it is in. In a **story spec** it is journaled and dropped, like an unknown id: the spec is generated mid-run by a dev skill, and a malformed field there must not be able to fail a story that succeeded. In **`stories.yaml`** it is a schema error, exactly like every other manifest field of the wrong type, and the manifest fails to load — the breakdown is hand-authored before the run, where `bmad-loop validate` reports it up front and a typo is still cheap to fix. `validate` warns about unknown ids in both queue modes and about a malformed spec declaration; a malformed manifest is the manifest's own `queue.stories-manifest` failure.

> **Ledger outside the repo.** If `implementation_artifacts` is configured outside the project tree, the ledger is shared between worktrees and cannot be part of any commit. Closure still happens, but for an isolated (`scm.isolation = "worktree"`) run it is held until the unit's branch has merged — a story whose integration fails leaves its entries `open`. The run journals `deferred-close-external-ledger` so the annotation's absence from git history is not a surprise. If the location itself is unavailable when the write comes due (a shared mount that has gone away), the closure is retried before the run ends and on any later resume; a run that finishes with it still unreachable leaves those entries `open` and journals `deferred-close-abandoned` for a sweep to re-verify.

**Answering missed decisions later.** An unattended sweep (`--no-prompt`) skips decisions, and an interactive one can be abandoned before you answer them all — those answers would otherwise be lost, since triage re-derives the decision set from the ledger every run. `bmad-loop decisions` (or press `d` in the TUI) surfaces every decision past sweeps left unanswered, reconstructed from their triage output, and lets you answer them out of band. A `close` is applied immediately; a `build`/`keep-open` is saved to `.bmad-loop/decisions.json` and consumed by the next sweep (build → bundle, keep-open → recorded) with no re-prompt. `--list` shows them without answering; `bmad-loop status` reports the outstanding count.

Sweeps are their own resumable runs (`bmad-loop resume <id>`). `[sweep] auto` in the policy fires an unattended sweep automatically at epic boundaries or run end; a failed/paused child sweep never interrupts the parent run.
Expand Down
1 change: 1 addition & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
### Deferred-work sweeps

- Skills accumulate an append-only ledger (`deferred-work.md`, `DW-<n>` entries): split-off goals, pre-existing findings, "needs human decision" items.
- Story-declared closure (`closes_deferred: [DW-5, DW-6]`, human-authored on a `stories.yaml` entry or in a story spec's frontmatter — the two are unioned): when the story commits, the orchestrator flips each declared entry to `status: done <date>` + `resolution: resolved by story <id>` — the same annotation a sweep bundle writes — so the ledger stops being one-way (filed automatically, marked resolved by hand). Written at the commit boundary, after verification/review/checkpoints and just before the squash, so an in-repo ledger carries the annotation in the story's own commit and a story that fails, is rejected by review, or escalates closes nothing. Declared, never inferred from a diff; re-read at the commit so a declaration edited after implementation still counts; idempotent across a resume; an unknown id, an unreadable entry status, or a non-list declaration in a story spec is journaled, never fatal (a non-list declaration in `stories.yaml` is a manifest schema error like any other, caught before the run). `bmad-loop validate` warns about all of those before the run starts, in both queue modes. An artifact dir configured outside the repo is shared across worktrees and cannot be committed — an isolated run holds its closure until the story's branch merges.
- `bmad-loop sweep` triages every open entry against the actual code (ledger statuses treated as unreliable) → partition: already-resolved (auto-closed with evidence) / bundles / blocked / skip / decisions.
- Bundles run the full pipeline (dev `--dw-bundle` → review → verify → commit); the review gate checks every bundle entry is `status: done`.
- Interactive decision walkthrough (build / close / keep-open per option, with a recommendation); answers written back as `decision:` lines. Unattended runs leave decisions open.
Expand Down
4 changes: 4 additions & 0 deletions src/bmad_loop/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"skills.stories-dispatch",
"skills.stories-dispatch-missing",
"skills.stories-dispatch-stale",
"deferred.closes-unknown",
"deferred.closes-malformed",
"deferred.closes-entry-unreadable",
"deferred.ledger-unreadable",
}
)

Expand Down
Loading