From 3321d4827935f8fdf0514b0649d62f92efa6aeb6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 29 Mar 2026 11:55:11 -0700 Subject: [PATCH] fix(stats): require verification for Complete status, add Executed state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phases with all summaries but no passing VERIFICATION.md now show as "Executed" instead of "Complete", preventing false progress reporting. Adds determinePhaseStatus() helper used by both cmdStats() and cmdProgressRender(). Also fixes duplicate phase directory accumulation in cmdStats() — plans/summaries from directories sharing the same phase number are now summed instead of silently overwritten. New statuses: Executed (summaries done, no verification), Needs Review (verification exists with human_needed status). Closes #1459 Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/commands.cjs | 43 +++++++++++----- tests/commands.test.cjs | 80 +++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 13 deletions(-) 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'); + }); });