From 2f5323ee97e95dea51d26598160bdb9c3c01c5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Fri, 20 Mar 2026 10:39:24 -0600 Subject: [PATCH] fix: add auto-fix for premature slice completion deadlock in doctor (#1611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a slice is marked [x] in ROADMAP but tasks are incomplete and no summary exists, doctor detects slice_checked_missing_summary (declared fixable) but had no shouldFix handler — creating an unrecoverable deadlock. Add handler that unchecks the slice when tasks are incomplete, and add markSliceUndoneInRoadmap to both doctor.ts and roadmap-mutations.ts. Closes #1591 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/doctor.ts | 21 ++++++++++++++ .../extensions/gsd/roadmap-mutations.ts | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) 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.