diff --git a/agents/gsd-roadmapper.md b/agents/gsd-roadmapper.md index 5f3dafbf8..60f0b8f1b 100644 --- a/agents/gsd-roadmapper.md +++ b/agents/gsd-roadmapper.md @@ -286,12 +286,46 @@ After roadmap creation, REQUIREMENTS.md gets updated with phase mappings: ## ROADMAP.md Structure -Use template from `~/.claude/get-shit-done/templates/roadmap.md`. +**CRITICAL: ROADMAP.md requires TWO phase representations. Both are mandatory.** -Key sections: -- Overview (2-3 sentences) -- Phases with Goal, Dependencies, Requirements, Success Criteria -- Progress table +### 1. Summary Checklist (under `## Phases`) + +```markdown +- [ ] **Phase 1: Name** - One-line description +- [ ] **Phase 2: Name** - One-line description +- [ ] **Phase 3: Name** - One-line description +``` + +### 2. Detail Sections (under `## Phase Details`) + +```markdown +### Phase 1: Name +**Goal**: What this phase delivers +**Depends on**: Nothing (first phase) +**Requirements**: REQ-01, REQ-02 +**Success Criteria** (what must be TRUE): + 1. Observable behavior from user perspective + 2. Observable behavior from user perspective +**Plans**: TBD + +### Phase 2: Name +**Goal**: What this phase delivers +**Depends on**: Phase 1 +... +``` + +**The `### Phase X:` headers are parsed by downstream tools.** If you only write the summary checklist, phase lookups will fail. + +### 3. Progress Table + +```markdown +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. Name | 0/3 | Not started | - | +| 2. Name | 0/2 | Not started | - | +``` + +Reference full template: `~/.claude/get-shit-done/templates/roadmap.md` ## STATE.md Structure diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index b04e65b93..d0e088d41 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -835,14 +835,33 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) { // Escape special regex chars in phase number, handle decimal const escapedPhase = phaseNum.replace(/\./g, '\\.'); - // Match "### Phase X:" or "### Phase X.Y:" with optional name + // Match "## Phase X:" or "### Phase X:" with optional name const phasePattern = new RegExp( - `###\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, + `#{2,3}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i' ); const headerMatch = content.match(phasePattern); if (!headerMatch) { + // Fallback: check if phase exists in summary list but missing detail section + const checklistPattern = new RegExp( + `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`, + 'i' + ); + const checklistMatch = content.match(checklistPattern); + + if (checklistMatch) { + // Phase exists in summary but missing detail section - malformed ROADMAP + output({ + found: false, + phase_number: phaseNum, + phase_name: checklistMatch[1].trim(), + error: 'malformed_roadmap', + message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.` + }, raw, ''); + return; + } + output({ found: false, phase_number: phaseNum }, raw, ''); return; } @@ -850,9 +869,9 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) { const phaseName = headerMatch[1].trim(); const headerIndex = headerMatch.index; - // Find the end of this section (next ### or end of file) + // Find the end of this section (next ## or ### phase header, or end of file) const restOfContent = content.slice(headerIndex); - const nextHeaderMatch = restOfContent.match(/\n###\s+Phase\s+\d/i); + const nextHeaderMatch = restOfContent.match(/\n#{2,3}\s+Phase\s+\d/i); const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length; @@ -2430,8 +2449,8 @@ function cmdRoadmapAnalyze(cwd, raw) { const content = fs.readFileSync(roadmapPath, 'utf-8'); const phasesDir = path.join(cwd, '.planning', 'phases'); - // Extract all phase headings: ### Phase N: Name - const phasePattern = /###\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi; + // Extract all phase headings: ## Phase N: Name or ### Phase N: Name + const phasePattern = /#{2,3}\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi; const phases = []; let match; @@ -2442,7 +2461,7 @@ function cmdRoadmapAnalyze(cwd, raw) { // Extract goal from the section const sectionStart = match.index; const restOfContent = content.slice(sectionStart); - const nextHeader = restOfContent.match(/\n###\s+Phase\s+\d/i); + const nextHeader = restOfContent.match(/\n#{2,3}\s+Phase\s+\d/i); const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length; const section = content.slice(sectionStart, sectionEnd); @@ -2520,6 +2539,16 @@ function cmdRoadmapAnalyze(cwd, raw) { const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0); const completedPhases = phases.filter(p => p.disk_status === 'complete').length; + // Detect phases in summary list without detail sections (malformed ROADMAP) + const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+(?:\.\d+)?)/gi; + const checklistPhases = new Set(); + let checklistMatch; + while ((checklistMatch = checklistPattern.exec(content)) !== null) { + checklistPhases.add(checklistMatch[1]); + } + const detailPhases = new Set(phases.map(p => p.number)); + const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p)); + const result = { milestones, phases, @@ -2530,6 +2559,7 @@ function cmdRoadmapAnalyze(cwd, raw) { progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0, current_phase: currentPhase ? currentPhase.number : null, next_phase: nextPhase ? nextPhase.number : null, + missing_phase_details: missingDetails.length > 0 ? missingDetails : null, }; output(result, raw); @@ -2551,7 +2581,7 @@ function cmdPhaseAdd(cwd, description, raw) { const slug = generateSlugInternal(description); // Find highest integer phase number - const phasePattern = /###\s*Phase\s+(\d+)(?:\.\d+)?:/gi; + const phasePattern = /#{2,3}\s*Phase\s+(\d+)(?:\.\d+)?:/gi; let maxPhase = 0; let m; while ((m = phasePattern.exec(content)) !== null) { @@ -2609,7 +2639,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { // Verify target phase exists const afterPhaseEscaped = afterPhase.replace(/\./g, '\\.'); - const targetPattern = new RegExp(`###\\s*Phase\\s+${afterPhaseEscaped}:`, 'i'); + const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i'); if (!targetPattern.test(content)) { error(`Phase ${afterPhase} not found in ROADMAP.md`); } @@ -2641,7 +2671,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { 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`; // Insert after the target phase section - const headerPattern = new RegExp(`(###\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); + const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); const headerMatch = content.match(headerPattern); if (!headerMatch) { error(`Could not find Phase ${afterPhase} header`); @@ -2649,7 +2679,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { const headerIdx = content.indexOf(headerMatch[0]); const afterHeader = content.slice(headerIdx + headerMatch[0].length); - const nextPhaseMatch = afterHeader.match(/\n###\s+Phase\s+\d/i); + const nextPhaseMatch = afterHeader.match(/\n#{2,3}\s+Phase\s+\d/i); let insertIdx; if (nextPhaseMatch) { @@ -2832,7 +2862,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { // Remove the target phase section const targetEscaped = targetPhase.replace(/\./g, '\\.'); const sectionPattern = new RegExp( - `\\n?###\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n###\\s+Phase\\s+\\d|$)`, + `\\n?#{2,3}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,3}\\s+Phase\\s+\\d|$)`, 'i' ); roadmapContent = roadmapContent.replace(sectionPattern, ''); @@ -2858,9 +2888,9 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { const oldPad = oldStr.padStart(2, '0'); const newPad = newStr.padStart(2, '0'); - // Phase headings: ### Phase 18: → ### Phase 17: + // Phase headings: ## Phase 18: or ### Phase 18: → ## Phase 17: or ### Phase 17: roadmapContent = roadmapContent.replace( - new RegExp(`(###\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), + new RegExp(`(#{2,3}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2` ); @@ -2971,7 +3001,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { // Update plan count in phase section const planCountPattern = new RegExp( - `(###\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, + `(#{2,3}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, 'i' ); roadmapContent = roadmapContent.replace( @@ -3206,7 +3236,7 @@ function cmdValidateConsistency(cwd, raw) { // Extract phases from ROADMAP const roadmapPhases = new Set(); - const phasePattern = /###\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi; + const phasePattern = /#{2,3}\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi; let m; while ((m = phasePattern.exec(roadmapContent)) !== null) { roadmapPhases.add(m[1]); diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index c79786433..ac358ad84 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -520,6 +520,50 @@ This phase covers: assert.strictEqual(output.found, false, 'should return not found'); assert.strictEqual(output.error, 'ROADMAP.md not found', 'should explain why'); }); + + test('accepts ## phase headers (two hashes)', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 + +## Phase 1: Foundation +**Goal:** Set up project infrastructure +**Plans:** 2 plans + +## Phase 2: API +**Goal:** Build REST API +` + ); + + const result = runGsdTools('roadmap get-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.found, true, 'phase with ## header should be found'); + assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted'); + assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted'); + }); + + test('detects malformed ROADMAP with summary list but no detail sections', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 + +## Phases + +- [ ] **Phase 1: Foundation** - Set up project +- [ ] **Phase 2: API** - Build REST API +` + ); + + const result = runGsdTools('roadmap get-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.found, false, 'phase should not be found'); + assert.strictEqual(output.error, 'malformed_roadmap', 'should identify malformed roadmap'); + assert.ok(output.message.includes('missing'), 'should explain the issue'); + }); }); // ─────────────────────────────────────────────────────────────────────────────