Skip to content

Review session can revert the orchestrator's own sprint-status write and dead-end the run #437

Description

@didostap

Description

The review prompt that bmad-loop dispatches tells the session to keep its hands off
deferred-work.md, but there is no equivalent sentence for sprint-status.yaml. I hit that gap.

Here is the review prompt from my run's journal.jsonl, verbatim:

/bmad-dev-auto - If this review defers new findings, append them to
the deferred-work ledger as NEW entries only; do NOT modify, re-open, or rewrite
existing ledger entries - the orchestrator owns their status and resolution.

So the loop already knows it has to say "hands off, the orchestrator owns this" for one of
its two ledgers. There is nothing like it for the sprint board.

What happened: my story spec's Boundaries section had a line saying "Never edit epics.md,
the PRD, the spine, or sprint-status.yaml." The dev session finished, the orchestrator
flipped the row from backlog to done as its sign-off. The review session then read the
spec, saw the uncommitted done in git status, filed it as a [high] finding for "directly
violating the intent-contract's Never edit ... sprint-status.yaml", reverted the row to
backlog, and committed the revert.

From the reviewer's side that is a defensible read. It had no way to know that write came
from the orchestrator and not from a misbehaving dev session. My spec prohibited writing
the file, and the reviewer turned that into a mandate to revert someone else's write.

The contradiction detection worked exactly as advertised. on_status_contradiction =
"escalate" caught it and paused with a very clear message instead of burning the remaining
review cycles. No complaints there. The escalation is the symptom being caught correctly,
the bug is that it was reachable at all.

Two things I think are worth fixing:

  1. Extend the review prompt appendix to cover sprint-status.yaml, same shape as the
    deferred-work sentence.

  2. Optionally, commit the orchestrator's sprint-status write together with the dev pass
    rather than leaving it uncommitted. What the reviewer actually saw was a loose
    modification in git status with no author attached to it, which is what made it look
    like a violation.

Two things I found while digging that might matter to whoever picks this up:

The CHANGELOG entry for #334 says "Review prompts are unchanged by design: forbidding the
revert would make a correct reviewer comply and the story would commit without sign-off."
I do not think that holds any more. verify_review checks the spec's frontmatter status
first and returns retry there, before it ever reads the board. So a reviewer that withholds
sign-off through the spec already blocks the commit, and the board revert was never the
channel doing the work. Worth noting that #334 is still under Unreleased, so if this gets
fixed the two entries would want to agree.

Also, docs/FEATURES.md line 38 currently says sprint-status.yaml is "owned by BMAD skills;
orchestrator only reads it". That contradicts AGENTS.md and the code, and line 43 of the
same file says the opposite. I suspect that line is part of why I wrote the prohibition
into my spec in the first place, since nothing told me who actually owns the file.

Secondary annoyance from the same run: the review session ran git commit, and earlier in
the reflog, git reset --hard HEAD~1 on main. So the revert is in my history, not just my
worktree. I know scm.isolation = "worktree" is the answer and I am switching, just flagging
that with the default isolation = "none" a review session has unrestricted write access to
the branch it is reviewing.

I have a fix on a local branch already but I do not have push access to this repo,
so I am filing this first. Happy to open a PR if someone can grant access, or someone can
take the approach and run with it.

Steps to reproduce

  1. Author a story spec whose Boundaries/Constraints section tells the agent not to edit
    sprint-status.yaml. This is a pretty natural thing for a spec author to write, since the
    file gets described in project-context.md without ever saying who owns it.
  2. Set review.enabled = true, review.trigger = "recommended", scm.isolation = "none".
  3. bmad-loop run --epic 1
  4. Dev session finishes with status: done and followup_review_recommended: true. The
    orchestrator flips the sprint-status row to done.
  5. Review session starts, reads the spec, and finds the modified sprint-status.yaml sitting
    in git status with nothing naming its author.
  6. It files the orchestrator's own write as a finding and reverts it.

Expected behavior

The review session should never touch sprint-status.yaml, and specifically should never
revert a change the orchestrator made to it. The dispatched review prompt already carries
this exact instruction for deferred-work.md, so the sprint board should be getting the same
protection.

The story should have gone to done and the run should have continued to 1-2.

Actual behavior

Review returned status: done with escalations: [] and followup_review_recommended: false.
As far as it was concerned the story passed. But it had also written the sprint-status row
back to backlog and committed that.

The orchestrator then found spec frontmatter at status: done and the board row at backlog,
flagged a status contradiction, and paused the run:

paused [escalation] - review revoked the sprint sign-off for 1-1-...: the orchestrator
advanced the board to 'done' after dev verified, and the review session wrote it back to
'backlog' while leaving the spec frontmatter at 'done'. [...]

Run summary: 1 task, 0 done, 0 deferred, 1 escalated, 3.8M weighted tokens (24.7M raw)
spent on a story where both sessions actually agreed the work was finished.

Screenshots

No response

Which area is this for?

Orchestrator / control loop

bmad-loop Version

0.9.0

Which coding CLI are you using?

Claude (claude)

Operating System

macOS

Relevant log output

{"kind": "session-start", "task_id": "1-1-...-review-1", "role": "review", "adapter": "claude", "model": "sonnet", "prompt": "/bmad-dev-auto <spec-path> - If this review defers new findings, append them to the deferred-work ledger as NEW entries only; do NOT modify, re-open, or rewrite existing ledger entries - the orchestrator owns their status and resolution."}
{"kind": "review-result", "cycle": 1, "status": "done", "followup_review_recommended": false}
{"kind": "review-verify-failed", "reason": "review revoked the sprint sign-off for 1-1-...: the orchestrator advanced the board to 'done' after dev verified, and the review session wrote it back to 'backlog' while leaving the spec frontmatter at 'done'. [...]", "env_fault": false, "contradiction": true}
{"kind": "story-escalated", "story_key": "1-1-..."}
{"kind": "run-paused", "stage": "escalation"}

The reviewer's own writeup, from the spec's Review Triage Log:

  - `[high]` `[patch]` `sprint-status.yaml` had been flipped to `done` for this story,
    directly violating the intent-contract's `Never edit ... sprint-status.yaml`.
    Reverted to `backlog`.

Relevant policy.toml:
  [review]
  enabled = true
  trigger = "recommended"
  on_timeout = "retry"
  on_status_contradiction = "escalate"
  [adapter.review]
  name = "claude"
  model = "sonnet"
  [scm]
  isolation = "none"

Diagnostic dump (bmad-loop diagnose)

No response

Confirm

  • I've searched for existing issues
  • If I attached a diagnose dump, I reviewed it and confirmed it contains no PII, secrets, or project-specific information
  • I'm using the latest version

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions