diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index c334eaa34..449e81f54 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -609,8 +609,12 @@ function cmdValidateHealth(cwd, options, raw) { 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'); + addIssue( + 'warning', + 'W002', + `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, + 'Review STATE.md manually before changing it; /gsd:health --repair will not overwrite an existing STATE.md for phase mismatches' + ); } } } diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md index 0cb95fd1b..c8c6b4eeb 100644 --- a/get-shit-done/workflows/health.md +++ b/get-shit-done/workflows/health.md @@ -72,8 +72,8 @@ Errors: N | Warnings: N | Info: N ``` ## Warnings -- [W001] STATE.md references phase 5, but only phases 1-3 exist - Fix: Run /gsd:health --repair to regenerate +- [W002] STATE.md references phase 5, but only phases 1-3 exist + Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md - [W005] Phase directory "1-setup" doesn't follow NN-name format Fix: Rename to match pattern (e.g., 01-setup) @@ -130,7 +130,7 @@ Report final status. | 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 | +| W002 | warning | STATE.md references invalid phase | No | | W003 | warning | config.json not found | Yes | | W004 | warning | config.json invalid field value | No | | W005 | warning | Phase directory naming mismatch | No | @@ -148,7 +148,7 @@ Report final status. |--------|--------|------| | 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 | +| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history | | addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default | **Not repairable (too risky):** diff --git a/tests/verify-health.test.cjs b/tests/verify-health.test.cjs index c06a8404a..f22dd3ad7 100644 --- a/tests/verify-health.test.cjs +++ b/tests/verify-health.test.cjs @@ -191,10 +191,9 @@ describe('validate health command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); - assert.ok( - output.warnings.some(w => w.code === 'W002'), - `Expected W002 in warnings: ${JSON.stringify(output.warnings)}` - ); + const w002 = output.warnings.find(w => w.code === 'W002'); + assert.ok(w002, `Expected W002 in warnings: ${JSON.stringify(output.warnings)}`); + assert.strictEqual(w002.repairable, false, 'W002 should not be auto-repairable'); }); // ─── Check 5: config.json valid JSON + valid schema ─────────────────────── @@ -613,16 +612,13 @@ describe('validate health --repair command', () => { assert.ok(stateContent.includes('# Session State'), 'regenerated STATE.md should contain "# Session State"'); }); - test('backs up existing STATE.md before regenerating', () => { + test('does not rewrite existing STATE.md for invalid phase references', () => { writeValidConfigJson(tmpDir); const statePath = path.join(tmpDir, '.planning', 'STATE.md'); - const originalContent = '# Session State\n\nOriginal content here.\n'; - fs.writeFileSync(statePath, originalContent); - - // Make STATE.md reference a nonexistent phase so repair is triggered + const originalContent = '# Session State\n\nPhase 99 is current.\n'; fs.writeFileSync( statePath, - '# Session State\n\nPhase 99 is current.\n' + originalContent ); const result = runGsdTools('validate health --repair', tmpDir); @@ -630,19 +626,17 @@ describe('validate health --repair command', () => { const output = JSON.parse(result.output); assert.ok( - Array.isArray(output.repairs_performed), - `Expected repairs_performed: ${JSON.stringify(output)}` + !Array.isArray(output.repairs_performed) || !output.repairs_performed.some(r => r.action === 'regenerateState'), + `Did not expect regenerateState for W002: ${JSON.stringify(output)}` ); - // Verify a .bak- file exists alongside STATE.md + const stateContent = fs.readFileSync(statePath, 'utf-8'); + assert.strictEqual(stateContent, originalContent, 'existing STATE.md should be preserved'); + const planningDir = path.join(tmpDir, '.planning'); const planningFiles = fs.readdirSync(planningDir); const backupFile = planningFiles.find(f => f.startsWith('STATE.md.bak-')); - assert.ok(backupFile, `Expected a STATE.md.bak- file. Found files: ${planningFiles.join(', ')}`); - - // Verify backup contains the original content - const backupContent = fs.readFileSync(path.join(planningDir, backupFile), 'utf-8'); - assert.ok(backupContent.includes('Phase 99'), 'backup should contain the original STATE.md content'); + assert.strictEqual(backupFile, undefined, `Did not expect backup file for non-destructive repair. Found: ${planningFiles.join(', ')}`); }); test('adds nyquist_validation key to config.json via addNyquistKey repair', () => { @@ -688,4 +682,18 @@ describe('validate health --repair command', () => { `Expected repairable_count >= 2, got ${output.repairable_count}. Full output: ${JSON.stringify(output)}` ); }); + + test('phase mismatch warnings do not count as repairable issues', () => { + writeValidConfigJson(tmpDir); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Session State\n\nPhase 99 is the current phase.\n' + ); + + const result = runGsdTools('validate health', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.repairable_count, 0, `Expected no repairable issues for W002: ${JSON.stringify(output)}`); + }); });