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>
This commit is contained in:
parent
35131a6bb6
commit
097cd3c8e0
2 changed files with 97 additions and 1 deletions
|
|
@ -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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue