implement-loop: guard the plan-extract against silent truncation on big plans #22
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
A plan iteration silently ships only part of itself. On a large plan the
plan-extractstep returns fewer tasks than requested, and the loop accepts theshort list and runs to a
DONEverdict over the truncated subset — the missingtasks are never attempted, yet the end-report reads as a clean completion. The
positive-evidence precondition on
DONE(issue #12) does not catch this: thetruncated subset does land real edits, so the git-tree count is non-zero and the
run looks successful. The orchestrator only discovers the shortfall by manually
diffing
tasks_totalagainst the range it asked for; a run that skips thatmanual check commits an incomplete iteration.
Problem
implement-loop.jsextracts every requested task's verbatim block through asingle schema-bound
agent()call:Two gaps compound:
is returned in one agent response. A large plan (many tasks, or a few tasks
carrying long verbatim code blocks) overflows that one response's output
budget, so the agent returns a prefix of the tasks — commonly just task 1.
Observed on a ~5200-line plan with
task_range: [1, 10]yieldingtasks.length === 1.:291only rejects anempty list. A truncated non-empty list (e.g. 1 of 10) passes, and the loop
proceeds over it. Nothing compares
extracted.tasks.length(or the returnedids) to the requested
[from, to]cardinality, so the truncation is silent.Suggested directions (either or both):
:290: whentask_rangeis set, requireextracted.tasks.length === task_range[1] - task_range[0] + 1(and/or that thereturned ids cover the range); on a mismatch,
BLOCKED(orNEEDS_CONTEXT)with a "plan-extract truncated: got N of M" reason instead of proceeding.
agent()call per task id in therange — so no single response has to carry every task's verbatim block. This
removes the output-budget ceiling entirely and makes the loop robust to plan
size.
Current workaround (orchestrator-side, not a fix): copy the workflow script,
hardcode a
[from, to]slice of at most ~2 tasks, run per slice, and manuallyassert
tasks_totalmatches the slice before committing. Large single tasks arerun one per slice. This is the discipline a fix would make unnecessary.
Fixed in
a75a821(main).Design decisions (both suggested directions taken, plus one the issue did not scope):
plan-indexpass returns only ids + one-line titles (bounded output, self-limits far later than a verbatim dump); oneplan-extract:<id>call then carries each task's verbatim block.total_tasksself-consistency backstop for the whole-plan path. The issue's direction-1 scoped the cardinality check to the ranged[from,to]case. But the default invocation has no range, so the enumerate would be the sole authority with nothing to check it against — the exact #22 symptom relocated to the (bounded) enumerate pass.plan-indexnow also reportstotal_tasks(counted from the fully-readable plan text, independent of the emitted list);enumerated ≠ totalis a hard BLOCKED. This closes the asymmetry.parallel()kept over a sequential loop. Extraction is read-only and order-independent, so the calls fan out — andparallel()(unlikePromise.all) honours the concurrency cap, which matters precisely on the big plans this issue targets. Association is made robust by binding the requested id inside each thunk and re-associating by it — never by array position, never by the agent's self-reported id — so the mapping is correct regardless of resolution order or a mislabelled response.task_range(from > to, non-integer, < 1) is now its own explicit BLOCKED instead of being swallowed into an empty task list that later mis-reports as a no-op iteration.Verification: an adversarial three-lens review of the diff (correctness / contract / completeness) plus a stubbed harness driving the real script through 11 extraction scenarios (happy range, truncation →
got N of M, range overshoot, unordered enumerate, big-plan ceiling removed, empty → NEEDS_CONTEXT, agent-death, enumerate self-limit8 of 40, inverted range, from<1, single-task range) and a dedicated order- and self-id-independence association test.Note: the review's headline objection — that
parallel()is an undefined global — was a false alarm (it read only the repo's own docs);parallel()is a documented Workflow-substrate primitive and was exercised live by the review workflow itself. The valid findings (id-association robustness, the no-range backstop, the inverted-range guard, a comment overclaim) were all incorporated above.