diff --git a/src/resources/extensions/gsd/doctor.ts b/src/resources/extensions/gsd/doctor.ts index f6e86ab60..475e1f92e 100644 --- a/src/resources/extensions/gsd/doctor.ts +++ b/src/resources/extensions/gsd/doctor.ts @@ -280,6 +280,21 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli } } +async function markSliceUndoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise { + const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP"); + if (!roadmapPath) return; + const content = await loadFile(roadmapPath); + if (!content) return; + const updated = content.replace( + new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"), + `$1[ ] **${sliceId}:`, + ); + if (updated !== content) { + await saveFile(roadmapPath, updated); + fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`); + } +} + function matchesScope(unitId: string, scope?: string): boolean { if (!scope) return true; return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`); @@ -863,6 +878,12 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"), fixable: true, }); + if (!allTasksDone) { + dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`); + if (shouldFix("slice_checked_missing_summary")) { + await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied); + } + } } if (slice.done && !hasSliceUat) { diff --git a/src/resources/extensions/gsd/roadmap-mutations.ts b/src/resources/extensions/gsd/roadmap-mutations.ts index 3a89fbba6..85119c5b3 100644 --- a/src/resources/extensions/gsd/roadmap-mutations.ts +++ b/src/resources/extensions/gsd/roadmap-mutations.ts @@ -39,6 +39,35 @@ export function markSliceDoneInRoadmap(basePath: string, mid: string, sid: strin return true; } +/** + * Mark a slice as not done ([ ]) in the milestone roadmap. + * Idempotent — no-op if already unchecked or if the slice isn't found. + * + * @returns true if the roadmap was modified, false if no change was needed + */ +export function markSliceUndoneInRoadmap(basePath: string, mid: string, sid: string): boolean { + const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP"); + if (!roadmapFile) return false; + + let content: string; + try { + content = readFileSync(roadmapFile, "utf-8"); + } catch { + return false; + } + + const updated = content.replace( + new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sid}:`, "m"), + `$1[ ] **${sid}:`, + ); + + if (updated === content) return false; + + atomicWriteSync(roadmapFile, updated); + clearParseCache(); + return true; +} + /** * Mark a task as done ([x]) in the slice plan. * Idempotent — no-op if already checked or if the task isn't found.