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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ breaking changes may land in a minor release.

### Added

- Add `limits.quota_wait_max_min` (default 300; 0 = off): while the visible pane shows
Claude Code waiting out a usage-window reset and the transcript is idle, freeze the
session timeout and hold the stall nudge, so the session finishes after the CLI's own
auto-continue. Past the cap, or when the parsed reset lies beyond it, pause with a
usage-limit reason. Surface the wait and the reset time in `heartbeat.json`
(`quota_waiting`, `quota_wait_s`, `quota_resets_at`), `session-end` (`quota_wait_s`,
`usage_limit_resets`), the journal (`quota-wait-*`), the pause reason
(`; resets <time>`) and the TUI header (`quota wait`).
- Add the experimental `tickets` work source for BMAD v7 initiative ticket trees
(`[stories] source = "tickets"`): dispatch `bmad-build-auto ticket <ref>` in initiative
build order, verify each plan at `built`, and mark it `done` through `tickets.py mark`
Expand Down Expand Up @@ -175,6 +183,11 @@ breaking changes may land in a minor release.

### Fixed

- 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
`usage_limit_patterns` and `usage_wait_patterns`, seeded from lines captured in
community reports (#610).
- Read an epic the initiative declares but has not sliced yet: `status` no longer
faults the board on its folderless row, and it triggers no retrospective or closure.
- Name the T1 ticket plan an interrupted commit window left `done` or `blocked` when
Expand Down
11 changes: 7 additions & 4 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ bmad-loop drives a BMAD sprint unattended: a deterministic Python loop launches
- A session silent for `dev_stall_grace_s` (default 600) gets up to `dev_stall_nudges` wake nudges, then ends `stalled` and is charged as a failed attempt.
- Every dev and review session ends as `timeout` after `[limits] session_timeout_min` (default 90).
- A dev session that did no work, or any session parked on a prompt only a human should answer, pauses the run instead of being charged.
- While the visible pane shows the coding CLI waiting out a usage-limit reset and the transcript is idle, the session timeout is frozen and the stall nudge held, for at most `[limits] quota_wait_max_min` (default 300; 0 = off) cumulative minutes per session; past the cap, or when the parsed reset lies beyond it, the session pauses with a usage-limit reason.
- Before an attempt is reset, its commits go to an `attempt-preserve/<run>-<tip8>` branch and uncommitted work to a `refs/attempt-preserve-dirty/*` snapshot.
- **Config:** `[limits] max_dev_attempts`, `[limits] max_review_cycles`, `[limits] session_timeout_min`, `[limits] dev_stall_grace_s`, `[limits] dev_stall_nudges`, `[limits] dev_stall_nudges_cap`, `[scm] preserve_keep`
- **Config:** `[limits] max_dev_attempts`, `[limits] max_review_cycles`, `[limits] session_timeout_min`, `[limits] dev_stall_grace_s`, `[limits] dev_stall_nudges`, `[limits] dev_stall_nudges_cap`, `[limits] quota_wait_max_min`, `[scm] preserve_keep`
- **Tested by:** `tests/test_escalation.py`, `tests/test_engine.py`, `tests/test_generic_tmux.py`, `tests/test_verify.py`, `tests/test_recovery_flow.py`
- **Docs:** [Preserved attempts](escalations-guide.md#preserved-attempts), [[limits] reference](reference/configuration.md#limits), [[scm] reference](reference/configuration.md#scm)

Expand Down Expand Up @@ -161,9 +162,11 @@ bmad-loop drives a BMAD sprint unattended: a deterministic Python loop launches
- A non-completed session whose log tail matches its profile's `env_fault_patterns` pauses instead of spending a dev attempt.
- opencode is classified from model-free files only: the server's stdout, plus on OpenCode 2.x the adapter-written `provider-errors.log` (one line per provider-error event). Its 2.x patterns cover the `provider.rate-limit`, `provider.quota` and `provider.transport` error types.
- A pattern match is overruled when the session's tracked token usage is nonzero; that session is charged like any failure.
- **Config:** `[environment] probes`, `[environment] probe_timeout_s`, `[verify] env_fault_rc`; profile key `env_fault_patterns`
- **Tested by:** `tests/test_engine.py`, `tests/test_escalation.py`, `tests/test_env_fault_patterns.py`, `tests/test_env_fault.py`, `tests/test_generic_tmux.py`, `tests/test_opencode_http.py`
- **Docs:** [Environment faults](escalations-guide.md#environment-faults), [[environment] reference](reference/configuration.md#environment), [[verify] reference](reference/configuration.md#verify), [Profile field reference](adapter-authoring-guide.md#cliprofile)
- A session whose visible pane shows a usage-limit line from its profile's `usage_limit_patterns` or `usage_wait_patterns`, on an idle session at stall-grace expiry or when the timeout fires, pauses with a `usage limit:` reason and is never charged, whatever its token usage; the same line found only in the log tail stays subject to the overrule.
- When the usage-limit line names a reset time, the pause reason ends its evidence with `; resets <time>` (ISO-8601 when the time resolves, the raw text otherwise).
- **Config:** `[environment] probes`, `[environment] probe_timeout_s`, `[verify] env_fault_rc`, `[limits] quota_wait_max_min`; profile keys `env_fault_patterns`, `usage_limit_patterns`, `usage_wait_patterns`
- **Tested by:** `tests/test_engine.py`, `tests/test_escalation.py`, `tests/test_env_fault_patterns.py`, `tests/test_env_fault.py`, `tests/test_generic_tmux.py`, `tests/test_opencode_http.py`, `tests/test_usagereset.py`
- **Docs:** [Environment faults](escalations-guide.md#environment-faults), [Usage-limit pauses](escalations-guide.md#usage-limit-pauses), [Usage-limit signals](adapter-authoring-guide.md#usage-limit-signals), [[environment] reference](reference/configuration.md#environment), [[verify] reference](reference/configuration.md#verify), [Profile field reference](adapter-authoring-guide.md#cliprofile)

### Resumable runs and graceful stop

Expand Down
70 changes: 70 additions & 0 deletions docs/adapter-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ a new `name` extends the set. The aliases `claude-code-tmux` and `opencode` reso
| `seed_files` | | `[]` | Project-relative gitignored files copied into each provisioned worktree. Must stay inside the project. |
| `env_fault_patterns` | | `[]` | Regexes that classify a non-completed session as an environment fault. See [Environment-fault patterns](#environment-fault-patterns). |
| `parked_prompt_patterns` | | `[]` | Regexes matched against the visible pane at each stall-grace expiry. See [Parked-session signals](#parked-session-signals). |
| `usage_limit_patterns` | | `[]` | Regexes for a usage-limit line the CLI stays blocked behind until an operator acts. See [Usage-limit signals](#usage-limit-signals). |
| `usage_wait_patterns` | | `[]` | Regexes for a usage-limit line the CLI waits out and continues from by itself. Disjoint from `usage_limit_patterns`. See [Usage-limit signals](#usage-limit-signals). |
| `[workspace_trust]` | | unset | The [`WorkspaceTrustSpec`](#workspacetrustspec-the-workspace_trust-table) table. Unset: nothing under `~` is read or written. |
| `session_id_flag` | | `""` | Launch flag for a caller-chosen session id (claude: `--session-id`). The generic adapter appends `[flag, uuid4]` and pins attribution to that id. |
| `subagent_transcript_dir` | | derived from `usage_parser` (`{stem}/subagents` for `claude-jsonl`, else `""`) | Subagent transcript tree beside the transcript, walked recursively and stat-only; its writes count as idle-notice activity. Must contain `{stem}` (the transcript's filename stem) and stay relative; `""` = off. |
Expand Down Expand Up @@ -426,6 +428,10 @@ is in [Environment faults](escalations-guide.md#environment-faults).
- Seeded for `claude` (connection loss and two captured provider 5xx errors) and
`opencode` (provider quota/rate-limit and connection on 1.x; `provider.rate-limit`,
`provider.quota` and `provider.transport` events on 2.x). Empty means inert.
- The tail scan also covers `usage_limit_patterns` and `usage_wait_patterns` as ordinary
pattern families, under the same token-usage overrule: a session that got past a limit
and later failed for another reason is charged. Only a live pane sighting of a usage
line exempts a session from the overrule; see [Usage-limit signals](#usage-limit-signals).

### Parked-session signals

Expand Down Expand Up @@ -455,6 +461,70 @@ turn without a result and sits idle pauses as parked instead of being nudged. A
overlay (`.bmad-loop/profiles/claude.toml`) that drops `idle_prompt` from
`[hooks.notification_types]` restores the wake nudge.

### Usage-limit signals

A CLI stopped by a subscription or credit limit either stays blocked until someone acts
(credit exhaustion) or waits for the limit's reset and continues by itself (a
session-window limit under Claude Code's auto-continue). Two opt-in profile lists name
those lines, both matched line by line against the ANSI-stripped visible pane:

- **`usage_limit_patterns`**: the CLI is blocked and will not continue on its own.
- **`usage_wait_patterns`**: the CLI is blocked and waiting for a reset it continues from.

The lists are disjoint by contract; "usage-limited" means either one matched. They follow
the env-fault evidentiary bar: reproduce the captured sentence prefix (never a bare
`session limit`, `resets` or `usage credits`), use single-character classes so a pattern
cannot match its own profile line, and seed only lines captured off the CLI's screen. The
shipped `claude` profile seeds one line in each list; every other profile leaves both
empty, which keeps them inert.

**At stall-grace expiry** the pane is captured once and checked against the parked
patterns and both usage lists. A usage match withholds the nudge (even when a hook latch
such as `idle_prompt` is set, since an idle CLI behind a limit latches exactly that) and
ends the session `stalled` + `parked` with `SessionResult.usage_limited` holding the
matched evidence. **When the session timeout fires**, one capture against both lists sets
`usage_limited` on the `timeout` result. Neither gate takes a usage match from a session
that is visibly working: a transcript written within the last 90 s (as of the last
heartbeat) or an unanswered tool call means the line is left over from a limit it
recovered from, and the stall or timeout is an ordinary one. A dead window is never
usage-limited.

A `usage_limited` result is classified as an environment fault with no token-usage
overrule and no tail scan, and pauses with a `usage limit:` reason (see
[Environment faults](escalations-guide.md#environment-faults)). When the matched line
carries a `resets <time>` clause, the time is parsed (`H[:MM]am|pm`, optionally preceded by
a weekday or `<Mon> <D> at` and followed by a ` (<Area/City>)` zone) into
`usage_limit_resets`: ISO-8601 when it resolves, otherwise the raw clause, with a
`usage-reset-unparsed` lifecycle crumb (`reason` is `unparsed`, or `zone-unavailable` when
the host has no tz data for the named zone).

**The quota wait.** With `[limits] quota_wait_max_min` above 0 (default 300) and
`usage_wait_patterns` non-empty, the generic adapter admits a wait at a heartbeat when all
of these hold: the transcript has been idle for at least 90 s, no tool call is open, the
visible pane matches `usage_wait_patterns`, no `QuotaPrompt` or `PermissionPrompt` latch is
set, and the session's cumulative wait is under the cap. While waiting:

- the session timeout is frozen on every tick, at no less than 120 s remaining;
- a stall-grace expiry re-arms the grace with no nudge (a `QuotaPrompt` or
`PermissionPrompt` latch still parks; an `IdlePrompt` latch never ends a wait);
- a `Stop` without a result withholds the stop nudge (`stop-nudge-withheld`,
`reason="quota-wait"`); a `Stop` with a result completes normally;
- the token-budget grace keeps counting, because a crossed budget is real spend.

The wait ends when the transcript moves again (the timeout resumes from where it was
frozen) or when a `QuotaPrompt`/`PermissionPrompt` latch arrives. The pane no longer
matching does not end it, since the line can scroll away. A line the CLI continued from is
spent: if it stays on screen, no live usage gate (admission, stall-grace expiry or the
timeout look) takes it as evidence again, and a later limit has to print a new line. At
the cap, measured on both the monotonic and the wall clock so a host suspend cannot
stretch it, the session ends `stalled` + `parked` + `usage_limited` (`quota-wait-cap`). A
line whose parsed reset lies beyond the remaining cap is not waited on: the session ends
the same way at once (`quota-wait-skipped`, `reason="reset-beyond-cap"`). An unresolved
reset is admitted, and the cap bounds it.

The `opencode-http` adapter runs its own wait loop with no pane, so neither the usage gate
nor the quota wait applies there.

### Worked TOML — copilot

The shipped `copilot` profile uses the non-default tuning knobs and a camelCase event map,
Expand Down
17 changes: 17 additions & 0 deletions docs/escalations-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,23 @@ What to do depends on where it hit:
| `probe:decision:dev`, `probe:decision:fix`, `probe:claim:dev`, `probe:claim:fix` | yes, if the latest dev session completed |
| `probe:dispatch:dev`, `probe:dispatch:review` | no: it is an `environment` pause; use `resume` |

### Usage-limit pauses

A session the adapter saw blocked by a coding-CLI usage limit (out of credits, or a session-window limit) pauses with a reason of the form:

```text
usage limit: dev session stalled (<pane line>; resets <time>; the coding CLI is blocked by a usage limit — add credits, switch model or wait for the reset, then bmad-loop resolve <run>)
```

The attempt is not charged, whatever tokens the session used before the limit hit: the line was on screen when the session ended. `<pane line>` is the matched line from the CLI's screen. `; resets <time>` appears only when that line names a reset: an ISO-8601 time when it resolves, otherwise the text as printed (for example when the host has no time-zone data for the zone the CLI named).

To recover:

1. Clear the limit: add credits, switch the session's model (`[adapter] model`, or the stage's `[adapter.<stage>]`), or wait until the reset time.
2. `bmad-loop resolve <run-id> --no-interactive` re-arms the story with a fresh attempt budget and resumes. The spec needs no fix.

A session-window limit the CLI waits out by itself does not pause at all at first. While the pane shows that wait and the transcript is idle, the session timeout stands still and no stall nudge is sent, so the session finishes after the CLI continues. `[limits] quota_wait_max_min` (default 300, one five-hour window; 0 = off) caps the minutes one session may spend waiting. Past the cap, or when the parsed reset already lies beyond what is left of it, the session pauses with the reason above. A heartbeat shows the wait as `quota_waiting` / `quota_wait_s` / `quota_resets_at`, and the TUI header as `quota wait`.

## Worktree merge-back failures

Under `[scm] isolation = "worktree"`, each story merges back into the target branch in the main checkout. When the merge fails, the unit escalates: its branch `bmad-loop/<run-id>[/<story>]` and worktree are kept, the diff is saved to `.bmad-loop/runs/<run-id>/failed/<unit>/changes.patch`, and the run pauses at `escalation`.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Cycle/attempt caps, timeout & token budget.
| `dev_stall_nudges` | integer | `2` | ≥ 0 | best-effort wake nudges for a silent dev/review session on grace expiry before it is called stalled; transport activity re-arms the grace and fresh Stop/idle evidence restores this budget · 0 = stall on grace expiry |
| `dev_stall_nudges_cap` | integer | `6` | ≥ 0 | total (never-restored) best-effort stall nudges for a dev/review session; bounds launch-time recovery and Stop/idle budget refills because an accepted nudge does not guarantee a wake · 0 = stall on first grace expiry |
| `workflow_stall_nudges_cap` | integer | `3` | ≥ 0 | total (never-restored) stall nudges for an injected plugin-workflow session before it is called stalled; bounds a session that finished its work but never wrote its completion marker · 0 = stall on first grace expiry |
| `quota_wait_max_min` | integer | `300` | ≥ 0 | cumulative minutes one session may sit in a usage-limit wait (the visible pane shows the coding CLI waiting out a usage-window reset it continues from by itself, and the transcript is idle) with its session timeout frozen and its stall nudge held · past the cap the session ends parked with usage-limit evidence · 0 = off (the timeout keeps counting through the wait) |
| `dev_contract_nudge` | boolean | `true` | | ON: one targeted nudge per session when a Stop finds a spec finalized to a terminal frontmatter status but missing its `## Auto Run Result` marker, asking the skill to append it and end its turn (sent exactly once, never refilled, touches no stall counters) · OFF: rely only on harness-side frontmatter synthesis |
| `max_tokens_per_story` | integer | `2000000` | ≥ 1 | advisory cap on one story's cumulative weighted spend across all its sessions, re-checked at every session boundary · crossing it raises one ATTENTION + desktop notice per story and nothing else — the story runs on · for a cap that can END a session see session budget mode below |
| `cache_read_weight` | float | `0.1` | 0.0–1.0 | weight of cache-read tokens in every token total — budgets and displays alike · cache reads bill at ~0.1x base input on all supported vendors and are 80-95% of the raw count on agentic work, so the default keeps totals proportional to spend rather than to context re-reads · 1.0 = count cache reads at full price (raw) |
Expand Down
Loading
Loading