Skip to content
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ breaking changes may land in a minor release.
fsynced before the replace. A run with no recorded run-dir identity, or one whose device
number changed, records one `journal-unpinned` row per writer; `resume` records or
re-binds the identity before its first row (DW-559).
- Refuse `delete` (even `--force`), the TUI delete and `clean` on a run whose state
cannot be read, or on an isolated v7 sweep run still holding shared-store ledger
closes for unlanded code; a new sweep warns while such a run exists (DW-571).

### Removed

Expand All @@ -154,6 +157,14 @@ breaking changes may land in a minor release.

- Name the T1 ticket plan an interrupted commit window left `done` or `blocked` when
the resumed commit fails, instead of a bare `commit failed:` (DW-555).
- Commit a v7 sweep's own ledger writes (bundle closes, reopened closes, harvested
deferrals) in a T3/T4 ticket store's repository, best effort, instead of leaving
them uncommitted (DW-556, DW-567, DW-570).
- Reopen an isolated sweep bundle's close in a shared ticket store's ledger when the
bundle escalates, defers or is vetoed, instead of leaving entries `done` for code
that never landed (DW-567, DW-569).
- Stop pausing an isolated sweep on `artifact publication intent is missing` when its
artifacts dir is in a shared ticket store (DW-557).
- Resolve a relative v6 config path against the project root instead of the process
cwd (DW-551).
- Re-validate an ordinary unit's merge against the target on replay instead of trusting its
Expand Down
5 changes: 4 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ bmad-loop delete [--project PROJECT] [--force] run_id
| `--force` | Hard-stop a live engine first (this ends its agent session), and skip the live-agent-session guard (skipping it kills no session). |

- Without `--force`, refuses (exit 1) when the engine is provably alive or an agent session that may belong to the project is live. Unverifiable liveness only warns.
- Refuses (exit 1), even with `--force` and before `--force` stops anything, an unfinished worktree-isolated sweep run that still holds ledger closes in a shared ticket store for code that never landed. The message names the entries and `bmad-loop resume <run>`, which lands or reopens them. See [the sweep guide](../sweep-guide.md#sweeps-on-bmad-v7).
- Also refuses (exit 1), even with `--force`, any run of any type whose state cannot be read, and such a sweep run whose config, store topology or ledger cannot be read, or whose pinned initiative is no longer active while its store is shared, since it cannot be told whether it holds such closes. Repair what could not be read and retry; a run you have confirmed holds no closes can be removed by hand.
- Also prunes git worktree entries for worktrees the run held.

### `archive`
Expand All @@ -374,7 +376,7 @@ Compresses a run into `.bmad-loop/archive/<run-id>.tar.gz`, then removes the run
bmad-loop archive [--project PROJECT] [--force] run_id
```

- `--force` and the refusals are the same as `delete`. The tarball holds the in-tree run directory only.
- `--force` and the live-engine and live-session refusals are the same as `delete`. `archive` does not refuse a run that holds unlanded sweep ledger closes. The tarball holds the in-tree run directory only.

### `cleanup`

Expand Down Expand Up @@ -410,6 +412,7 @@ bmad-loop clean [--project PROJECT] [--dry-run] [--keep RUN_ID] [--retain N] [--
| `--json` | Emit one JSON document instead of text. |

- Exits 0 once the policy loads; per-run faults are warnings on stderr or fields in the JSON document.
- A run past retention that `delete` would refuse because it holds unlanded sweep ledger closes is skipped on the delete arm (`--hard`, or `[cleanup] archive_old = false`) before anything of it is trimmed. Stderr names it and the entries; `--json` lists it under `protected`. The archive arm keeps the run in its tarball and is not affected.

JSON: [`clean --json`](#clean---json).

Expand Down
56 changes: 32 additions & 24 deletions docs/sweep-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ A sweep runs on a v7 project the same way, with these differences:
- **Store layout.** The store-topology check judges a v7 sweep as a tickets run, because its ledger lives in the ticket store. The sweep records the store's topology at its start, and a resume refuses a store that changed since, or whose initiative folder is gone.
- **Pinned initiative.** A sweep records the active initiative it started on. A resume, `resolve` or the dashboard's re-arm refuses while `[core] active_initiative` names another: set it back, or start a new sweep.
- **Decisions per initiative.** Each initiative has its own ledger and DW numbering, so pre-answers are kept per initiative in `.bmad-loop/decisions-<initiative>.json`. `bmad-loop decisions` and the TUI answer the active initiative's decisions and offer only those its own sweeps raised. An answer given after another initiative was made active than the one its decision was listed from is refused; list the decisions again. With no initiative active, the project store stays `.bmad-loop/decisions.json` and only sweeps that ran with no initiative are offered.
- **Plans in a store repository.** When the ticket store is its own git repository (nested or outside the checkout), the bundle's code commit cannot carry its plan. Once the code has landed, the accepted plan gets its own commit there, `chore(sweep): <bundle> plan [bmad-loop <run>]`, with `Bmad-Code-*` trailers naming the code. This is best effort, like a tickets run's store commit: a failure is journaled (`store-commit-failed` or `store-commit-foreign-dirt`) and the plan stays uncommitted for a human.
- **Worktree isolation over a shared store.** When the ticket store is shared rather than carried by each checkout (unversioned and ignored, or its own repository), the session in the unit worktree writes its plan straight into the main checkout's store, so nothing is copied back at the merge. The bundle's ledger close lands in that shared ledger the same way, so a bundle that ends without landing its code reopens its close before that end is recorded: when it defers, when it escalates (a failed merge-back included), and when a plugin vetoes it. Resume skips an escalated bundle, so after you merge a failed merge-back's kept branch by hand, its entries stay open until a later sweep triages them as already resolved.
- **A reopen that faults.** A ledger fault during that reopen is journaled as `sweep-bundle-reopen-failed`. An escalation or a `pause` veto still pauses, leaving the close for a human to reopen. A `defer` or `skip` veto, which would let the run finish with the close standing, crashes the run instead, so it stays unfinished and resumable.
- **A run that still holds closes.** A run that pauses for any other reason, is stopped or crashes after a bundle's accepted dev keeps that bundle's close: the entries read `done` in the shared ledger, and no later sweep re-bundles a `done` entry. Only that run can land or reopen them, so resume it (`bmad-loop resume <run>`). Until then, each new sweep prints a warning naming the run and its entries, and journals `sweep-prior-run-holds-closes`; it still runs and leaves those entries alone. A run it cannot check (unreadable state, config or ledger) is journaled as `sweep-prior-run-closes-unknown`. `bmad-loop delete`, the dashboard's delete and `bmad-loop clean`'s delete arm refuse to remove such a run, and `--force` does not override that.
- **Plans in a store repository.** When the ticket store is its own git repository (nested or outside the checkout), the bundle's code commit cannot carry its plan. Once the code has landed, the accepted plan gets its own commit there, `chore(sweep): <bundle> plan [bmad-loop <run>]`, with `Bmad-Code-*` trailers naming the code. This is best effort, like a tickets run's store commit: a failure is journaled (`store-commit-failed` or `store-commit-foreign-dirt`) and the plan stays uncommitted for a human. The bundle's ledger close is committed there the same best-effort way, as `chore(sweep): close resolved deferred-work entries` with the same trailers and the same journaled failures (a ledger gone or unreadable there is journaled `store-commit-failed` too), and is withheld (`sweep-ledger-commit-withheld`) while the run holds its ledger in doubt.
- **Deferred bundles in a store repository.** A bundle that defers never lands code, but its ledger writes (the reopen of its close, any harvested deferrals) are committed in the store repository as `chore(sweep): record a deferred bundle's ledger writes`, the same best-effort and withheld way, with only the `Bmad-Code-Repo` trailer. A landed bundle's harvested deferrals ride its close commit; one that landed with no close commits them as `chore(deferred-work): carry harvested findings from <bundle>`, as does a resume that replays a deferred bundle's harvest. These are best effort too.

## Story-declared closure (`closes_deferred`)

Expand Down Expand Up @@ -177,17 +181,19 @@ A stored answer is usable only if it is a JSON object whose `effect` is `build`,

The sweep commits each ledger or store change it makes as its own file-scoped commit: only that file is staged, and the whole file is committed.

| Commit message | When |
| --------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `chore(sweep): record deferred-work decisions` | after the decision phase |
| `chore(sweep): close resolved deferred-work entries` | already-resolved entries and decided closes |
| `chore(sweep): commit ledger at the sweep cycle boundary` | between repeat cycles |
| `chore(sweep): commit ledger after recovering in-flight bundles` | on resume, after in-flight bundles are recovered |
| `chore(sweep): commit a ledger write an interrupted phase left unpublished` | on resume, for a write an interrupted phase did not commit |
| `chore(sweep): drop consumed deferred-work pre-answers` | end-of-cycle prune of the project store |
| `chore(sweep): drop stale deferred-work pre-answer` | a `stale-option` drop |
| `chore(sweep): migrate legacy deferred-work entries to DW format` | legacy-ledger migration |
| `chore(sweep): ingest retro action items` | retro action items added to the ledger at sweep start |
| Commit message | When |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chore(sweep): record deferred-work decisions` | after the decision phase |
| `chore(sweep): close resolved deferred-work entries` | already-resolved entries and decided closes; a landed bundle's close when the ledger is in a store repository |
| `chore(sweep): record a deferred bundle's ledger writes` | a deferred bundle's reopened close and harvested deferrals when the ledger is in a store repository |
| `chore(deferred-work): carry harvested findings from <bundle>` | an isolated bundle's harvested deferrals when the ledger is in a store repository and the bundle landed with no close, or on resume of a deferred bundle's carry |
| `chore(sweep): commit ledger at the sweep cycle boundary` | between repeat cycles |
| `chore(sweep): commit ledger after recovering in-flight bundles` | on resume, after in-flight bundles are recovered |
| `chore(sweep): commit a ledger write an interrupted phase left unpublished` | on resume, for a write an interrupted phase did not commit |
| `chore(sweep): drop consumed deferred-work pre-answers` | end-of-cycle prune of the project store |
| `chore(sweep): drop stale deferred-work pre-answer` | a `stale-option` drop |
| `chore(sweep): migrate legacy deferred-work entries to DW format` | legacy-ledger migration |
| `chore(sweep): ingest retro action items` | retro action items added to the ledger at sweep start |

`bmad-loop decisions` commits each file it wrote separately as `chore(decisions): pre-answer <id>`: the ledger only if a `decision:` line landed, and the project store only for `build` or `keep-open`. A ledger the sweep marked as in doubt blocks every further ledger publish for that run (`sweep-ledger-commit-withheld`).

Expand All @@ -209,18 +215,20 @@ An empty ledger is publishable. A store that is not valid JSON is still publisha

`bmad-loop diagnose [run-id]` collects the publication entries in a "Sweep publication and repeat stops" section. For the raw `message`, `repo` and `error` text, read `.bmad-loop/runs/<run-id>/journal.jsonl`.

| Journal kind | Meaning |
| ----------------------------------- | ------------------------------------------------------------------------------------------------- |
| `sweep-ledger-commit` | committed (`message`, `commit`, `file`) |
| `sweep-ledger-commit-clean` | nothing to commit; also seen when the path is gitignored |
| `sweep-ledger-commit-refused` | refused before staging (`refuse_cause`) |
| `sweep-ledger-commit-unavailable` | the ledger is in no git repo, or a store commit failed (`repo`, `error`) |
| `sweep-ledger-commit-withheld` | blocked because the ledger is in doubt (`reason: ledger-in-doubt`) |
| `sweep-bundles-withheld` | bundles not dispatched (this cycle's, or in-flight ones on resume) because the ledger is in doubt |
| `sweep-bundle-skipped` | a bundle skipped whole (`entry-not-open`, `failed-or-escalated-earlier`, `human-chose-keep-open`) |
| `sweep-decision-effect-unavailable` | an answer's ledger effect did not land |
| `sweep-decision-effect-reapplied` | a stored `close` the ledger had not taken was applied again |
| `sweep-repeat-done` | a repeat run stopped (`stop_cause`) |
| Journal kind | Meaning |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `sweep-ledger-commit` | committed (`message`, `commit`, `file`) |
| `sweep-ledger-commit-clean` | nothing to commit; also seen when the path is gitignored |
| `sweep-ledger-commit-refused` | refused before staging (`refuse_cause`) |
| `sweep-ledger-commit-unavailable` | the ledger is in no git repo, or a store commit failed (`repo`, `error`) |
| `sweep-ledger-commit-withheld` | blocked because the ledger is in doubt (`reason: ledger-in-doubt`) |
| `sweep-bundles-withheld` | bundles not dispatched (this cycle's, or in-flight ones on resume) because the ledger is in doubt |
| `sweep-bundle-skipped` | a bundle skipped whole (`entry-not-open`, `failed-or-escalated-earlier`, `human-chose-keep-open`) |
| `sweep-decision-effect-unavailable` | an answer's ledger effect did not land |
| `sweep-decision-effect-reapplied` | a stored `close` the ledger had not taken was applied again |
| `sweep-repeat-done` | a repeat run stopped (`stop_cause`) |
| `sweep-prior-run-holds-closes` | at a sweep's start: an earlier run still holds closes whose code never landed (`run_id`, `dw_ids`) |
| `sweep-prior-run-closes-unknown` | at a sweep's start: an earlier run could not be checked (`run_id`, absent when the runs dir could not be listed; `error`) |

A ledger `git status` fault other than "not a repository", or a commit git refuses, ends the sweep as a crash: the run records `run-crash` and writes `crash.txt` in the run dir.

Expand Down
Loading
Loading