diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index 3ec18f9e4..f850b20d8 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -1038,6 +1038,270 @@ function cmdTemplateSelect(cwd, planPath, raw) { } } +function cmdPhasePlanIndex(cwd, phase, raw) { + if (!phase) { + error('phase required for phase-plan-index'); + } + + const phasesDir = path.join(cwd, '.planning', 'phases'); + const normalized = normalizePhaseName(phase); + + // Find phase directory + let phaseDir = null; + let phaseDirName = null; + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + const match = dirs.find(d => d.startsWith(normalized)); + if (match) { + phaseDir = path.join(phasesDir, match); + phaseDirName = match; + } + } catch { + // phases dir doesn't exist + } + + if (!phaseDir) { + output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw); + return; + } + + // Get all files in phase directory + const phaseFiles = fs.readdirSync(phaseDir); + const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort(); + const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); + + // Build set of plan IDs with summaries + const completedPlanIds = new Set( + summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '')) + ); + + const plans = []; + const waves = {}; + const incomplete = []; + let hasCheckpoints = false; + + for (const planFile of planFiles) { + const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', ''); + const planPath = path.join(phaseDir, planFile); + const content = fs.readFileSync(planPath, 'utf-8'); + const fm = extractFrontmatter(content); + + // Count tasks (## Task N patterns) + const taskMatches = content.match(/##\s*Task\s*\d+/gi) || []; + const taskCount = taskMatches.length; + + // Parse wave as integer + const wave = parseInt(fm.wave, 10) || 1; + + // Parse autonomous (default true if not specified) + let autonomous = true; + if (fm.autonomous !== undefined) { + autonomous = fm.autonomous === 'true' || fm.autonomous === true; + } + + if (!autonomous) { + hasCheckpoints = true; + } + + // Parse files-modified + let filesModified = []; + if (fm['files-modified']) { + filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']]; + } + + const hasSummary = completedPlanIds.has(planId); + if (!hasSummary) { + incomplete.push(planId); + } + + const plan = { + id: planId, + wave, + autonomous, + objective: fm.objective || null, + files_modified: filesModified, + task_count: taskCount, + has_summary: hasSummary, + }; + + plans.push(plan); + + // Group by wave + const waveKey = String(wave); + if (!waves[waveKey]) { + waves[waveKey] = []; + } + waves[waveKey].push(planId); + } + + const result = { + phase: normalized, + plans, + waves, + incomplete, + has_checkpoints: hasCheckpoints, + }; + + output(result, raw); +} + +function cmdStateSnapshot(cwd, raw) { + const statePath = path.join(cwd, '.planning', 'STATE.md'); + + if (!fs.existsSync(statePath)) { + output({ error: 'STATE.md not found' }, raw); + return; + } + + const content = fs.readFileSync(statePath, 'utf-8'); + + // Helper to extract **Field:** value patterns + const extractField = (fieldName) => { + const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i'); + const match = content.match(pattern); + return match ? match[1].trim() : null; + }; + + // Extract basic fields + const currentPhase = extractField('Current Phase'); + const currentPhaseName = extractField('Current Phase Name'); + const totalPhasesRaw = extractField('Total Phases'); + const currentPlan = extractField('Current Plan'); + const totalPlansRaw = extractField('Total Plans in Phase'); + const status = extractField('Status'); + const progressRaw = extractField('Progress'); + const lastActivity = extractField('Last Activity'); + const lastActivityDesc = extractField('Last Activity Description'); + const pausedAt = extractField('Paused At'); + + // Parse numeric fields + const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null; + const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null; + const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null; + + // Extract decisions table + const decisions = []; + const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); + if (decisionsMatch) { + const tableBody = decisionsMatch[1]; + const rows = tableBody.trim().split('\n').filter(r => r.includes('|')); + for (const row of rows) { + const cells = row.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 3) { + decisions.push({ + phase: cells[0], + summary: cells[1], + rationale: cells[2], + }); + } + } + } + + // Extract blockers list + const blockers = []; + const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); + if (blockersMatch) { + const blockersSection = blockersMatch[1]; + const items = blockersSection.match(/^-\s+(.+)$/gm) || []; + for (const item of items) { + blockers.push(item.replace(/^-\s+/, '').trim()); + } + } + + // Extract session info + const session = { + last_date: null, + stopped_at: null, + resume_file: null, + }; + + const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i); + if (sessionMatch) { + const sessionSection = sessionMatch[1]; + const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i); + const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i); + const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i); + + if (lastDateMatch) session.last_date = lastDateMatch[1].trim(); + if (stoppedAtMatch) session.stopped_at = stoppedAtMatch[1].trim(); + if (resumeFileMatch) session.resume_file = resumeFileMatch[1].trim(); + } + + const result = { + current_phase: currentPhase, + current_phase_name: currentPhaseName, + total_phases: totalPhases, + current_plan: currentPlan, + total_plans_in_phase: totalPlansInPhase, + status, + progress_percent: progressPercent, + last_activity: lastActivity, + last_activity_desc: lastActivityDesc, + decisions, + blockers, + paused_at: pausedAt, + session, + }; + + output(result, raw); +} + +function cmdSummaryExtract(cwd, summaryPath, fields, raw) { + if (!summaryPath) { + error('summary-path required for summary-extract'); + } + + const fullPath = path.join(cwd, summaryPath); + + if (!fs.existsSync(fullPath)) { + output({ error: 'File not found', path: summaryPath }, raw); + return; + } + + const content = fs.readFileSync(fullPath, 'utf-8'); + const fm = extractFrontmatter(content); + + // Parse key-decisions into structured format + const parseDecisions = (decisionsList) => { + if (!decisionsList || !Array.isArray(decisionsList)) return []; + return decisionsList.map(d => { + const colonIdx = d.indexOf(':'); + if (colonIdx > 0) { + return { + summary: d.substring(0, colonIdx).trim(), + rationale: d.substring(colonIdx + 1).trim(), + }; + } + return { summary: d, rationale: null }; + }); + }; + + // Build full result + const fullResult = { + path: summaryPath, + one_liner: fm['one-liner'] || null, + key_files: fm['key-files'] || [], + tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [], + patterns: fm['patterns-established'] || [], + decisions: parseDecisions(fm['key-decisions']), + }; + + // If fields specified, filter to only those fields + if (fields && fields.length > 0) { + const filtered = { path: summaryPath }; + for (const field of fields) { + if (fullResult[field] !== undefined) { + filtered[field] = fullResult[field]; + } + } + output(filtered, raw); + return; + } + + output(fullResult, raw); +} + // ─── Compound Commands ──────────────────────────────────────────────────────── function resolveModelInternal(cwd, agentType) { @@ -1870,6 +2134,24 @@ function main() { break; } + case 'phase-plan-index': { + cmdPhasePlanIndex(cwd, args[1], raw); + break; + } + + case 'state-snapshot': { + cmdStateSnapshot(cwd, raw); + break; + } + + case 'summary-extract': { + const summaryPath = args[1]; + const fieldsIndex = args.indexOf('--fields'); + const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null; + cmdSummaryExtract(cwd, summaryPath, fields, raw); + break; + } + default: error(`Unknown command: ${command}`); } diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index 6fb963077..cddb80fea 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -597,3 +597,458 @@ describe('phase next-decimal command', () => { assert.strictEqual(output.next, '06.1', 'should still suggest 06.1'); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// phase-plan-index command +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase-plan-index command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('empty phase directory returns empty plans array', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true }); + + const result = runGsdTools('phase-plan-index 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase, '03', 'phase number correct'); + assert.deepStrictEqual(output.plans, [], 'plans should be empty'); + assert.deepStrictEqual(output.waves, {}, 'waves should be empty'); + assert.deepStrictEqual(output.incomplete, [], 'incomplete should be empty'); + assert.strictEqual(output.has_checkpoints, false, 'no checkpoints'); + }); + + test('extracts single plan with frontmatter', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '03-01-PLAN.md'), + `--- +wave: 1 +autonomous: true +objective: Set up database schema +files-modified: [prisma/schema.prisma, src/lib/db.ts] +--- + +## Task 1: Create schema +## Task 2: Generate client +` + ); + + const result = runGsdTools('phase-plan-index 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.plans.length, 1, 'should have 1 plan'); + assert.strictEqual(output.plans[0].id, '03-01', 'plan id correct'); + assert.strictEqual(output.plans[0].wave, 1, 'wave extracted'); + assert.strictEqual(output.plans[0].autonomous, true, 'autonomous extracted'); + assert.strictEqual(output.plans[0].objective, 'Set up database schema', 'objective extracted'); + assert.deepStrictEqual(output.plans[0].files_modified, ['prisma/schema.prisma', 'src/lib/db.ts'], 'files extracted'); + assert.strictEqual(output.plans[0].task_count, 2, 'task count correct'); + assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet'); + }); + + test('groups multiple plans by wave', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '03-01-PLAN.md'), + `--- +wave: 1 +autonomous: true +objective: Database setup +--- + +## Task 1: Schema +` + ); + + fs.writeFileSync( + path.join(phaseDir, '03-02-PLAN.md'), + `--- +wave: 1 +autonomous: true +objective: Auth setup +--- + +## Task 1: JWT +` + ); + + fs.writeFileSync( + path.join(phaseDir, '03-03-PLAN.md'), + `--- +wave: 2 +autonomous: false +objective: API routes +--- + +## Task 1: Routes +` + ); + + const result = runGsdTools('phase-plan-index 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.plans.length, 3, 'should have 3 plans'); + assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans'); + assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan'); + }); + + test('detects incomplete plans (no matching summary)', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + // Plan with summary + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), `---\nwave: 1\n---\n## Task 1`); + fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`); + + // Plan without summary + fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), `---\nwave: 2\n---\n## Task 1`); + + const result = runGsdTools('phase-plan-index 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.plans[0].has_summary, true, 'first plan has summary'); + assert.strictEqual(output.plans[1].has_summary, false, 'second plan has no summary'); + assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct'); + }); + + test('detects checkpoints (autonomous: false)', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '03-01-PLAN.md'), + `--- +wave: 1 +autonomous: false +objective: Manual review needed +--- + +## Task 1: Review +` + ); + + const result = runGsdTools('phase-plan-index 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_checkpoints, true, 'should detect checkpoint'); + assert.strictEqual(output.plans[0].autonomous, false, 'plan marked non-autonomous'); + }); + + test('phase not found returns error', () => { + const result = runGsdTools('phase-plan-index 99', tmpDir); + assert.ok(result.success, `Command should succeed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.error, 'Phase not found', 'should report phase not found'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// state-snapshot command +// ───────────────────────────────────────────────────────────────────────────── + +describe('state-snapshot command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('missing STATE.md returns error', () => { + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command should succeed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.error, 'STATE.md not found', 'should report missing file'); + }); + + test('extracts basic fields from STATE.md', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State + +**Current Phase:** 03 +**Current Phase Name:** API Layer +**Total Phases:** 6 +**Current Plan:** 03-02 +**Total Plans in Phase:** 3 +**Status:** In progress +**Progress:** 45% +**Last Activity:** 2024-01-15 +**Last Activity Description:** Completed 03-01-PLAN.md +` + ); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.current_phase, '03', 'current phase extracted'); + assert.strictEqual(output.current_phase_name, 'API Layer', 'phase name extracted'); + assert.strictEqual(output.total_phases, 6, 'total phases extracted'); + assert.strictEqual(output.current_plan, '03-02', 'current plan extracted'); + assert.strictEqual(output.total_plans_in_phase, 3, 'total plans extracted'); + assert.strictEqual(output.status, 'In progress', 'status extracted'); + assert.strictEqual(output.progress_percent, 45, 'progress extracted'); + assert.strictEqual(output.last_activity, '2024-01-15', 'last activity date extracted'); + }); + + test('extracts decisions table', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State + +**Current Phase:** 01 + +## Decisions Made + +| Phase | Decision | Rationale | +|-------|----------|-----------| +| 01 | Use Prisma | Better DX than raw SQL | +| 02 | JWT auth | Stateless authentication | +` + ); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.decisions.length, 2, 'should have 2 decisions'); + assert.strictEqual(output.decisions[0].phase, '01', 'first decision phase'); + assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'first decision summary'); + assert.strictEqual(output.decisions[0].rationale, 'Better DX than raw SQL', 'first decision rationale'); + }); + + test('extracts blockers list', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State + +**Current Phase:** 03 + +## Blockers + +- Waiting for API credentials +- Need design review for dashboard +` + ); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.deepStrictEqual(output.blockers, [ + 'Waiting for API credentials', + 'Need design review for dashboard', + ], 'blockers extracted'); + }); + + test('extracts session continuity info', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State + +**Current Phase:** 03 + +## Session + +**Last Date:** 2024-01-15 +**Stopped At:** Phase 3, Plan 2, Task 1 +**Resume File:** .planning/phases/03-api/03-02-PLAN.md +` + ); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.session.last_date, '2024-01-15', 'session date extracted'); + assert.strictEqual(output.session.stopped_at, 'Phase 3, Plan 2, Task 1', 'stopped at extracted'); + assert.strictEqual(output.session.resume_file, '.planning/phases/03-api/03-02-PLAN.md', 'resume file extracted'); + }); + + test('handles paused_at field', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State + +**Current Phase:** 03 +**Paused At:** Phase 3, Plan 1, Task 2 - mid-implementation +` + ); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// summary-extract command +// ───────────────────────────────────────────────────────────────────────────── + +describe('summary-extract command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('missing file returns error', () => { + const result = runGsdTools('summary-extract .planning/phases/01-test/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command should succeed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.error, 'File not found', 'should report missing file'); + }); + + test('extracts all fields from SUMMARY.md', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +one-liner: Set up Prisma with User and Project models +key-files: + - prisma/schema.prisma + - src/lib/db.ts +tech-stack: + added: + - prisma + - zod +patterns-established: + - Repository pattern + - Dependency injection +key-decisions: + - Use Prisma over Drizzle: Better DX and ecosystem + - Single database: Start simple, shard later +--- + +# Summary + +Full summary content here. +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.path, '.planning/phases/01-foundation/01-01-SUMMARY.md', 'path correct'); + assert.strictEqual(output.one_liner, 'Set up Prisma with User and Project models', 'one-liner extracted'); + assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma', 'src/lib/db.ts'], 'key files extracted'); + assert.deepStrictEqual(output.tech_added, ['prisma', 'zod'], 'tech added extracted'); + assert.deepStrictEqual(output.patterns, ['Repository pattern', 'Dependency injection'], 'patterns extracted'); + assert.strictEqual(output.decisions.length, 2, 'decisions extracted'); + }); + + test('selective extraction with --fields', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +one-liner: Set up database +key-files: + - prisma/schema.prisma +tech-stack: + added: + - prisma +patterns-established: + - Repository pattern +key-decisions: + - Use Prisma: Better DX +--- +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.one_liner, 'Set up database', 'one_liner included'); + assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma'], 'key_files included'); + assert.strictEqual(output.tech_added, undefined, 'tech_added excluded'); + assert.strictEqual(output.patterns, undefined, 'patterns excluded'); + assert.strictEqual(output.decisions, undefined, 'decisions excluded'); + }); + + test('handles missing frontmatter fields gracefully', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +one-liner: Minimal summary +--- + +# Summary +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.one_liner, 'Minimal summary', 'one-liner extracted'); + assert.deepStrictEqual(output.key_files, [], 'key_files defaults to empty'); + assert.deepStrictEqual(output.tech_added, [], 'tech_added defaults to empty'); + assert.deepStrictEqual(output.patterns, [], 'patterns defaults to empty'); + assert.deepStrictEqual(output.decisions, [], 'decisions defaults to empty'); + }); + + test('parses key-decisions with rationale', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +key-decisions: + - Use Prisma: Better DX than alternatives + - JWT tokens: Stateless auth for scalability +--- +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'decision summary parsed'); + assert.strictEqual(output.decisions[0].rationale, 'Better DX than alternatives', 'decision rationale parsed'); + assert.strictEqual(output.decisions[1].summary, 'JWT tokens', 'second decision summary'); + assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale'); + }); +}); diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md index 564667510..e7f4c47ed 100644 --- a/get-shit-done/workflows/complete-milestone.md +++ b/get-shit-done/workflows/complete-milestone.md @@ -128,12 +128,13 @@ Milestone Stats: -Read all phase SUMMARY.md files in milestone range: +Extract one-liners from SUMMARY.md files using summary-extract: ```bash -cat .planning/phases/01-*/01-*-SUMMARY.md -cat .planning/phases/02-*/02-*-SUMMARY.md -# ... for each phase in milestone +# For each phase in milestone, extract one-liner +for summary in .planning/phases/*-*/*-SUMMARY.md; do + node ~/.claude/get-shit-done/bin/gsd-tools.js summary-extract "$summary" --fields one_liner | jq -r '.one_liner' +done ``` Extract 4-6 key accomplishments. Present: diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 183d79eab..d20ad404d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -47,31 +47,16 @@ From init JSON: `phase_dir`, `plan_count`, `incomplete_count`. Report: "Found {plan_count} plans in {phase_dir} ({incomplete_count} incomplete)" - + +Load plan inventory with wave grouping in one call: ```bash -ls -1 "$PHASE_DIR"/*-PLAN.md 2>/dev/null | sort -ls -1 "$PHASE_DIR"/*-SUMMARY.md 2>/dev/null | sort +PLAN_INDEX=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase-plan-index "${PHASE_NUMBER}") ``` -For each plan, read frontmatter: `wave`, `autonomous`, `gap_closure`. +Parse JSON for: `phase`, `plans[]` (each with `id`, `wave`, `autonomous`, `objective`, `files_modified`, `task_count`, `has_summary`), `waves` (map of wave number → plan IDs), `incomplete`, `has_checkpoints`. -Build inventory: path, plan ID, wave, autonomous flag, gap_closure flag, completion (SUMMARY exists = complete). - -**Filtering:** Skip completed plans. If `--gaps-only`: also skip non-gap_closure plans. If all filtered: "No matching incomplete plans" → exit. - - - - -```bash -for plan in $PHASE_DIR/*-PLAN.md; do - wave=$(grep "^wave:" "$plan" | cut -d: -f2 | tr -d ' ') - autonomous=$(grep "^autonomous:" "$plan" | cut -d: -f2 | tr -d ' ') - echo "$plan:$wave:$autonomous" -done -``` - -Group by wave number. **No dependency analysis needed** — waves pre-computed during `/gsd:plan-phase`. +**Filtering:** Skip plans where `has_summary: true`. If `--gaps-only`: also skip non-gap_closure plans. If all filtered: "No matching incomplete plans" → exit. Report: ``` diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index ec6a954af..1b1d47c2a 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -75,7 +75,8 @@ Display banner: ```bash PHASE_DESC=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap get-phase "${PHASE}" | jq -r '.section') REQUIREMENTS=$(cat .planning/REQUIREMENTS.md 2>/dev/null | grep -A100 "## Requirements" | head -50) -DECISIONS=$(grep -A20 "### Decisions Made" .planning/STATE.md 2>/dev/null) +STATE_SNAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js state-snapshot) +# Extract decisions from state-snapshot JSON: jq '.decisions[] | "\(.phase): \(.summary) - \(.rationale)"' ``` Research prompt: diff --git a/get-shit-done/workflows/research-phase.md b/get-shit-done/workflows/research-phase.md index bd6fb63de..003f26c32 100644 --- a/get-shit-done/workflows/research-phase.md +++ b/get-shit-done/workflows/research-phase.md @@ -38,7 +38,8 @@ If exists: Offer update/view/skip options. echo "$PHASE_INFO" | jq -r '.section' cat .planning/REQUIREMENTS.md 2>/dev/null cat .planning/phases/${PHASE}-*/*-CONTEXT.md 2>/dev/null -grep -A30 "### Decisions Made" .planning/STATE.md 2>/dev/null +# Decisions from state-snapshot (structured JSON) +node ~/.claude/get-shit-done/bin/gsd-tools.js state-snapshot | jq '.decisions' ``` ## Step 4: Spawn Researcher