From 8d04ec19fdfed195d7c6d85cd1f2900f42f5d309 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:07:22 -0400 Subject: [PATCH] fix: add defensive guards against undefined .filter() in auto-mode dispatch/recovery (#1180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-mode crashed with 'Cannot read properties of undefined (reading filter)' during partial execute-task recovery when derived state was structurally incomplete. Added ?? [] fallback guards on all .filter()/.find()/.map() calls that access state.registry, roadmap.slices, or similar derived arrays in the dispatch and recovery paths: - auto.ts: 3 state.registry.filter() calls - auto-recovery.ts: 1 roadmap.slices.find() call - auto-start.ts: 1 state.registry.filter() call These are belt-and-suspenders guards — the parsers always return arrays, but crash recovery can encounter partially written or corrupt state files where the parsers return unexpected shapes. Fixes #1176 --- src/resources/extensions/gsd/auto-recovery.ts | 2 +- src/resources/extensions/gsd/auto-start.ts | 2 +- src/resources/extensions/gsd/auto.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index e2615b5d2..4b2155921 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -227,7 +227,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s try { const roadmapContent = readFileSync(roadmapFile, "utf-8"); const roadmap = parseRoadmap(roadmapContent); - const slice = roadmap.slices.find(s => s.id === sid); + const slice = (roadmap.slices ?? []).find(s => s.id === sid); if (slice && !slice.done) return false; } catch { // Corrupt/unparseable roadmap — fail verification so the unit diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 6d7570175..1386ba7c1 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -415,7 +415,7 @@ export async function bootstrapAutoSession( ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto"); ctx.ui.setFooter(hideFooter); const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode"; - const pendingCount = state.registry.filter(m => m.status !== 'complete' && m.status !== 'parked').length; + const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length; const scopeMsg = pendingCount > 1 ? `Will loop through ${pendingCount} milestones.` : "Will loop until milestone complete."; diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 4afa7370f..acdcbba22 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -877,7 +877,7 @@ async function showStepWizard( : "previous unit"; if (!mid || state.phase === "complete") { - const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked"); + const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked"); if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked" && state.phase !== "pre-planning") { const ids = incomplete.map(m => m.id).join(", "); const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`; @@ -1171,7 +1171,7 @@ async function dispatchNextUnit( } } - const pendingIds = state.registry + const pendingIds = (state.registry ?? []) .filter(m => m.status !== "complete") .map(m => m.id); pruneQueueOrder(s.basePath, pendingIds); @@ -1186,7 +1186,7 @@ async function dispatchNextUnit( await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id)); } - const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked"); + const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked"); if (incomplete.length === 0) { // Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962) if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {