Skip to content

Commit f73f374

Browse files
authored
Merge pull request #116 from eduralph/fix/105-batch-iterate-plan-stall
fix(flow): keep the batch sweep alive across an iterate-plan re-open
2 parents 075a45c + 48ac4d6 commit f73f374

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

template/src/pdca_harness/flow.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ def _drive_and_act(
390390
# Each bundle is isolated — one that raises (a leaf left it half-written) is
391391
# skipped this pass, never crashing the sweep and losing the others' progress.
392392
# Serial by default; fans out across cfg.lanes lanes when configured (docs 09).
393+
before = [state.state(d) for d in bundles]
393394
_build_all(cfg, bundles)
394395
# Sign-off, cheap-first, restricted to this batch. ONE interactive session
395396
# per chunk (≤ SIGNOFF_BATCH_SIZE) walks several bundles — like batch Plan —
@@ -398,7 +399,14 @@ def _drive_and_act(
398399
# build-all above applies all the iterations together.
399400
pending = [e.bundle for e in queue.awaiting_signoff(cfg) if e.bundle.name in names]
400401
if not pending:
401-
break
402+
# Break only when the band made NO progress this pass. iterate-plan archives
403+
# a bundle back to UNPLANNED — a HALTED state that needs the Plan pre-pass on a
404+
# LATER pass; on the pass where that archive happens nothing is awaiting
405+
# sign-off, so a bare `break` stranded it at UNPLANNED (#105). A state change
406+
# means progress (the re-open) — loop again so the next pass re-plans + rebuilds.
407+
if [state.state(d) for d in bundles] == before:
408+
break # genuinely stuck (all terminal / planner declined an UNPLANNED)
409+
continue # progress — give the re-opened bundle its Plan pass
402410
for chunk in _chunks(pending, SIGNOFF_BATCH_SIZE):
403411
# The session writes a decision per bundle as it goes; a dropped session
404412
# still leaves the finished ones, applied below. Isolate it so a crashed

template/tests/test_flow_slice.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,34 @@ def signoff_batch(cfg: Config, bundles: list[Path]) -> None:
195195
self.assertEqual(set(results), {"BATCH1", "BATCH2"})
196196
self.assertTrue(all(s == state.COMPLETE for s in results.values()))
197197

198+
def test_batch_iterate_plan_then_complete(self) -> None:
199+
# iterate-plan re-opens a batch member to UNPLANNED (archiving its attempt to
200+
# iteration-v1/); the sweep must keep looping so a LATER pass re-plans + rebuilds
201+
# it, rather than break at UNPLANNED when nothing is awaiting sign-off (#105).
202+
iterated = {"done": False}
203+
204+
def signoff_batch(cfg: Config, bundles: list[Path]) -> None:
205+
for d in bundles:
206+
summ = d / "SUMMARY.md"
207+
if d.name == "issue_BATCH1" and not iterated["done"]:
208+
iterated["done"] = True
209+
(d / leaves.SIGNOFF_DECISION).write_text("iterate-plan\n", encoding="utf-8")
210+
continue
211+
summ.write_text(summ.read_text().replace("- [ ]", "- [x]"), encoding="utf-8")
212+
(d / leaves.SIGNOFF_DECISION).write_text("accept\n", encoding="utf-8")
213+
214+
orig = leaves.run_signoff_batch
215+
leaves.run_signoff_batch = signoff_batch
216+
try:
217+
results = flow.flow_batch(self.cfg, today="2026-06-04", max_passes=6)
218+
finally:
219+
leaves.run_signoff_batch = orig
220+
self.assertTrue(iterated["done"])
221+
self.assertEqual(set(results), {"BATCH1", "BATCH2"})
222+
self.assertTrue(all(s == state.COMPLETE for s in results.values()))
223+
# the re-opened bundle re-planned, rebuilt, and preserved its first attempt
224+
self.assertTrue((self.cfg.bundle("BATCH1") / "iteration-v1").is_dir())
225+
198226
def test_batch_signoff_chunks_into_sessions(self) -> None:
199227
# The cheap-first queue is signed off in ONE session per chunk of
200228
# SIGNOFF_BATCH_SIZE (=5): six halted bundles → sessions of 5 then 1, all

0 commit comments

Comments
 (0)