Skip to content
Merged
3 changes: 2 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ bmad-loop drives a BMAD sprint unattended: a deterministic Python loop launches
- **Properties:**
- A run is pinned to `[core] active_initiative` at start; a changed initiative pauses the run at `ticket-tree` instead of building another initiative's ticket.
- The tree is read in the layout the installed `tickets.py` uses: a `tickets/` folder inside each initiative and epic, or the preview-era flat layout. A parent initiative passes the preflight only under worktree isolation without `gates.retrospective = "auto"`, with the store checks run for each sliced child (`tickets.initiative-parent`), and an epic declared but not sliced yet holds no tickets.
- `run` on a parent initiative builds its children one run each in build order, writing every drained child's initiative `status: done` before each pick (one a stopped chain left open included), under one `--max-stories` budget; a child run that does not finish holds the chain until `resume` or `resolve` drives it to finished, and the chain's end names what each waiting child waits on.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- A hitl ticket pauses at `ticket-hitl` and a ticket that needs refining at `ticket-refine`, both before any session runs for it.
- bmad-loop writes a plan's `status` and `blocked_*` only through `tickets.py mark`, under a lock, read back afterwards and checked against one plan transition table; it never writes `dropped` or an epic file.
- Tickets this run did not start are never dispatched or marked.
Expand All @@ -59,7 +60,7 @@ bmad-loop drives a BMAD sprint unattended: a deterministic Python loop launches
- `run` refuses a tracker store and the multi-repo workspace layout before creating a run directory; resume and resolve refuse a store whose layout changed.
- When a run without `--story` finishes an epic, it runs the retrospective and pauses once at `epic-boundary` for a person to close the epic; a `rejected` verdict pauses every later pick while the epic stays open, and an epic gated on an open one waits for its closure.
- **Config:** `[stories] source = "tickets"`, `[core] active_initiative`, `[tickets] mark_done`, `[tickets] thorough_review`, `[gates] retrospective`; `bmad-loop run --epic ID|SLUG`, `--story REF`, `--dry-run`; `bmad-loop confirm <ref>`
- **Tested by:** `tests/test_tickets_engine.py`, `tests/test_tickets_source.py`, `tests/test_tickettree.py`, `tests/test_ticketmodel.py`, `tests/test_ticketstore.py`, `tests/test_statemachine.py`, `tests/test_tickets_e2e.py`
- **Tested by:** `tests/test_tickets_engine.py`, `tests/test_tickets_source.py`, `tests/test_tickettree.py`, `tests/test_ticketmodel.py`, `tests/test_ticketstore.py`, `tests/test_statemachine.py`, `tests/test_initiativechain.py`, `tests/test_cli.py`, `tests/test_tickets_e2e.py`
- **Docs:** [Ticket-tree mode guide](tickets-guide.md), [Setup: BMAD v7](setup-guide.md#installing-on-bmad-v7-bmad-setup), [Configuration: [tickets]](reference/configuration.md#tickets), [Resolve in tickets mode](escalations-guide.md#resolve-in-tickets-mode), [TUI: tickets mode](tui-guide.md#tickets-mode)

### Trust-nothing verification
Expand Down
1 change: 1 addition & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ bmad-loop run [--project PROJECT] [--spec FOLDER] [--epic EPIC] [--story STORY]
- In tickets mode `run` resolves the selectors against the active initiative before anything is created. `--epic` must name one epic of the initiative table. `--story` goes through `tickets.py find` and must name one ticket that `next` lists as ready to start or to refine (and that is in the `--epic` epic, when both are given). Anything else exits 1. The run then picks only within the selection, and finishes once it has nothing left to start there. `--dry-run` applies the same selection.
- Refused with exit 1 before anything is created on: git below the supported floor, a dirty worktree, a worktree-isolation conflict, missing base skills, or a failing plugin-trust verdict.
- `[stories] source = "tickets"` runs the active initiative's ticket tree. Before anything is created, `run` executes the `tickets.*` preflight (capability probe included) and refuses with exit 1 on a problem, including a ticket store it cannot classify or drive (`tickets.store-topology` and the refusals below). The classified topology is recorded on the run, and `resume`, `resolve` and the TUI refuse a store that changed since; `branch_per = "run"` prints the `tickets.branch-per-run` warning. Each picked ticket is dispatched as `ticket <ref>` and, once verified at `built`, marked `done` under `[tickets] mark_done = "auto"` (left at `built` under `"never"`): inside its unit's commit for a store tracked in the code checkout (T1), after the code lands for a shared store, with a plan-only commit in a store that is its own repository (T3/T4). Under worktree isolation a unit's branch is `bmad-loop/<run>/<epic-slug>/<ref>`.
- On a parent initiative, `run` builds the children one run each: the first child `next <parent>` lists as ready, skipping one with no folder or nothing left to dispatch, then the next after each run that finishes. Before each pick, its first included, it marks every drained child's initiative `done` and prints `... closed <child> (drained)`, or `... leaves <child> open (<why>: ...)` while a run holds the child, its first pick still finds work, or its store is refused. It prints `initiative chain <parent>: <child> -> <next>` per switch, or `... ends (<why>)` with each waiting child's unmet prerequisites and their status, and stops at a run that does not finish; `resume` or `resolve` driving that run to finished continues the chain. `--epic` and `--story` exit 1 there; `--run-id` names the first child's run; `--max-stories` is one budget for the whole chain, and the chain ends `max-stories` once it is spent.
- `--dry-run` in tickets mode reads the active initiative through its `tickets.py` and marks nothing. It prints the startable tickets in build order (initiative epic table, then entry order), each with its badges, its `ticket <ref>` dispatch (`thorough` when `[tickets] thorough_review` selects it, `Halt after planning.` on an unrefined plan-checkpoint ticket) and its env, then the first pause a run would reach (`predicted:`), the `next` group counts and the tree's problems. A `tickets.py` failure exits 1.
- Prints `run <id> starting (attach: bmad-loop attach)`, then the run summary; exits 0 once the engine has run.

Expand Down
6 changes: 3 additions & 3 deletions docs/reference/run-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ A run paused on a usage limit also records `paused_limit_class` (`blocked` for a

A tickets run's `state.json` also records `initiative` and `initiative_folder`, the BMAD v7 initiative the run is pinned to: every ticket-tree read refuses while the live `[core] active_initiative` names another. Each task carries a `ticket` binding resolved when it was picked (ref, id, epic slug and id, title, plan, risk, and the refined, hitl and checkpoint flags). `ticket_epic_filter`, `ticket_story_approvals`, `ticket_epic_closures`, `ticket_epic_closure_resumed` and `ticket_epic_closure_acked` hold the epic selector, the refs whose story file an operator approved (resuming the pick's `plan-checkpoint` pause records the ref), each finished epic's closure (its retrospective `verdict`, or `unknown`), the epics whose closure pause a resume answered (a closure recorded without one pauses again), and the epics whose one closure pause was resumed past. Tickets pauses add the `paused_stage` values `ticket-hitl`, `ticket-refine` and `ticket-tree` (causes below). `run` resolves a tickets run's selectors before it starts: `ticket_epic_filter` holds the `--epic` epic slug, `story_filter` the `--story` ticket ref, and `epic_filter` stays null.

`parent_initiative` names the parent initiative when a tickets run builds one of its children, and is null otherwise.
`parent_initiative` names the parent initiative when a tickets run builds one of its children, and is null otherwise. A child run's `max_stories` is what was left of the chain's `--max-stories` when it launched. When `run`, or a resume of a child run, builds a parent's children, each finished child run journals the switch: `initiative-mark` and its store commit for each drained child closed, `initiative-close-skipped` (`parent`, `slug`, `why`: `run-live`, `run-unfinished`, `runs-unread`, `work-left`, `store-refused` or `uncommitted-edit`, `blocking_run`, `detail`) for each one left open, then `initiative-chain-next` (`parent`, `from_child`, `to_child`, and the children `passed_over` with `passed_over_why`) or `initiative-chain-end` (`parent`, `end`, the `waiting` children, one row per unmet prerequisite across `waiting_child`, `waiting_on` and `waiting_on_status`, and the passed-over ones). The next child's run journals `initiative-chain` (`parent`, `previous_run`, `previous_child`). The closes `run` makes before its first child's run are printed, not journaled: no run exists yet.

A v7 sweep records `initiative` and `initiative_folder` too, and its resume, `resolve` and the dashboard's re-arm refuse while the live `[core] active_initiative` names another, because its ledger and pre-answers belong to the initiative it started on.
A v7 sweep records `initiative` and `initiative_folder` too, and its resume, `resolve` and the dashboard's re-arm refuse while the live `[core] active_initiative` names another, because its ledger and pre-answers belong to the initiative it started on. The run-end sweep of a run that builds a child initiative works that child's ledger: its `initiative` and `initiative_folder` name the child and its `parent_initiative` the parent, and it is refused while the live `[core] active_initiative` names anything but the parent.

A pick pause, and a resume's stale-binding pause (`binding-moved`: an in-flight unit's ref no longer names its plan, or its verified plan is gone), journal `ticket-pick-halt` with its `cause` and `stage`:

Expand All @@ -106,7 +106,7 @@ A pick pause, and a resume's stale-binding pause (`binding-moved`: an in-flight
| `blocked-plan` | `escalation` |
| `foreign-active`, `stalled`, `tree-fault`, `pin-mismatch`, `dirty-initiative`, `ignored-plan`, `binding-moved` | `ticket-tree` |

`store_topology` is the ticket store classified at the start of a tickets run or a v7 sweep: `layout`, `placement`, `versioning`, `store_root` and `store_repo`. Resume, resolve and the TUI re-classify the store and refuse when it differs, or when the initiative folder is gone. When no checkout carries the store, every artifact dir under it stays at the main checkout for a worktree unit, and the mount's `_bmad/custom/config.user.toml` is rewritten so `[core] output_folder` names that store.
`store_topology` is the ticket store classified at the start of a tickets run or a v7 sweep: `layout`, `placement`, `versioning`, `store_root` and `store_repo`. Resume, resolve and the TUI re-classify the store and refuse when it differs, or when the initiative folder is gone. A chained child run's store, and its run-end sweep's, is classified on the child's own folder, as at its launch. When no checkout carries the store, every artifact dir under it stays at the main checkout for a worktree unit, and the mount's `_bmad/custom/config.user.toml` is rewritten so `[core] output_folder` names that store.

For a store tracked in the code checkout (T1), the commit window marks the plan itself, so the marked plan rides the unit's own commit. The save that records `COMMITTING` latches the plan's status and the sha256 of its bytes before that mark: `ticket_premark_status` and `ticket_premark_digest`, cleared when no T1 mark is owed or the plan cannot be read. A commit failure after a mark this window wrote puts the pre-mark bytes back. A resumed window whose mark is a no-op on a plan that reads another status than the latched one and no longer matches the digest finds it marked since that save, for example by an interrupted earlier window, and no bytes survive to restore it. If its commit then fails, the escalation names the plan left marked for a commit that does not exist, so the operator restores it by hand.

Expand Down
14 changes: 14 additions & 0 deletions docs/tickets-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ bmad-loop reads each folder the way the installed `tickets.py` does: a folder wi

A parent initiative, whose `tickets.toml` lists child initiatives in `[[initiative]]` tables, holds no tickets of its own, and each child's folder sits beside the parent's. The preflight refuses a parent (`tickets.initiative-parent`) under `[scm] isolation = "none"`, because sessions in the main checkout read the parent from `config.user.toml`, and under `gates.retrospective = "auto"`, whose session also runs in the main checkout. Set `active_initiative` to one child, or switch to worktree isolation without the auto retrospective. There the parent is an ok line naming its children, and the store checks run once for each child that has a folder: a child declared but not sliced yet is skipped. Each child is held to the `config.user.toml` rule of a shared store whatever its layout (`tickets.own-repo-isolation`). An epic the initiative declares but has not sliced yet has no folder; it holds no tickets, and the tickets gated after it wait for it.

`bmad-loop run` on a parent builds its children one run at a time, taking the first child `next <parent>` lists as ready. It passes over a ready child that has no folder yet (`no-folder`) and one whose first pick finds nothing to dispatch (`drained`). Each child's run records the parent as `parent_initiative`. Before every pick, bmad-loop closes each drained child (below), so the children waiting on it become ready. One line per switch names the next child or why the chain ended. The chain ends when:

- a child's run does not finish (paused, escalated, stopped or crashed). Once `resume` or `resolve` (from the CLI or the TUI) drives that run to finished, the chain goes on to the next child as it does after `run`.
- a stop was asked of the run.
- the `--max-stories` budget is spent (`max-stories`).
- it would build the same child again after a run that landed no ticket (`stalled`).
- no child can start: some still wait on a prerequisite (`waiting`), every child left was passed over (`passed-over`), or every child is done or dropped (`all-done`). A child waiting on a dropped prerequisite still counts as waiting. The end line names each waiting child's unmet prerequisites with their status, for example `initiative-billing on initiative-search (dropped), initiative-ledger (no-folder)`; `open` is a prerequisite whose file sets no status.

Closing a child writes `status: done` into its `<child>.md`. It happens before every pick, both when `run` starts and after each child's run finishes, for every child whose tickets are all done or dropped. That includes a child a stopped or interrupted chain left open, and one drained outside the chain. A line names each child closed. A drained child is left open, with a line naming why, while a run of it is live, or while a run that builds it is paused or otherwise unfinished (resume that run). A sweep of it holds it only while live. It is also left open while its first pick still finds work, such as an epic awaiting closure, which a run of the child then does, and while its store is refused.

Where the store is versioned, a close is committed as `chore(tickets): <child> done [bmad-loop <run>]` (`[bmad-loop chain <parent>]` for a close before the first child's run), in the code repo (T1) or in the store's own repository (T3/T4). A close that cannot be committed ends the chain (`store-commit-failed`, exit 1).

`--epic` and `--story` select within one initiative, so `run` on a parent refuses them with exit 1. `--run-id` names the first child's run; each later child's run gets its own id. `--max-stories` caps the whole chain: each child's run starts with what the runs before it left, counting every ticket they dispatched.

## Store layouts

The ticket store is the initiative folder and its plans. Where it lives decides where a ticket's `done` mark lands. `validate --json` reports the layout as `ticket_store`, and every run records it. Resume, resolve and the TUI refuse a store whose layout changed since the run started.
Expand Down
Loading
Loading