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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ story <id>`, the same annotation a sweep bundle writes. Both sprint and stories

### Changed

- **The story token budget is checked while the story runs (#336).** `max_tokens_per_story` was
read once, after the story had already been marked done — so an overrun was reported only after
every token was spent, and a story that deferred or escalated was never checked at all (one field
report burned 8.35M weighted against a 2M cap in silence). The cumulative weighted spend is now
re-checked at every session boundary, on every path a story can take, and the first crossing
raises an ATTENTION + desktop notice naming the spend and the cap. Latched per story and
persisted, so a resume does not re-notify. Still advisory: nothing is terminated — the
session-ending cap remains `limits.max_tokens_per_session`. The `token-budget-exceeded` journal
entry gains a `budget` field.

- **A failed worktree snapshot now blocks the rollback reset (#340).** The two preserve steps were
asymmetric: an auto-rollback refused to reset past commits it could not park, but a failed
_uncommitted_-work snapshot was journaled and the `reset --hard` ran anyway — destroying the
Expand Down Expand Up @@ -164,6 +174,20 @@ story <id>`, the same annotation a sweep bundle writes. Both sprint and stories

### Fixed

- **An unwritable `ATTENTION` file can no longer crash a run.** `gates.notify` promised "never
raises", but only its desktop half was guarded — the `ATTENTION` append was bare IO, so an
unwritable run dir turned an advisory notice into a run crash at every site that journals a
decision and then announces it (defer, escalate, plugin veto, manual-recovery pause, budget
warnings). The file sink now degrades like the desktop one, matching the observe-degrade rule the
adapter budget guards already applied to the same call. The journal entry each caller writes
first remains the durable record.

- **`limits.max_tokens_per_story` is validated like its per-session sibling.** It was parsed with a
bare `int()`, so `true` silently became a 1-token story cap and `0` was accepted — both against
the documented `int >= 1`. Non-integers and values below 1 now raise `PolicyError` at load, the
same rule `max_tokens_per_session` has always used. Note this rejects a `policy.toml` that
previously loaded; there is no `0 = off` semantic (set the cap high instead — it only warns).

- **A pause inside a defer's rollback no longer loses the defer record (#342).** `_defer` advanced
the task to terminal DEFERRED before recovering the tree, so a rollback that paused instead
(rollback OFF — the default — or a preserve/snapshot failure) unwound past the tail forever: no
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ One generic driver (`adapters/generic.py`) runs any coding CLI that fits the inj

**Copilot — pin a capable model:** Copilot's free default (GPT-5 mini) is unreliable for the multi-step dev/review skills — it silently skips steps mid-workflow and fails the story. Set a capable model in policy, e.g. `[adapter] model = "claude-sonnet-4-6"` (passed through as `--model`), for end-to-end reliability. Because Copilot fires `agentStop` per response turn, a thorough multi-turn review needs more than one nudge to finish; the profile ships `stop_without_result_nudges = 5`, and you can tune it per stage (e.g. `[adapter.review] stop_without_result_nudges = …`). Both knobs are editable in the settings TUI under `[adapter]`.

**On budgets:** agentic sessions are dominated by cache reads (80–90%+ of raw tokens), which every supported vendor bills at ~0.1x base input. The `max_tokens_per_story` check therefore uses a cost-weighted total — cache reads count at `limits.cache_read_weight` (default 0.1). Every display leads with that same weighted figure and names both units, so what you read tracks spend rather than context re-reads: the run-finished summary (stdout, `ATTENTION`, desktop notification) and `bmad-loop status` read `<weighted> weighted tokens (<raw> raw incl. cache reads)`, the TUI pairs a weighted `tokens` column with a `raw` one, and each `session-end` journal entry carries `tokens` (raw) beside `tokens_weighted`. Set the weight to 1.0 to budget — and display — raw tokens. Editing the weight and then resuming a paused run re-weights that run's _whole_ accumulated history, not just the sessions after the resume: totals are recomputed from raw counts at the resuming process's weight. That is what the budget has always done (`max_tokens_per_story` weights cumulative raw counts with the live policy), so the displays now match enforcement instead of drifting from it; the `run-resume` journal entry records both weights, so per-session totals written under the old one stay reconstructible. A separate mid-session guard (`limits.max_tokens_per_session`, default 4M weighted; `limits.session_budget_mode` = `off`/`warn`/`enforce`, default `warn`) samples a _running_ session's spend every ~30s and, in enforce mode, nudges it to wrap up and then terminates it `over_budget` (ordinary retry→defer) if it doesn't finish within `limits.session_budget_grace_s`. Mid-session sampling is live-verified on `claude`; other transcript-reading profiles sample the same cumulative files best-effort (mid-turn flush behavior unverified), and profiles with no mid-session usage signal (`usage_parser = "none"`, Copilot's shutdown-only flush) leave the guard inert.
**On budgets:** agentic sessions are dominated by cache reads (80–90%+ of raw tokens), which every supported vendor bills at ~0.1x base input. The `max_tokens_per_story` check therefore uses a cost-weighted total — cache reads count at `limits.cache_read_weight` (default 0.1). The story's cumulative spend is re-checked after every session it runs, so a story that blows its budget says so while it is still running rather than after it lands — and a story that defers or escalates is checked just like one that commits. The first crossing raises one ATTENTION + desktop notice (`story token budget exceeded: <key>`); the cap is advisory, so nothing is terminated and the story runs on — stop the run yourself if the spend is no longer worth it. The warning is latched per story and persisted, so a resumed run does not repeat it. Every display leads with that same weighted figure and names both units, so what you read tracks spend rather than context re-reads: the run-finished summary (stdout, `ATTENTION`, desktop notification) and `bmad-loop status` read `<weighted> weighted tokens (<raw> raw incl. cache reads)`, the TUI pairs a weighted `tokens` column with a `raw` one, and each `session-end` journal entry carries `tokens` (raw) beside `tokens_weighted`. Set the weight to 1.0 to budget — and display — raw tokens. Editing the weight and then resuming a paused run re-weights that run's _whole_ accumulated history, not just the sessions after the resume: totals are recomputed from raw counts at the resuming process's weight. That is what the budget has always done (`max_tokens_per_story` weights cumulative raw counts with the live policy), so the displays now match enforcement instead of drifting from it; the `run-resume` journal entry records both weights, so per-session totals written under the old one stay reconstructible. A separate mid-session guard (`limits.max_tokens_per_session`, default 4M weighted; `limits.session_budget_mode` = `off`/`warn`/`enforce`, default `warn`) samples a _running_ session's spend every ~30s and, in enforce mode, nudges it to wrap up and then terminates it `over_budget` (ordinary retry→defer) if it doesn't finish within `limits.session_budget_grace_s`. Mid-session sampling is live-verified on `claude`; other transcript-reading profiles sample the same cumulative files best-effort (mid-turn flush behavior unverified), and profiles with no mid-session usage signal (`usage_parser = "none"`, Copilot's shutdown-only flush) leave the guard inert.

**Shared prerequisites:** the `bmad-loop-*` skills must be present in `.agents/skills/` (codex and gemini read it; Claude Code reads `.claude/skills/`), and each CLI must have been run once interactively in the project for auth/trust — `bmad-loop init --cli codex --cli gemini` installs the skills into `.agents/skills/`, registers the hook relay, and prints the per-CLI first-run steps.

Expand Down
2 changes: 1 addition & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
### Budgeting & cost tracking

- Mid-session per-session token budget (`max_tokens_per_session`, default 4M weighted): both adapter wait loops sample cumulative usage on the ~30s heartbeat cadence and trip once on crossing the cap, per `session_budget_mode` — `warn` (default) raises an ATTENTION + lifecycle breadcrumb only; `enforce` additionally sends a wrap-up nudge, grants `session_budget_grace_s` (default 240s) to finish, then terminates the session `over_budget` (ordinary retry→defer routing; an artifact flushed at kill time is still honored). Mid-session sampling is live-verified on `claude`; other transcript-reading profiles sample the same cumulative files best-effort (early transcript-path delivery and mid-turn flush behavior are unverified vendor behavior), the wrap-up nudge into a busy pane is best-effort everywhere (the termination is the guarantee, the nudge a courtesy), and adapters with no mid-session usage signal (`usage_parser = "none"`, Copilot's shutdown-only flush) leave the guard inert.
- Per-story token budget (`max_tokens_per_story`, advisory, checked post-done) using the same cost-weighted total — cache reads counted at `cache_read_weight` (default 0.1, matching ~0.1x vendor billing). Every operator-facing total leads with that weighted figure and labels both units — the run-finished summary and `bmad-loop status` read `<weighted> weighted tokens (<raw> raw incl. cache reads)`, the TUI pairs a weighted `tokens` column with a `raw` one, and `session-end` journal entries carry `tokens` beside `tokens_weighted`.
- Per-story token budget (`max_tokens_per_story`, default 2M weighted, advisory) using the same cost-weighted total — cache reads counted at `cache_read_weight` (default 0.1, matching ~0.1x vendor billing). The story's cumulative spend is re-checked at **every session boundary**, so an overrun surfaces while the story is still running and regardless of how it ends — a story that defers or escalates is judged like one that commits. The first crossing raises one ATTENTION + desktop notice (`story token budget exceeded: <key>`) and a `token-budget-exceeded` journal entry carrying `weighted`, `total` and `budget`; the warning is latched per story (and persisted, so a resume does not re-notify) and nothing is terminated — advisory means the story runs on. Every operator-facing total leads with that weighted figure and labels both units — the run-finished summary and `bmad-loop status` read `<weighted> weighted tokens (<raw> raw incl. cache reads)`, the TUI pairs a weighted `tokens` column with a `raw` one, and `session-end` journal entries carry `tokens` beside `tokens_weighted`.
- Token usage read from each CLI's local session transcript (per-profile `usage_parser`), aggregated per story (`bmad-loop status`).

### Configuration (`.bmad-loop/policy.toml`)
Expand Down
2 changes: 1 addition & 1 deletion docs/tui-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ behavior.
| `limits.dev_stall_nudges` | int ≥ 0 | 2 | wake-nudges on grace expiry before stalling; a Stop response restores the budget · 0 = stall on grace expiry |
| `limits.dev_stall_nudges_cap` | int ≥ 0 | 6 | total (never-restored) wake-nudges per dev/review session — bounds the refill loop when the reply to a nudge is itself a result-less Stop |
| `limits.workflow_stall_nudges_cap` | int ≥ 0 | 3 | same monotonic cap for an injected plugin-workflow session that finished its work but never wrote its completion marker · 0 = stall on first grace expiry |
| `limits.max_tokens_per_story` | int ≥ 1 | 2000000 | cost-weighted budget |
| `limits.max_tokens_per_story` | int ≥ 1 | 2000000 | advisory cost-weighted cap on a story's cumulative spend, re-checked at every session boundary; one ATTENTION + desktop notice per story on the crossing, nothing terminated |
| `limits.cache_read_weight` | float 0.0–1.0 | 0.1 | cache-read weight in every token total, budgets and displays alike; 1.0 = raw |
| `limits.session_budget_mode` | select | `warn` | mid-session per-session budget guard: `off` (no sampling) / `warn` (one ATTENTION + breadcrumb) / `enforce` (wrap-up nudge, then `over_budget` termination → retry/defer) |
| `limits.max_tokens_per_session` | int ≥ 1 | 4000000 | weighted per-session cap sampled every ~30s mid-session; healthy sessions run ~1–2.5M weighted, so the default trips only true runaways |
Expand Down
2 changes: 2 additions & 0 deletions src/bmad_loop/data/settings/core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ key = "max_tokens_per_story"
kind = "int"
minimum = 1
default_ref = "LimitsPolicy.max_tokens_per_story"
label = "story budget (weighted tokens)"
description = "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"
[[section.field]]
key = "cache_read_weight"
kind = "float"
Expand Down
Loading