diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 425199dde..9deaa4839 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -8,6 +8,33 @@ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, com const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); +/** + * Determine phase status by checking plan/summary counts AND verification state. + * Introduces "Executed" for phases with all summaries but no passing verification. + */ +function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) { + if (plans === 0) return defaultPending; + if (summaries < plans && summaries > 0) return 'In Progress'; + if (summaries < plans) return 'Planned'; + + // summaries >= plans — check verification + try { + const files = fs.readdirSync(phaseDir); + const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md')); + if (verificationFile) { + const content = fs.readFileSync(path.join(phaseDir, verificationFile), 'utf-8'); + if (/status:\s*passed/i.test(content)) return 'Complete'; + if (/status:\s*human_needed/i.test(content)) return 'Needs Review'; + if (/status:\s*gaps_found/i.test(content)) return 'Executed'; + // Verification exists but unrecognized status — treat as executed + return 'Executed'; + } + } catch { /* directory read failed — fall through */ } + + // No verification file — executed but not verified + return 'Executed'; +} + function cmdGenerateSlug(text, raw) { if (!text) { error('text required for slug generation'); @@ -528,11 +555,7 @@ function cmdProgressRender(cwd, format, raw) { 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'; + const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Pending'); phases.push({ number: phaseNum, name: phaseName, plans, summaries, status }); } @@ -828,18 +851,14 @@ function cmdStats(cwd, format, raw) { totalPlans += plans; totalSummaries += summaries; - let status; - if (plans === 0) status = 'Not Started'; - else if (summaries >= plans) status = 'Complete'; - else if (summaries > 0) status = 'In Progress'; - else status = 'Planned'; + const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started'); const existing = phasesByNumber.get(phaseNum); phasesByNumber.set(phaseNum, { number: phaseNum, name: existing?.name || phaseName, - plans, - summaries, + plans: (existing?.plans || 0) + plans, + summaries: (existing?.summaries || 0) + summaries, status, }); } diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index d465b52e3..e5e1858ad 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -1409,11 +1409,12 @@ describe('stats command', () => { fs.mkdirSync(p1, { recursive: true }); fs.mkdirSync(p2, { recursive: true }); - // Phase 1: 2 plans, 2 summaries (complete) + // Phase 1: 2 plans, 2 summaries, passing verification (complete) fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification'); // Phase 2: 1 plan, 0 summaries (planned) fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan'); @@ -1485,8 +1486,10 @@ describe('stats command', () => { fs.mkdirSync(p2, { recursive: true }); fs.writeFileSync(path.join(p1, '14-01-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(p1, '14-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified'); fs.writeFileSync(path.join(p2, '15-01-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(p2, '15-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(p2, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified'); fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), @@ -1569,6 +1572,7 @@ describe('stats command', () => { 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(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified'); const result = runGsdTools('stats table', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); @@ -1580,4 +1584,78 @@ describe('stats command', () => { assert.ok(parsed.rendered.includes('| 1 |'), 'should include phase row'); assert.ok(parsed.rendered.includes('1/1 phases'), 'should report phase progress'); }); + + test('phase with summaries but no verification is Executed, not Complete', () => { + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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('stats', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const stats = JSON.parse(result.output); + const phase = stats.phases.find(p => p.number === '01' || p.number === '1'); + assert.strictEqual(phase.status, 'Executed', 'should be Executed without verification'); + assert.strictEqual(stats.phases_completed, 0, 'unverified phase should not count as completed'); + }); + + test('phase with passing verification is Complete', () => { + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification'); + const result = runGsdTools('stats', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const stats = JSON.parse(result.output); + const phase = stats.phases.find(p => p.number === '01' || p.number === '1'); + assert.strictEqual(phase.status, 'Complete', 'should be Complete with passing verification'); + assert.strictEqual(stats.phases_completed, 1); + }); + + test('phase with gaps_found verification is Executed', () => { + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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(p1, 'VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Verification'); + const result = runGsdTools('stats', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const stats = JSON.parse(result.output); + const phase = stats.phases.find(p => p.number === '01' || p.number === '1'); + assert.strictEqual(phase.status, 'Executed', 'gaps_found should show as Executed'); + }); + + test('phase with human_needed verification shows Needs Review', () => { + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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(p1, 'VERIFICATION.md'), '---\nstatus: human_needed\n---\n# Verification'); + const result = runGsdTools('stats', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const stats = JSON.parse(result.output); + const phase = stats.phases.find(p => p.number === '01' || p.number === '1'); + assert.strictEqual(phase.status, 'Needs Review', 'human_needed should show as Needs Review'); + }); + + test('progress command also uses verification-aware status', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 MVP\n` + ); + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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('progress json', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phases[0].status, 'Executed', 'progress should show Executed without verification'); + }); });