diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 03eec7354..910cdc931 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -18,7 +18,7 @@ import type { import { deriveState } from "./state.js"; import type { GSDState } from "./types.js"; -import { loadFile, parseContinue, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary } from "./files.js"; +import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary } from "./files.js"; export { inlinePriorMilestoneSummary }; import type { UatType } from "./files.js"; import { loadPrompt } from "./prompt-loader.js"; @@ -48,6 +48,7 @@ import { formatValidationIssues, } from "./observability-validator.js"; import { ensureGitignore } from "./gitignore.js"; +import { runGSDDoctor, rebuildState } from "./doctor.js"; import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js"; import { initMetrics, resetMetrics, snapshotUnitMetrics, getLedger, @@ -376,6 +377,28 @@ export async function handleAgentEnd( } catch { // Non-fatal } + + // Post-hook: fix mechanical bookkeeping the LLM may have skipped. + // 1. Doctor handles: checkbox marking, stub summaries/UATs. + // 2. STATE.md is always rebuilt from disk state (purely derived, no LLM needed). + // This is more reliable than prompt instructions for mechanical tasks. + // Scope to slice level (M001/S01) so doctor checks all tasks within the slice. + try { + const scopeParts = currentUnit.id.split("/").slice(0, 2); + const doctorScope = scopeParts.join("/"); + const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope }); + if (report.fixesApplied.length > 0) { + ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info"); + } + } catch { + // Non-fatal — doctor failure should never block dispatch + } + try { + await rebuildState(basePath); + autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id); + } catch { + // Non-fatal + } } // In step mode, pause and show a wizard instead of immediately dispatching diff --git a/src/resources/extensions/gsd/doctor.ts b/src/resources/extensions/gsd/doctor.ts index 9bcb434e6..55fe9f864 100644 --- a/src/resources/extensions/gsd/doctor.ts +++ b/src/resources/extensions/gsd/doctor.ts @@ -147,6 +147,13 @@ async function updateStateFile(basePath: string, fixesApplied: string[]): Promis fixesApplied.push(`updated ${path}`); } +/** Rebuild STATE.md from current disk state. Exported for auto-mode post-hooks. */ +export async function rebuildState(basePath: string): Promise { + const state = await deriveState(basePath); + const path = resolveGsdRootFile(basePath, "STATE"); + await saveFile(path, buildStateMarkdown(state)); +} + async function ensureSliceSummaryStub(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise { const path = join(resolveSlicePath(basePath, milestoneId, sliceId) ?? relSlicePath(basePath, milestoneId, sliceId), `${sliceId}-SUMMARY.md`); const absolute = resolveSliceFile(basePath, milestoneId, sliceId, "SUMMARY") ?? join(resolveSlicePath(basePath, milestoneId, sliceId)!, `${sliceId}-SUMMARY.md`);