From cb7d4dbc3c4b3e9b67a610eb9c47362e14e65c70 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sun, 15 Feb 2026 16:25:01 -0600 Subject: [PATCH] feat(health): add /gsd:health command for planning directory validation Validates .planning/ integrity and reports actionable issues: - Missing/malformed files (PROJECT.md, ROADMAP.md, STATE.md, config.json) - Phase directory naming, orphaned plans, roadmap/disk consistency - Auto-repair for config.json and STATE.md with --repair flag Closes #338 Co-Authored-By: Claude Opus 4.5 --- commands/gsd/health.md | 22 +++ get-shit-done/bin/gsd-tools.cjs | 247 +++++++++++++++++++++++++++++- get-shit-done/workflows/health.md | 156 +++++++++++++++++++ 3 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 commands/gsd/health.md create mode 100644 get-shit-done/workflows/health.md diff --git a/commands/gsd/health.md b/commands/gsd/health.md new file mode 100644 index 000000000..260c83334 --- /dev/null +++ b/commands/gsd/health.md @@ -0,0 +1,22 @@ +--- +name: gsd:health +description: Diagnose planning directory health and optionally repair issues +argument-hint: [--repair] +allowed-tools: + - Read + - Bash + - Write + - AskUserQuestion +--- + +Validate `.planning/` directory integrity and report actionable issues. Checks for missing files, invalid configurations, inconsistent state, and orphaned plans. + + + +@~/.claude/get-shit-done/workflows/health.md + + + +Execute the health workflow from @~/.claude/get-shit-done/workflows/health.md end-to-end. +Parse --repair flag from arguments and pass to workflow. + diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 7cba6bd96..a4cabaaf0 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -48,6 +48,7 @@ * * Validation: * validate consistency Check phase numbering, disk/roadmap sync + * validate health [--repair] Check .planning/ integrity, optionally repair * * Progress: * progress [json|table|bar] Render progress in various formats @@ -3524,6 +3525,247 @@ function cmdValidateConsistency(cwd, raw) { output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed'); } +// ─── Validate Health ────────────────────────────────────────────────────────── + +function cmdValidateHealth(cwd, options, raw) { + const planningDir = path.join(cwd, '.planning'); + const projectPath = path.join(planningDir, 'PROJECT.md'); + const roadmapPath = path.join(planningDir, 'ROADMAP.md'); + const statePath = path.join(planningDir, 'STATE.md'); + const configPath = path.join(planningDir, 'config.json'); + const phasesDir = path.join(planningDir, 'phases'); + + const errors = []; + const warnings = []; + const info = []; + const repairs = []; + + // Helper to add issue + const addIssue = (severity, code, message, fix, repairable = false) => { + const issue = { code, message, fix, repairable }; + if (severity === 'error') errors.push(issue); + else if (severity === 'warning') warnings.push(issue); + else info.push(issue); + }; + + // ─── Check 1: .planning/ exists ─────────────────────────────────────────── + if (!fs.existsSync(planningDir)) { + addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd:new-project to initialize'); + output({ + status: 'broken', + errors, + warnings, + info, + repairable_count: 0, + }, raw); + return; + } + + // ─── Check 2: PROJECT.md exists and has required sections ───────────────── + if (!fs.existsSync(projectPath)) { + addIssue('error', 'E002', 'PROJECT.md not found', 'Run /gsd:new-project to create'); + } else { + const content = fs.readFileSync(projectPath, 'utf-8'); + const requiredSections = ['## What This Is', '## Core Value', '## Requirements']; + for (const section of requiredSections) { + if (!content.includes(section)) { + addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually'); + } + } + } + + // ─── Check 3: ROADMAP.md exists ─────────────────────────────────────────── + if (!fs.existsSync(roadmapPath)) { + addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /gsd:new-milestone to create roadmap'); + } + + // ─── Check 4: STATE.md exists and references valid phases ───────────────── + if (!fs.existsSync(statePath)) { + addIssue('error', 'E004', 'STATE.md not found', 'Run /gsd:health --repair to regenerate', true); + repairs.push('regenerateState'); + } else { + const stateContent = fs.readFileSync(statePath, 'utf-8'); + // Extract phase references from STATE.md + const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map(m => m[1]); + // Get disk phases + const diskPhases = new Set(); + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory()) { + const m = e.name.match(/^(\d+(?:\.\d+)?)/); + if (m) diskPhases.add(m[1]); + } + } + } catch {} + // Check for invalid references + for (const ref of phaseRefs) { + const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0'); + if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) { + // Only warn if phases dir has any content (not just an empty project) + if (diskPhases.size > 0) { + addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /gsd:health --repair to regenerate STATE.md', true); + if (!repairs.includes('regenerateState')) repairs.push('regenerateState'); + } + } + } + } + + // ─── Check 5: config.json valid JSON + valid schema ─────────────────────── + if (!fs.existsSync(configPath)) { + addIssue('warning', 'W003', 'config.json not found', 'Run /gsd:health --repair to create with defaults', true); + repairs.push('createConfig'); + } else { + try { + const raw = fs.readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw); + // Validate known fields + const validProfiles = ['quality', 'balanced', 'budget']; + if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) { + addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`); + } + } catch (err) { + addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, 'Run /gsd:health --repair to reset to defaults', true); + repairs.push('resetConfig'); + } + } + + // ─── Check 6: Phase directory naming (NN-name format) ───────────────────── + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)?-[\w-]+$/)) { + addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)'); + } + } + } catch {} + + // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ─────────────────────── + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + for (const e of entries) { + if (!e.isDirectory()) continue; + const phaseFiles = fs.readdirSync(path.join(phasesDir, e.name)); + const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md'); + const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); + const summaryBases = new Set(summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))); + + for (const plan of plans) { + const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', ''); + if (!summaryBases.has(planBase)) { + addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress'); + } + } + } + } catch {} + + // ─── Check 8: Run existing consistency checks ───────────────────────────── + // Inline subset of cmdValidateConsistency + if (fs.existsSync(roadmapPath)) { + const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); + const roadmapPhases = new Set(); + const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi; + let m; + while ((m = phasePattern.exec(roadmapContent)) !== null) { + roadmapPhases.add(m[1]); + } + + const diskPhases = new Set(); + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory()) { + const dm = e.name.match(/^(\d+(?:\.\d+)?)/); + if (dm) diskPhases.add(dm[1]); + } + } + } catch {} + + // Phases in ROADMAP but not on disk + for (const p of roadmapPhases) { + const padded = String(parseInt(p, 10)).padStart(2, '0'); + if (!diskPhases.has(p) && !diskPhases.has(padded)) { + addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap'); + } + } + + // 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)) { + addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory'); + } + } + } + + // ─── Perform repairs if requested ───────────────────────────────────────── + const repairActions = []; + if (options.repair && repairs.length > 0) { + for (const repair of repairs) { + try { + switch (repair) { + case 'createConfig': + case 'resetConfig': { + const defaults = { + model_profile: 'balanced', + commit_docs: true, + search_gitignored: false, + branching_strategy: 'none', + research: true, + plan_checker: true, + verifier: true, + parallelization: true, + }; + fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8'); + repairActions.push({ action: repair, success: true, path: 'config.json' }); + break; + } + case 'regenerateState': { + // Generate minimal STATE.md from ROADMAP.md structure + const milestone = getMilestoneInfo(cwd); + let stateContent = `# Session State\n\n`; + stateContent += `## Project Reference\n\n`; + stateContent += `See: .planning/PROJECT.md\n\n`; + stateContent += `## Position\n\n`; + stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`; + stateContent += `**Current phase:** (determining...)\n`; + stateContent += `**Status:** Resuming\n\n`; + stateContent += `## Session Log\n\n`; + stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /gsd:health --repair\n`; + fs.writeFileSync(statePath, stateContent, 'utf-8'); + repairActions.push({ action: repair, success: true, path: 'STATE.md' }); + break; + } + } + } catch (err) { + repairActions.push({ action: repair, success: false, error: err.message }); + } + } + } + + // ─── Determine overall status ───────────────────────────────────────────── + let status; + if (errors.length > 0) { + status = 'broken'; + } else if (warnings.length > 0) { + status = 'degraded'; + } else { + status = 'healthy'; + } + + const repairableCount = errors.filter(e => e.repairable).length + + warnings.filter(w => w.repairable).length; + + output({ + status, + errors, + warnings, + info, + repairable_count: repairableCount, + repairs_performed: repairActions.length > 0 ? repairActions : undefined, + }, raw); +} + // ─── Progress Render ────────────────────────────────────────────────────────── function cmdProgressRender(cwd, format, raw) { @@ -4838,8 +5080,11 @@ async function main() { const subcommand = args[1]; if (subcommand === 'consistency') { cmdValidateConsistency(cwd, raw); + } else if (subcommand === 'health') { + const repairFlag = args.includes('--repair'); + cmdValidateHealth(cwd, { repair: repairFlag }, raw); } else { - error('Unknown validate subcommand. Available: consistency'); + error('Unknown validate subcommand. Available: consistency, health'); } break; } diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md new file mode 100644 index 000000000..377d69673 --- /dev/null +++ b/get-shit-done/workflows/health.md @@ -0,0 +1,156 @@ + +Validate `.planning/` directory integrity and report actionable issues. Checks for missing files, invalid configurations, inconsistent state, and orphaned plans. Optionally repairs auto-fixable issues. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +**Parse arguments:** + +Check if `--repair` flag is present in the command arguments. + +``` +REPAIR_FLAG="" +if arguments contain "--repair"; then + REPAIR_FLAG="--repair" +fi +``` + + + +**Run health validation:** + +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs validate health $REPAIR_FLAG +``` + +Parse JSON output: +- `status`: "healthy" | "degraded" | "broken" +- `errors[]`: Critical issues (code, message, fix, repairable) +- `warnings[]`: Non-critical issues +- `info[]`: Informational notes +- `repairable_count`: Number of auto-fixable issues +- `repairs_performed[]`: Actions taken if --repair was used + + + +**Format and display results:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD Health Check +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Status: HEALTHY | DEGRADED | BROKEN +Errors: N | Warnings: N | Info: N +``` + +**If repairs were performed:** +``` +## Repairs Performed + +- ✓ config.json: Created with defaults +- ✓ STATE.md: Regenerated from roadmap +``` + +**If errors exist:** +``` +## Errors + +- [E001] config.json: JSON parse error at line 5 + Fix: Run /gsd:health --repair to reset to defaults + +- [E002] PROJECT.md not found + Fix: Run /gsd:new-project to create +``` + +**If warnings exist:** +``` +## Warnings + +- [W001] STATE.md references phase 5, but only phases 1-3 exist + Fix: Run /gsd:health --repair to regenerate + +- [W005] Phase directory "1-setup" doesn't follow NN-name format + Fix: Rename to match pattern (e.g., 01-setup) +``` + +**If info exists:** +``` +## Info + +- [I001] 02-implementation/02-01-PLAN.md has no SUMMARY.md + Note: May be in progress +``` + +**Footer (if repairable issues exist and --repair was NOT used):** +``` +--- +N issues can be auto-repaired. Run: /gsd:health --repair +``` + + + +**If repairable issues exist and --repair was NOT used:** + +Ask user if they want to run repairs: + +``` +Would you like to run /gsd:health --repair to fix N issues automatically? +``` + +If yes, re-run with --repair flag and display results. + + + +**If repairs were performed:** + +Re-run health check without --repair to confirm issues are resolved: + +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs validate health +``` + +Report final status. + + + + + + +| Code | Severity | Description | Repairable | +|------|----------|-------------|------------| +| E001 | error | .planning/ directory not found | No | +| E002 | error | PROJECT.md not found | No | +| E003 | error | ROADMAP.md not found | No | +| E004 | error | STATE.md not found | Yes | +| E005 | error | config.json parse error | Yes | +| W001 | warning | PROJECT.md missing required section | No | +| W002 | warning | STATE.md references invalid phase | Yes | +| W003 | warning | config.json not found | Yes | +| W004 | warning | config.json invalid field value | No | +| W005 | warning | Phase directory naming mismatch | No | +| W006 | warning | Phase in ROADMAP but no directory | No | +| W007 | warning | Phase on disk but not in ROADMAP | No | +| I001 | info | Plan without SUMMARY (may be in progress) | No | + + + + + +| Action | Effect | Risk | +|--------|--------|------| +| createConfig | Create config.json with defaults | None | +| resetConfig | Delete + recreate config.json | Loses custom settings | +| regenerateState | Create STATE.md from ROADMAP structure | Loses session history | + +**Not repairable (too risky):** +- PROJECT.md, ROADMAP.md content +- Phase directory renaming +- Orphaned plan cleanup + +