Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ breaking changes may land in a minor release.

### Fixed

- Let `resolve` re-arm, adopt or abandon a story left escalated after a `resume` cleared
the run's pause and its engine exited before run end (#958). Warn on `resume` and hint
in `status` and the TUI header when a story is still escalated; restore the pause
(journaling `resume-aborted`) when a resume fails before its engine starts; report a
live engine before the pause stage.
- Pause a Claude session seen blocked by a usage limit (credit exhaustion or a
session-window limit) with a `usage limit:` reason instead of charging a dev attempt;
a live pane sighting is exempt from the token veto. Add the profile fields
Expand Down
4 changes: 3 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,14 @@ bmad-loop drives a BMAD sprint unattended: a deterministic Python loop launches
- **Properties:**
- A CRITICAL escalation marks the story `escalated`, sends a `CRITICAL escalation: <story>` notice and pauses the run at `escalation`.
- A PREFERENCE escalation is journaled as `preference-escalation` and the run continues.
- Resolve also admits an unfinished story run with no pause stage and a story still escalated, as a resume whose engine exited before run end leaves it; `resume` warns of each escalated story it skips, and `status` and the TUI header name the resolve that clears it.
- A resume that fails before its engine starts puts back the pause it cleared and journals `resume-aborted`.
- `--abandon` (in-progress) closes an escalated replay run unmerged, keeping its branch and parking dirty work on a preserve ref.
- Re-arming resets the story to `pending` with fresh dev-attempt and review-cycle budgets.
- `--reverify` re-runs `[verify]` on a deferred or environment-fault story's kept work without a dev session; on a finished run it replays a kept worktree unit in a new run.
- In tickets mode the re-arm moves the ticket's plan through `tickets.py mark` and clears its `blocked_*` fields; a refused move puts the plan back byte for byte.
- **Config:** `bmad-loop resolve --story`, `--no-interactive`, `--restore-patch`, `--adopt-branch`, `--reverify`, `--resume` / `--no-resume`, `--force`, `--abandon`
- **Tested by:** `tests/test_engine.py`, `tests/test_cli.py`, `tests/test_escalation.py`, `tests/test_resolve.py`
- **Tested by:** `tests/test_engine.py`, `tests/test_cli.py`, `tests/test_escalation.py`, `tests/test_resolve.py`, `tests/test_runs.py`, `tests/test_tui_app.py`
- **Docs:** [Escalations](escalations-guide.md#escalations), [Resolving an escalation](escalations-guide.md#resolving-an-escalation), [Replay a finished run's kept unit](escalations-guide.md#resolve-reverify-finished), [Abandon a replay run](escalations-guide.md#abandon-a-replay-run), [Resolve in tickets mode](escalations-guide.md#resolve-in-tickets-mode), [resolve CLI reference](reference/cli.md#resolve)

### Human-only actions
Expand Down
8 changes: 4 additions & 4 deletions docs/escalations-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ A run pauses when it needs a human: an escalation, an environment fault, a merge
## Where a pause shows up

- **Notification.** A desktop notice (`[notify] desktop`, default on) and a line appended to `.bmad-loop/runs/<run-id>/ATTENTION` (`[notify] file`, default on). If neither channel works, the run warns that no alert channel is configured. Displayed text is capped at 2000 characters; the full reason is in `journal.jsonl`.
- **`bmad-loop status <run-id>`** shows the pause stage, the paused story and the reason. A parked attempt's recovery ref appears as `[ref]` (`preserve_ref` in `status --json`).
- **`bmad-loop status <run-id>`** shows the pause stage, the paused story and the reason. A parked attempt's recovery ref appears as `[ref]` (`preserve_ref` in `status --json`). A run with no pause stage and a story still escalated prints `escalated: story '<key>' awaiting resolve — bmad-loop resolve <id>`.
- **The TUI** shows the same, and its keys launch the commands below: `R` resolve, `V` re-verify, `e` resume, `p` review the paused run.

| Pause stage | Cause | Clear it with |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `escalation` | a CRITICAL escalation, or a story still escalated at run end | [`bmad-loop resolve`](#resolving-an-escalation) |
| `escalation` | a CRITICAL escalation, or a story still escalated at run end; with no pause stage, a story a resume left escalated | [`bmad-loop resolve`](#resolving-an-escalation) |
| `environment` | an environment probe failed before a session launched | fix the environment, then `bmad-loop resume` ([details](#environment-faults)) |
| `story-gate` | a ledger `gate:`, or a ledger that cannot be read; in tickets mode also a `tickets.toml` that cannot be read for its `v6_key` claims while an unlanded entry carries a v6 story-key gate | see the [sweep guide](sweep-guide.md#hard-gates) |
| `spec-approval`, `epic-boundary` | a configured gate or a retrospective | `bmad-loop resume` |
Expand All @@ -32,11 +32,11 @@ Escalations come in two severities:
- **CRITICAL**: the story moves to `escalated`, `story-escalated` is journaled, you get a `CRITICAL escalation: <story>` notice, and the run pauses at `escalation`.
- **PREFERENCE**: journaled as `preference-escalation`; the run continues.

A story stays escalated until you resolve it. A plain `bmad-loop resume` skips it and runs the rest of the queue, then pauses again with `story '<key>' is still escalated — run bmad-loop resolve <id>`. A run is never marked finished while a story is escalated.
A story stays escalated until you resolve it. A plain `bmad-loop resume` warns `story '<key>' is still escalated — resume skips it`, skips it and runs the rest of the queue, then pauses again with `story '<key>' is still escalated — run bmad-loop resolve <id>`. A run is never marked finished while a story is escalated. A resume clears the pause before its engine starts, so an engine that exits before run end (killed, stopped, crashed) leaves the run with no pause stage and the story still escalated; resolve it the same way.

## Resolving an escalation

`bmad-loop resolve <run-id>` works on a run paused at `escalation` whose engine is not running. `--story <key>` picks a story other than the paused one. By default it asks before it re-arms and then resumes the run itself:
`bmad-loop resolve <run-id>` works on a run paused at `escalation`, or on an unfinished run with no pause stage and a story still escalated, whose engine is not running. Any other pause stage, a finished run, and a sweep with no pause stage are refused. `--story <key>` picks a story other than the default: the paused story, or with no pause stage the first escalated one (`--story` must then name an escalated story). By default it asks before it re-arms and then resumes the run itself:

- `--resume` re-arms and resumes without asking.
- `--no-resume` only re-arms and prints `resume when ready: bmad-loop resume <id>`.
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ bmad-loop resume [--project PROJECT] [--accept-baseline] run_id
- Refused with exit 1 for a live engine, a finished run, a sweep whose ledger cannot be read, and the same git-floor, isolation, skill and plugin-trust gates as `run`. Does not require a clean worktree.
- Refused with exit 1, before any side effect, for a story run whose work source this version does not know (the run was started by a newer bmad-loop) or cannot run yet. Do not downgrade bmad-loop while a run is in progress.
- Discards any pending stop request and re-reads the policy and config before continuing.
- For a story run, prints `warning: run <id>: story '<key>' is still escalated — resume skips it; resolve it with bmad-loop resolve <id>` on stderr for each escalated story, then continues.
- Clears the pause before the engine starts. A resume that fails before that (an adapter, profile or multiplexer error, `error: …`) puts the cleared pause fields back and journals `resume-aborted` with `error` set to the exception class, unless the run finished or was paused again meanwhile; a restore that fails prints `could not restore the pause this resume cleared` and the original error still ends the command. After a resolve re-arm the story is already `pending`, so a plain `resume` re-drives it.

### `resolve`

Expand All @@ -222,7 +224,7 @@ bmad-loop resolve [--project PROJECT] [--story STORY] [--no-interactive]

| Option | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `--story STORY` | Story to resolve. Default: the run's paused story. |
| `--story STORY` | Story to resolve. Default: the paused story, or with no pause stage the first escalated story. |
| `--no-interactive` | Skip the resolve agent (the spec was fixed by hand), then re-arm and resume. With `--abandon`: skip the confirm prompt. |
| `--restore-patch PATH` | Re-arm the spec to `in-review` and re-apply this saved patch, so review resumes on the attempted change. |
| `--adopt-branch` | Commit and merge the kept worktree branch as-is and mark the story done. Worktree isolation only; not for sweeps. |
Expand All @@ -234,7 +236,8 @@ bmad-loop resolve [--project PROJECT] [--story STORY] [--no-interactive]
- `--restore-patch`, `--adopt-branch`, `--reverify` and `--abandon` are mutually exclusive.
- Tickets mode: `--story` takes a ticket ref. The re-arm moves the plan through `tickets.py mark` to `ready-for-dev`, or `in-review` for a restore, and a refused move aborts it. Refused with exit 1 when the ticket store's layout changed since the run started. See the [tickets guide](../tickets-guide.md#parking-and-resolve).
- `--reverify --story KEY` on a finished sprint run creates a new replay run for a kept DEFERRED worktree unit; the finished run is not modified.
- Refuses with exit 1, leaving the run unchanged, when the run is not paused at an escalation (`--reverify --story` accepts any pause), the engine is live, there is no escalated story, or a DEFERRED story is targeted without `--reverify`. Declining the confirm prompt exits 0 and leaves the run paused.
- Acts on a run paused at `escalation`, or on an unfinished story run with no pause stage and a story still escalated (a resume whose engine exited before run end leaves this). With no pause stage, `--story` must name an escalated story.
- Refuses with exit 1, leaving the run unchanged, in this order: the engine is live (`still live`; stop it first), the run is neither of the shapes above (any other pause stage, a finished run, a sweep with no pause stage; `--reverify --story` accepts any pause), there is no escalated story, or a DEFERRED story is targeted without `--reverify`. Declining the confirm prompt exits 0 and leaves the run paused.

### `decisions`

Expand Down Expand Up @@ -298,6 +301,7 @@ bmad-loop status [--project PROJECT] [--json] [run_id]
```

- The status line (`status: …`) is `finished`, `PAUSED (<stage>) — <reason>`, `in progress — graceful stop pending (will stop after the current item)`, or `in progress (or interrupted)`.
- For an unfinished story run with no pause stage, each story still escalated adds `escalated: story '<key>' awaiting resolve — bmad-loop resolve <id>` below the status line. Not in `--json`.
- For a tickets run, the footer is the tickets board, read live through the project's `tickets.py status` and pinned to the run's initiative. It shows the run's selectors, the ticket counts by state, then one block per epic in build order: done count, epic status and any recorded closure, and each ticket's state, plan status, flags and this run's phase. A tree that cannot be read prints `tickets board unavailable: <reason>`, and `status` still exits 0. Marks or store commits still owed after landing are listed last.
- Exits 1 with `no runs found`, or on a bad or ambiguous ref (stdout stays empty under `--json`).

Expand Down
11 changes: 9 additions & 2 deletions docs/tui-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ Situational banners below that:
a crash; the error follows on the next line.
- `✖ engine gone — run was interrupted · press e to resume`: the recorded
engine pid is dead.
- `escalated: story '<key>' awaiting resolve — press R (bmad-loop resolve <id>)`:
an unfinished run with no pause stage still has an escalated story, as a
resume whose engine exited before run end leaves it.
- `⚠ auto-sweep not run: <trigger> (<why>) — deferred work is untouched`: an
auto-sweep was refused; run `bmad-loop sweep` with a clean worktree. A sweep
that ran shows a dim `auto-sweep ran: <triggers>` line.
Expand Down Expand Up @@ -422,6 +425,9 @@ selects the new run.
`e` resumes the selected unfinished run: paused, stopped, crashed or
interrupted. It refuses finished runs and runs whose state is unreadable. The confirmation shows `paused at <stage> — <reason>` for a paused
run, or `run is not paused — it looks interrupted`.
For a story run it adds a yellow
`story '<key>' is still escalated — resume skips it; resolve it with bmad-loop resolve <id>`
line for each escalated story.

If the original engine still appears to be running, the confirmation adds, in
bold red, `engine.pid may still be live — resuming could double-drive this run`.
Expand Down Expand Up @@ -457,8 +463,9 @@ with `run <id> is live — stop it first`.

## Resolving an escalation (`R`)

`R` works only on a run paused at the `escalation` stage and refuses a run
whose engine may still be live. A CRITICAL escalation parks its story at
`R` works on a run paused at the `escalation` stage, or on an unfinished story
run with no pause stage and a story still escalated (it resolves the first such
story), and refuses a run whose engine may still be live. A CRITICAL escalation parks its story at
`escalated`, which a plain resume skips; `R` is how you un-stick it.

1. Confirming launches `bmad-loop resolve <run-id>` in a control window and
Expand Down
13 changes: 4 additions & 9 deletions src/bmad_loop/checkpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

from . import gates
from .engine import RunPaused
from .escalation import unresolved_escalation_keys
from .model import (
PAUSE_ESCALATION,
PAUSE_PLAN_CHECKPOINT,
Expand Down Expand Up @@ -125,15 +126,9 @@ def _unresolved_escalation_key(self) -> str | None:
from disk every pick (see ``_repause_inrun_escalation``), so reaching run end
means the human fixed it by hand; its stale ESCALATED record must not
re-pause the run, whose only way past would be a re-arm that re-drives the
fixed story."""
return next(
(
k
for k, t in self.state.tasks.items()
if t.phase == Phase.ESCALATED and t.attempt > 0
),
None,
)
fixed story. ``escalation.unresolved_escalation_keys`` holds the narrowing
for the stories and tickets sources, so resolve's admission shares it."""
return next(iter(unresolved_escalation_keys(self.state)), None)

def _pause_wedged(
self,
Expand Down
Loading
Loading