From b2fb12813fce4e6e57b153c15e11644d1d4e58c1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 21 Mar 2026 14:38:13 -0400 Subject: [PATCH] fix(doctor): fix roadmap checkbox and UAT stub immediately instead of deferring (#1819) Remove all_tasks_done_missing_slice_uat and all_tasks_done_roadmap_not_checked from COMPLETION_TRANSITION_CODES so they are fixed at task fixLevel. Only all_tasks_done_missing_slice_summary remains deferred (requires LLM content). This closes the fragile handoff window where a session crash between last task and complete-slice left the project inconsistent. Fixes #1808 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/doctor-types.ts | 7 +- .../tests/doctor-completion-deferral.test.ts | 143 ++++++++++++++++++ .../gsd/tests/doctor-fixlevel.test.ts | 10 +- 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts diff --git a/src/resources/extensions/gsd/doctor-types.ts b/src/resources/extensions/gsd/doctor-types.ts index a53057dc0..b6428b992 100644 --- a/src/resources/extensions/gsd/doctor-types.ts +++ b/src/resources/extensions/gsd/doctor-types.ts @@ -76,11 +76,14 @@ export type DoctorIssueCode = * they are resolved by the complete-slice/complete-milestone dispatch units. * Consumers (e.g. auto-post-unit health tracking) should exclude these from * error counts when running at task fixLevel to avoid false escalation. + * + * Only the slice summary is deferred here because it requires LLM-generated + * content. Roadmap checkbox and UAT stub are mechanical bookkeeping and are + * fixed immediately to avoid inconsistent state if the session stops before + * complete-slice runs (#1808). */ export const COMPLETION_TRANSITION_CODES = new Set([ "all_tasks_done_missing_slice_summary", - "all_tasks_done_missing_slice_uat", - "all_tasks_done_roadmap_not_checked", ]); /** diff --git a/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts b/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts new file mode 100644 index 000000000..26beebbdb --- /dev/null +++ b/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts @@ -0,0 +1,143 @@ +/** + * Regression test for #1808: Completion-transition doctor fix deferral + * creates fragile handoff window. + * + * Only slice summary should be deferred (needs LLM content). + * Roadmap checkbox and UAT stub are mechanical bookkeeping and must be + * fixed immediately at task fixLevel to prevent inconsistent state if the + * session stops between last task and complete-slice. + */ + +import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import test from "node:test"; +import assert from "node:assert/strict"; +import { runGSDDoctor } from "../doctor.ts"; +import { COMPLETION_TRANSITION_CODES } from "../doctor-types.ts"; + +function makeTmp(name: string): string { + const dir = join(tmpdir(), `doctor-deferral-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(dir, { recursive: true }); + return dir; +} + +/** + * Build a minimal .gsd structure: milestone with one slice, one task + * marked done with a summary — but no slice summary, no UAT, and + * roadmap unchecked. This is the state after the last task completes. + */ +function buildScaffold(base: string) { + const gsd = join(base, ".gsd"); + const m = join(gsd, "milestones", "M001"); + const s = join(m, "slices", "S01", "tasks"); + mkdirSync(s, { recursive: true }); + + writeFileSync(join(m, "M001-ROADMAP.md"), `# M001: Test + +## Slices + +- [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\` + > Demo text +`); + + writeFileSync(join(m, "slices", "S01", "S01-PLAN.md"), `# S01: Test Slice + +**Goal:** test + +## Tasks + +- [x] **T01: Do stuff** \`est:5m\` +`); + + writeFileSync(join(s, "T01-SUMMARY.md"), `--- +id: T01 +parent: S01 +milestone: M001 +duration: 5m +verification_result: passed +completed_at: 2026-01-01 +--- + +# T01: Do stuff + +Done. +`); +} + +test("COMPLETION_TRANSITION_CODES only contains slice summary code", () => { + assert.ok( + COMPLETION_TRANSITION_CODES.has("all_tasks_done_missing_slice_summary"), + "summary code should still be deferred" + ); + assert.ok( + !COMPLETION_TRANSITION_CODES.has("all_tasks_done_missing_slice_uat"), + "UAT code should NOT be deferred" + ); + assert.ok( + !COMPLETION_TRANSITION_CODES.has("all_tasks_done_roadmap_not_checked"), + "roadmap code should NOT be deferred" + ); +}); + +test("fixLevel:task — fixes roadmap checkbox and UAT stub immediately, defers only summary (#1808)", async () => { + const tmp = makeTmp("partial-deferral"); + try { + buildScaffold(tmp); + + const report = await runGSDDoctor(tmp, { fix: true, fixLevel: "task" }); + + // Should detect all three issues + const codes = report.issues.map(i => i.code); + assert.ok(codes.includes("all_tasks_done_missing_slice_summary"), "should detect missing summary"); + assert.ok(codes.includes("all_tasks_done_missing_slice_uat"), "should detect missing UAT"); + assert.ok(codes.includes("all_tasks_done_roadmap_not_checked"), "should detect unchecked roadmap"); + + // Summary should NOT be created (still deferred — needs LLM content) + const sliceSummaryPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"); + assert.ok(!existsSync(sliceSummaryPath), "should NOT have created summary stub (deferred)"); + + // UAT stub SHOULD be created (mechanical bookkeeping, no longer deferred) + const sliceUatPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md"); + assert.ok(existsSync(sliceUatPath), "should have created UAT stub immediately"); + + // Roadmap checkbox SHOULD be marked done (mechanical bookkeeping, no longer deferred) + const roadmapContent = readFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf8"); + assert.ok(roadmapContent.includes("- [x] **S01"), "roadmap should show S01 as checked"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +}); + +test("fixLevel:task — session crash after last task leaves roadmap and UAT consistent (#1808)", async () => { + const tmp = makeTmp("crash-consistency"); + try { + buildScaffold(tmp); + + // Simulate: doctor runs at task level (as auto-mode does after last task) + await runGSDDoctor(tmp, { fix: true, fixLevel: "task" }); + + // Now simulate a session crash — no complete-slice ever runs. + // A new session starts and runs doctor again at task level. + const report2 = await runGSDDoctor(tmp, { fix: true, fixLevel: "task" }); + + // The only remaining issue should be the deferred summary. + // Roadmap and UAT should already be fixed from the first run. + const remainingCodes = report2.issues.map(i => i.code); + assert.ok( + !remainingCodes.includes("all_tasks_done_roadmap_not_checked"), + "roadmap should already be fixed from first doctor run" + ); + assert.ok( + !remainingCodes.includes("all_tasks_done_missing_slice_uat"), + "UAT should already be fixed from first doctor run" + ); + // Summary is still missing (deferred), that is expected + assert.ok( + remainingCodes.includes("all_tasks_done_missing_slice_summary"), + "summary should still be detected as missing (deferred)" + ); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +}); diff --git a/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts b/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts index 05f6f7f74..8308dab6b 100644 --- a/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +++ b/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts @@ -63,7 +63,7 @@ Done. `); } -test("fixLevel:task — detects completion issues but does NOT create summary stub or mark roadmap", async () => { +test("fixLevel:task — defers only summary stub, fixes roadmap and UAT immediately (#1808)", async () => { const tmp = makeTmp("task-level"); try { buildScaffold(tmp); @@ -75,17 +75,17 @@ test("fixLevel:task — detects completion issues but does NOT create summary st assert.ok(codes.includes("all_tasks_done_missing_slice_summary"), "should detect missing summary"); assert.ok(codes.includes("all_tasks_done_roadmap_not_checked"), "should detect unchecked roadmap"); - // Should NOT have fixed them + // Summary should NOT be created (still deferred — needs LLM content) const sliceSummaryPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"); assert.ok(!existsSync(sliceSummaryPath), "should NOT have created summary stub"); + // Roadmap SHOULD be marked done (mechanical bookkeeping, no longer deferred) const roadmapContent = readFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf8"); - assert.ok(roadmapContent.includes("- [ ] **S01"), "roadmap should still show S01 as unchecked"); + assert.ok(roadmapContent.includes("- [x] **S01"), "roadmap should show S01 as checked"); - // Fixes applied should NOT include completion artifacts + // Fixes applied should NOT include summary but SHOULD include roadmap for (const f of report.fixesApplied) { assert.ok(!f.includes("SUMMARY"), `should not have fixed summary: ${f}`); - assert.ok(!f.includes("roadmap"), `should not have fixed roadmap: ${f}`); } } finally { rmSync(tmp, { recursive: true, force: true });