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 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-11 16:12:08 -06:00
parent 5bb3229a85
commit b4ccbadd09
2 changed files with 31 additions and 1 deletions

View file

@ -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

View file

@ -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<void> {
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<void> {
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`);