fix(gsd): assess recovery from paused worktree state
Use paused-session worktree metadata when deriving interrupted-session state so worktree isolation decisions follow the authoritative disk state without changing /gsd discuss auto handoff semantics.
This commit is contained in:
parent
3bb3ca8020
commit
71f14269a5
4 changed files with 34 additions and 10 deletions
|
|
@ -544,12 +544,12 @@ export async function showDiscuss(
|
|||
const seed = draftContent
|
||||
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
||||
: basePrompt;
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: true };
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
||||
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
||||
} else if (choice === "discuss_fresh") {
|
||||
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
||||
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: true };
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
||||
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
||||
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
||||
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export async function assessInterruptedSession(
|
|||
basePath: string,
|
||||
): Promise<InterruptedSessionAssessment> {
|
||||
const pausedSession = readPausedSessionMetadata(basePath);
|
||||
const assessmentBasePath = pausedSession?.worktreePath || basePath;
|
||||
const rawLock = readCrashLock(basePath);
|
||||
const lock = rawLock && rawLock.pid !== process.pid ? rawLock : null;
|
||||
|
||||
|
|
@ -108,22 +109,22 @@ export async function assessInterruptedSession(
|
|||
}
|
||||
|
||||
const isBootstrapCrash = isBootstrapCrashLock(lock);
|
||||
const state = await deriveState(basePath);
|
||||
const state = await deriveState(assessmentBasePath);
|
||||
const hasResumableDiskState = hasResumableDerivedState(state);
|
||||
const artifactSatisfied = !!(
|
||||
lock &&
|
||||
!isBootstrapCrash &&
|
||||
verifyExpectedArtifact(lock.unitType, lock.unitId, basePath)
|
||||
verifyExpectedArtifact(lock.unitType, lock.unitId, assessmentBasePath)
|
||||
);
|
||||
|
||||
let recovery: RecoveryBriefing | null = null;
|
||||
if (lock && !isBootstrapCrash && !artifactSatisfied) {
|
||||
recovery = synthesizeCrashRecovery(
|
||||
basePath,
|
||||
assessmentBasePath,
|
||||
lock.unitType,
|
||||
lock.unitId,
|
||||
lock.sessionFile,
|
||||
join(gsdRoot(basePath), "activity"),
|
||||
join(gsdRoot(assessmentBasePath), "activity"),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,12 +98,17 @@ function writeCompleteMilestoneSummary(base: string): void {
|
|||
writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
|
||||
}
|
||||
|
||||
function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
|
||||
function writePausedSession(
|
||||
base: string,
|
||||
milestoneId = "M001",
|
||||
stepMode = false,
|
||||
worktreePath?: string,
|
||||
): void {
|
||||
const runtimeDir = join(base, ".gsd", "runtime");
|
||||
mkdirSync(runtimeDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(runtimeDir, "paused-session.json"),
|
||||
JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
|
||||
JSON.stringify({ milestoneId, originalBasePath: base, stepMode, worktreePath }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
|
@ -227,6 +232,24 @@ test("assessInterruptedSession marks stale paused-session metadata as stale when
|
|||
}
|
||||
});
|
||||
|
||||
test("assessInterruptedSession prefers paused worktree state when worktreePath is recorded", async () => {
|
||||
const base = makeTmpBase();
|
||||
const worktree = join(base, "worktree-copy");
|
||||
try {
|
||||
writeRoadmap(base, false);
|
||||
writeRoadmap(worktree, true);
|
||||
writeCompleteSliceArtifacts(worktree);
|
||||
writeCompleteMilestoneSummary(worktree);
|
||||
writePausedSession(base, "M001", false, worktree);
|
||||
|
||||
const assessment = await assessInterruptedSession(base);
|
||||
assert.equal(assessment.classification, "stale");
|
||||
assert.equal(assessment.hasResumableDiskState, false);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("assessInterruptedSession keeps unfinished derived state recoverable without trace", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -127,12 +127,12 @@ test("guided-flow stale paused-session scenario is suppressed when no resumable
|
|||
}
|
||||
});
|
||||
|
||||
test("guided-flow source uses step-aware resume label, step-aware discuss handoff, and stale paused cleanup", () => {
|
||||
test("guided-flow source uses step-aware resume and clears stale paused metadata without changing discuss handoff semantics", () => {
|
||||
const source = readFileSync(join(import.meta.dirname, "..", "guided-flow.ts"), "utf-8");
|
||||
assert.ok(source.includes('const interrupted = await assessInterruptedSession(basePath);'));
|
||||
assert.ok(source.includes('resumeLabel = interrupted.pausedSession?.stepMode'));
|
||||
assert.ok(source.includes('step: interrupted.pausedSession?.stepMode ?? false'));
|
||||
assert.ok(source.includes('unlinkSync(join(gsdRoot(basePath), "runtime", "paused-session.json"))'));
|
||||
assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: true };'));
|
||||
assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };'));
|
||||
assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: true };'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue