Found while fixing the bundle-close carry in #406 (5c401f3), which turned out not to reach this shape — so it is filed separately rather than claimed as fixed.
verify.verify_review_bundle (verify.py:1679) requires every id in task.dw_ids to be done in paths.deferred_work, and SweepEngine._verify_review hands it self.workspace.paths — under scm.isolation = "worktree", the unit worktree's paths.
A worktree checks out tracked files only, and scm.worktree_seed defaults to () (policy.py:468). So for a project that gitignores its ledger — an ordinary shape, and this repo's own: the ledger's default home is under a gitignored artifacts dir — the ledger is absent from the worktree entirely. deferredwork.mark_done returns False on a missing file (deferredwork.py:88-89), the close lands nowhere, and the gate then reads a file that does not exist.
The result is a bundle that can never land, on any run, however correct the work is.
Measured
Sandbox project fixture, .gitignore = .bmad-loop/runs/ + deferred-work.md, ledger holding DW-1: open, isolation = "worktree", worktree_seed = (), one bundle over DW-1 whose dev session does real source work and finalizes its spec to done:
SUMMARY done=1 deferred=1 paused=False
PHASE deferred
KINDS [... 'dev-decision', 'review-not-recommended', 'review-skipped',
'session-start', 'session-end', 'fix-decision',
'review-verify-failed', 'story-deferred', 'unit-closed', 'worktree-kept']
LEDGER {'DW-1': 'open'}
The dev gate passes; the review gate fails with fixable=True; a repair round runs and cannot help (the ledger is still not in the worktree); the bundle defers. The entries stay open, deferredwork.open_ids re-bundles them, and the next sweep drives the same work again — an unbounded re-drive, though a loud one: review-verify-failed fires every time.
Same run with the ledger named in scm.worktree_seed: the bundle lands (done=1 deferred=0), which is the configuration 5c401f3's carry covers. Same run with the ledger tracked: lands, and the flip rides the branch — the pre-existing test_sweep_worktree_bundle_merges_to_target shape.
So the exposure is exactly: gitignored and unseeded. That is the default.
Why the #406 carry does not reach it
SweepEngine._carry_isolated_ledger_writes runs from _integrate_unit's DONE arm. A unit that defers never reaches it — deliberately, since a defer discarded the code a close claims to have resolved. Here the unit never reaches DONE at all, so no DONE-leg carry of any kind can help.
Fix shapes (not chosen)
- Have
verify_review_bundle read the main checkout's ledger when the orchestrator is the ledger writer (_generic_dev()). The gate exists to confirm the orchestrator's own write happened, and under isolation that write is now carried to the main checkout anyway — so the worktree's copy is arguably the wrong oracle regardless. Wrong for the legacy --dw-bundle path, where the session writes the ledger inside the worktree, so it would need to stay path-dependent.
- Seed the ledger into every unit worktree automatically when it is gitignored, rather than requiring the operator to name it in
worktree_seed. Smallest behavioural surface, and it also makes 5c401f3's carry the universal path instead of a configuration-dependent one. Costs a copy per unit and needs a rule for what "the ledger" resolves to before the workspace is rebased.
- Document
worktree_seed as required for a gitignored ledger and fail fast at run start rather than at the review gate. Cheapest, but it leaves a valid configuration unrunnable.
(2) looks best on the current reading; not measured.
Cross-refs: #405, #406 (5c401f3 and its commit message, which states this scope limit), #425 (the third producer in the same add -A family).
Found while fixing the bundle-close carry in #406 (
5c401f3), which turned out not to reach this shape — so it is filed separately rather than claimed as fixed.verify.verify_review_bundle(verify.py:1679) requires every id intask.dw_idsto bedoneinpaths.deferred_work, andSweepEngine._verify_reviewhands itself.workspace.paths— underscm.isolation = "worktree", the unit worktree's paths.A worktree checks out tracked files only, and
scm.worktree_seeddefaults to()(policy.py:468). So for a project that gitignores its ledger — an ordinary shape, and this repo's own: the ledger's default home is under a gitignored artifacts dir — the ledger is absent from the worktree entirely.deferredwork.mark_donereturns False on a missing file (deferredwork.py:88-89), the close lands nowhere, and the gate then reads a file that does not exist.The result is a bundle that can never land, on any run, however correct the work is.
Measured
Sandbox
projectfixture,.gitignore=.bmad-loop/runs/+deferred-work.md, ledger holdingDW-1: open,isolation = "worktree",worktree_seed = (), one bundle overDW-1whose dev session does real source work and finalizes its spec todone:The dev gate passes; the review gate fails with
fixable=True; a repair round runs and cannot help (the ledger is still not in the worktree); the bundle defers. The entries stay open,deferredwork.open_idsre-bundles them, and the next sweep drives the same work again — an unbounded re-drive, though a loud one:review-verify-failedfires every time.Same run with the ledger named in
scm.worktree_seed: the bundle lands (done=1 deferred=0), which is the configuration5c401f3's carry covers. Same run with the ledger tracked: lands, and the flip rides the branch — the pre-existingtest_sweep_worktree_bundle_merges_to_targetshape.So the exposure is exactly: gitignored and unseeded. That is the default.
Why the #406 carry does not reach it
SweepEngine._carry_isolated_ledger_writesruns from_integrate_unit's DONE arm. A unit that defers never reaches it — deliberately, since a defer discarded the code a close claims to have resolved. Here the unit never reaches DONE at all, so no DONE-leg carry of any kind can help.Fix shapes (not chosen)
verify_review_bundleread the main checkout's ledger when the orchestrator is the ledger writer (_generic_dev()). The gate exists to confirm the orchestrator's own write happened, and under isolation that write is now carried to the main checkout anyway — so the worktree's copy is arguably the wrong oracle regardless. Wrong for the legacy--dw-bundlepath, where the session writes the ledger inside the worktree, so it would need to stay path-dependent.worktree_seed. Smallest behavioural surface, and it also makes5c401f3's carry the universal path instead of a configuration-dependent one. Costs a copy per unit and needs a rule for what "the ledger" resolves to before the workspace is rebased.worktree_seedas required for a gitignored ledger and fail fast at run start rather than at the review gate. Cheapest, but it leaves a valid configuration unrunnable.(2) looks best on the current reading; not measured.
Cross-refs: #405, #406 (
5c401f3and its commit message, which states this scope limit), #425 (the third producer in the sameadd -Afamily).