diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 185e1d9ec..d87302b51 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -40,10 +40,28 @@ function cmdInitExecutePhase(cwd, phase, raw) { } const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); + let phaseInfo = findPhaseInternal(cwd, phase); const milestone = getMilestoneInfo(cwd); const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + + // Fallback to ROADMAP.md if no phase directory exists yet + if (!phaseInfo && roadmapPhase?.found) { + const phaseName = roadmapPhase.phase_name; + phaseInfo = { + found: true, + directory: null, + phase_number: roadmapPhase.phase_number, + phase_name: phaseName, + phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, + plans: [], + summaries: [], + incomplete_plans: [], + has_research: false, + has_context: false, + has_verification: false, + }; + } const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ') @@ -115,9 +133,27 @@ function cmdInitPlanPhase(cwd, phase, raw) { } const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); + let phaseInfo = findPhaseInternal(cwd, phase); const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + + // Fallback to ROADMAP.md if no phase directory exists yet + if (!phaseInfo && roadmapPhase?.found) { + const phaseName = roadmapPhase.phase_name; + phaseInfo = { + found: true, + directory: null, + phase_number: roadmapPhase.phase_number, + phase_name: phaseName, + phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, + plans: [], + summaries: [], + incomplete_plans: [], + has_research: false, + has_context: false, + has_verification: false, + }; + } const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ') @@ -409,7 +445,28 @@ function cmdInitVerifyWork(cwd, phase, raw) { } const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); + let phaseInfo = findPhaseInternal(cwd, phase); + + // Fallback to ROADMAP.md if no phase directory exists yet + if (!phaseInfo) { + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + if (roadmapPhase?.found) { + const phaseName = roadmapPhase.phase_name; + phaseInfo = { + found: true, + directory: null, + phase_number: roadmapPhase.phase_number, + phase_name: phaseName, + phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, + plans: [], + summaries: [], + incomplete_plans: [], + has_research: false, + has_context: false, + has_verification: false, + }; + } + } const result = { // Models diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 740fca823..e7655d0e0 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -199,6 +199,89 @@ describe('init commands', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// ROADMAP fallback for init plan-phase / execute-phase / verify-work (#1238) +// ───────────────────────────────────────────────────────────────────────────── + +describe('init commands ROADMAP fallback when phase directory does not exist (#1238)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 1: Foundation Setup\n**Goal:** Bootstrap project\n**Requirements**: R-01, R-02\n**Plans:** TBD\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('init plan-phase falls back to ROADMAP when no phase directory exists', () => { + const result = runGsdTools('init plan-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true, 'phase_found should be true from ROADMAP fallback'); + assert.strictEqual(output.phase_dir, null, 'phase_dir should be null (no directory yet)'); + assert.strictEqual(output.phase_number, '1'); + assert.strictEqual(output.phase_name, 'Foundation Setup'); + assert.strictEqual(output.phase_slug, 'foundation-setup'); + assert.strictEqual(output.padded_phase, '01'); + }); + + test('init execute-phase falls back to ROADMAP when no phase directory exists', () => { + const result = runGsdTools('init execute-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true, 'phase_found should be true from ROADMAP fallback'); + assert.strictEqual(output.phase_dir, null, 'phase_dir should be null (no directory yet)'); + assert.strictEqual(output.phase_number, '1'); + assert.strictEqual(output.phase_name, 'Foundation Setup'); + assert.strictEqual(output.phase_slug, 'foundation-setup'); + assert.strictEqual(output.phase_req_ids, 'R-01, R-02'); + }); + + test('init verify-work falls back to ROADMAP when no phase directory exists', () => { + const result = runGsdTools('init verify-work 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true, 'phase_found should be true from ROADMAP fallback'); + assert.strictEqual(output.phase_dir, null, 'phase_dir should be null (no directory yet)'); + assert.strictEqual(output.phase_number, '1'); + assert.strictEqual(output.phase_name, 'Foundation Setup'); + }); + + test('init plan-phase returns phase_found false when neither directory nor ROADMAP entry exists', () => { + const result = runGsdTools('init plan-phase 99', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, false); + assert.strictEqual(output.phase_dir, null); + assert.strictEqual(output.phase_number, null); + assert.strictEqual(output.phase_name, null); + }); + + test('init plan-phase prefers disk directory over ROADMAP fallback', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('init plan-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.ok(output.phase_dir !== null, 'phase_dir should point to disk directory'); + assert.ok(output.phase_dir.includes('01-foundation-setup')); + assert.strictEqual(output.plan_count, 1); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // cmdInitTodos (INIT-01) // ─────────────────────────────────────────────────────────────────────────────