Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
311654d
add verification results to post-dev hook
kamiotakaaki Aug 17, 2026
374a285
test timeout verification streams
kamiotakaaki Aug 17, 2026
fa69b23
style: apply trunk fmt to post-dev verify changes
Aug 19, 2026
dd22a4f
fix(verify): decode timeout output on the run's codec, not UTF-8
Aug 19, 2026
e72089d
fix(verify): collapse newlines on the timeout path too
Aug 19, 2026
5b8bcb8
fix(verify): store verifier streams outside the adapters' logs/
Aug 19, 2026
bfa0922
fix(verify): sanitize the whole verify stream filename composition
Aug 19, 2026
eed034c
fix(verify): bound, gate and degrade the verifier stream capture
Aug 19, 2026
b82ba46
fix(verify): seed the verification counter, discriminate the two emits
Aug 19, 2026
d795fd5
fix(verify): emit post_dev_verify before the fix leg escalates
Aug 19, 2026
91594af
docs(plugins): state the dev-phase boundary on command_results (#641)
Aug 19, 2026
f20440d
test(policy): escape the stream_capture_kb match= pattern
Aug 19, 2026
96b1beb
fix(journal): anchor verify-stream writes against a planted verify/ s…
Aug 19, 2026
7d926f8
fix(journal): refuse a junctioned verify/ on the win32 fallback too
Aug 19, 2026
3282a83
Merge remote-tracking branch 'origin/main' into feature/post-dev-veri…
Aug 19, 2026
0e39fc6
fix(diagnostics): count the verifier stream store in file totals
Aug 19, 2026
1c80f64
test(engine): name a resolvable verifier command in the new fixtures
Aug 19, 2026
75ab658
fix(runs): reclaim the verifier stream store when a run is trimmed
Aug 19, 2026
0dc2928
fix(runs,cli): trim a planted redirect as a link, and never size thro…
Aug 19, 2026
f722d8a
fix(diagnostics,cli): never let a planted redirect widen a walk
Aug 19, 2026
336d519
fix(verify): bound verifier streams in memory, separately from the di…
Aug 19, 2026
68703aa
fix(plugins): deep-copy result_json so a plugin cannot erase a CRITICAL
Aug 19, 2026
9533ab6
fix(diagnostics): never open a non-regular file the walk hands back
Aug 19, 2026
635dc0a
Merge remote-tracking branch 'origin/main' into feature/post-dev-veri…
Aug 19, 2026
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
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ breaking changes may land in a minor release.

### Added

- **Plugins can now observe structured dev verification results (#641).** The existing
`post_dev_verify` hook receives immutable per-command results after normal and repair
verification, with separate `stdout`/`stderr` alongside the compatible bounded
`output_tail`. The context also carries `verification_stage` (`"dev"` or `"fix"`) and
`verification_sequence` — the only way to tell a dev verification from a repair one
(both emit the same stage from the same phase) and the key that joins the context to
its own journal records. Core writes `verify-command-result` journal records with
stream pointers under the run's `verify/` directory — its own store, kept out of the
adapter-owned, TUI-consumed `logs/`; plugins remain unable to alter verification or
commit decisions. Storage, upload, signing, and any policy response stay plugin-owned.
Scope is the dev phase: the review gate runs the same `[verify] commands` and retains
nothing, so the journal records are not a census of a run's verifier invocations —
`docs/plugin-authoring-guide.md` states the boundary, and #656 tracks closing it.
Retention is bounded by the new `[verify] stream_capture_kb` (default 256 KiB per
stream, `0` = capture nothing): the tail is kept, and the record carries the full
byte count plus a `*_truncated` flag so a cut file is never mistaken for a whole
one. A concluded run gives the store back: `bmad-loop clean` trims `verify/` with the
rest of a run's heavy scaffolding and counts it in the reclaimed total, leaving the
run listed and resumable. Separately from that on-disk cap, a hard 32 MiB per-stream
ceiling bounds what is held in memory while the remaining commands run, so a
pathologically chatty suite cannot grow peak memory with the number of configured
verify commands; the record still reports what the command emitted, so a stream the
ceiling cut is never mistaken for a whole one. Retaining a stream is observation, so a failed write (ENOSPC, a read-only run
dir) degrades — the record still lands, with a null pointer and `capture_error` —
instead of taking down a dev pass whose verify commands passed.

- **A refused auto-sweep is now visible outside the journal (#501).** A run whose deferred-work
sweep was refused ended looking exactly like one that swept, and under `[sweep] auto = "run-end"`
there is one trigger per run that is never re-asked once the run finishes — so the journal was
Expand Down Expand Up @@ -38,6 +64,14 @@ breaking changes may land in a minor release.

### Changed

- **`post_dev_verify` now fires on the repair leg too, not only after dev verification (#641).**
A plugin written against "once per story, after the dev session" will see the stage again after
every repair session's verification, and on the way to a pause: an attempt whose session reported
a CRITICAL escalation now emits before the run stops, on either leg, where the repair leg used to
escalate without emitting at all. Discriminate the legs with `ctx.verification_stage`
(`"dev"` / `"fix"`) and de-duplicate on `ctx.verification_sequence`; handlers that assumed one
call per story must be idempotent.

- **`probe-adapter` now bounds how long a single scrubbed line can be (#481).** `scrub_text` capped
how many lines it emitted but never how long one of them could be, so a single very long line —
from a foreign CLI's `--version`/`--help`, or from a log tail — reached the `probe-adapter`
Expand Down Expand Up @@ -82,6 +116,15 @@ breaking changes may land in a minor release.

### Fixed

- **A plugin can no longer erase a CRITICAL escalation out from under the engine's audit.**
`HookContext` copies the session `result_json` precisely so a plugin observes history rather
than rewriting it, but `dict()` is shallow: the nested `escalations` list stayed the engine's
own object, and both verify legs emit `post_dev_verify` before reading
`critical_escalations(result.result_json)`. An in-process plugin that cleared that list
therefore erased the escalation before the audit ran, and a verify-green repair proceeded
where the run owed a pause. The copy is now deep, so the observe-only guarantee holds at the
depth escalations actually live.

- **The egress self-check now sees Windows→WSL UNC home paths (#512).** `diagnose` and
`probe-adapter` re-scan their own rendered bytes before emitting and refuse to emit at all on a
hit, but the absolute-home-path rule knew only forward-slash spellings — so a path reached through
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ Game-engine (Unity) runs read a wider `BMAD_LOOP_UNITY_*` / `BMAD_LOOP_ENGINE_*`

## Run state

Everything about a run lives in `.bmad-loop/runs/<run-id>/` (gitignored): `state.json` (resumable engine state), `journal.jsonl` (every decision), `tasks/<id>/` (per-session prompt + result + escalations, plus diagnostic breadcrumbs — `session-lifecycle.jsonl` records when a timeout fired, `heartbeat.json` is the wait loop's proof-of-life, `resultless-stops.jsonl` records give-up Stops), `logs/` (raw pane output, debugging only), `deferred/` (stashed specs from deferred stories), `resolve/<story>/` (escalation `context.json` + the resolve agent's `resolution.json`), `ATTENTION` (human-readable alerts), and — only while a graceful stop is pending — `stop-request.json` (the control file the engine consumes at the next item boundary).
Everything about a run lives in `.bmad-loop/runs/<run-id>/` (gitignored): `state.json` (resumable engine state), `journal.jsonl` (every decision), `tasks/<id>/` (per-session prompt + result + escalations, plus diagnostic breadcrumbs — `session-lifecycle.jsonl` records when a timeout fired, `heartbeat.json` is the wait loop's proof-of-life, `resultless-stops.jsonl` records give-up Stops), `logs/` (raw pane output, debugging only), `verify/` (verifier command stdout/stderr, pointed at by the journal's `verify-command-result` records; the retained tail is capped per stream by `[verify] stream_capture_kb`, `0` to keep nothing), `deferred/` (stashed specs from deferred stories), `resolve/<story>/` (escalation `context.json` + the resolve agent's `resolution.json`), `ATTENTION` (human-readable alerts), and — only while a graceful stop is pending — `stop-request.json` (the control file the engine consumes at the next item boundary).

One piece deliberately lives elsewhere: the **hook-event channel** (the session completion signals the orchestrator waits on) sits under the user-scoped state root at `<state root>/<project>/<run-id>/events/`, outside the project tree — a branch switch, a worktree mount or a rollback must not be able to take a live run's control plane away. See `BMAD_LOOP_STATE_DIR` above for where that root resolves. The orchestrator also keeps polling the old in-tree `events/` location, so a project whose installed hook relay predates the move still completes its sessions; re-run `bmad-loop init` to refresh the relay.

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

- Every run is a resumable on-disk state machine: `bmad-loop resume <run-id>` continues from a gate, escalation, or interruption.
- A graceful stop (`stop --graceful` / TUI `S`) is resumable too: unlike a hard stop killed mid-item, it lets the in-flight item finish through commit and finalizes cleanly, ending as a `stopped` run that `resume` picks up at the next item.
- All run state in `.bmad-loop/runs/<run-id>/` (gitignored): `state.json`; `journal.jsonl` (every decision, including the `session-synthesized-from-frontmatter` catch and its `spec-marker-repaired` repair, #276); `tasks/<id>/` (per-session prompt + `result.json` + breadcrumbs — `session-lifecycle.jsonl` records timeout fires, budget-guard trips (`budget-tripped` / `over-budget-fired`), transport-failure classification (`env-fault-classified`, #194), a mux session lost under the run (`session-vanished`, #489) and the #276 forensics (`spec-status-transition-observed`, `frontmatter-unmodified-refused`, `contract-nudge-sent`); `heartbeat.json` is the wait loop's proof-of-life; `resultless-stops.jsonl` records give-up Stops with a verdict — `no-artifact`, `ambiguous-frontmatter`, `unmodified-since-launch`, `terminal-frontmatter-pending`); `logs/`; `deferred/`; `resolve/`; `ATTENTION`; `ctl-window` (the control-session window id the last TUI launch minted, so attach/stop follow the live window, #482).
- All run state in `.bmad-loop/runs/<run-id>/` (gitignored): `state.json`; `journal.jsonl` (every decision, including the `session-synthesized-from-frontmatter` catch and its `spec-marker-repaired` repair, #276, plus one `verify-command-result` per verifier command — emitted on the dev and repair legs alike — whose stream pointers name the `verify/` directory below); `tasks/<id>/` (per-session prompt + `result.json` + breadcrumbs — `session-lifecycle.jsonl` records timeout fires, budget-guard trips (`budget-tripped` / `over-budget-fired`), transport-failure classification (`env-fault-classified`, #194), a mux session lost under the run (`session-vanished`, #489) and the #276 forensics (`spec-status-transition-observed`, `frontmatter-unmodified-refused`, `contract-nudge-sent`); `heartbeat.json` is the wait loop's proof-of-life; `resultless-stops.jsonl` records give-up Stops with a verdict — `no-artifact`, `ambiguous-frontmatter`, `unmodified-since-launch`, `terminal-frontmatter-pending`); `logs/`; `verify/` (verifier command stdout/stderr, one file per stream, pointed at by the journal's `verify-command-result` records — its own store, because every name in `logs/` is a session task id the TUI resolves as a pane log; each stream is retained tail-first up to `[verify] stream_capture_kb` (256 KiB, `0` = keep nothing) and the record carries the full byte count, a truncation flag, and a `capture_error` when the write itself failed); `deferred/`; `resolve/`; `ATTENTION`; `ctl-window` (the control-session window id the last TUI launch minted, so attach/stop follow the live window, #482).
- One piece deliberately lives **outside** that directory: the hook-event channel (#494) is at `<state root>/<project>/<run-id>/events/` under the user-scoped state root (`BMAD_LOOP_STATE_DIR`, see the [transport section](#hook-based-transport-no-pane-scraping) below and the README's env-var table), not `<run-dir>/events/`. The orchestrator still polls the legacy in-tree location, so a project whose installed relay predates the move keeps completing its sessions. `delete`, `archive` and `clean` remove the out-of-tree counterpart along with the run dir, and `clean` sweeps counterparts whose run dir is already gone; an **archived** run's tarball therefore no longer contains `events/` — those files are transient completion signals, consumed while the run was live, and everything an archive is read for later is in the run dir.
- `journal.jsonl` records `session-end` for every session unconditionally — even a teardown that throws still lands one (status `aborted` when the outcome is unknowable). A timed-out session's entry carries `fired_at` (wall time the deadline was declared), `teardown_s` (wall seconds from that fire to this entry — the teardown gap), and `expired_clock` (`monotonic` / `wall` / `both` — `wall` alone fingerprints a host suspend that froze the monotonic clock). Every entry whose usage was read carries `tokens` (raw) and `tokens_weighted` (cache reads at `limits.cache_read_weight`), keeping per-session spend reconstructible; both are `null` when the usage read failed, and both are absent on an `aborted` end. `tokens_weighted` is the end-of-session total — distinct from a tripped session's `budget_weighted`, the guard's mid-session sample at trip time.

Expand Down
Loading