implement-loop resume ignores plan edits (cached extractor) and trips the discard guard after a subset commit #32
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 standard-mode
implement-looprun went PARTIAL on a plan-text defect (a wrong path literal in one task). The documented repair flow — fix the plan, commit the good subset,rm BLOCKED.md, re-invoke withresumeFromRunIdso the completed tasks replay from cache and the fixed task runs live — silently did nothing: the resume returned in 33 ms with all 78 agents cached, zero fresh tokens, and the identical old PARTIAL end-report including the stale BLOCKED verdict for the very task whose plan text had been corrected. The run had to be redone as a fresh invocation withtask_range, which also sidesteps a second hazard (see below). An orchestrator that trusted the resume's end-report would have concluded the fix changed nothing.Problem
Two interacting causes, both in
workflows/implement-loop.js:Plan content is invisible to the resume cache key. The Workflow substrate caches per
agent()on(prompt, opts).implement-loopextracts task text via an agent whose prompt carries the plan path, not the plan bytes — so editing the plan file changes no prompt, every agent (including the extractor and the blocked task's implementer) replays from cache, and the documented edit-and-resume iteration loop is a no-op for plan edits. Fix directions: hash the plan file into the extractor prompt (content-addressing the carrier), or document loudly that plan edits require a fresh run withtask_rangeinstead of a resume.Snapshot boundaries go stale across an interim commit. The cross-task discard guard compares "HEAD-modified at boundary N" vs "HEAD-identical at boundary N+1". The sanctioned PARTIAL repair (commit the known-good subset, then continue) moves HEAD, so every cached boundary's modified-path list is measured against a different HEAD than the fresh boundaries — on the first live task after a resume the guard would report the entire committed subset as "lost paths" and hard-BLOCK. Orthogonal to (1) but triggered by the same repair flow; a resume that survives (1) walks straight into (2).
Evidence: run wf_81a13333-be3 (aura, 2026-07-21) — original PARTIAL 9/13; resume after plan edit: 78/78 cached, 0 subagent tokens, 33 ms, byte-identical end-report. The fresh
task_range: [10,13]invocation completed DONE.Fixed in
5d1cffa. Design decisions:task_rangerun" was rejected — it keeps the trap armed and forfeits the cache replay a resume exists for. Instead a new required standard-mode carrier fieldplan_sha256(computed by the orchestrator,sha256sum <plan_path>, recomputed at every (re-)invocation) is stamped into all three plan-reading prompts. A plan edit re-runs the extraction; downstream tasks replay from cache iff their re-extracted text is byte-identical — worst case a redundant live re-run, never stale text.git rev-parse HEAD; a boundary pair straddling a HEAD move resets the guard and pushes a concern instead of comparing. This unblocks the sanctioned commit-subset-then-resume repair while still flagging a rogue mid-run commit (the loop itself never commits, so an in-run HEAD move is always worth eyes). Same-HEAD discards still hard-BLOCK; blank shas fall through to the comparison so a flaky report cannot disarm the guard.Known residuals (accepted, documented in the carrier contract / SKILL.md): the script has no filesystem access and cannot verify the hash matches the plan bytes — a stale/constant
plan_sha256re-opens the no-op resume (operator obligation); a coincident HEAD-move + genuine discard in the same boundary pair surfaces as a concern rather than a BLOCK.Verified via a zero-token mock harness (AsyncFunction + stubbed
agent(), 15 checks) plus a 4-verifier adversarial review workflow (all SOUND).