diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index 4b304d356..6792f83e7 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -130,6 +130,16 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s if (!absPath) return unitType === "replan-slice"; if (!existsSync(absPath)) return false; + // plan-slice must produce a plan with actual task entries, not just a scaffold. + // The plan file may exist from a prior discussion/context step with only headings + // but no tasks. Without this check the artifact is considered "complete" and the + // unit gets skipped — but deriveState still returns phase:"planning" because the + // plan has no tasks, creating an infinite skip loop (#699). + if (unitType === "plan-slice") { + const planContent = readFileSync(absPath, "utf-8"); + if (!/^- \[[xX ]\] \*\*T\d+:/m.test(planContent)) return false; + } + // execute-task must also have its checkbox marked [x] in the slice plan if (unitType === "execute-task") { const parts = unitId.split("/"); diff --git a/src/resources/extensions/gsd/tests/auto-recovery.test.ts b/src/resources/extensions/gsd/tests/auto-recovery.test.ts index 4ea508ac4..3d0f0fd61 100644 --- a/src/resources/extensions/gsd/tests/auto-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/auto-recovery.test.ts @@ -320,3 +320,67 @@ test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () cleanup(base); } }); + +// ─── verifyExpectedArtifact: plan-slice empty scaffold regression (#699) ── + +test("verifyExpectedArtifact rejects plan-slice with empty scaffold", () => { + const base = makeTmpBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + mkdirSync(sliceDir, { recursive: true }); + writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n\n"); + assert.strictEqual( + verifyExpectedArtifact("plan-slice", "M001/S01", base), + false, + "Empty scaffold should not be treated as completed artifact", + ); + } finally { + cleanup(base); + } +}); + +test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => { + const base = makeTmpBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + mkdirSync(sliceDir, { recursive: true }); + writeFileSync(join(sliceDir, "S01-PLAN.md"), [ + "# S01: Test Slice", + "", + "## Tasks", + "", + "- [ ] **T01: Implement feature** `est:2h`", + "- [ ] **T02: Write tests** `est:1h`", + ].join("\n")); + assert.strictEqual( + verifyExpectedArtifact("plan-slice", "M001/S01", base), + true, + "Plan with task entries should be treated as completed artifact", + ); + } finally { + cleanup(base); + } +}); + +test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => { + const base = makeTmpBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + mkdirSync(sliceDir, { recursive: true }); + writeFileSync(join(sliceDir, "S01-PLAN.md"), [ + "# S01: Test Slice", + "", + "## Tasks", + "", + "- [x] **T01: Implement feature** `est:2h`", + "- [ ] **T02: Write tests** `est:1h`", + ].join("\n")); + assert.strictEqual( + verifyExpectedArtifact("plan-slice", "M001/S01", base), + true, + "Plan with completed task entries should be treated as completed artifact", + ); + } finally { + cleanup(base); + } +});