Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a8cf5eb
feat(deferred): make a ledger hard gate enforceable with `gate:`
pirony Aug 8, 2026
d5bc0d3
test(deferred): pin the hard-gate check and its token boundaries
pirony Aug 8, 2026
e2411a1
docs(deferred): document `gate:` in the format, README and FEATURES
pirony Aug 8, 2026
4918f87
fix(deferred): align gate semantics with the adversarial review
pirony Aug 8, 2026
5a9c48c
docs(deferred): describe the split, mid-line and empty-gate rules
pirony Aug 8, 2026
47a7df1
Merge remote-tracking branch 'origin/main' into HEAD
pbean Aug 9, 2026
c3a124a
fix(deferred): close the gate's false-refusal, lost-line and crash paths
pbean Aug 9, 2026
7550802
test(deferred): make the word-id false-refusal guard actually bite
pbean Aug 9, 2026
75f1f43
fix(validate): name the hard gate in the unreadable-ledger warning
pbean Aug 9, 2026
93207d1
feat(deferred): enforce hard gates at dispatch, and close three fail-…
Aug 9, 2026
891417e
test(deferred): pin that the dispatch gate survives a resume that fix…
Aug 9, 2026
6c74c7a
style: reflow the README hard-gate paragraph (prettier)
Aug 9, 2026
6ad16d5
refactor(deferred): make the token matchability helper private
Aug 9, 2026
fd5d47b
docs(deferred): drop the leading spaces from an inline code span (MD038)
Aug 9, 2026
3c7768d
test: move write_gated_ledger into conftest beside write_ledger
Aug 9, 2026
11f4b07
fix(deferred): a fenced `gate:` example is not a declaration
Aug 9, 2026
2ab1f13
test(deferred): isolate the info-string closer rule from the fence-le…
Aug 9, 2026
00b5185
refactor(fences): share one fence reader between devcontract and defe…
Aug 9, 2026
6c4a5c7
fix(deferredwork): read fence state at file scope so a quoted example…
pbean Aug 9, 2026
597572c
docs(gate): say that quoting a whole entry is safe, and name the stor…
pbean Aug 9, 2026
6eb075d
fix(fences): reject backticks in a backtick info string; mask example…
pbean Aug 9, 2026
854c0e7
docs(test): name the real helper in the legacy-fence docstring
Aug 9, 2026
831be5c
fix(deferredwork): close the status line the reader chose, not the fi…
Aug 9, 2026
24a089e
fix(deferredwork,cli): hoist the fence walk out of the parse; gate on…
Aug 9, 2026
cab6f66
test(deferredwork): pin the example index at its span bounds
Aug 9, 2026
b279fe6
fix(deferredwork): mask fenced examples out of the prose-gate scan too
Aug 9, 2026
416bbaf
docs(gate): describe stories-mode gating as the scheduler's predicate
Aug 9, 2026
bae8f1f
docs(validate): say which half of the scheduler preflight shares
Aug 9, 2026
4b79ede
fix(deferredwork): ask the gate scans' fence question at file scope
Aug 9, 2026
1728267
fix(engine): re-ask the story gate for a task registered but never st…
Aug 10, 2026
c6b0a53
fix(engine): ask the story gate for the whole resume-restart arm
Aug 10, 2026
83f45ae
test(engine): pin the pre-launch attempt window the restart-arm gate …
Aug 10, 2026
986a3e5
fix(engine): ask the restart-arm gate before the rollback rewinds the…
Aug 10, 2026
77dddc1
test(engine): pin that the restart-arm gate re-asks until the entry l…
Aug 10, 2026
578ca38
fix(deferredwork): fire the split arm only for a bare <epic>-<story> …
Aug 10, 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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ whose seams had diverged enough that several ports needed a different fix, and t

### Added

- **A deferred-work entry can block a story: `gate:`.** An entry that must land before specific
stories run could only say so in prose (`HARD GATE: must land before 3-2`), and prose stopped
nothing — `run` drove the story anyway. A `gate: 3-2, 3-3` line names the blocked story keys, and
it is enforced on both sides: `bmad-loop validate` fails (`deferred.hard-gate`) for every
actionable story a token matches, in both queue modes, and `run` pauses (`story-gate`) rather
than dispatch a gated story — so the refusal no longer depends on remembering to run the
preflight. The pause happens before the story is recorded, so closing the entry and resuming
runs it. Sweeps are exempt: they are what closes the gating entry. The only deferred check that
gates rather than advises; cleared by closing the entry or dropping the token. A gate that can
enforce nothing — an unusable token, an empty `gate:` line, a `gate:` not written lowercase at
the start of a line, or a prose-only `HARD GATE:` — warns instead
(`deferred.hard-gate-unstructured`). Silent on a ledger that gates nothing, as before.

- **Deferred review findings are harvested from spec frontmatter (#433).** BMAD-METHOD#2640 moved
`defer`-triaged findings into the spec's unfiled `deferred:` list. A successful dev, review,
repair or review-timeout-salvage pass now files each as `### DW-<n>` (`spec-deferrals-harvested`),
Expand Down Expand Up @@ -103,6 +116,12 @@ whose seams had diverged enough that several ports needed a different fix, and t

### Changed

- **An unreadable deferred-work ledger fails `validate` instead of warning
(`deferred.ledger-unreadable`).** The hard gate rides on the same bytes, so a warning exited 0
with the gate never evaluated — a fail-open on the one deferred check that refuses, and one that
cannot be narrowed by asking whether the project uses gates, because the file that would answer
is the unreadable one. `run` pauses on the same fault, so preflight and dispatch now agree.

- **Every spec-frontmatter status read goes through `status_of` (#358 follow-up).** Five inline
status reads remained in the engine and the generic adapter, each reading a blank `status:` as the
token `none` — the defect #358 fixed at the shared reader. Three were neutral; the pair that was
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,23 @@ The orchestrator writes the same annotation a bundle close writes — `status: d

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.

**Blocking a story from the ledger.** Some entries do not merely defer work, they block it: a leg nobody has wired yet is not a nice-to-have for the first story that consumes it. Entries said so in prose (`HARD GATE: must land before 3-2`) long before anything could act on it, and prose gates nothing — `run` picked the story off the board and drove it anyway, and the gate surfaced afterwards, in a diff built on the missing leg. A `gate:` field line makes the claim enforceable:

```markdown
### DW-1: wire the blob-storage credentials

status: open
gate: 3-2, 3-3
```

Until that entry lands, `bmad-loop validate` **fails** for every actionable story a token matches, and `run` **pauses** rather than dispatch one — the gate is enforced at dispatch as well as at preflight, so it no longer depends on remembering to run `validate` first. A token gates a story key when it is that key, or is its prefix at a key boundary — a `-`, or the split-story suffix (one lowercase letter then `-`). So `3-2` covers the sprint key `3-2-invite-link-student-surface`, the stories-mode id `3-2`, and both halves of a split (`3-2a-…`/`3-2b-…`), but never `3-20-later-story`; the split arm is there because breakdown can split a gated story after the gate was written, and a gate that quietly stops matching is worse than none. Closing the entry clears it, and so does dropping the token. This is the only deferred-work check that is a gate rather than an advisory: the `closes_deferred` checks above describe traceability that may be wrong and must never block a run, while this one describes work that must not start.

Only an explicit `status: done <date>` retires a gate. An entry whose status the format cannot read — `status: opne`, or no `status:` line at all — still gates, because an unreadable status is not evidence the work landed; letting it read as closed would have meant one keystroke silently disabling the refusal.

The dispatch pause (`story-gate`, reviewable in the TUI like any other gate) fires before the story is recorded as touched, so closing the entry — by hand or with `bmad-loop sweep` — and resuming runs it. **Sweeps themselves are never gated**: a sweep is what closes the gating entry, so gating it would deadlock the gate against its own remedy. A story whose session already completed **finishes** when the run resumes — its recorded result replays through to commit rather than stranding half-done work; the gate is about work that must not _start_. A resume that instead **restarts** a story, discarding its worktree or resetting to baseline and re-running from scratch, is a start and is asked again: so a gate landing while a run was down still stops the story that run had picked but never got underway, and stops a re-drive of one whose escalation or wedge you have just resolved.

Four shapes declare a gate nothing can enforce, and each is a warning while the entry is unlanded: a token that cannot name a story key (a space-separated `gate: 3-2 3-3`, which is one bad token rather than two good ones, or an unmatchable `gate: 3.2` — note `.` and `_` are fine inside a sprint slug, so `gate: 3-2-a_b` is a real gate); a `gate:` line with nothing usable after the colon; a `gate:` not written lowercase at the very start of a line (`Gate:`, or indented — surfaced rather than accepted, so a fenced example inside an entry cannot become a refusal); and prose declaring `HARD GATE:` on an entry that carries no `gate:` line. The prose arm matches mid-line, because `reason:` prose is hard-wrapped and that is where a real declaration lands — but not directly after a quote character, so an entry that merely cites the phrase stays silent.

> **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. The annotation is written all the same, at the same moment, and the run journals `deferred-close-external-ledger` so its absence from git history is not a surprise. A location that cannot be read or written when the write comes due (a shared mount that has gone away) closes nothing and is journaled — those entries stay `open` for a sweep to re-verify, and an outage is never read as "no such entries".

**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.
Expand Down
1 change: 1 addition & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se

- 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, each declared entry flips to `status: done <date>` + `resolution: resolved by story <id>` — the annotation a sweep bundle writes — so the ledger stops being one-way. Written at the commit boundary, 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, and `bmad-loop validate` warns about all of them before the run starts. (A non-list `closes_deferred` in `stories.yaml` is different: the manifest is a schema the parser owns, so it is refused outright, before the run.) An artifact dir outside the repo cannot be committed — the annotation is written anyway and journaled (`deferred-close-external-ledger`).
- Hard gates (`gate: 3-2, 3-3` on an entry): until the entry lands, `bmad-loop validate` fails (`deferred.hard-gate`) for every actionable story a token matches and `run` pauses (`story-gate`) rather than dispatch one — a token gates a key it equals or prefixes at a key boundary (`-`, or a split-story suffix), so `3-2` covers `3-2-invite-link`, the stories-mode id `3-2` and both halves of a `3-2a`/`3-2b` split, but never `3-20-later`. Only an explicit `status: done` retires a gate; an unreadable status (`opne`, or no status line) still gates. The dispatch pause precedes the story's own run record, so closing the entry and resuming runs it; sweeps are exempt, since a sweep is what closes the gating entry. The only deferred check that gates rather than advises; cleared by closing the entry or dropping the token. A warning (`deferred.hard-gate-unstructured`) covers the four gates nothing can enforce: a token that cannot name a story key (`3-2 3-3`, or an unmatchable `3.2` — `.`/`_` are legal inside a sprint slug), an empty `gate:` line, a `gate:` not lowercase at the start of a line, and prose declaring `HARD GATE:` (matched mid-line, since `reason:` prose wraps — but not straight after a quote, so a citation stays silent) on an entry with no `gate:` line.
- `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
8 changes: 5 additions & 3 deletions docs/tui-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,11 @@ artifacts the engine already wrote.
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.
- **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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`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
Expand Down
2 changes: 2 additions & 0 deletions src/bmad_loop/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"deferred.closes-unknown",
"deferred.closes-malformed",
"deferred.closes-entry-unreadable",
"deferred.hard-gate",
"deferred.hard-gate-unstructured",
"deferred.ledger-unreadable",
}
)
Expand Down
Loading