From 35131a6bb680c3a9f64704f26b4cc376c9ca6d30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:22:21 +0000 Subject: [PATCH 1/2] Initial plan From 097cd3c8e0bc66226852f40dafd8150657fcc407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:34:54 +0000 Subject: [PATCH 2/2] fix: break infinite skip loop in gsd auto by adding roadmap [x] check to verifyExpectedArtifact After a crash where complete-slice wrote SUMMARY+UAT but didn't mark the roadmap [x], the idempotency check incorrectly reported the unit as "done" (artifacts exist), while the state machine kept returning the same complete-slice unit (roadmap shows [ ]). This caused dispatchNextUnit to recurse forever. Fix: verifyExpectedArtifact for complete-slice now also checks that the slice is marked [x] in the roadmap. If not, it returns false so the stale completion key is evicted and the unit re-runs. Lenient if roadmap file is missing or corrupt (returns true). Co-authored-by: glittercowboy <186001655+glittercowboy@users.noreply.github.com> --- src/resources/extensions/gsd/auto.ts | 17 +++- .../gsd/tests/idle-recovery.test.ts | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index aadb740d5..f23e0fa36 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -3065,7 +3065,11 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s } } - // complete-slice must also produce a UAT file + // complete-slice must also produce a UAT file AND mark the slice [x] in the roadmap. + // Without the roadmap check, a crash after writing SUMMARY+UAT but before updating + // the roadmap causes an infinite skip loop: the idempotency key says "done" but the + // state machine keeps returning the same complete-slice unit (roadmap still shows + // the slice incomplete), so dispatchNextUnit recurses forever. if (unitType === "complete-slice") { const parts = unitId.split("/"); const mid = parts[0]; @@ -3076,6 +3080,17 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s const uatPath = join(dir, buildSliceFileName(sid, "UAT")); if (!existsSync(uatPath)) return false; } + // Verify the roadmap has the slice marked [x]. If not, the completion + // record is stale — the unit must re-run to update the roadmap. + const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP"); + if (roadmapFile && existsSync(roadmapFile)) { + try { + const roadmapContent = readFileSync(roadmapFile, "utf-8"); + const roadmap = parseRoadmap(roadmapContent); + const slice = roadmap.slices.find(s => s.id === sid); + if (slice && !slice.done) return false; + } catch { /* corrupt roadmap — be lenient and treat as verified */ } + } } } diff --git a/src/resources/extensions/gsd/tests/idle-recovery.test.ts b/src/resources/extensions/gsd/tests/idle-recovery.test.ts index ef1492e01..4ca52330e 100644 --- a/src/resources/extensions/gsd/tests/idle-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/idle-recovery.test.ts @@ -409,6 +409,87 @@ function createGitBase(): string { } } +// ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════ +// Regression for #indefinite-hang: complete-slice must verify roadmap [x] or +// the idempotency skip loops forever after a crash that wrote SUMMARY+UAT but +// did not mark the roadmap done. + +const ROADMAP_INCOMPLETE = `# M001: Test Milestone + +## Slices + +- [ ] **S01: Test Slice** \`risk:low\` +> After this: something works +`; + +const ROADMAP_COMPLETE = `# M001: Test Milestone + +## Slices + +- [x] **S01: Test Slice** \`risk:low\` +> After this: something works +`; + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — all artifacts present + roadmap marked [x] returns true ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === true, "SUMMARY + UAT + roadmap [x] should verify as true"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY + UAT present but roadmap NOT marked [x] returns false ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_INCOMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === false, "roadmap not marked [x] should return false (crash recovery scenario)"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY present but UAT missing returns false ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + // no UAT file + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === false, "missing UAT should return false"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — no roadmap file present is lenient (returns true) ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + // no roadmap file + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === true, "missing roadmap file should be lenient and return true"); + } finally { + cleanup(base); + } +} + // ═════════════════════════════════════════════════════════════════════════════ // Results // ═════════════════════════════════════════════════════════════════════════════