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
A story's isolated-worktree task.baseline_commit is meant to stay fixed for its whole dev-retry
loop (the comment at engine.py around line 1229 says so explicitly), captured once via verify.rev_parse_head(self.workspace.root) when the worktree opens (engine.py:1707). We
observed it change to a later commit — the shared target branch's tip, many stories ahead —
while a dev session was still mid-flight, with the story's own worktree never having moved. _verify_shared_gates (verify.py:1211-1235) then compares the dev session's own (correct) baseline_revision against the drifted task.baseline_commit, finds a mismatch, and the story
is permanently deferred (spec baseline ... does not match orchestrator-recorded baseline ...)
even though the completed attempt did real, reviewed, honest work. The run then dispatches the next story rather than pausing or escalating, so the failure is silent until an operator
notices via other means.
This report also carries a sibling work-loss mode (same session, same package): an intent_gap halt correctly reverts the attempt but leaves noattempt-preserve/* branch
and no failed/*/changes.patch — unlike the deferred-story path that already preserves via scm.keep_failed. Evidence for that mode is in the second section below.
Gate record (filing checklist)
Repo access / org relationship (checked 2026-08-23): viewer rxm7706 has pull only on bmad-code-org/bmad-loop (no push/triage/maintain). Issues enabled; Discussions disabled.
Filing channel = GitHub Issue (not PR, not Discussion).
Duplicate search (checked 2026-08-23): searched for baseline_commit, orchestrator-recorded, mid-flight drift, intent_gap / attempt-preserve / keep_failed.
No open issue describes mid-flight task.baseline_commit overwrite during an in-flight
automatic retry, nor intent-gap revert without preserve. Adjacent but distinct: #640 (rearm_escalation advances
task baseline without updating the spec's baseline_revision) — related shape on the re-arm path, not the mid-flight automatic-retry drift this report covers.
Evidence (one occurrence, full journal timeline)
Run 20260813-094919-bfcb, story 9-6-plan-and-action-types-repo-fingerprint-and-the-plan-builder, journal.jsonl:
worktree-opened story=9-6 (branch/worktree created — baseline should be ~523e938c79 here)
dev-decision attempt=1 session_status=timeout action=retry
rollback-auto baseline=26102ea12c6d... <-- ALREADY drifted, ~3h after worktree-opened
attempt-commits-preserved ref=attempt-preserve/...-523e938c count=124
dev-decision attempt=2 session_status=completed action=defer
reason="spec baseline 523e938c7978 does not match orchestrator-recorded baseline 26102ea12c6d"
story-deferred reason=(same)
worktree-opened story=10-1 (dispatcher moves on)
The worktree's own preserve-ref name (attempt-preserve/20260813-094919-bfcb-523e938c) confirms
its real basis never moved past 523e938c79. task.baseline_commit was already the later commit
by the time rollback-auto fired after attempt 1's timeout — i.e. it drifted during attempt 1,
not at any point our own code touched. Between those two commits, the shared target branch
(loop/pyforge-marshal) had several sibling stories land (an operator-driven out-of-band
recovery in our case, but plausibly any concurrent landing on the same branch).
What we found in the source (bmad-loop 0.9.0)
engine.py:1707 (_dev_phase) sets task.baseline_commit exactly once per dev-phase
invocation, from self.workspace.root's HEAD — which is the per-story isolated worktree
(self.workspace = unit.workspace at engine.py:622, set before drive() runs). We could
not find, via static reading, the exact call site that overwrites it again mid-flight for an
already-open worktree; happy to help narrow this down further if useful.
runs.py:872-908 (the operator-driven resolve/re-arm path) already handles an equivalent
hazard explicitly: it re-stamps BOTH task.baseline_commit (from state.project's live HEAD)
AND the spec's baseline_revision together, with a comment naming precisely this failure
("without this, the re-driven step-04 would build its review diff... clawing back the very
resolve-session commits the advance above just blessed"). This suggests the maintainers are
already aware of the shape of this problem for the re-arm path; we couldn't find the
equivalent protection on the automatic retry path. See also adjacent #640.
Three occurrences in one session across two different projects (9 stories, ~8 hours; then this
one story; then 3 more stories in a sibling project) — each required manual git archaeology
(attempt-preserve/* branches, failed/*/changes.patch) to recover, since scm.keep_failed's
safety net preserved the work but nothing in the automatic path re-lands it.
Environment
bmad-loop 0.9.0 (and later fleet pins), multi-worktree isolated mode, branch_per targeting a
single shared loop/<project> branch across concurrently-scheduled stories.
Sibling mode: intent_gap revert discards work with no preserve artifact
Summary
When a bmad-loop dev session finds an intent_gap, it correctly reverts the attempt rather
than committing contract-violating work — but that revert leaves no recoverable git artifact:
no attempt-preserve/* branch, no failed/*/changes.patch, a clean reflog. The deferred-story
path already preserves via scm.keep_failed / _preserve_attempt_commits; the intent-gap path
does not.
Evidence (one occurrence)
Marshal Story 10.1 (2026-08-14): full implementation + suite green + two adversarial reviews,
then a correct intent-gap halt on a real contract contradiction. Revert left the worktree
"confirmed clean, matching baseline_revision" with nothing to recover via git. Recovery was
only possible by reconstructing the diff from adversarial-review Agent prompts embedded in the
Claude Code session transcript — fragile and not a substitute for the preserve path.
Ask
Same preservation guarantee on the intent-gap revert path that deferred stories already get
(attempt-preserve/* or failed/<story>/changes.patch), without weakening the intent-gap halt
itself.
Local containment (not a package patch)
We are shipping Marshal-side detectors / loud ATTENTION containment for baseline-drift and plan
adapter-seam preservation for intent-gap — we are not patching the installed bmad_loop
package in place. Happy to iterate on a minimal repro if useful.
Summary
A story's isolated-worktree
task.baseline_commitis meant to stay fixed for its whole dev-retryloop (the comment at
engine.pyaround line 1229 says so explicitly), captured once viaverify.rev_parse_head(self.workspace.root)when the worktree opens (engine.py:1707). Weobserved it change to a later commit — the shared target branch's tip, many stories ahead —
while a dev session was still mid-flight, with the story's own worktree never having moved.
_verify_shared_gates(verify.py:1211-1235) then compares the dev session's own (correct)baseline_revisionagainst the driftedtask.baseline_commit, finds a mismatch, and the storyis permanently deferred (
spec baseline ... does not match orchestrator-recorded baseline ...)even though the completed attempt did real, reviewed, honest work. The run then dispatches the
next story rather than pausing or escalating, so the failure is silent until an operator
notices via other means.
This report also carries a sibling work-loss mode (same session, same package): an
intent_gaphalt correctly reverts the attempt but leaves noattempt-preserve/*branchand no
failed/*/changes.patch— unlike the deferred-story path that already preserves viascm.keep_failed. Evidence for that mode is in the second section below.Gate record (filing checklist)
rxm7706haspullonly onbmad-code-org/bmad-loop(no push/triage/maintain). Issues enabled; Discussions disabled.Filing channel = GitHub Issue (not PR, not Discussion).
baseline_commit,orchestrator-recorded, mid-flight drift,intent_gap/attempt-preserve/keep_failed.No open issue describes mid-flight
task.baseline_commitoverwrite during an in-flightautomatic retry, nor intent-gap revert without preserve. Adjacent but distinct:
#640 (
rearm_escalationadvancestask baseline without updating the spec's
baseline_revision) — related shape on there-arm path, not the mid-flight automatic-retry drift this report covers.
Evidence (one occurrence, full journal timeline)
Run
20260813-094919-bfcb, story9-6-plan-and-action-types-repo-fingerprint-and-the-plan-builder,journal.jsonl:The worktree's own preserve-ref name (
attempt-preserve/20260813-094919-bfcb-523e938c) confirmsits real basis never moved past
523e938c79.task.baseline_commitwas already the later commitby the time
rollback-autofired after attempt 1's timeout — i.e. it drifted during attempt 1,not at any point our own code touched. Between those two commits, the shared target branch
(
loop/pyforge-marshal) had several sibling stories land (an operator-driven out-of-bandrecovery in our case, but plausibly any concurrent landing on the same branch).
What we found in the source (
bmad-loop0.9.0)engine.py:1707(_dev_phase) setstask.baseline_commitexactly once per dev-phaseinvocation, from
self.workspace.root's HEAD — which is the per-story isolated worktree(
self.workspace = unit.workspaceatengine.py:622, set beforedrive()runs). We couldnot find, via static reading, the exact call site that overwrites it again mid-flight for an
already-open worktree; happy to help narrow this down further if useful.
runs.py:872-908(the operator-drivenresolve/re-arm path) already handles an equivalenthazard explicitly: it re-stamps BOTH
task.baseline_commit(fromstate.project's live HEAD)AND the spec's
baseline_revisiontogether, with a comment naming precisely this failure("without this, the re-driven step-04 would build its review diff... clawing back the very
resolve-session commits the advance above just blessed"). This suggests the maintainers are
already aware of the shape of this problem for the re-arm path; we couldn't find the
equivalent protection on the automatic retry path. See also adjacent #640.
verify.py:1220-1235'sallow_ancestor_baselinecarve-out already treats an ancestorbaseline as sound for one specific case (Sweep bundle that routes into an existing done spec always fails the baseline gate; recovery message then gives wrong (in-place) instructions in worktree mode #161, deferred-work-bundle re-arm). Extending that
carve-out (or an equivalent) to the plain-retry path looks like it would fix or at least
contain this.
Impact
Three occurrences in one session across two different projects (9 stories, ~8 hours; then this
one story; then 3 more stories in a sibling project) — each required manual git archaeology
(
attempt-preserve/*branches,failed/*/changes.patch) to recover, sincescm.keep_failed'ssafety net preserved the work but nothing in the automatic path re-lands it.
Environment
bmad-loop 0.9.0(and later fleet pins), multi-worktree isolated mode,branch_pertargeting asingle shared
loop/<project>branch across concurrently-scheduled stories.Sibling mode: intent_gap revert discards work with no preserve artifact
Summary
When a
bmad-loopdev session finds an intent_gap, it correctly reverts the attempt ratherthan committing contract-violating work — but that revert leaves no recoverable git artifact:
no
attempt-preserve/*branch, nofailed/*/changes.patch, a clean reflog. The deferred-storypath already preserves via
scm.keep_failed/_preserve_attempt_commits; the intent-gap pathdoes not.
Evidence (one occurrence)
Marshal Story 10.1 (2026-08-14): full implementation + suite green + two adversarial reviews,
then a correct intent-gap halt on a real contract contradiction. Revert left the worktree
"confirmed clean, matching
baseline_revision" with nothing to recover via git. Recovery wasonly possible by reconstructing the diff from adversarial-review Agent prompts embedded in the
Claude Code session transcript — fragile and not a substitute for the preserve path.
Ask
Same preservation guarantee on the intent-gap revert path that deferred stories already get
(
attempt-preserve/*orfailed/<story>/changes.patch), without weakening the intent-gap haltitself.
Local containment (not a package patch)
We are shipping Marshal-side detectors / loud ATTENTION containment for baseline-drift and plan
adapter-seam preservation for intent-gap — we are not patching the installed
bmad_looppackage in place. Happy to iterate on a minimal repro if useful.