fix(guided-flow): add self-heal for stale runtime records on wizard start (#436)

auto.ts has selfHealRuntimeRecords() which cleans up stale .gsd/runtime/units/
records when /gsd auto starts. However, guided-flow.ts (used by /gsd manual
mode) had zero awareness of runtime records — it only checked auto.lock.

This means if auto-mode crashes mid-unit, the stale runtime records persist
until the next /gsd auto run. Users who alternate between manual and auto
mode, or who only use manual mode after a crash, would accumulate stale
records that could cause spurious re-dispatch or confusing state.

Add selfHealRuntimeRecords() to guided-flow.ts that:
- Clears records where the expected artifact already exists (completed but
  closeout didn't finish)
- Clears records stuck in dispatched or timeout phase (process died mid-unit)
- Notifies the user how many stale records were cleaned

Called in showSmartEntry() before the crash lock check so the wizard always
starts from a clean state regardless of how the previous session ended.

Co-authored-by: Thomas <twilliams1234@gmail.com>
This commit is contained in:
gkd67pjznr-ctrl 2026-03-14 20:54:16 -06:00 committed by GitHub
parent 0e3284215a
commit 3c931b2e19

View file

@ -13,6 +13,8 @@ import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
import { deriveState } from "./state.js";
import { startAuto } from "./auto.js";
import { readCrashLock, clearLock, formatCrashInfo } from "./crash-recovery.js";
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
import { resolveExpectedArtifactPath } from "./auto.js";
import {
gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath,
resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
@ -518,6 +520,42 @@ export async function showDiscuss(
/**
* The one wizard. Reads state, shows contextual options, dispatches into the workflow doc.
*/
/**
* Self-heal: scan runtime records and clear stale ones left behind when
* auto-mode crashed mid-unit. auto.ts has its own selfHealRuntimeRecords()
* but guided-flow (manual /gsd mode) never called it meaning stale records
* persisted until the next /gsd auto run. This ensures the wizard always
* starts from a clean state regardless of how the previous session ended.
*/
function selfHealRuntimeRecords(basePath: string, ctx: ExtensionContext): { cleared: number } {
try {
const records = listUnitRuntimeRecords(basePath);
let cleared = 0;
for (const record of records) {
const { unitType, unitId, phase } = record;
// Clear records whose expected artifact already exists (completed but not cleaned up)
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
if (artifactPath && existsSync(artifactPath)) {
clearUnitRuntimeRecord(basePath, unitType, unitId);
cleared++;
continue;
}
// Clear records stuck in dispatched or timeout phase (process died mid-unit)
if (phase === "dispatched" || phase === "timeout") {
clearUnitRuntimeRecord(basePath, unitType, unitId);
cleared++;
}
}
if (cleared > 0) {
ctx.ui.notify(`Self-heal: cleared ${cleared} stale runtime record(s) from a previous session.`, "info");
}
return { cleared };
} catch {
// Non-fatal — self-heal should never block the wizard
return { cleared: 0 };
}
}
export async function showSmartEntry(
ctx: ExtensionCommandContext,
pi: ExtensionAPI,
@ -556,6 +594,9 @@ export async function showSmartEntry(
}
}
// ── Self-heal stale runtime records from crashed auto-mode sessions ──
selfHealRuntimeRecords(basePath, ctx);
// Check for crash from previous auto-mode session
const crashLock = readCrashLock(basePath);
if (crashLock) {