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
+
+