From b4ccbadd090fa95c1e7a88c3b00c11908bf8f718 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 11 Mar 2026 16:12:08 -0600 Subject: [PATCH] feat(gsd): add post-hook bookkeeping after each auto-mode unit Run doctor (fix mode) and rebuild STATE.md after each unit completes in handleAgentEnd. Catches missed checkboxes and stub summaries the LLM may have skipped, and keeps STATE.md in sync with disk state. Co-Authored-By: Claude Opus 4.6 --- src/resources/extensions/gsd/auto.ts | 25 ++++++++++++++++++++++++- src/resources/extensions/gsd/doctor.ts | 7 +++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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`);