fix(auto): clear parse cache in dispatch + verify completion in handleAgentEnd

Kills two independent failure paths causing the recurring dispatch loop bug:

Path B: dispatchNextUnit() called clearPathCache() but not clearParseCache(),
allowing stale parsed roadmap data (with [ ] instead of [x]) to persist
through the doctor→dispatch transition.

Path A: handleAgentEnd() never verified whether the just-completed unit
produced its expected artifact before re-entering the dispatch loop.
Now persists completion key after verification, so the idempotency
check in dispatchNextUnit() skips already-completed units.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-15 08:23:57 -06:00
parent 85b9c10265
commit 85960464eb

View file

@ -836,6 +836,24 @@ export async function handleAgentEnd(
} catch {
// Non-fatal
}
// ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
// After doctor + rebuildState, check whether the just-completed unit actually
// produced its expected artifact. If so, persist the completion key now so the
// idempotency check at the top of dispatchNextUnit() skips it — even if
// deriveState() still returns this unit as active (e.g. branch mismatch).
try {
if (verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath)) {
const completionKey = `${currentUnit.type}/${currentUnit.id}`;
if (!completedKeySet.has(completionKey)) {
persistCompletedKey(basePath, completionKey);
completedKeySet.add(completionKey);
}
invalidateStateCache();
}
} catch {
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
}
}
// ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
@ -1391,6 +1409,9 @@ async function dispatchNextUnit(
// Clear stale directory listing cache so deriveState sees fresh disk state (#431)
clearPathCache();
// Clear parsed roadmap/plan cache — doctor may have re-populated it with
// stale data between handleAgentEnd and this dispatch call (Path B fix).
clearParseCache();
let state = await deriveState(basePath);
let mid = state.activeMilestone?.id;