When a slice plan (S03-PLAN.md) was pre-created during roadmapping but plan-slice never ran to generate per-task files (tasks/T01-PLAN.md), deriveState returned 'executing' phase. execute-task then failed because the task plan didn't exist, creating an infinite restart loop. Fix: In deriveState, when the tasks directory exists but has zero .md files and the slice plan references tasks, return 'planning' phase instead of 'executing'. This causes plan-slice to dispatch and generate the missing task plans. Tests updated: 6 test files that create synthetic state fixtures now include a stub task plan file so their 'executing' phase assertions remain valid.
This commit is contained in:
parent
e0420f5981
commit
7869312769
7 changed files with 49 additions and 1 deletions
|
|
@ -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<GSDState> {
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ async function main(): Promise<void> {
|
|||
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<void> {
|
|||
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<void> {
|
|||
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<void> {
|
|||
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<void> {
|
|||
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, {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue