From 36f5bb3d5f859406b7007f7ff54c9a95c267bbb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Feb 2026 13:53:09 +0000 Subject: [PATCH] feat: delegate deterministic workflow operations to gsd-tools CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 10 new CLI commands that replace manual AI orchestration of mechanical, deterministic operations — saving tokens and reducing errors on the most complex GSD workflows. New commands: - phase add — append phase to roadmap + create directory - phase insert — insert decimal phase - phase remove [--force] — remove phase with full renumbering - phase complete — mark done, update state + roadmap, detect milestone end - roadmap analyze — unified roadmap parser with disk status - milestone complete [--name] — archive roadmap/requirements/audit - validate consistency — check phase numbering and disk/roadmap sync - progress [json|table|bar] — render progress in various formats - todo complete — move todo from pending to completed - scaffold [context|uat|verification|phase-dir] — template generation Updated workflows to use new CLI commands: - remove-phase.md: 13 manual steps → 1 CLI call + confirm + commit - add-phase.md: 6 manual steps → 1 CLI call + state update - insert-phase.md: 7 manual steps → 1 CLI call + state update - transition.md: roadmap/state updates delegated to phase complete - progress.md: roadmap parsing delegated to roadmap analyze - complete-milestone.md: archival delegated to milestone complete 75 tests passing (33 new). https://claude.ai/code/session_01UhMGMrNGGn2bfeAi7MBphs --- get-shit-done/bin/gsd-tools.js | 1162 ++++++++++++++++- get-shit-done/bin/gsd-tools.test.js | 842 +++++++++++- get-shit-done/workflows/add-phase.md | 94 +- get-shit-done/workflows/complete-milestone.md | 184 +-- get-shit-done/workflows/insert-phase.md | 133 +- get-shit-done/workflows/progress.md | 50 +- get-shit-done/workflows/remove-phase.md | 210 +-- get-shit-done/workflows/transition.md | 119 +- 8 files changed, 2176 insertions(+), 618 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index fd339f1dd..5959aea9b 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -11,10 +11,52 @@ * Atomic Commands: * state load Load project config + state * state update Update a STATE.md field + * state get [section] Get STATE.md content or section + * state patch --field val ... Batch update STATE.md fields * resolve-model Get model for agent based on profile * find-phase Find phase directory by number * commit [--files f1 f2] Commit planning docs * verify-summary Verify a SUMMARY.md file + * generate-slug Convert text to URL-safe slug + * current-timestamp [format] Get timestamp (full|date|filename) + * list-todos [area] Count and enumerate pending todos + * verify-path-exists Check file/directory existence + * config-ensure-section Initialize .planning/config.json + * history-digest Aggregate all SUMMARY.md data + * summary-extract [--fields] Extract structured data from SUMMARY.md + * state-snapshot Structured parse of STATE.md + * phase-plan-index Index plans with waves and status + * + * Phase Operations: + * phase next-decimal Calculate next decimal phase number + * phase add Append new phase to roadmap + create dir + * phase insert Insert decimal phase after existing + * phase remove [--force] Remove phase, renumber all subsequent + * phase complete Mark phase done, update state + roadmap + * + * Roadmap Operations: + * roadmap get-phase Extract phase section from ROADMAP.md + * roadmap analyze Full roadmap parse with disk status + * + * Milestone Operations: + * milestone complete Archive milestone, create MILESTONES.md + * [--name ] + * + * Validation: + * validate consistency Check phase numbering, disk/roadmap sync + * + * Progress: + * progress [json|table|bar] Render progress in various formats + * + * Todos: + * todo complete Move todo from pending to completed + * + * Scaffolding: + * scaffold context --phase Create CONTEXT.md template + * scaffold uat --phase Create UAT.md template + * scaffold verification --phase Create VERIFICATION.md template + * scaffold phase-dir --phase Create phase directory + * --name * * Compound Commands (workflow-specific initialization): * init execute-phase All context for execute-phase workflow @@ -1318,6 +1360,1061 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) { output(fullResult, raw); } +// ─── Roadmap Analysis ───────────────────────────────────────────────────────── + +function cmdRoadmapAnalyze(cwd, raw) { + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + + if (!fs.existsSync(roadmapPath)) { + output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw); + return; + } + + 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; + const phases = []; + let match; + + while ((match = phasePattern.exec(content)) !== null) { + const phaseNum = match[1]; + const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim(); + + // 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 sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length; + const section = content.slice(sectionStart, sectionEnd); + + const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i); + const goal = goalMatch ? goalMatch[1].trim() : null; + + const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i); + const depends_on = dependsMatch ? dependsMatch[1].trim() : null; + + // Check completion on disk + const normalized = normalizePhaseName(phaseNum); + let diskStatus = 'no_directory'; + let planCount = 0; + let summaryCount = 0; + let hasContext = false; + let hasResearch = false; + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); + const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized); + + if (dirMatch) { + const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch)); + planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length; + summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length; + hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); + hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); + + if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete'; + else if (summaryCount > 0) diskStatus = 'partial'; + else if (planCount > 0) diskStatus = 'planned'; + else if (hasResearch) diskStatus = 'researched'; + else if (hasContext) diskStatus = 'discussed'; + else diskStatus = 'empty'; + } + } catch {} + + // Check ROADMAP checkbox status + const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i'); + const checkboxMatch = content.match(checkboxPattern); + const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false; + + phases.push({ + number: phaseNum, + name: phaseName, + goal, + depends_on, + plan_count: planCount, + summary_count: summaryCount, + has_context: hasContext, + has_research: hasResearch, + disk_status: diskStatus, + roadmap_complete: roadmapComplete, + }); + } + + // Extract milestone info + const milestones = []; + const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi; + let mMatch; + while ((mMatch = milestonePattern.exec(content)) !== null) { + milestones.push({ + heading: mMatch[1].trim(), + version: 'v' + mMatch[2], + }); + } + + // Find current and next phase + const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null; + const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null; + + // Aggregated stats + const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0); + const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0); + const completedPhases = phases.filter(p => p.disk_status === 'complete').length; + + const result = { + milestones, + phases, + phase_count: phases.length, + completed_phases: completedPhases, + total_plans: totalPlans, + total_summaries: totalSummaries, + progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0, + current_phase: currentPhase ? currentPhase.number : null, + next_phase: nextPhase ? nextPhase.number : null, + }; + + output(result, raw); +} + +// ─── Phase Add ──────────────────────────────────────────────────────────────── + +function cmdPhaseAdd(cwd, description, raw) { + if (!description) { + error('description required for phase add'); + } + + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + if (!fs.existsSync(roadmapPath)) { + error('ROADMAP.md not found'); + } + + const content = fs.readFileSync(roadmapPath, 'utf-8'); + const slug = generateSlugInternal(description); + + // Find highest integer phase number + const phasePattern = /###\s*Phase\s+(\d+)(?:\.\d+)?:/gi; + let maxPhase = 0; + let m; + while ((m = phasePattern.exec(content)) !== null) { + const num = parseInt(m[1], 10); + if (num > maxPhase) maxPhase = num; + } + + const newPhaseNum = maxPhase + 1; + const paddedNum = String(newPhaseNum).padStart(2, '0'); + const dirName = `${paddedNum}-${slug}`; + const dirPath = path.join(cwd, '.planning', 'phases', dirName); + + // Create directory + fs.mkdirSync(dirPath, { recursive: true }); + + // 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`; + + // Find insertion point: before last "---" or at end + let updatedContent; + const lastSeparator = content.lastIndexOf('\n---'); + if (lastSeparator > 0) { + updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator); + } else { + updatedContent = content + phaseEntry; + } + + fs.writeFileSync(roadmapPath, updatedContent, 'utf-8'); + + const result = { + phase_number: newPhaseNum, + padded: paddedNum, + name: description, + slug, + directory: `.planning/phases/${dirName}`, + }; + + output(result, raw, paddedNum); +} + +// ─── Phase Insert (Decimal) ────────────────────────────────────────────────── + +function cmdPhaseInsert(cwd, afterPhase, description, raw) { + if (!afterPhase || !description) { + error('after-phase and description required for phase insert'); + } + + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + if (!fs.existsSync(roadmapPath)) { + error('ROADMAP.md not found'); + } + + const content = fs.readFileSync(roadmapPath, 'utf-8'); + const slug = generateSlugInternal(description); + + // Verify target phase exists + const afterPhaseEscaped = afterPhase.replace(/\./g, '\\.'); + const targetPattern = new RegExp(`###\\s*Phase\\s+${afterPhaseEscaped}:`, 'i'); + if (!targetPattern.test(content)) { + error(`Phase ${afterPhase} not found in ROADMAP.md`); + } + + // Calculate next decimal using existing logic + const phasesDir = path.join(cwd, '.planning', 'phases'); + const normalizedBase = normalizePhaseName(afterPhase); + let existingDecimals = []; + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); + const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`); + for (const dir of dirs) { + const dm = dir.match(decimalPattern); + if (dm) existingDecimals.push(parseInt(dm[1], 10)); + } + } catch {} + + const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1; + const decimalPhase = `${normalizedBase}.${nextDecimal}`; + const dirName = `${decimalPhase}-${slug}`; + const dirPath = path.join(cwd, '.planning', 'phases', dirName); + + // Create directory + fs.mkdirSync(dirPath, { recursive: true }); + + // 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`; + + // Insert after the target phase section + const headerPattern = new RegExp(`(###\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); + const headerMatch = content.match(headerPattern); + if (!headerMatch) { + error(`Could not find Phase ${afterPhase} header`); + } + + const headerIdx = content.indexOf(headerMatch[0]); + const afterHeader = content.slice(headerIdx + headerMatch[0].length); + const nextPhaseMatch = afterHeader.match(/\n###\s+Phase\s+\d/i); + + let insertIdx; + if (nextPhaseMatch) { + insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index; + } else { + insertIdx = content.length; + } + + const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx); + fs.writeFileSync(roadmapPath, updatedContent, 'utf-8'); + + const result = { + phase_number: decimalPhase, + after_phase: afterPhase, + name: description, + slug, + directory: `.planning/phases/${dirName}`, + }; + + output(result, raw, decimalPhase); +} + +// ─── Phase Remove ───────────────────────────────────────────────────────────── + +function cmdPhaseRemove(cwd, targetPhase, options, raw) { + if (!targetPhase) { + error('phase number required for phase remove'); + } + + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const phasesDir = path.join(cwd, '.planning', 'phases'); + const force = options.force || false; + + if (!fs.existsSync(roadmapPath)) { + error('ROADMAP.md not found'); + } + + // Normalize the target + const normalized = normalizePhaseName(targetPhase); + const isDecimal = targetPhase.includes('.'); + + // Find and validate target directory + let targetDir = null; + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized); + } catch {} + + // Check for executed work (SUMMARY.md files) + if (targetDir && !force) { + const targetPath = path.join(phasesDir, targetDir); + const files = fs.readdirSync(targetPath); + const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); + if (summaries.length > 0) { + error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`); + } + } + + // Delete target directory + if (targetDir) { + fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true }); + } + + // Renumber subsequent phases + const renamedDirs = []; + const renamedFiles = []; + + if (isDecimal) { + // Decimal removal: renumber sibling decimals (e.g., removing 06.2 → 06.3 becomes 06.2) + const baseParts = normalized.split('.'); + const baseInt = baseParts[0]; + const removedDecimal = parseInt(baseParts[1], 10); + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + + // Find sibling decimals with higher numbers + const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`); + const toRename = []; + for (const dir of dirs) { + const dm = dir.match(decPattern); + if (dm && parseInt(dm[1], 10) > removedDecimal) { + toRename.push({ dir, oldDecimal: parseInt(dm[1], 10), slug: dm[2] }); + } + } + + // Sort descending to avoid conflicts + toRename.sort((a, b) => b.oldDecimal - a.oldDecimal); + + for (const item of toRename) { + const newDecimal = item.oldDecimal - 1; + const oldPhaseId = `${baseInt}.${item.oldDecimal}`; + const newPhaseId = `${baseInt}.${newDecimal}`; + const newDirName = `${baseInt}.${newDecimal}-${item.slug}`; + + // Rename directory + fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName)); + renamedDirs.push({ from: item.dir, to: newDirName }); + + // Rename files inside + const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName)); + for (const f of dirFiles) { + // Files may have phase prefix like "06.2-01-PLAN.md" + if (f.includes(oldPhaseId)) { + const newFileName = f.replace(oldPhaseId, newPhaseId); + fs.renameSync( + path.join(phasesDir, newDirName, f), + path.join(phasesDir, newDirName, newFileName) + ); + renamedFiles.push({ from: f, to: newFileName }); + } + } + } + } catch {} + + } else { + // Integer removal: renumber all subsequent integer phases + const removedInt = parseInt(normalized, 10); + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + + // Collect directories that need renumbering (integer phases > removed, and their decimals) + const toRename = []; + for (const dir of dirs) { + const dm = dir.match(/^(\d+)(?:\.(\d+))?-(.+)$/); + if (!dm) continue; + const dirInt = parseInt(dm[1], 10); + if (dirInt > removedInt) { + toRename.push({ + dir, + oldInt: dirInt, + decimal: dm[2] ? parseInt(dm[2], 10) : null, + slug: dm[3], + }); + } + } + + // Sort descending to avoid conflicts + toRename.sort((a, b) => { + if (a.oldInt !== b.oldInt) return b.oldInt - a.oldInt; + return (b.decimal || 0) - (a.decimal || 0); + }); + + for (const item of toRename) { + const newInt = item.oldInt - 1; + const newPadded = String(newInt).padStart(2, '0'); + const oldPadded = String(item.oldInt).padStart(2, '0'); + const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : ''; + const oldPrefix = `${oldPadded}${decimalSuffix}`; + const newPrefix = `${newPadded}${decimalSuffix}`; + const newDirName = `${newPrefix}-${item.slug}`; + + // Rename directory + fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName)); + renamedDirs.push({ from: item.dir, to: newDirName }); + + // Rename files inside + const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName)); + for (const f of dirFiles) { + if (f.startsWith(oldPrefix)) { + const newFileName = newPrefix + f.slice(oldPrefix.length); + fs.renameSync( + path.join(phasesDir, newDirName, f), + path.join(phasesDir, newDirName, newFileName) + ); + renamedFiles.push({ from: f, to: newFileName }); + } + } + } + } catch {} + } + + // Update ROADMAP.md + let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); + + // 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|$)`, + 'i' + ); + roadmapContent = roadmapContent.replace(sectionPattern, ''); + + // Remove from phase list (checkbox) + const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi'); + roadmapContent = roadmapContent.replace(checkboxPattern, ''); + + // Remove from progress table + const tableRowPattern = new RegExp(`\\n?\\|\\s*${targetEscaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'); + roadmapContent = roadmapContent.replace(tableRowPattern, ''); + + // Renumber references in ROADMAP for subsequent phases + if (!isDecimal) { + const removedInt = parseInt(normalized, 10); + + // Collect all integer phases > removedInt + const maxPhase = 99; // reasonable upper bound + for (let oldNum = maxPhase; oldNum > removedInt; oldNum--) { + const newNum = oldNum - 1; + const oldStr = String(oldNum); + const newStr = String(newNum); + const oldPad = oldStr.padStart(2, '0'); + const newPad = newStr.padStart(2, '0'); + + // Phase headings: ### Phase 18: → ### Phase 17: + roadmapContent = roadmapContent.replace( + new RegExp(`(###\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), + `$1${newStr}$2` + ); + + // Checkbox items: - [ ] **Phase 18:** → - [ ] **Phase 17:** + roadmapContent = roadmapContent.replace( + new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), + `$1${newStr}$2` + ); + + // Plan references: 18-01 → 17-01 + roadmapContent = roadmapContent.replace( + new RegExp(`${oldPad}-(\\d{2})`, 'g'), + `${newPad}-$1` + ); + + // Table rows: | 18. → | 17. + roadmapContent = roadmapContent.replace( + new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), + `$1${newStr}. ` + ); + + // Depends on references + roadmapContent = roadmapContent.replace( + new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), + `$1${newStr}` + ); + } + } + + fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); + + // Update STATE.md phase count + const statePath = path.join(cwd, '.planning', 'STATE.md'); + if (fs.existsSync(statePath)) { + let stateContent = fs.readFileSync(statePath, 'utf-8'); + // Update "Total Phases" field + const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/; + const totalMatch = stateContent.match(totalPattern); + if (totalMatch) { + const oldTotal = parseInt(totalMatch[2], 10); + stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`); + } + // Update "Phase: X of Y" pattern + const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i; + const ofMatch = stateContent.match(ofPattern); + if (ofMatch) { + const oldTotal = parseInt(ofMatch[2], 10); + stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`); + } + fs.writeFileSync(statePath, stateContent, 'utf-8'); + } + + const result = { + removed: targetPhase, + directory_deleted: targetDir || null, + renamed_directories: renamedDirs, + renamed_files: renamedFiles, + roadmap_updated: true, + state_updated: fs.existsSync(statePath), + }; + + output(result, raw); +} + +// ─── Phase Complete (Transition) ────────────────────────────────────────────── + +function cmdPhaseComplete(cwd, phaseNum, raw) { + if (!phaseNum) { + error('phase number required for phase complete'); + } + + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const statePath = path.join(cwd, '.planning', 'STATE.md'); + const phasesDir = path.join(cwd, '.planning', 'phases'); + const normalized = normalizePhaseName(phaseNum); + const today = new Date().toISOString().split('T')[0]; + + // Verify phase info + const phaseInfo = findPhaseInternal(cwd, phaseNum); + if (!phaseInfo) { + error(`Phase ${phaseNum} not found`); + } + + const planCount = phaseInfo.plans.length; + const summaryCount = phaseInfo.summaries.length; + + // Update ROADMAP.md: mark phase complete + if (fs.existsSync(roadmapPath)) { + let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); + + // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE) + const checkboxPattern = new RegExp( + `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}[:\\s][^\\n]*)`, + 'i' + ); + roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`); + + // Progress table: update Status to Complete, add date + const phaseEscaped = phaseNum.replace('.', '\\.'); + const tablePattern = new RegExp( + `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, + 'i' + ); + roadmapContent = roadmapContent.replace( + tablePattern, + `$1 Complete $2 ${today} $3` + ); + + // Update plan count in phase section + const planCountPattern = new RegExp( + `(###\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, + 'i' + ); + roadmapContent = roadmapContent.replace( + planCountPattern, + `$1${summaryCount}/${planCount} plans complete` + ); + + fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); + } + + // Find next phase + let nextPhaseNum = null; + let nextPhaseName = null; + let isLastPhase = true; + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + const currentFloat = parseFloat(phaseNum); + + // Find the next phase directory after current + for (const dir of dirs) { + const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/); + if (dm) { + const dirFloat = parseFloat(dm[1]); + if (dirFloat > currentFloat) { + nextPhaseNum = dm[1]; + nextPhaseName = dm[2] || null; + isLastPhase = false; + break; + } + } + } + } catch {} + + // Update STATE.md + if (fs.existsSync(statePath)) { + let stateContent = fs.readFileSync(statePath, 'utf-8'); + + // Update Current Phase + stateContent = stateContent.replace( + /(\*\*Current Phase:\*\*\s*).*/, + `$1${nextPhaseNum || phaseNum}` + ); + + // Update Current Phase Name + if (nextPhaseName) { + stateContent = stateContent.replace( + /(\*\*Current Phase Name:\*\*\s*).*/, + `$1${nextPhaseName.replace(/-/g, ' ')}` + ); + } + + // Update Status + stateContent = stateContent.replace( + /(\*\*Status:\*\*\s*).*/, + `$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}` + ); + + // Update Current Plan + stateContent = stateContent.replace( + /(\*\*Current Plan:\*\*\s*).*/, + `$1Not started` + ); + + // Update Last Activity + stateContent = stateContent.replace( + /(\*\*Last Activity:\*\*\s*).*/, + `$1${today}` + ); + + // Update Last Activity Description + stateContent = stateContent.replace( + /(\*\*Last Activity Description:\*\*\s*).*/, + `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}` + ); + + fs.writeFileSync(statePath, stateContent, 'utf-8'); + } + + const result = { + completed_phase: phaseNum, + phase_name: phaseInfo.phase_name, + plans_executed: `${summaryCount}/${planCount}`, + next_phase: nextPhaseNum, + next_phase_name: nextPhaseName, + is_last_phase: isLastPhase, + date: today, + roadmap_updated: fs.existsSync(roadmapPath), + state_updated: fs.existsSync(statePath), + }; + + output(result, raw); +} + +// ─── Milestone Complete ─────────────────────────────────────────────────────── + +function cmdMilestoneComplete(cwd, version, options, raw) { + if (!version) { + error('version required for milestone complete (e.g., v1.0)'); + } + + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); + const statePath = path.join(cwd, '.planning', 'STATE.md'); + const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); + const archiveDir = path.join(cwd, '.planning', 'milestones'); + const phasesDir = path.join(cwd, '.planning', 'phases'); + const today = new Date().toISOString().split('T')[0]; + const milestoneName = options.name || version; + + // Ensure archive directory exists + fs.mkdirSync(archiveDir, { recursive: true }); + + // Gather stats from phases + let phaseCount = 0; + let totalPlans = 0; + let totalTasks = 0; + const accomplishments = []; + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + + for (const dir of dirs) { + phaseCount++; + const phaseFiles = fs.readdirSync(path.join(phasesDir, dir)); + const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md'); + const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); + totalPlans += plans.length; + + // Extract one-liners from summaries + for (const s of summaries) { + try { + const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8'); + const fm = extractFrontmatter(content); + if (fm['one-liner']) { + accomplishments.push(fm['one-liner']); + } + // Count tasks + const taskMatches = content.match(/##\s*Task\s*\d+/gi) || []; + totalTasks += taskMatches.length; + } catch {} + } + } + } catch {} + + // Archive ROADMAP.md + if (fs.existsSync(roadmapPath)) { + const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); + fs.writeFileSync(path.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8'); + } + + // Archive REQUIREMENTS.md + if (fs.existsSync(reqPath)) { + const reqContent = fs.readFileSync(reqPath, 'utf-8'); + const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`; + fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8'); + } + + // Archive audit file if exists + const auditFile = path.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`); + if (fs.existsSync(auditFile)) { + fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)); + } + + // Create/append MILESTONES.md entry + const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n'); + const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`; + + if (fs.existsSync(milestonesPath)) { + const existing = fs.readFileSync(milestonesPath, 'utf-8'); + fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8'); + } else { + fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8'); + } + + // Update STATE.md + if (fs.existsSync(statePath)) { + let stateContent = fs.readFileSync(statePath, 'utf-8'); + stateContent = stateContent.replace( + /(\*\*Status:\*\*\s*).*/, + `$1${version} milestone complete` + ); + stateContent = stateContent.replace( + /(\*\*Last Activity:\*\*\s*).*/, + `$1${today}` + ); + stateContent = stateContent.replace( + /(\*\*Last Activity Description:\*\*\s*).*/, + `$1${version} milestone completed and archived` + ); + fs.writeFileSync(statePath, stateContent, 'utf-8'); + } + + const result = { + version, + name: milestoneName, + date: today, + phases: phaseCount, + plans: totalPlans, + tasks: totalTasks, + accomplishments, + archived: { + roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)), + requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)), + audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)), + }, + milestones_updated: true, + state_updated: fs.existsSync(statePath), + }; + + output(result, raw); +} + +// ─── Validate Consistency ───────────────────────────────────────────────────── + +function cmdValidateConsistency(cwd, raw) { + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const phasesDir = path.join(cwd, '.planning', 'phases'); + const errors = []; + const warnings = []; + + // Check for ROADMAP + if (!fs.existsSync(roadmapPath)) { + errors.push('ROADMAP.md not found'); + output({ passed: false, errors, warnings }, raw, 'failed'); + return; + } + + const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); + + // Extract phases from ROADMAP + const roadmapPhases = new Set(); + const phasePattern = /###\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi; + let m; + while ((m = phasePattern.exec(roadmapContent)) !== null) { + roadmapPhases.add(m[1]); + } + + // Get phases on disk + const diskPhases = new Set(); + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); + for (const dir of dirs) { + const dm = dir.match(/^(\d+(?:\.\d+)?)/); + if (dm) diskPhases.add(dm[1]); + } + } catch {} + + // Check: phases in ROADMAP but not on disk + for (const p of roadmapPhases) { + if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) { + warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`); + } + } + + // Check: phases on disk but not in ROADMAP + for (const p of diskPhases) { + const unpadded = String(parseInt(p, 10)); + if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) { + warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`); + } + } + + // Check: sequential phase numbers (integers only) + const integerPhases = [...diskPhases] + .filter(p => !p.includes('.')) + .map(p => parseInt(p, 10)) + .sort((a, b) => a - b); + + for (let i = 1; i < integerPhases.length; i++) { + if (integerPhases[i] !== integerPhases[i - 1] + 1) { + warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`); + } + } + + // Check: plan numbering within phases + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + + for (const dir of dirs) { + const phaseFiles = fs.readdirSync(path.join(phasesDir, dir)); + const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort(); + + // Extract plan numbers + const planNums = plans.map(p => { + const pm = p.match(/-(\d{2})-PLAN\.md$/); + return pm ? parseInt(pm[1], 10) : null; + }).filter(n => n !== null); + + for (let i = 1; i < planNums.length; i++) { + if (planNums[i] !== planNums[i - 1] + 1) { + warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} → ${planNums[i]}`); + } + } + + // Check: plans without summaries (completed plans) + const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md')); + const planIds = new Set(plans.map(p => p.replace('-PLAN.md', ''))); + const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', ''))); + + // Summary without matching plan is suspicious + for (const sid of summaryIds) { + if (!planIds.has(sid)) { + warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`); + } + } + } + } catch {} + + // Check: frontmatter in plans has required fields + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); + + for (const dir of dirs) { + const phaseFiles = fs.readdirSync(path.join(phasesDir, dir)); + const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')); + + for (const plan of plans) { + const content = fs.readFileSync(path.join(phasesDir, dir, plan), 'utf-8'); + const fm = extractFrontmatter(content); + + if (!fm.wave) { + warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`); + } + } + } + } catch {} + + const passed = errors.length === 0; + output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed'); +} + +// ─── Progress Render ────────────────────────────────────────────────────────── + +function cmdProgressRender(cwd, format, raw) { + const phasesDir = path.join(cwd, '.planning', 'phases'); + const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const milestone = getMilestoneInfo(cwd); + + const phases = []; + let totalPlans = 0; + let totalSummaries = 0; + + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => { + const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0'); + const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0'); + return aNum - bNum; + }); + + for (const dir of dirs) { + const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/); + const phaseNum = dm ? dm[1] : dir; + const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : ''; + const phaseFiles = fs.readdirSync(path.join(phasesDir, dir)); + const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length; + const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length; + + totalPlans += plans; + totalSummaries += summaries; + + let status; + if (plans === 0) status = 'Pending'; + else if (summaries >= plans) status = 'Complete'; + else if (summaries > 0) status = 'In Progress'; + else status = 'Planned'; + + phases.push({ number: phaseNum, name: phaseName, plans, summaries, status }); + } + } catch {} + + const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0; + + if (format === 'table') { + // Render markdown table + const barWidth = 10; + const filled = Math.round((percent / 100) * barWidth); + const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled); + let out = `# ${milestone.version} ${milestone.name}\n\n`; + out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n\n`; + out += `| Phase | Name | Plans | Status |\n`; + out += `|-------|------|-------|--------|\n`; + for (const p of phases) { + out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`; + } + output({ rendered: out }, raw, out); + } else if (format === 'bar') { + const barWidth = 20; + const filled = Math.round((percent / 100) * barWidth); + const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled); + const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`; + output({ bar: text, percent, completed: totalSummaries, total: totalPlans }, raw, text); + } else { + // JSON format + output({ + milestone_version: milestone.version, + milestone_name: milestone.name, + phases, + total_plans: totalPlans, + total_summaries: totalSummaries, + percent, + }, raw); + } +} + +// ─── Todo Complete ──────────────────────────────────────────────────────────── + +function cmdTodoComplete(cwd, filename, raw) { + if (!filename) { + error('filename required for todo complete'); + } + + const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); + const completedDir = path.join(cwd, '.planning', 'todos', 'completed'); + const sourcePath = path.join(pendingDir, filename); + + if (!fs.existsSync(sourcePath)) { + error(`Todo not found: ${filename}`); + } + + // Ensure completed directory exists + fs.mkdirSync(completedDir, { recursive: true }); + + // Read, add completion timestamp, move + let content = fs.readFileSync(sourcePath, 'utf-8'); + const today = new Date().toISOString().split('T')[0]; + content = `completed: ${today}\n` + content; + + fs.writeFileSync(path.join(completedDir, filename), content, 'utf-8'); + fs.unlinkSync(sourcePath); + + output({ completed: true, file: filename, date: today }, raw, 'completed'); +} + +// ─── Scaffold ───────────────────────────────────────────────────────────────── + +function cmdScaffold(cwd, type, options, raw) { + const { phase, name } = options; + const padded = phase ? normalizePhaseName(phase) : '00'; + const today = new Date().toISOString().split('T')[0]; + + // Find phase directory + const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null; + const phaseDir = phaseInfo ? path.join(cwd, phaseInfo.directory) : null; + + if (phase && !phaseDir && type !== 'phase-dir') { + error(`Phase ${phase} directory not found`); + } + + let filePath, content; + + switch (type) { + case 'context': { + filePath = path.join(phaseDir, `${padded}-CONTEXT.md`); + content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /gsd:discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`; + break; + } + case 'uat': { + filePath = path.join(phaseDir, `${padded}-UAT.md`); + content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`; + break; + } + case 'verification': { + filePath = path.join(phaseDir, `${padded}-VERIFICATION.md`); + content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Verification\n\n## Goal-Backward Verification\n\n**Phase Goal:** [From ROADMAP.md]\n\n## Checks\n\n| # | Requirement | Status | Evidence |\n|---|------------|--------|----------|\n\n## Result\n\n_Pending verification_\n`; + break; + } + case 'phase-dir': { + if (!phase || !name) { + error('phase and name required for phase-dir scaffold'); + } + const slug = generateSlugInternal(name); + const dirName = `${padded}-${slug}`; + const phasesParent = path.join(cwd, '.planning', 'phases'); + fs.mkdirSync(phasesParent, { recursive: true }); + const dirPath = path.join(phasesParent, dirName); + fs.mkdirSync(dirPath, { recursive: true }); + output({ created: true, directory: `.planning/phases/${dirName}`, path: dirPath }, raw, dirPath); + return; + } + default: + error(`Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`); + } + + if (fs.existsSync(filePath)) { + output({ created: false, reason: 'already_exists', path: filePath }, raw, 'exists'); + return; + } + + fs.writeFileSync(filePath, content, 'utf-8'); + const relPath = path.relative(cwd, filePath); + output({ created: true, path: relPath }, raw, relPath); +} + // ─── Compound Commands ──────────────────────────────────────────────────────── function resolveModelInternal(cwd, agentType) { @@ -2169,8 +3266,10 @@ function main() { const subcommand = args[1]; if (subcommand === 'get-phase') { cmdRoadmapGetPhase(cwd, args[2], raw); + } else if (subcommand === 'analyze') { + cmdRoadmapAnalyze(cwd, raw); } else { - error('Unknown roadmap subcommand. Available: get-phase'); + error('Unknown roadmap subcommand. Available: get-phase, analyze'); } break; } @@ -2179,12 +3278,71 @@ function main() { const subcommand = args[1]; if (subcommand === 'next-decimal') { cmdPhaseNextDecimal(cwd, args[2], raw); + } else if (subcommand === 'add') { + cmdPhaseAdd(cwd, args.slice(2).join(' '), raw); + } else if (subcommand === 'insert') { + cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw); + } else if (subcommand === 'remove') { + const forceFlag = args.includes('--force'); + cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw); + } else if (subcommand === 'complete') { + cmdPhaseComplete(cwd, args[2], raw); } else { - error('Unknown phase subcommand. Available: next-decimal'); + error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete'); } break; } + case 'milestone': { + const subcommand = args[1]; + if (subcommand === 'complete') { + const nameIndex = args.indexOf('--name'); + const milestoneName = nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null; + cmdMilestoneComplete(cwd, args[2], { name: milestoneName }, raw); + } else { + error('Unknown milestone subcommand. Available: complete'); + } + break; + } + + case 'validate': { + const subcommand = args[1]; + if (subcommand === 'consistency') { + cmdValidateConsistency(cwd, raw); + } else { + error('Unknown validate subcommand. Available: consistency'); + } + break; + } + + case 'progress': { + const subcommand = args[1] || 'json'; + cmdProgressRender(cwd, subcommand, raw); + break; + } + + case 'todo': { + const subcommand = args[1]; + if (subcommand === 'complete') { + cmdTodoComplete(cwd, args[2], raw); + } else { + error('Unknown todo subcommand. Available: complete'); + } + break; + } + + case 'scaffold': { + const scaffoldType = args[1]; + const phaseIndex = args.indexOf('--phase'); + const nameIndex = args.indexOf('--name'); + const scaffoldOptions = { + phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null, + name: nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null, + }; + cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw); + break; + } + case 'init': { const workflow = args[1]; const includes = parseIncludeFlag(args); diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index 5810aa741..c79786433 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -1,5 +1,5 @@ /** - * GSD Tools Tests — Schema validation for history-digest command + * GSD Tools Tests */ const { test, describe, beforeEach, afterEach } = require('node:test'); @@ -1191,3 +1191,843 @@ describe('init commands with --include flag', () => { assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined'); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// roadmap analyze command +// ───────────────────────────────────────────────────────────────────────────── + +describe('roadmap analyze command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('missing ROADMAP.md returns error', () => { + const result = runGsdTools('roadmap analyze', tmpDir); + assert.ok(result.success, `Command should succeed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.error, 'ROADMAP.md not found'); + }); + + test('parses phases with goals and disk status', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 + +### Phase 1: Foundation +**Goal:** Set up infrastructure + +### Phase 2: Authentication +**Goal:** Add user auth + +### Phase 3: Features +**Goal:** Build core features +` + ); + + // Create phase dirs with varying completion + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const p2 = path.join(tmpDir, '.planning', 'phases', '02-authentication'); + fs.mkdirSync(p2, { recursive: true }); + fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('roadmap analyze', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 3, 'should find 3 phases'); + assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete'); + assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned'); + assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory'); + assert.strictEqual(output.completed_phases, 1, '1 phase complete'); + assert.strictEqual(output.total_plans, 2, '2 total plans'); + assert.strictEqual(output.total_summaries, 1, '1 total summary'); + assert.strictEqual(output.progress_percent, 50, '50% complete'); + assert.strictEqual(output.current_phase, '2', 'current phase is 2'); + }); + + test('extracts goals and dependencies', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Setup +**Goal:** Initialize project +**Depends on:** Nothing + +### Phase 2: Build +**Goal:** Build features +**Depends on:** Phase 1 +` + ); + + const result = runGsdTools('roadmap analyze', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phases[0].goal, 'Initialize project'); + assert.strictEqual(output.phases[0].depends_on, 'Nothing'); + assert.strictEqual(output.phases[1].goal, 'Build features'); + assert.strictEqual(output.phases[1].depends_on, 'Phase 1'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// phase add command +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase add command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('adds phase after highest existing', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 + +### Phase 1: Foundation +**Goal:** Setup + +### Phase 2: API +**Goal:** Build API + +--- +` + ); + + const result = runGsdTools('phase add User Dashboard', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_number, 3, 'should be phase 3'); + assert.strictEqual(output.slug, 'user-dashboard'); + + // Verify directory created + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-user-dashboard')), + 'directory should be created' + ); + + // Verify ROADMAP updated + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('### Phase 3: User Dashboard'), 'roadmap should include new phase'); + assert.ok(roadmap.includes('**Depends on:** Phase 2'), 'should depend on previous'); + }); + + test('handles empty roadmap', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + + const result = runGsdTools('phase add Initial Setup', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_number, 1, 'should be phase 1'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// phase insert command +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase insert command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('inserts decimal phase after target', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup + +### Phase 2: API +**Goal:** Build API +` + ); + 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 output = JSON.parse(result.output); + assert.strictEqual(output.phase_number, '01.1', 'should be 01.1'); + assert.strictEqual(output.after_phase, '1'); + + // Verify directory + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '01.1-fix-critical-bug')), + 'decimal phase directory should be created' + ); + + // Verify ROADMAP + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('Phase 01.1: Fix Critical Bug (INSERTED)'), 'roadmap should include inserted phase'); + }); + + test('increments decimal when siblings exist', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup + +### Phase 2: API +**Goal:** Build API +` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01.1-hotfix'), { recursive: true }); + + const result = runGsdTools('phase insert 1 Another Fix', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_number, '01.2', 'should be 01.2'); + }); + + test('rejects missing phase', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n` + ); + + const result = runGsdTools('phase insert 99 Fix Something', tmpDir); + assert.ok(!result.success, 'should fail for missing phase'); + assert.ok(result.error.includes('not found'), 'error mentions not found'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// phase remove command +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase remove command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('removes phase directory and renumbers subsequent', () => { + // Setup 3 phases + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup +**Depends on:** Nothing + +### Phase 2: Auth +**Goal:** Authentication +**Depends on:** Phase 1 + +### Phase 3: Features +**Goal:** Core features +**Depends on:** Phase 2 +` + ); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + const p2 = path.join(tmpDir, '.planning', 'phases', '02-auth'); + fs.mkdirSync(p2, { recursive: true }); + fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan'); + const p3 = path.join(tmpDir, '.planning', 'phases', '03-features'); + fs.mkdirSync(p3, { recursive: true }); + fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p3, '03-02-PLAN.md'), '# Plan 2'); + + // Remove phase 2 + const result = runGsdTools('phase remove 2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.removed, '2'); + assert.strictEqual(output.directory_deleted, '02-auth'); + + // Phase 3 should be renumbered to 02 + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features')), + 'phase 3 should be renumbered to 02-features' + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-features')), + 'old 03-features should not exist' + ); + + // Files inside should be renamed + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-01-PLAN.md')), + 'plan file should be renumbered to 02-01' + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-02-PLAN.md')), + 'plan 2 should be renumbered to 02-02' + ); + + // ROADMAP should be updated + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(!roadmap.includes('Phase 2: Auth'), 'removed phase should not be in roadmap'); + assert.ok(roadmap.includes('Phase 2: Features'), 'phase 3 should be renumbered to 2'); + }); + + test('rejects removal of phase with summaries unless --force', () => { + const p1 = path.join(tmpDir, '.planning', 'phases', '01-test'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n` + ); + + // Should fail without --force + const result = runGsdTools('phase remove 1', tmpDir); + assert.ok(!result.success, 'should fail without --force'); + assert.ok(result.error.includes('executed plan'), 'error mentions executed plans'); + + // Should succeed with --force + const forceResult = runGsdTools('phase remove 1 --force', tmpDir); + assert.ok(forceResult.success, `Force remove failed: ${forceResult.error}`); + }); + + test('removes decimal phase and renumbers siblings', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 6: Main\n**Goal:** Main\n### Phase 6.1: Fix A\n**Goal:** Fix A\n### Phase 6.2: Fix B\n**Goal:** Fix B\n### Phase 6.3: Fix C\n**Goal:** Fix C\n` + ); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-main'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-fix-a'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-b'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c'), { recursive: true }); + + const result = runGsdTools('phase remove 6.2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // 06.3 should become 06.2 + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-c')), + '06.3 should be renumbered to 06.2' + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c')), + 'old 06.3 should not exist' + ); + }); + + test('updates STATE.md phase count', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: A\n**Goal:** A\n### Phase 2: B\n**Goal:** B\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 1\n**Total Phases:** 2\n` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true }); + + runGsdTools('phase remove 2', tmpDir); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// phase complete command +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase complete command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('marks phase complete and transitions to next', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation +- [ ] Phase 2: API + +### Phase 1: Foundation +**Goal:** Setup +**Plans:** 1 plans + +### Phase 2: API +**Goal:** Build API +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true }); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.completed_phase, '1'); + assert.strictEqual(output.plans_executed, '1/1'); + assert.strictEqual(output.next_phase, '02'); + assert.strictEqual(output.is_last_phase, false); + + // Verify STATE.md updated + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('**Current Phase:** 02'), 'should advance to phase 02'); + assert.ok(state.includes('**Status:** Ready to plan'), 'status should be ready to plan'); + assert.ok(state.includes('**Current Plan:** Not started'), 'plan should be reset'); + + // Verify ROADMAP checkbox + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('[x]'), 'phase should be checked off'); + assert.ok(roadmap.includes('completed'), 'completion date should be added'); + }); + + test('detects last phase in milestone', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: Only Phase\n**Goal:** Everything\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-only-phase'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.is_last_phase, true, 'should detect last phase'); + assert.strictEqual(output.next_phase, null, 'no next phase'); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('Milestone complete'), 'status should be milestone complete'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// milestone complete command +// ───────────────────────────────────────────────────────────────────────────── + +describe('milestone complete command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('archives roadmap, requirements, creates MILESTONES.md', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync( + path.join(p1, '01-01-SUMMARY.md'), + `---\none-liner: Set up project infrastructure\n---\n# Summary\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.version, 'v1.0'); + assert.strictEqual(output.phases, 1); + assert.ok(output.archived.roadmap, 'roadmap should be archived'); + assert.ok(output.archived.requirements, 'requirements should be archived'); + + // Verify archive files exist + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')), + 'archived roadmap should exist' + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')), + 'archived requirements should exist' + ); + + // Verify MILESTONES.md created + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')), + 'MILESTONES.md should be created' + ); + const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8'); + assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name'); + assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed'); + }); + + test('appends to existing MILESTONES.md', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'MILESTONES.md'), + `# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8'); + assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved'); + assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// validate consistency command +// ───────────────────────────────────────────────────────────────────────────── + +describe('validate consistency command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('passes for consistent project', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true }); + + const result = runGsdTools('validate consistency', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.passed, true, 'should pass'); + assert.strictEqual(output.warning_count, 0, 'no warnings'); + }); + + test('warns about phase on disk but not in roadmap', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: A\n` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true }); + + const result = runGsdTools('validate consistency', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.warning_count > 0, 'should have warnings'); + assert.ok( + output.warnings.some(w => w.includes('disk but not in ROADMAP')), + 'should warn about orphan directory' + ); + }); + + test('warns about gaps in phase numbering', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n### Phase 1: A\n### Phase 3: C\n` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true }); + + const result = runGsdTools('validate consistency', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok( + output.warnings.some(w => w.includes('Gap in phase numbering')), + 'should warn about gap' + ); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// progress command +// ───────────────────────────────────────────────────────────────────────────── + +describe('progress command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('renders JSON progress', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 MVP\n` + ); + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done'); + fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2'); + + const result = runGsdTools('progress json', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.total_plans, 2, '2 total plans'); + assert.strictEqual(output.total_summaries, 1, '1 summary'); + assert.strictEqual(output.percent, 50, '50%'); + assert.strictEqual(output.phases.length, 1, '1 phase'); + assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress'); + }); + + test('renders bar format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + const p1 = path.join(tmpDir, '.planning', 'phases', '01-test'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done'); + + const result = runGsdTools('progress bar --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + assert.ok(result.output.includes('1/1'), 'should include count'); + assert.ok(result.output.includes('100%'), 'should include 100%'); + }); + + test('renders table format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 MVP\n` + ); + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('progress table --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + assert.ok(result.output.includes('Phase'), 'should have table header'); + assert.ok(result.output.includes('foundation'), 'should include phase name'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// todo complete command +// ───────────────────────────────────────────────────────────────────────────── + +describe('todo complete command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('moves todo from pending to completed', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync( + path.join(pendingDir, 'add-dark-mode.md'), + `title: Add dark mode\narea: ui\ncreated: 2025-01-01\n` + ); + + const result = runGsdTools('todo complete add-dark-mode.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.completed, true); + + // Verify moved + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')), + 'should be removed from pending' + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')), + 'should be in completed' + ); + + // Verify completion timestamp added + const content = fs.readFileSync( + path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'), + 'utf-8' + ); + assert.ok(content.startsWith('completed:'), 'should have completed timestamp'); + }); + + test('fails for nonexistent todo', () => { + const result = runGsdTools('todo complete nonexistent.md', tmpDir); + assert.ok(!result.success, 'should fail'); + assert.ok(result.error.includes('not found'), 'error mentions not found'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// scaffold command +// ───────────────────────────────────────────────────────────────────────────── + +describe('scaffold command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('scaffolds context file', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + + const result = runGsdTools('scaffold context --phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + + // Verify file content + const content = fs.readFileSync( + path.join(tmpDir, '.planning', 'phases', '03-api', '03-CONTEXT.md'), + 'utf-8' + ); + assert.ok(content.includes('Phase 3'), 'should reference phase number'); + assert.ok(content.includes('Decisions'), 'should have decisions section'); + assert.ok(content.includes('Discretion Areas'), 'should have discretion section'); + }); + + test('scaffolds UAT file', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + + const result = runGsdTools('scaffold uat --phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + + const content = fs.readFileSync( + path.join(tmpDir, '.planning', 'phases', '03-api', '03-UAT.md'), + 'utf-8' + ); + assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading'); + assert.ok(content.includes('Test Results'), 'should have test results section'); + }); + + test('scaffolds verification file', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + + const result = runGsdTools('scaffold verification --phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + + const content = fs.readFileSync( + path.join(tmpDir, '.planning', 'phases', '03-api', '03-VERIFICATION.md'), + 'utf-8' + ); + assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading'); + }); + + test('scaffolds phase directory', () => { + const result = runGsdTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '05-user-dashboard')), + 'directory should be created' + ); + }); + + test('does not overwrite existing files', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content'); + + const result = runGsdTools('scaffold context --phase 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, false, 'should not overwrite'); + assert.strictEqual(output.reason, 'already_exists'); + }); +}); diff --git a/get-shit-done/workflows/add-phase.md b/get-shit-done/workflows/add-phase.md index 39596ccc4..6d04e76d0 100644 --- a/get-shit-done/workflows/add-phase.md +++ b/get-shit-done/workflows/add-phase.md @@ -38,96 +38,30 @@ ERROR: No roadmap found (.planning/ROADMAP.md) Run /gsd:new-project to initialize. ``` Exit. - -Read roadmap content for parsing. - -Parse the roadmap to find the current milestone section: - -1. Locate the "## Current Milestone:" heading -2. Extract milestone name and version -3. Identify all phases under this milestone (before next "---" separator or next milestone heading) -4. Parse existing phase numbers (including decimals if present) - -Example structure: - -``` -## Current Milestone: v1.0 Foundation - -### Phase 4: Focused Command System -### Phase 5: Path Routing & Validation -### Phase 6: Documentation & Distribution -``` - - - -Find the highest integer phase number in the current milestone: - -1. Extract all phase numbers from phase headings (### Phase N:) -2. Filter to integer phases only (ignore decimals like 4.1, 4.2) -3. Find the maximum integer value -4. Add 1 to get the next phase number - -Example: If phases are 4, 5, 5.1, 6 → next is 7 - -Format as two-digit: `printf "%02d" $next_phase` - - - -Convert the phase description to a kebab-case slug. - -Use `init phase-op` which provides `phase_slug` computed from phase name, or call: -```bash -slug=$(node ~/.claude/get-shit-done/bin/gsd-tools.js generate-slug "$description" --raw) -``` - -Phase directory name: `{two-digit-phase}-{slug}` -Example: `07-add-authentication` - - - -Create the phase directory structure: + +**Delegate the phase addition to gsd-tools:** ```bash -phase_dir=".planning/phases/${phase_num}-${slug}" -mkdir -p "$phase_dir" +RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase add "${description}") ``` -Confirm: "Created directory: $phase_dir" - +The CLI handles: +- Finding the highest existing integer phase number +- Calculating next phase number (max + 1) +- Generating slug from description +- Creating the phase directory (`.planning/phases/{NN}-{slug}/`) +- Inserting the phase entry into ROADMAP.md with Goal, Depends on, and Plans sections - -Add the new phase entry to the roadmap: - -1. Find the insertion point (after last phase in current milestone, before "---" separator) -2. Insert new phase heading: - - ``` - ### Phase {N}: {Description} - - **Goal:** [To be planned] - **Depends on:** Phase {N-1} - **Plans:** 0 plans - - Plans: - - [ ] TBD (run /gsd:plan-phase {N} to break down) - - **Details:** - [To be added during planning] - ``` - -3. Write updated roadmap back to file - -Preserve all other content exactly (formatting, spacing, other phases). +Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory`. Update STATE.md to reflect the new phase: 1. Read `.planning/STATE.md` -2. Under "## Current Position" → "**Next Phase:**" add reference to new phase -3. Under "## Accumulated Context" → "### Roadmap Evolution" add entry: +2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry: ``` - Phase {N} added: {description} ``` @@ -145,7 +79,6 @@ Phase {N} added to current milestone: - Status: Not planned yet Roadmap updated: .planning/ROADMAP.md -Project state updated: .planning/STATE.md --- @@ -170,10 +103,9 @@ Project state updated: .planning/STATE.md -- [ ] Phase directory created: `.planning/phases/{NN}-{slug}/` +- [ ] `gsd-tools phase add` executed successfully +- [ ] Phase directory created - [ ] Roadmap updated with new phase entry - [ ] STATE.md updated with roadmap evolution note -- [ ] New phase appears at end of current milestone -- [ ] Next phase number calculated correctly (ignoring decimals) - [ ] User informed of next steps diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md index e7f4c47ed..fe0092c7e 100644 --- a/get-shit-done/workflows/complete-milestone.md +++ b/get-shit-done/workflows/complete-milestone.md @@ -37,18 +37,16 @@ When a milestone completes: -Check milestone completion: +**Use `roadmap analyze` for comprehensive readiness check:** ```bash -cat .planning/ROADMAP.md -ls .planning/phases/*/SUMMARY.md 2>/dev/null | wc -l +ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze) ``` -Verify: +This returns all phases with plan/summary counts and disk status. Use this to verify: - Which phases belong to this milestone? -- All phases complete (all plans have summaries)? -- Work tested/validated? -- Ready to ship/tag? +- All phases complete (all plans have summaries)? Check `disk_status === 'complete'` for each. +- `progress_percent` should be 100%. Present: @@ -61,7 +59,7 @@ Includes: - Phase 3: Core Features (3/3 plans complete) - Phase 4: Polish (1/1 plan complete) -Total: 4 phases, 8 plans, all complete +Total: {phase_count} phases, {total_plans} plans, all complete ``` @@ -152,36 +150,9 @@ Key accomplishments for this milestone: -Create or update `.planning/MILESTONES.md`. +**Note:** MILESTONES.md entry is now created automatically by `gsd-tools milestone complete` in the archive_milestone step. The entry includes version, date, phase/plan/task counts, and accomplishments extracted from SUMMARY.md files. -If file doesn't exist, create with project name header. If exists, prepend new entry (reverse chronological). - -Use template from `templates/milestone.md`: - -```markdown -## v[Version] [Name] (Shipped: YYYY-MM-DD) - -**Delivered:** [One sentence from user] - -**Phases completed:** [X-Y] ([Z] plans total) - -**Key accomplishments:** - -- [List from previous step] - -**Stats:** - -- [Files] files created/modified -- [LOC] lines of [language] -- [Phases] phases, [Plans] plans, [Tasks] tasks -- [Days] days from [start] to ship - -**Git range:** `feat(XX-XX)` → `feat(YY-YY)` - -**What's next:** [Ask user: what's the next goal?] - ---- -``` +If additional details are needed (e.g., user-provided "Delivered" summary, git range, LOC stats), add them manually after the CLI creates the base entry. @@ -370,103 +341,71 @@ Update `.planning/ROADMAP.md` — group completed milestone phases: -Extract completed milestone details to archive. +**Delegate archival to gsd-tools:** -1. Create `.planning/milestones/v[X.Y]-ROADMAP.md` -2. Read `~/.claude/get-shit-done/templates/milestone-archive.md` template -3. Extract from ROADMAP.md: all phases in milestone (by number range), full phase details, plan lists with checkmarks -4. Extract from PROJECT.md: key decisions, validated requirements -5. Fill template placeholders: {{VERSION}}, {{MILESTONE_NAME}}, {{DATE}}, {{PHASE_START}}, {{PHASE_END}}, {{TOTAL_PLANS}}, {{MILESTONE_DESCRIPTION}}, {{PHASES_SECTION}}, {{DECISIONS_FROM_PROJECT}}, {{ISSUES_RESOLVED_DURING_MILESTONE}} -6. Write to `.planning/milestones/v[X.Y]-ROADMAP.md` -7. Delete ROADMAP.md: - ```bash - rm .planning/ROADMAP.md - ``` -8. Verify: - ```bash - ls .planning/milestones/v[X.Y]-ROADMAP.md - ``` -9. Confirm: - ``` - ✅ v[X.Y] roadmap archived to milestones/v[X.Y]-ROADMAP.md - ✅ ROADMAP.md deleted (fresh one for next milestone) - ``` +```bash +ARCHIVE=$(node ~/.claude/get-shit-done/bin/gsd-tools.js milestone complete "v[X.Y]" --name "[Milestone Name]") +``` + +The CLI handles: +- Creating `.planning/milestones/` directory +- Archiving ROADMAP.md to `milestones/v[X.Y]-ROADMAP.md` +- Archiving REQUIREMENTS.md to `milestones/v[X.Y]-REQUIREMENTS.md` with archive header +- Moving audit file to milestones if it exists +- Creating/appending MILESTONES.md entry with accomplishments from SUMMARY.md files +- Updating STATE.md (status, last activity) + +Extract from result: `version`, `date`, `phases`, `plans`, `tasks`, `accomplishments`, `archived`. + +Verify: `✅ Milestone archived to .planning/milestones/` **Note:** Phase directories (`.planning/phases/`) are NOT deleted — they accumulate across milestones as raw execution history. Phase numbering continues (v1.0 phases 1-4, v1.1 phases 5-8, etc.). - - - - -Archive requirements for next milestone. - -1. Read REQUIREMENTS.md: - ```bash - cat .planning/REQUIREMENTS.md - ``` - -2. Create `.planning/milestones/v[X.Y]-REQUIREMENTS.md` - -3. Transform: mark all requirements `[x]` complete, add outcome notes, update traceability table to "Complete", add milestone summary (total shipped, scope changes, drops) - -4. Write archive with header: - ```markdown - # Requirements Archive: v[X.Y] [Milestone Name] - - **Archived:** [DATE] - **Status:** ✅ SHIPPED - - For current requirements, see `.planning/REQUIREMENTS.md`. - - --- - - [Full REQUIREMENTS.md content with checkboxes marked complete] - - --- - - ## Milestone Summary - - **Shipped:** [X] of [Y] requirements - **Adjusted:** [list any that changed] - **Dropped:** [list any removed and why] - - --- - *Archived: [DATE] as part of v[X.Y] milestone completion* - ``` - -5. Delete original: - ```bash - rm .planning/REQUIREMENTS.md - ``` - -6. Confirm: - ``` - ✅ Requirements archived to milestones/v[X.Y]-REQUIREMENTS.md - ✅ REQUIREMENTS.md deleted (fresh one for next milestone) - ``` - -**Note:** Next milestone starts with `/gsd:new-milestone` which includes requirements definition. PROJECT.md's Validated section carries cumulative record across milestones. +After archival, the AI still handles: +- Reorganizing ROADMAP.md with milestone grouping (requires judgment) +- Full PROJECT.md evolution review (requires understanding) +- Deleting original ROADMAP.md and REQUIREMENTS.md +- These are NOT fully delegated because they require AI interpretation of content - + -Move audit file to archive (if exists). Use init context or check directly: +After `milestone complete` has archived, reorganize ROADMAP.md with milestone groupings, then delete originals: -```bash -# Check if audit file exists -[ -f ".planning/v[X.Y]-MILESTONE-AUDIT.md" ] && mv .planning/v[X.Y]-MILESTONE-AUDIT.md .planning/milestones/ +**Reorganize ROADMAP.md** — group completed milestone phases: + +```markdown +# Roadmap: [Project Name] + +## Milestones + +- ✅ **v1.0 MVP** — Phases 1-4 (shipped YYYY-MM-DD) +- 🚧 **v1.1 Security** — Phases 5-6 (in progress) + +## Phases + +
+✅ v1.0 MVP (Phases 1-4) — SHIPPED YYYY-MM-DD + +- [x] Phase 1: Foundation (2/2 plans) — completed YYYY-MM-DD +- [x] Phase 2: Authentication (2/2 plans) — completed YYYY-MM-DD + +
``` -Confirm: `✅ Audit archived to milestones/v[X.Y]-MILESTONE-AUDIT.md` +**Then delete originals:** -Skip silently if no audit file — audit is optional. +```bash +rm .planning/ROADMAP.md +rm .planning/REQUIREMENTS.md +```
-Update STATE.md for milestone completion. +Most STATE.md updates were handled by `milestone complete`, but verify and update remaining fields: **Project Reference:** @@ -479,17 +418,6 @@ See: .planning/PROJECT.md (updated [today]) **Current focus:** [Next milestone or "Planning next milestone"] ``` -**Current Position:** - -```markdown -Phase: [Next phase] of [Total] ([Phase name]) -Plan: Not started -Status: Ready to plan -Last activity: [today] — v[X.Y] milestone complete - -Progress: [updated progress bar] -``` - **Accumulated Context:** - Clear decisions summary (full log in PROJECT.md) - Clear resolved blockers diff --git a/get-shit-done/workflows/insert-phase.md b/get-shit-done/workflows/insert-phase.md index 347d6d51b..51964a021 100644 --- a/get-shit-done/workflows/insert-phase.md +++ b/get-shit-done/workflows/insert-phase.md @@ -17,31 +17,17 @@ Example: `/gsd:insert-phase 72 Fix critical auth bug` -> after = 72 -> description = "Fix critical auth bug" -Validation: +If arguments missing: -```bash -if [ $# -lt 2 ]; then - echo "ERROR: Both phase number and description required" - echo "Usage: /gsd:insert-phase " - echo "Example: /gsd:insert-phase 72 Fix critical auth bug" - exit 1 -fi +``` +ERROR: Both phase number and description required +Usage: /gsd:insert-phase +Example: /gsd:insert-phase 72 Fix critical auth bug ``` -Parse first argument as integer: - -```bash -after_phase=$1 -shift -description="$*" - -# Validate after_phase is an integer -if ! [[ "$after_phase" =~ ^[0-9]+$ ]]; then - echo "ERROR: Phase number must be an integer" - exit 1 -fi -``` +Exit. +Validate first argument is an integer. @@ -56,111 +42,35 @@ Check `roadmap_exists` from init JSON. If false: ERROR: No roadmap found (.planning/ROADMAP.md) ``` Exit. - -Read roadmap content for parsing. - -Verify that the target phase exists in the roadmap: - -1. Search for "### Phase {after_phase}:" heading -2. If not found: - - ``` - ERROR: Phase {after_phase} not found in roadmap - Available phases: [list phase numbers] - ``` - - Exit. - -3. Verify phase is in current milestone (not completed/archived) - - - -Calculate next decimal phase number: + +**Delegate the phase insertion to gsd-tools:** ```bash -DECIMAL_INFO=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase next-decimal "${after_phase}") +RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase insert "${after_phase}" "${description}") ``` -Extract from JSON: -- `next`: The next available decimal (e.g., "06.1", "06.3") -- `existing`: Array of existing decimals (e.g., ["06.1", "06.2"]) -- `base_phase`: Normalized base phase (e.g., "06") +The CLI handles: +- Verifying target phase exists in ROADMAP.md +- Calculating next decimal phase number (checking existing decimals on disk) +- Generating slug from description +- Creating the phase directory (`.planning/phases/{N.M}-{slug}/`) +- Inserting the phase entry into ROADMAP.md after the target phase with (INSERTED) marker -Store the result: -```bash -decimal_phase=$(echo "$DECIMAL_INFO" | jq -r '.next') -``` - -Examples: -- Phase 72 with no decimals -> next is 72.1 -- Phase 72 with 72.1 -> next is 72.2 -- Phase 72 with 72.1, 72.2 -> next is 72.3 - - - -Convert the phase description to a kebab-case slug. - -Use `generate-slug` command (init phase-op provides `phase_slug` for existing phase, but this is a new phase): -```bash -slug=$(node ~/.claude/get-shit-done/bin/gsd-tools.js generate-slug "$description" --raw) -``` - -Phase directory name: `{decimal-phase}-{slug}` -Example: `06.1-fix-critical-auth-bug` (phase 6 insertion) - - - -Create the phase directory structure: - -```bash -phase_dir=".planning/phases/${decimal_phase}-${slug}" -mkdir -p "$phase_dir" -``` - -Confirm: "Created directory: $phase_dir" - - - -Insert the new phase entry into the roadmap: - -1. Find insertion point: immediately after Phase {after_phase}'s content (before next phase heading or "---") -2. Insert new phase heading with (INSERTED) marker: - - ``` - ### Phase {decimal_phase}: {Description} (INSERTED) - - **Goal:** [Urgent work - to be planned] - **Depends on:** Phase {after_phase} - **Plans:** 0 plans - - Plans: - - [ ] TBD (run /gsd:plan-phase {decimal_phase} to break down) - - **Details:** - [To be added during planning] - ``` - -3. Write updated roadmap back to file - -The "(INSERTED)" marker helps identify decimal phases as urgent insertions. - -Preserve all other content exactly (formatting, spacing, other phases). +Extract from result: `phase_number`, `after_phase`, `name`, `slug`, `directory`. Update STATE.md to reflect the inserted phase: 1. Read `.planning/STATE.md` -2. Under "## Accumulated Context" -> "### Roadmap Evolution" add entry: +2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry: ``` - Phase {decimal_phase} inserted after Phase {after_phase}: {description} (URGENT) ``` If "Roadmap Evolution" section doesn't exist, create it. - -Add note about insertion reason if appropriate. @@ -173,7 +83,7 @@ Phase {decimal_phase} inserted after Phase {after_phase}: - Status: Not planned yet - Marker: (INSERTED) - indicates urgent work -Roadmap updated: {roadmap-path} +Roadmap updated: .planning/ROADMAP.md Project state updated: .planning/STATE.md --- @@ -211,10 +121,9 @@ Project state updated: .planning/STATE.md Phase insertion is complete when: -- [ ] Phase directory created: `.planning/phases/{N.M}-{slug}/` +- [ ] `gsd-tools phase insert` executed successfully +- [ ] Phase directory created - [ ] Roadmap updated with new phase entry (includes "(INSERTED)" marker) -- [ ] Phase inserted in correct position (after target phase, before next integer phase) - [ ] STATE.md updated with roadmap evolution note -- [ ] Decimal number calculated correctly (based on existing decimals) - [ ] User informed of next steps and dependency implications diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index e6d74ceb0..987a3e243 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -50,42 +50,68 @@ All file contents are already loaded via `--include` in init_context step: No additional file reads needed. + +**Get comprehensive roadmap analysis (replaces manual parsing):** + +```bash +ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze) +``` + +This returns structured JSON with: +- All phases with disk status (complete/partial/planned/empty/no_directory) +- Goal and dependencies per phase +- Plan and summary counts per phase +- Aggregated stats: total plans, summaries, progress percent +- Current and next phase identification + +Use this instead of manually reading/parsing ROADMAP.md. + + **Gather recent work context:** - Find the 2-3 most recent SUMMARY.md files -- Extract from each: what was accomplished, key decisions, any issues logged +- Use `summary-extract` for efficient parsing: + ```bash + node ~/.claude/get-shit-done/bin/gsd-tools.js summary-extract --fields one_liner + ``` - This shows "what we've been working on" -**Parse current position from init context:** +**Parse current position from init context and roadmap analysis:** -- Use `current_phase` and `next_phase` from init for position -- Use `phases` array for plan counts per phase -- Note `paused_at` if work was paused -- Check for CONTEXT.md: For phases without PLAN.md files, check if `{phase}-CONTEXT.md` exists in phase directory +- Use `current_phase` and `next_phase` from roadmap analyze +- Use phase-level `has_context` and `has_research` flags from analyze +- Note `paused_at` if work was paused (from init context) - Count pending todos: use `init todos` or `list-todos` - Check for active debug sessions: `ls .planning/debug/*.md 2>/dev/null | grep -v resolved | wc -l` -**Present rich status report:** +**Generate progress bar from gsd-tools, then present rich status report:** + +```bash +# Get formatted progress bar +PROGRESS_BAR=$(node ~/.claude/get-shit-done/bin/gsd-tools.js progress bar --raw) +``` + +Present: ``` # [Project Name] -**Progress:** [████████░░] 8/10 plans complete +**Progress:** {PROGRESS_BAR} **Profile:** [quality/balanced/budget] ## Recent Work -- [Phase X, Plan Y]: [what was accomplished - 1 line] -- [Phase X, Plan Z]: [what was accomplished - 1 line] +- [Phase X, Plan Y]: [what was accomplished - 1 line from summary-extract] +- [Phase X, Plan Z]: [what was accomplished - 1 line from summary-extract] ## Current Position Phase [N] of [total]: [phase-name] Plan [M] of [phase-total]: [status] -CONTEXT: [✓ if CONTEXT.md exists | - if not] +CONTEXT: [✓ if has_context | - if not] ## Key Decisions Made - [decision 1 from STATE.md] @@ -102,7 +128,7 @@ CONTEXT: [✓ if CONTEXT.md exists | - if not] (Only show this section if count > 0) ## What's Next -[Next phase/plan objective from ROADMAP] +[Next phase/plan objective from roadmap analyze] ``` diff --git a/get-shit-done/workflows/remove-phase.md b/get-shit-done/workflows/remove-phase.md index 0cae3f0cd..1fab5231a 100644 --- a/get-shit-done/workflows/remove-phase.md +++ b/get-shit-done/workflows/remove-phase.md @@ -37,20 +37,6 @@ Extract: `phase_found`, `phase_dir`, `phase_number`, `commit_docs`, `roadmap_exi Also read STATE.md and ROADMAP.md content for parsing current position. - -Verify the target phase exists in ROADMAP.md: - -1. Search for `### Phase {target}:` heading -2. If not found: - - ``` - ERROR: Phase {target} not found in roadmap - Available phases: [list phase numbers] - ``` - - Exit. - - Verify the phase is a future phase (not started): @@ -70,46 +56,6 @@ To abandon current work, use /gsd:pause-work instead. ``` Exit. - -3. Check for SUMMARY.md files in phase directory: - -```bash -ls .planning/phases/{target}-*/*-SUMMARY.md 2>/dev/null -``` - -If any SUMMARY.md files exist: - -``` -ERROR: Phase {target} has completed work - -Found executed plans: -- {list of SUMMARY.md files} - -Cannot remove phases with completed work. -``` - -Exit. - - - -Collect information about the phase being removed: - -1. Extract phase name from ROADMAP.md heading: `### Phase {target}: {Name}` -2. Find phase directory: `.planning/phases/{target}-{slug}/` -3. Find all subsequent phases (integer and decimal) that need renumbering - -**Subsequent phase detection:** - -For integer phase removal (e.g., 17): -- Find all phases > 17 (integers: 18, 19, 20...) -- Find all decimal phases >= 17.0 and < 18.0 (17.1, 17.2...) → these become 16.x -- Find all decimal phases for subsequent integers (18.1, 19.1...) → renumber with their parent - -For decimal phase removal (e.g., 17.1): -- Find all decimal phases > 17.1 and < 18 (17.2, 17.3...) → renumber down -- Integer phases unchanged - -List all phases that will be renumbered. @@ -120,11 +66,8 @@ Removing Phase {target}: {Name} This will: - Delete: .planning/phases/{target}-{slug}/ -- Renumber {N} subsequent phases: - - Phase 18 → Phase 17 - - Phase 18.1 → Phase 17.1 - - Phase 19 → Phase 18 - [etc.] +- Renumber all subsequent phases +- Update: ROADMAP.md, STATE.md Proceed? (y/n) ``` @@ -132,110 +75,27 @@ Proceed? (y/n) Wait for confirmation. - -Use the init context from earlier. The `phase_dir` from `init phase-op` provides the path. - -Delete the target phase directory if it exists: + +**Delegate the entire removal operation to gsd-tools:** ```bash -# phase_dir from init phase-op contains the path if found -if [ -n "$phase_dir" ]; then - rm -rf "$phase_dir" - echo "Deleted: $phase_dir/" -fi +RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase remove "${target}") ``` -If directory doesn't exist (`phase_found=false` from init), note: "No directory to delete (phase not yet created)" - - - -Rename all subsequent phase directories: - -For each phase directory that needs renumbering (in reverse order to avoid conflicts): +If the phase has executed plans (SUMMARY.md files), gsd-tools will error. Use `--force` only if the user confirms: ```bash -# Example: renaming 18-dashboard to 17-dashboard -mv ".planning/phases/18-dashboard" ".planning/phases/17-dashboard" +RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase remove "${target}" --force) ``` -Process in descending order (20→19, then 19→18, then 18→17) to avoid overwriting. +The CLI handles: +- Deleting the phase directory +- Renumbering all subsequent directories (in reverse order to avoid conflicts) +- Renaming all files inside renumbered directories (PLAN.md, SUMMARY.md, etc.) +- Updating ROADMAP.md (removing section, renumbering all phase references, updating dependencies) +- Updating STATE.md (decrementing phase count) -Also rename decimal phase directories: -- `17.1-fix-bug` → `16.1-fix-bug` (if removing integer 17) -- `17.2-hotfix` → `17.1-hotfix` (if removing decimal 17.1) - - - -Rename plan files inside renumbered directories: - -For each renumbered directory, rename files that contain the phase number: - -```bash -# Inside 17-dashboard (was 18-dashboard): -mv "18-01-PLAN.md" "17-01-PLAN.md" -mv "18-02-PLAN.md" "17-02-PLAN.md" -mv "18-01-SUMMARY.md" "17-01-SUMMARY.md" # if exists -# etc. -``` - -Also handle CONTEXT.md and DISCOVERY.md (these don't have phase prefixes, so no rename needed). - - - -Update ROADMAP.md: - -1. **Remove the phase section entirely:** - - Delete from `### Phase {target}:` to the next phase heading (or section end) - -2. **Remove from phase list:** - - Delete line `- [ ] **Phase {target}: {Name}**` or similar - -3. **Remove from Progress table:** - - Delete the row for Phase {target} - -4. **Renumber all subsequent phases:** - - `### Phase 18:` → `### Phase 17:` - - `- [ ] **Phase 18:` → `- [ ] **Phase 17:` - - Table rows: `| 18. Dashboard |` → `| 17. Dashboard |` - - Plan references: `18-01:` → `17-01:` - -5. **Update dependency references:** - - `**Depends on:** Phase 18` → `**Depends on:** Phase 17` - - For the phase that depended on the removed phase: - - `**Depends on:** Phase 17` (removed) → `**Depends on:** Phase 16` - -6. **Renumber decimal phases:** - - `### Phase 17.1:` → `### Phase 16.1:` (if integer 17 removed) - - Update all references consistently - -Write updated ROADMAP.md. - - - -Update STATE.md: - -1. **Update total phase count:** - - `Phase: 16 of 20` → `Phase: 16 of 19` - -2. **Recalculate progress percentage:** - - New percentage based on completed plans / new total plans - -Do NOT add a "Roadmap Evolution" note - the git commit is the record. - -Write updated STATE.md. - - - -Search for and update phase references inside plan files: - -```bash -# Find files that reference the old phase numbers -grep -r "Phase 18" .planning/phases/17-*/ 2>/dev/null -grep -r "Phase 19" .planning/phases/18-*/ 2>/dev/null -# etc. -``` - -Update any internal references to reflect new numbering. +Extract from result: `removed`, `directory_deleted`, `renamed_directories`, `renamed_files`, `roadmap_updated`, `state_updated`. @@ -256,13 +116,10 @@ Phase {target} ({original-name}) removed. Changes: - Deleted: .planning/phases/{target}-{slug}/ -- Renumbered: Phases {first-renumbered}-{last-old} → {first-renumbered-1}-{last-new} +- Renumbered: {N} directories and {M} files - Updated: ROADMAP.md, STATE.md - Committed: chore: remove phase {target} ({original-name}) -Current roadmap: {total-remaining} phases -Current position: Phase {current} of {new-total} - --- ## What's Next @@ -280,47 +137,18 @@ Would you like to: -- Don't remove completed phases (have SUMMARY.md files) +- Don't remove completed phases (have SUMMARY.md files) without --force - Don't remove current or past phases -- Don't leave gaps in numbering - always renumber -- Don't add "removed phase" notes to STATE.md - git commit is the record -- Don't ask about each decimal phase - just renumber them +- Don't manually renumber — use `gsd-tools phase remove` which handles all renumbering +- Don't add "removed phase" notes to STATE.md — git commit is the record - Don't modify completed phase directories - - -**Removing a decimal phase (e.g., 17.1):** -- Only affects other decimals in same series (17.2 → 17.1, 17.3 → 17.2) -- Integer phases unchanged -- Simpler operation - -**No subsequent phases to renumber:** -- Removing the last phase (e.g., Phase 20 when that's the end) -- Just delete and update ROADMAP.md, no renumbering needed - -**Phase directory doesn't exist:** -- Phase may be in ROADMAP.md but directory not created yet -- Skip directory deletion, proceed with ROADMAP.md updates - -**Decimal phases under removed integer:** -- Removing Phase 17 when 17.1, 17.2 exist -- 17.1 → 16.1, 17.2 → 16.2 -- They maintain their position in execution order (after current last integer) - - - Phase removal is complete when: - [ ] Target phase validated as future/unstarted -- [ ] Phase directory deleted (if existed) -- [ ] All subsequent phase directories renumbered -- [ ] Files inside directories renamed ({old}-01-PLAN.md → {new}-01-PLAN.md) -- [ ] ROADMAP.md updated (section removed, all references renumbered) -- [ ] STATE.md updated (phase count, progress percentage) -- [ ] Dependency references updated in subsequent phases +- [ ] `gsd-tools phase remove` executed successfully - [ ] Changes committed with descriptive message -- [ ] No gaps in phase numbering - [ ] User informed of changes diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 383a34c2a..d01f53c6c 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -118,39 +118,22 @@ If found, delete them — phase is complete, handoffs are stale. - + -Update the roadmap file: +**Delegate ROADMAP.md and STATE.md updates to gsd-tools:** ```bash -ROADMAP_FILE=".planning/ROADMAP.md" +TRANSITION=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase complete "${current_phase}") ``` -Update the file: +The CLI handles: +- Marking the phase checkbox as `[x]` complete with today's date +- Updating plan count to final (e.g., "3/3 plans complete") +- Updating the Progress table (Status → Complete, adding date) +- Advancing STATE.md to next phase (Current Phase, Status → Ready to plan, Current Plan → Not started) +- Detecting if this is the last phase in the milestone -- Mark current phase: `[x] Complete` -- Add completion date -- Update plan count to final (e.g., "3/3 plans complete") -- Update Progress table -- Keep next phase as `[ ] Not started` - -**Example:** - -```markdown -## Phases - -- [x] Phase 1: Foundation (completed 2025-01-15) -- [ ] Phase 2: Authentication ← Next -- [ ] Phase 3: Core Features - -## Progress - -| Phase | Plans Complete | Status | Completed | -| ----------------- | -------------- | ----------- | ---------- | -| 1. Foundation | 3/3 | Complete | 2025-01-15 | -| 2. Authentication | 0/2 | Not started | - | -| 3. Core Features | 0/1 | Not started | - | -``` +Extract from result: `completed_phase`, `plans_executed`, `next_phase`, `next_phase_name`, `is_last_phase`. @@ -250,61 +233,21 @@ After (Phase 2 shipped JWT auth, discovered rate limiting needed): -Update Current Position section in STATE.md to reflect phase completion and transition. +**Note:** Basic position updates (Current Phase, Status, Current Plan, Last Activity) were already handled by `gsd-tools phase complete` in the update_roadmap_and_state step. -**Format:** +Verify the updates are correct by reading STATE.md. If the progress bar needs updating, use: -```markdown -Phase: [next] of [total] ([Next phase name]) -Plan: Not started -Status: Ready to plan -Last activity: [today] — Phase [X] complete, transitioned to Phase [X+1] - -Progress: [updated progress bar] +```bash +PROGRESS=$(node ~/.claude/get-shit-done/bin/gsd-tools.js progress bar --raw) ``` -**Instructions:** - -- Increment phase number to next phase -- Reset plan to "Not started" -- Set status to "Ready to plan" -- Update last activity to describe transition -- Recalculate progress bar based on completed plans - -**Example — transitioning from Phase 2 to Phase 3:** - -Before: - -```markdown -## Current Position - -Phase: 2 of 4 (Authentication) -Plan: 2 of 2 in current phase -Status: Phase complete -Last activity: 2025-01-20 — Completed 02-02-PLAN.md - -Progress: ███████░░░ 60% -``` - -After: - -```markdown -## Current Position - -Phase: 3 of 4 (Core Features) -Plan: Not started -Status: Ready to plan -Last activity: 2025-01-20 — Phase 2 complete, transitioned to Phase 3 - -Progress: ███████░░░ 60% -``` +Update the progress bar line in STATE.md with the result. **Step complete when:** -- [ ] Phase number incremented to next phase -- [ ] Plan status reset to "Not started" -- [ ] Status shows "Ready to plan" -- [ ] Last activity describes the transition +- [ ] Phase number incremented to next phase (done by phase complete) +- [ ] Plan status reset to "Not started" (done by phase complete) +- [ ] Status shows "Ready to plan" (done by phase complete) - [ ] Progress bar reflects total completed plans @@ -394,26 +337,20 @@ Resume file: None **MANDATORY: Verify milestone status before presenting next steps.** -**Step 1: Read ROADMAP.md and identify phases in current milestone** +**Use the transition result from `gsd-tools phase complete`:** -Read the ROADMAP.md file and extract: -1. Current phase number (the phase just transitioned from) -2. All phase numbers in the current milestone section +The `is_last_phase` field from the phase complete result tells you directly: +- `is_last_phase: false` → More phases remain → Go to **Route A** +- `is_last_phase: true` → Milestone complete → Go to **Route B** -To find phases, look for: -- Phase headers: lines starting with `### Phase` or `#### Phase` -- Phase list items: lines like `- [ ] **Phase X:` or `- [x] **Phase X:` +The `next_phase` and `next_phase_name` fields give you the next phase details. -Count total phases and identify the highest phase number in the milestone. +If you need additional context, use: +```bash +ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze) +``` -State: "Current phase is {X}. Milestone has {N} phases (highest: {Y})." - -**Step 2: Route based on milestone status** - -| Condition | Meaning | Action | -|-----------|---------|--------| -| current phase < highest phase | More phases remain | Go to **Route A** | -| current phase = highest phase | Milestone complete | Go to **Route B** | +This returns all phases with goals, disk status, and completion info. ---