Skip to content

0.9.0 field report: human-gated story — defer visibility, review non-convergence, awaiting-operator, token-budget timing #337

Description

@pbean

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:

  1. 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.
  2. The review session caught it and reverted the status. That was the right call.
  3. 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.
  4. 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.
  5. 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.
  • Transport: on the v0.9.0 tag, gates.py was notify-send-only — no osascript — 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/_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 only sprint_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.

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:engineOrchestrator engine and run lifecyclebugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions