You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Field report from Wojtek Diset, who ran bmad-loop 0.9.0 on macOS (tmux 3.7b, claude/opus adapter, gates per-epic, max_review_cycles = 3, review.trigger = recommended) against a greenfield foundation epic. Thank you — this is one of the most useful reports we've had; it found four independent defects in a single run.
Every finding below was validated against the codebase at 3c68153 (0.9.0 plus ~60 unreleased commits) before filing. Two of the report's own diagnoses were slightly off, and the corrections change the framing — they're called out in "Validation corrections" so nobody chases the wrong layer.
What happened
The story was a foundation story mixing agent-doable work with human-only external actions — buy a Google Workspace seat, publish DNS / SPF / DKIM / DMARC records. The agent did everything it could, correctly. Then:
The board said done while 40 checklist entries were still unticked, several of them the human-only blocking ones. The sprint line ended up reading done # cannot be done - requires human — the status and a contradicting comment on the same YAML line.
The review session caught it and reverted the status. That was the right call.
The engine could not hear it.verify_review saw "status is not done", returned a non-fixable retry, and the loop re-ran the review. The disagreement was structural, so the next two passes produced the same review-verify-failed. All three review cycles were burned on a contradiction no retry could resolve.
Then it deferred and rolled back. The correct work was parked on attempt-preserve/<run-id>… — and nothing said so. The notification, bmad-loop status, and status --json never name that ref. Wojtek recovered by running git log --all and guessing.
8.35M weighted tokens against a 2M advisory story cap. The cap is read in exactly one place, after the story reaches done — so this story, which deferred, was never checked at all.
Validation corrections
(a) _defer does notify — the gap is content and transport, not silence. _defer calls gates.notify on both arms (engine.py:3002 worktree, :3022 in-place), and has since before 0.9.0. Two real gaps sit behind the "silent defer" experience:
Content: the message body is the bare reason with no action tail and no ref. Contrast escalation, which composes f"{reason} — resolve, then \bmad-loop resume {run_id}`" (engine.py:3079-3084`). Defer says what went wrong and nothing about what to do.
Zero tests pin that _defer/_escalate notify at all, so both are regression-invisible.
Open question for the reporter — how did the escalation toast reach you? The report says escalation notified but defer didn't; on 0.9.0's notify-send-only code path neither should have produced a macOS toast. Do you have notify-send installed via brew, or was what you saw the run pausing rather than a desktop notification? The answer decides whether there's a second transport bug hiding here.
(b) The orchestrator writes done at dev time — not the dev session. engine.py:2072-2080 advances sprint-status to done after the dev session (_dev_review_enabled is hardwired False for bmad-dev-auto → target="done"), and engine.py:2080 is the onlysprint_advance call site in the codebase. The dev skill separately sets spec frontmatter status: done unconditionally (step-04-review.md:93) with no acceptance-criteria gate. So "the agent marked its own homework done" is really "the orchestrator marks the board done whenever the dev session finishes".
The done # cannot be done - requires human artifact is sprintstatus.py's comment-preserving regex (:171, :180-182) doing its job — it rewrites the status token and keeps the trailing comment, so the agent's honest note and the orchestrator's optimistic token end up on the same line.
This correction is what makes the contradiction deterministically detectable: a sole advance site, plus verify_dev having asserted done, plus advance() never regressing, means a sprint status found below done at review time can only be a deliberate session write. See #334.
(c) on_escalation = pause was inert. gates.on_escalation is a reserved no-op: declared at policy.py:64, parsed at :670, zero readers. Pause is hardcoded. The setting did nothing — the behaviour matched only by coincidence. It needs a doc note (or a reader); tracked in #334.
(d) Praise passed along.bmad-loop diagnose was singled out as genuinely helpful for untangling the run after the fact. Noted, and thank you.
Field report from Wojtek Diset, who ran bmad-loop 0.9.0 on macOS (tmux 3.7b,
claude/opus adapter, gates per-epic,max_review_cycles = 3,review.trigger = recommended) against a greenfield foundation epic. Thank you — this is one of the most useful reports we've had; it found four independent defects in a single run.Every finding below was validated against the codebase at
3c68153(0.9.0 plus ~60 unreleased commits) before filing. Two of the report's own diagnoses were slightly off, and the corrections change the framing — they're called out in "Validation corrections" so nobody chases the wrong layer.What happened
The story was a foundation story mixing agent-doable work with human-only external actions — buy a Google Workspace seat, publish DNS / SPF / DKIM / DMARC records. The agent did everything it could, correctly. Then:
donewhile 40 checklist entries were still unticked, several of them the human-only blocking ones. The sprint line ended up readingdone # cannot be done - requires human— the status and a contradicting comment on the same YAML line.verify_reviewsaw "status is not done", returned a non-fixable retry, and the loop re-ran the review. The disagreement was structural, so the next two passes produced the samereview-verify-failed. All three review cycles were burned on a contradiction no retry could resolve.attempt-preserve/<run-id>…— and nothing said so. The notification,bmad-loop status, andstatus --jsonnever name that ref. Wojtek recovered by runninggit log --alland guessing.done— so this story, which deferred, was never checked at all.Validation corrections
(a)
_deferdoes notify — the gap is content and transport, not silence._defercallsgates.notifyon both arms (engine.py:3002worktree,:3022in-place), and has since before 0.9.0. Two real gaps sit behind the "silent defer" experience:f"{reason} — resolve, then \bmad-loop resume {run_id}`"(engine.py:3079-3084`). Defer says what went wrong and nothing about what to do.gates.pywas notify-send-only — noosascript— so macOS got no desktop toast for any event, defer or escalation. PR fix(notify): native desktop notifications on macOS/Windows + warn when inert (#231) #282 (Unreleased) added osascript/powershell.Zero tests pin that
_defer/_escalatenotify at all, so both are regression-invisible.Open question for the reporter — how did the escalation toast reach you? The report says escalation notified but defer didn't; on 0.9.0's notify-send-only code path neither should have produced a macOS toast. Do you have
notify-sendinstalled via brew, or was what you saw the run pausing rather than a desktop notification? The answer decides whether there's a second transport bug hiding here.(b) The orchestrator writes
doneat dev time — not the dev session.engine.py:2072-2080advances sprint-status todoneafter the dev session (_dev_review_enabledis hardwiredFalsefor bmad-dev-auto →target="done"), andengine.py:2080is the onlysprint_advancecall site in the codebase. The dev skill separately sets spec frontmatterstatus: doneunconditionally (step-04-review.md:93) with no acceptance-criteria gate. So "the agent marked its own homework done" is really "the orchestrator marks the board done whenever the dev session finishes".The
done # cannot be done - requires humanartifact issprintstatus.py's comment-preserving regex (:171,:180-182) doing its job — it rewrites the status token and keeps the trailing comment, so the agent's honest note and the orchestrator's optimistic token end up on the same line.This correction is what makes the contradiction deterministically detectable: a sole advance site, plus
verify_devhaving asserteddone, plusadvance()never regressing, means a sprint status found belowdoneat review time can only be a deliberate session write. See #334.(c)
on_escalation = pausewas inert.gates.on_escalationis a reserved no-op: declared atpolicy.py:64, parsed at:670, zero readers. Pause is hardcoded. The setting did nothing — the behaviour matched only by coincidence. It needs a doc note (or a reader); tracked in #334.(d) Praise passed along.
bmad-loop diagnosewas singled out as genuinely helpful for untangling the run after the fact. Noted, and thank you.Sub-issues
--jsonawaiting-operator: terminal state for stories owing human external actionsmax_tokens_per_storyis checked only post-done and only journals — warn while the story runs