diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 50d9716a1..b8d36094e 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) { const phaseInfo = findPhaseInternal(cwd, phase); const milestone = getMilestoneInfo(cwd); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + 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(', ') + : null; + const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null; + const result = { // Models executor_model: resolveModelInternal(cwd, 'gsd-executor'), @@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) { phase_number: phaseInfo?.phase_number || null, phase_name: phaseInfo?.phase_name || null, phase_slug: phaseInfo?.phase_slug || null, + phase_req_ids, // Plan inventory plans: phaseInfo?.plans || [], @@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) { const config = loadConfig(cwd); const phaseInfo = findPhaseInternal(cwd, phase); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + 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(', ') + : null; + const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null; + const result = { // Models researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'), @@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) { phase_name: phaseInfo?.phase_name || null, phase_slug: phaseInfo?.phase_slug || null, padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null, + phase_req_ids, // Existing artifacts has_research: phaseInfo?.has_research || false, diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 729c1b27c..10a4e602d 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -336,7 +336,7 @@ function cmdPhaseAdd(cwd, description, raw) { fs.writeFileSync(path.join(dirPath, '.gitkeep'), ''); // Build phase entry - const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`; + const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`; // Find insertion point: before last "---" or at end let updatedContent; @@ -407,7 +407,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { fs.writeFileSync(path.join(dirPath, '.gitkeep'), ''); // Build phase entry - const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`; + const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`; // Insert after the target phase section const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 4e19abd84..dbe309c29 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -19,7 +19,7 @@ Load all context in one call: INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs init execute-phase "${PHASE_ARG}") ``` -Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`. +Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`. **If `phase_found` is false:** Error — phase directory not found. **If `plan_count` is 0:** Error — no plans found in phase. @@ -294,10 +294,6 @@ node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "docs(phase-${PARENT_PHASE Verify phase achieved its GOAL, not just completed tasks. -```bash -PHASE_REQ_IDS=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs roadmap get-phase "${PHASE_NUMBER}" | jq -r '.section' | grep -i "Requirements:" | sed 's/.*Requirements:\*\*\s*//' | sed 's/[\[\]]//g') -``` - ``` Task( prompt="Verify phase {phase_number} goal achievement. diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index c7b023da0..f292e79e9 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -18,7 +18,7 @@ Load all context in one call (paths only to minimize orchestrator context): INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs init plan-phase "$PHASE") ``` -Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`. +Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`. **File paths (for blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`. These are null if files don't exist. @@ -84,7 +84,6 @@ Display banner: ```bash PHASE_DESC=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs roadmap get-phase "${PHASE}" | jq -r '.section') -PHASE_REQ_IDS=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs roadmap get-phase "${PHASE}" | jq -r '.section // empty' | grep -i "Requirements:" | head -1 | sed 's/.*Requirements:\*\*\s*//' | sed 's/[\[\]]//g' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' | tr '\n' ',' | sed 's/,$//') ``` Research prompt: diff --git a/tests/init.test.cjs b/tests/init.test.cjs index ffbd860d6..8ac9382a4 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -97,6 +97,92 @@ describe('init commands', () => { assert.strictEqual(output.context_path, undefined); assert.strictEqual(output.research_path, undefined); }); + + // ── phase_req_ids extraction (fix for #684) ────────────────────────────── + + test('init plan-phase extracts phase_req_ids from ROADMAP', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Requirements**: CP-01, CP-02, CP-03\n**Plans:** 0 plans\n` + ); + + const result = runGsdTools('init plan-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, 'CP-01, CP-02, CP-03'); + }); + + test('init plan-phase strips brackets from phase_req_ids', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Requirements**: [CP-01, CP-02]\n**Plans:** 0 plans\n` + ); + + const result = runGsdTools('init plan-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, 'CP-01, CP-02'); + }); + + test('init plan-phase returns null phase_req_ids when Requirements line is absent', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Plans:** 0 plans\n` + ); + + const result = runGsdTools('init plan-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, null); + }); + + test('init plan-phase returns null phase_req_ids when ROADMAP is absent', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + + const result = runGsdTools('init plan-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, null); + }); + + test('init execute-phase extracts phase_req_ids from ROADMAP', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Requirements**: EX-01, EX-02\n**Plans:** 1 plans\n` + ); + + const result = runGsdTools('init execute-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, 'EX-01, EX-02'); + }); + + test('init execute-phase returns null phase_req_ids when Requirements line is absent', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Plans:** 1 plans\n` + ); + + const result = runGsdTools('init execute-phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_req_ids, null); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 973971e91..b4d687030 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -420,6 +420,19 @@ describe('phase add command', () => { const output = JSON.parse(result.output); assert.strictEqual(output.phase_number, 1, 'should be phase 1'); }); + + test('phase add includes **Requirements**: TBD in new ROADMAP entry', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n\n---\n` + ); + + const result = runGsdTools('phase add User Dashboard', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('**Requirements**: TBD'), 'new phase entry should include Requirements TBD'); + }); }); // ───────────────────────────────────────────────────────────────────────────── @@ -528,6 +541,20 @@ describe('phase insert command', () => { assert.ok(roadmap.includes('(INSERTED)'), 'roadmap should include inserted phase'); }); + test('phase insert includes **Requirements**: TBD in new ROADMAP entry', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n\n### Phase 2: API\n**Goal:** Build API\n` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + + const result = runGsdTools('phase insert 1 Fix Critical Bug', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('**Requirements**: TBD'), 'inserted phase entry should include Requirements TBD'); + }); + test('handles #### heading depth from multi-milestone roadmaps', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'),