diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index d99a41bbb..5127621db 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -26,6 +26,7 @@ import { resolveSlicePath, resolveSliceFile, resolveTaskFile, + resolveTasksDir, resolveGsdRootFile, gsdRoot, } from './paths.js'; @@ -34,6 +35,7 @@ import { milestoneIdSort, findMilestoneIds } from './guided-flow.js'; import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js'; import { join, resolve } from 'path'; +import { existsSync, readdirSync } from 'node:fs'; import { debugCount, debugTime } from './debug-logger.js'; // ─── Query Functions ─────────────────────────────────────────────────────── @@ -573,6 +575,34 @@ async function _deriveStateImpl(basePath: string): Promise { title: activeTaskEntry.title, }; + // ── Task plan file check (#909) ────────────────────────────────────── + // The slice plan may reference tasks but per-task plan files may be + // missing — e.g. when the slice plan was pre-created during roadmapping. + // If the tasks dir exists but has literally zero files (empty dir from + // mkdir), fall back to planning so plan-slice generates task plans. + const tasksDir = resolveTasksDir(basePath, activeMilestone.id, activeSlice.id); + if (tasksDir && existsSync(tasksDir) && slicePlan.tasks.length > 0) { + const allFiles = readdirSync(tasksDir).filter(f => f.endsWith(".md")); + if (allFiles.length === 0) { + return { + activeMilestone, + activeSlice, + activeTask: null, + phase: 'planning', + recentDecisions: [], + blockers: [], + nextAction: `Task plan files missing for ${activeSlice.id}. Run plan-slice to generate task plans.`, + registry, + requirements, + progress: { + milestones: milestoneProgress, + slices: sliceProgress, + tasks: taskProgress, + }, + }; + } + } + // ── Blocker detection: scan completed task summaries ────────────────── // If any completed task has blocker_discovered: true and no REPLAN.md // exists yet, transition to replanning-slice instead of executing. diff --git a/src/resources/extensions/gsd/tests/derive-state-db.test.ts b/src/resources/extensions/gsd/tests/derive-state-db.test.ts index 39cbd4b1b..bf4092232 100644 --- a/src/resources/extensions/gsd/tests/derive-state-db.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state-db.test.ts @@ -103,6 +103,7 @@ async function main(): Promise { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT); writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT); writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', ''); + writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan'); writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT); // Derive state from files only (no DB) @@ -166,6 +167,7 @@ async function main(): Promise { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT); writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT); writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', ''); + writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan'); // No DB open — isDbAvailable() is false assertTrue(!isDbAvailable(), 'fallback: DB is not available'); @@ -189,6 +191,7 @@ async function main(): Promise { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT); writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT); writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', ''); + writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan'); // Open DB but insert nothing — empty artifacts table openDatabase(':memory:'); @@ -219,6 +222,7 @@ async function main(): Promise { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT); writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT); writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', ''); + writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan'); writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT); // Open DB but only insert the roadmap — plan and requirements missing from DB @@ -348,6 +352,7 @@ async function main(): Promise { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT); writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT); writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', ''); + writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan'); openDatabase(':memory:'); insertArtifactRow('milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT, { diff --git a/src/resources/extensions/gsd/tests/derive-state-deps.test.ts b/src/resources/extensions/gsd/tests/derive-state-deps.test.ts index 12b75c232..47ec46f9d 100644 --- a/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state-deps.test.ts @@ -45,6 +45,7 @@ function writeContext(base: string, mid: string, frontmatter: string): void { function writeSlicePlan(base: string, mid: string, sid: string, content: string): void { const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid); mkdirSync(join(dir, 'tasks'), { recursive: true }); + writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n"); writeFileSync(join(dir, `${sid}-PLAN.md`), content); } diff --git a/src/resources/extensions/gsd/tests/derive-state-draft.test.ts b/src/resources/extensions/gsd/tests/derive-state-draft.test.ts index 19ddc8247..5b17f1b40 100644 --- a/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state-draft.test.ts @@ -45,6 +45,7 @@ function writeRoadmap(base: string, mid: string, content: string): void { function writePlan(base: string, mid: string, sid: string, content: string): void { const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid); mkdirSync(join(dir, 'tasks'), { recursive: true }); + writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n"); writeFileSync(join(dir, `${sid}-PLAN.md`), content); } diff --git a/src/resources/extensions/gsd/tests/derive-state.test.ts b/src/resources/extensions/gsd/tests/derive-state.test.ts index f924e829b..08fc0f347 100644 --- a/src/resources/extensions/gsd/tests/derive-state.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state.test.ts @@ -22,8 +22,17 @@ function writeRoadmap(base: string, mid: string, content: string): void { function writePlan(base: string, mid: string, sid: string, content: string): void { const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid); - mkdirSync(join(dir, 'tasks'), { recursive: true }); + const tasksDir = join(dir, 'tasks'); + mkdirSync(tasksDir, { recursive: true }); writeFileSync(join(dir, `${sid}-PLAN.md`), content); + // Create stub task plan files for any tasks in the plan content (#909) + // so deriveState doesn't fall back to planning phase. + const taskMatches = content.matchAll(/\*\*(T\d+):/g); + for (const m of taskMatches) { + const tid = m[1]; + const planPath = join(tasksDir, `${tid}-PLAN.md`); + writeFileSync(planPath, `# ${tid} Plan\n\nTask plan stub for testing.\n`); + } } function writeContinue(base: string, mid: string, sid: string, content: string): void { diff --git a/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts b/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts index 4cec135ce..1732dd9cb 100644 --- a/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +++ b/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts @@ -42,6 +42,7 @@ function writeRoadmap(base: string, mid: string, content: string): void { function writePlan(base: string, mid: string, sid: string, content: string): void { const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid); mkdirSync(join(dir, 'tasks'), { recursive: true }); + writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n"); writeFileSync(join(dir, `${sid}-PLAN.md`), content); } diff --git a/src/resources/extensions/gsd/tests/replan-slice.test.ts b/src/resources/extensions/gsd/tests/replan-slice.test.ts index 9d98afed0..d9e0a9e11 100644 --- a/src/resources/extensions/gsd/tests/replan-slice.test.ts +++ b/src/resources/extensions/gsd/tests/replan-slice.test.ts @@ -40,6 +40,7 @@ function writeRoadmap(base: string, mid: string, content: string): void { function writePlan(base: string, mid: string, sid: string, content: string): void { const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid); mkdirSync(join(dir, 'tasks'), { recursive: true }); + writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n"); writeFileSync(join(dir, `${sid}-PLAN.md`), content); }