Merge pull request #483 from glittercowboy/claude/expand-gsd-automation-XVhXH

Add phase lifecycle and roadmap management commands
This commit is contained in:
TÂCHES
2026-02-08 08:15:27 -06:00
committed by GitHub
8 changed files with 2176 additions and 618 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/** /**
* GSD Tools Tests — Schema validation for history-digest command * GSD Tools Tests
*/ */
const { test, describe, beforeEach, afterEach } = require('node:test'); const { test, describe, beforeEach, afterEach } = require('node:test');
@@ -1191,3 +1191,843 @@ describe('init commands with --include flag', () => {
assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined'); assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined');
}); });
}); });
// ─────────────────────────────────────────────────────────────────────────────
// roadmap analyze command
// ─────────────────────────────────────────────────────────────────────────────
describe('roadmap analyze command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('missing ROADMAP.md returns error', () => {
const result = runGsdTools('roadmap analyze', tmpDir);
assert.ok(result.success, `Command should succeed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.error, 'ROADMAP.md not found');
});
test('parses phases with goals and disk status', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0
### Phase 1: Foundation
**Goal:** Set up infrastructure
### Phase 2: Authentication
**Goal:** Add user auth
### Phase 3: Features
**Goal:** Build core features
`
);
// Create phase dirs with varying completion
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
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 p2 = path.join(tmpDir, '.planning', 'phases', '02-authentication');
fs.mkdirSync(p2, { recursive: true });
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
const result = runGsdTools('roadmap analyze', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_count, 3, 'should find 3 phases');
assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete');
assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned');
assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory');
assert.strictEqual(output.completed_phases, 1, '1 phase complete');
assert.strictEqual(output.total_plans, 2, '2 total plans');
assert.strictEqual(output.total_summaries, 1, '1 total summary');
assert.strictEqual(output.progress_percent, 50, '50% complete');
assert.strictEqual(output.current_phase, '2', 'current phase is 2');
});
test('extracts goals and dependencies', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Setup
**Goal:** Initialize project
**Depends on:** Nothing
### Phase 2: Build
**Goal:** Build features
**Depends on:** Phase 1
`
);
const result = runGsdTools('roadmap analyze', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phases[0].goal, 'Initialize project');
assert.strictEqual(output.phases[0].depends_on, 'Nothing');
assert.strictEqual(output.phases[1].goal, 'Build features');
assert.strictEqual(output.phases[1].depends_on, 'Phase 1');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase add command
// ─────────────────────────────────────────────────────────────────────────────
describe('phase add command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('adds phase after highest existing', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0
### Phase 1: Foundation
**Goal:** Setup
### Phase 2: API
**Goal:** Build API
---
`
);
const result = runGsdTools('phase add User Dashboard', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_number, 3, 'should be phase 3');
assert.strictEqual(output.slug, 'user-dashboard');
// Verify directory created
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-user-dashboard')),
'directory should be created'
);
// Verify ROADMAP updated
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('### Phase 3: User Dashboard'), 'roadmap should include new phase');
assert.ok(roadmap.includes('**Depends on:** Phase 2'), 'should depend on previous');
});
test('handles empty roadmap', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0\n`
);
const result = runGsdTools('phase add Initial Setup', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_number, 1, 'should be phase 1');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase insert command
// ─────────────────────────────────────────────────────────────────────────────
describe('phase insert command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('inserts decimal phase after target', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
### Phase 2: API
**Goal:** Build API
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
const result = runGsdTools('phase insert 1 Fix Critical Bug', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_number, '01.1', 'should be 01.1');
assert.strictEqual(output.after_phase, '1');
// Verify directory
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '01.1-fix-critical-bug')),
'decimal phase directory should be created'
);
// Verify ROADMAP
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('Phase 01.1: Fix Critical Bug (INSERTED)'), 'roadmap should include inserted phase');
});
test('increments decimal when siblings exist', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
### Phase 2: API
**Goal:** Build API
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01.1-hotfix'), { recursive: true });
const result = runGsdTools('phase insert 1 Another Fix', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_number, '01.2', 'should be 01.2');
});
test('rejects missing phase', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
);
const result = runGsdTools('phase insert 99 Fix Something', tmpDir);
assert.ok(!result.success, 'should fail for missing phase');
assert.ok(result.error.includes('not found'), 'error mentions not found');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase remove command
// ─────────────────────────────────────────────────────────────────────────────
describe('phase remove command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('removes phase directory and renumbers subsequent', () => {
// Setup 3 phases
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
**Depends on:** Nothing
### Phase 2: Auth
**Goal:** Authentication
**Depends on:** Phase 1
### Phase 3: Features
**Goal:** Core features
**Depends on:** Phase 2
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
const p2 = path.join(tmpDir, '.planning', 'phases', '02-auth');
fs.mkdirSync(p2, { recursive: true });
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
const p3 = path.join(tmpDir, '.planning', 'phases', '03-features');
fs.mkdirSync(p3, { recursive: true });
fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p3, '03-02-PLAN.md'), '# Plan 2');
// Remove phase 2
const result = runGsdTools('phase remove 2', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.removed, '2');
assert.strictEqual(output.directory_deleted, '02-auth');
// Phase 3 should be renumbered to 02
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features')),
'phase 3 should be renumbered to 02-features'
);
assert.ok(
!fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-features')),
'old 03-features should not exist'
);
// Files inside should be renamed
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-01-PLAN.md')),
'plan file should be renumbered to 02-01'
);
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-02-PLAN.md')),
'plan 2 should be renumbered to 02-02'
);
// ROADMAP should be updated
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(!roadmap.includes('Phase 2: Auth'), 'removed phase should not be in roadmap');
assert.ok(roadmap.includes('Phase 2: Features'), 'phase 3 should be renumbered to 2');
});
test('rejects removal of phase with summaries unless --force', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
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(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
);
// Should fail without --force
const result = runGsdTools('phase remove 1', tmpDir);
assert.ok(!result.success, 'should fail without --force');
assert.ok(result.error.includes('executed plan'), 'error mentions executed plans');
// Should succeed with --force
const forceResult = runGsdTools('phase remove 1 --force', tmpDir);
assert.ok(forceResult.success, `Force remove failed: ${forceResult.error}`);
});
test('removes decimal phase and renumbers siblings', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 6: Main\n**Goal:** Main\n### Phase 6.1: Fix A\n**Goal:** Fix A\n### Phase 6.2: Fix B\n**Goal:** Fix B\n### Phase 6.3: Fix C\n**Goal:** Fix C\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-main'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-fix-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-b'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c'), { recursive: true });
const result = runGsdTools('phase remove 6.2', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
// 06.3 should become 06.2
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-c')),
'06.3 should be renumbered to 06.2'
);
assert.ok(
!fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c')),
'old 06.3 should not exist'
);
});
test('updates STATE.md phase count', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n**Goal:** A\n### Phase 2: B\n**Goal:** B\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 1\n**Total Phases:** 2\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
runGsdTools('phase remove 2', tmpDir);
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase complete command
// ─────────────────────────────────────────────────────────────────────────────
describe('phase complete command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('marks phase complete and transitions to next', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
- [ ] Phase 1: Foundation
- [ ] Phase 2: API
### Phase 1: Foundation
**Goal:** Setup
**Plans:** 1 plans
### Phase 2: API
**Goal:** Build API
`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
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.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
const result = runGsdTools('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.completed_phase, '1');
assert.strictEqual(output.plans_executed, '1/1');
assert.strictEqual(output.next_phase, '02');
assert.strictEqual(output.is_last_phase, false);
// Verify STATE.md updated
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('**Current Phase:** 02'), 'should advance to phase 02');
assert.ok(state.includes('**Status:** Ready to plan'), 'status should be ready to plan');
assert.ok(state.includes('**Current Plan:** Not started'), 'plan should be reset');
// Verify ROADMAP checkbox
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('[x]'), 'phase should be checked off');
assert.ok(roadmap.includes('completed'), 'completion date should be added');
});
test('detects last phase in milestone', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: Only Phase\n**Goal:** Everything\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-only-phase');
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('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.is_last_phase, true, 'should detect last phase');
assert.strictEqual(output.next_phase, null, 'no next phase');
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('Milestone complete'), 'status should be milestone complete');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// milestone complete command
// ─────────────────────────────────────────────────────────────────────────────
describe('milestone complete command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('archives roadmap, requirements, creates MILESTONES.md', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
`# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(
path.join(p1, '01-01-SUMMARY.md'),
`---\none-liner: Set up project infrastructure\n---\n# Summary\n`
);
const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.version, 'v1.0');
assert.strictEqual(output.phases, 1);
assert.ok(output.archived.roadmap, 'roadmap should be archived');
assert.ok(output.archived.requirements, 'requirements should be archived');
// Verify archive files exist
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')),
'archived roadmap should exist'
);
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')),
'archived requirements should exist'
);
// Verify MILESTONES.md created
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')),
'MILESTONES.md should be created'
);
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name');
assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed');
});
test('appends to existing MILESTONES.md', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'MILESTONES.md'),
`# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved');
assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// validate consistency command
// ─────────────────────────────────────────────────────────────────────────────
describe('validate consistency command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('passes for consistent project', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.passed, true, 'should pass');
assert.strictEqual(output.warning_count, 0, 'no warnings');
});
test('warns about phase on disk but not in roadmap', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.warning_count > 0, 'should have warnings');
assert.ok(
output.warnings.some(w => w.includes('disk but not in ROADMAP')),
'should warn about orphan directory'
);
});
test('warns about gaps in phase numbering', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.warnings.some(w => w.includes('Gap in phase numbering')),
'should warn about gap'
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// progress command
// ─────────────────────────────────────────────────────────────────────────────
describe('progress command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('renders JSON progress', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0 MVP\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2');
const result = runGsdTools('progress json', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.total_plans, 2, '2 total plans');
assert.strictEqual(output.total_summaries, 1, '1 summary');
assert.strictEqual(output.percent, 50, '50%');
assert.strictEqual(output.phases.length, 1, '1 phase');
assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress');
});
test('renders bar format', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
const result = runGsdTools('progress bar --raw', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(result.output.includes('1/1'), 'should include count');
assert.ok(result.output.includes('100%'), 'should include 100%');
});
test('renders table format', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0 MVP\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
const result = runGsdTools('progress table --raw', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(result.output.includes('Phase'), 'should have table header');
assert.ok(result.output.includes('foundation'), 'should include phase name');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// todo complete command
// ─────────────────────────────────────────────────────────────────────────────
describe('todo complete command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('moves todo from pending to completed', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(
path.join(pendingDir, 'add-dark-mode.md'),
`title: Add dark mode\narea: ui\ncreated: 2025-01-01\n`
);
const result = runGsdTools('todo complete add-dark-mode.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.completed, true);
// Verify moved
assert.ok(
!fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')),
'should be removed from pending'
);
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')),
'should be in completed'
);
// Verify completion timestamp added
const content = fs.readFileSync(
path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'),
'utf-8'
);
assert.ok(content.startsWith('completed:'), 'should have completed timestamp');
});
test('fails for nonexistent todo', () => {
const result = runGsdTools('todo complete nonexistent.md', tmpDir);
assert.ok(!result.success, 'should fail');
assert.ok(result.error.includes('not found'), 'error mentions not found');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// scaffold command
// ─────────────────────────────────────────────────────────────────────────────
describe('scaffold command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('scaffolds context file', () => {
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
const result = runGsdTools('scaffold context --phase 3', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, true);
// Verify file content
const content = fs.readFileSync(
path.join(tmpDir, '.planning', 'phases', '03-api', '03-CONTEXT.md'),
'utf-8'
);
assert.ok(content.includes('Phase 3'), 'should reference phase number');
assert.ok(content.includes('Decisions'), 'should have decisions section');
assert.ok(content.includes('Discretion Areas'), 'should have discretion section');
});
test('scaffolds UAT file', () => {
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
const result = runGsdTools('scaffold uat --phase 3', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, true);
const content = fs.readFileSync(
path.join(tmpDir, '.planning', 'phases', '03-api', '03-UAT.md'),
'utf-8'
);
assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading');
assert.ok(content.includes('Test Results'), 'should have test results section');
});
test('scaffolds verification file', () => {
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
const result = runGsdTools('scaffold verification --phase 3', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, true);
const content = fs.readFileSync(
path.join(tmpDir, '.planning', 'phases', '03-api', '03-VERIFICATION.md'),
'utf-8'
);
assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading');
});
test('scaffolds phase directory', () => {
const result = runGsdTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, true);
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '05-user-dashboard')),
'directory should be created'
);
});
test('does not overwrite existing files', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content');
const result = runGsdTools('scaffold context --phase 3', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, false, 'should not overwrite');
assert.strictEqual(output.reason, 'already_exists');
});
});

View File

@@ -38,96 +38,30 @@ ERROR: No roadmap found (.planning/ROADMAP.md)
Run /gsd:new-project to initialize. Run /gsd:new-project to initialize.
``` ```
Exit. Exit.
Read roadmap content for parsing.
</step> </step>
<step name="find_current_milestone"> <step name="add_phase">
Parse the roadmap to find the current milestone section: **Delegate the phase addition to gsd-tools:**
1. Locate the "## Current Milestone:" heading
2. Extract milestone name and version
3. Identify all phases under this milestone (before next "---" separator or next milestone heading)
4. Parse existing phase numbers (including decimals if present)
Example structure:
```
## Current Milestone: v1.0 Foundation
### Phase 4: Focused Command System
### Phase 5: Path Routing & Validation
### Phase 6: Documentation & Distribution
```
</step>
<step name="calculate_next_phase">
Find the highest integer phase number in the current milestone:
1. Extract all phase numbers from phase headings (### Phase N:)
2. Filter to integer phases only (ignore decimals like 4.1, 4.2)
3. Find the maximum integer value
4. Add 1 to get the next phase number
Example: If phases are 4, 5, 5.1, 6 → next is 7
Format as two-digit: `printf "%02d" $next_phase`
</step>
<step name="generate_slug">
Convert the phase description to a kebab-case slug.
Use `init phase-op` which provides `phase_slug` computed from phase name, or call:
```bash
slug=$(node ~/.claude/get-shit-done/bin/gsd-tools.js generate-slug "$description" --raw)
```
Phase directory name: `{two-digit-phase}-{slug}`
Example: `07-add-authentication`
</step>
<step name="create_phase_directory">
Create the phase directory structure:
```bash ```bash
phase_dir=".planning/phases/${phase_num}-${slug}" RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase add "${description}")
mkdir -p "$phase_dir"
``` ```
Confirm: "Created directory: $phase_dir" The CLI handles:
</step> - Finding the highest existing integer phase number
- Calculating next phase number (max + 1)
- Generating slug from description
- Creating the phase directory (`.planning/phases/{NN}-{slug}/`)
- Inserting the phase entry into ROADMAP.md with Goal, Depends on, and Plans sections
<step name="update_roadmap"> Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory`.
Add the new phase entry to the roadmap:
1. Find the insertion point (after last phase in current milestone, before "---" separator)
2. Insert new phase heading:
```
### Phase {N}: {Description}
**Goal:** [To be planned]
**Depends on:** Phase {N-1}
**Plans:** 0 plans
Plans:
- [ ] TBD (run /gsd:plan-phase {N} to break down)
**Details:**
[To be added during planning]
```
3. Write updated roadmap back to file
Preserve all other content exactly (formatting, spacing, other phases).
</step> </step>
<step name="update_project_state"> <step name="update_project_state">
Update STATE.md to reflect the new phase: Update STATE.md to reflect the new phase:
1. Read `.planning/STATE.md` 1. Read `.planning/STATE.md`
2. Under "## Current Position" → "**Next Phase:**" add reference to new phase 2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry:
3. Under "## Accumulated Context" → "### Roadmap Evolution" add entry:
``` ```
- Phase {N} added: {description} - Phase {N} added: {description}
``` ```
@@ -145,7 +79,6 @@ Phase {N} added to current milestone:
- Status: Not planned yet - Status: Not planned yet
Roadmap updated: .planning/ROADMAP.md Roadmap updated: .planning/ROADMAP.md
Project state updated: .planning/STATE.md
--- ---
@@ -170,10 +103,9 @@ Project state updated: .planning/STATE.md
</process> </process>
<success_criteria> <success_criteria>
- [ ] Phase directory created: `.planning/phases/{NN}-{slug}/` - [ ] `gsd-tools phase add` executed successfully
- [ ] Phase directory created
- [ ] Roadmap updated with new phase entry - [ ] Roadmap updated with new phase entry
- [ ] STATE.md updated with roadmap evolution note - [ ] STATE.md updated with roadmap evolution note
- [ ] New phase appears at end of current milestone
- [ ] Next phase number calculated correctly (ignoring decimals)
- [ ] User informed of next steps - [ ] User informed of next steps
</success_criteria> </success_criteria>

View File

@@ -37,18 +37,16 @@ When a milestone completes:
<step name="verify_readiness"> <step name="verify_readiness">
Check milestone completion: **Use `roadmap analyze` for comprehensive readiness check:**
```bash ```bash
cat .planning/ROADMAP.md ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze)
ls .planning/phases/*/SUMMARY.md 2>/dev/null | wc -l
``` ```
Verify: This returns all phases with plan/summary counts and disk status. Use this to verify:
- Which phases belong to this milestone? - Which phases belong to this milestone?
- All phases complete (all plans have summaries)? - All phases complete (all plans have summaries)? Check `disk_status === 'complete'` for each.
- Work tested/validated? - `progress_percent` should be 100%.
- Ready to ship/tag?
Present: Present:
@@ -61,7 +59,7 @@ Includes:
- Phase 3: Core Features (3/3 plans complete) - Phase 3: Core Features (3/3 plans complete)
- Phase 4: Polish (1/1 plan complete) - Phase 4: Polish (1/1 plan complete)
Total: 4 phases, 8 plans, all complete Total: {phase_count} phases, {total_plans} plans, all complete
``` ```
<config-check> <config-check>
@@ -152,36 +150,9 @@ Key accomplishments for this milestone:
<step name="create_milestone_entry"> <step name="create_milestone_entry">
Create or update `.planning/MILESTONES.md`. **Note:** MILESTONES.md entry is now created automatically by `gsd-tools milestone complete` in the archive_milestone step. The entry includes version, date, phase/plan/task counts, and accomplishments extracted from SUMMARY.md files.
If file doesn't exist, create with project name header. If exists, prepend new entry (reverse chronological). If additional details are needed (e.g., user-provided "Delivered" summary, git range, LOC stats), add them manually after the CLI creates the base entry.
Use template from `templates/milestone.md`:
```markdown
## v[Version] [Name] (Shipped: YYYY-MM-DD)
**Delivered:** [One sentence from user]
**Phases completed:** [X-Y] ([Z] plans total)
**Key accomplishments:**
- [List from previous step]
**Stats:**
- [Files] files created/modified
- [LOC] lines of [language]
- [Phases] phases, [Plans] plans, [Tasks] tasks
- [Days] days from [start] to ship
**Git range:** `feat(XX-XX)` → `feat(YY-YY)`
**What's next:** [Ask user: what's the next goal?]
---
```
</step> </step>
@@ -370,103 +341,71 @@ Update `.planning/ROADMAP.md` — group completed milestone phases:
<step name="archive_milestone"> <step name="archive_milestone">
Extract completed milestone details to archive. **Delegate archival to gsd-tools:**
1. Create `.planning/milestones/v[X.Y]-ROADMAP.md` ```bash
2. Read `~/.claude/get-shit-done/templates/milestone-archive.md` template ARCHIVE=$(node ~/.claude/get-shit-done/bin/gsd-tools.js milestone complete "v[X.Y]" --name "[Milestone Name]")
3. Extract from ROADMAP.md: all phases in milestone (by number range), full phase details, plan lists with checkmarks ```
4. Extract from PROJECT.md: key decisions, validated requirements
5. Fill template placeholders: {{VERSION}}, {{MILESTONE_NAME}}, {{DATE}}, {{PHASE_START}}, {{PHASE_END}}, {{TOTAL_PLANS}}, {{MILESTONE_DESCRIPTION}}, {{PHASES_SECTION}}, {{DECISIONS_FROM_PROJECT}}, {{ISSUES_RESOLVED_DURING_MILESTONE}} The CLI handles:
6. Write to `.planning/milestones/v[X.Y]-ROADMAP.md` - Creating `.planning/milestones/` directory
7. Delete ROADMAP.md: - Archiving ROADMAP.md to `milestones/v[X.Y]-ROADMAP.md`
```bash - Archiving REQUIREMENTS.md to `milestones/v[X.Y]-REQUIREMENTS.md` with archive header
rm .planning/ROADMAP.md - Moving audit file to milestones if it exists
``` - Creating/appending MILESTONES.md entry with accomplishments from SUMMARY.md files
8. Verify: - Updating STATE.md (status, last activity)
```bash
ls .planning/milestones/v[X.Y]-ROADMAP.md Extract from result: `version`, `date`, `phases`, `plans`, `tasks`, `accomplishments`, `archived`.
```
9. Confirm: Verify: `✅ Milestone archived to .planning/milestones/`
```
✅ v[X.Y] roadmap archived to milestones/v[X.Y]-ROADMAP.md
✅ ROADMAP.md deleted (fresh one for next milestone)
```
**Note:** Phase directories (`.planning/phases/`) are NOT deleted — they accumulate across milestones as raw execution history. Phase numbering continues (v1.0 phases 1-4, v1.1 phases 5-8, etc.). **Note:** Phase directories (`.planning/phases/`) are NOT deleted — they accumulate across milestones as raw execution history. Phase numbering continues (v1.0 phases 1-4, v1.1 phases 5-8, etc.).
</step> After archival, the AI still handles:
- Reorganizing ROADMAP.md with milestone grouping (requires judgment)
<step name="archive_requirements"> - Full PROJECT.md evolution review (requires understanding)
- Deleting original ROADMAP.md and REQUIREMENTS.md
Archive requirements for next milestone. - These are NOT fully delegated because they require AI interpretation of content
1. Read REQUIREMENTS.md:
```bash
cat .planning/REQUIREMENTS.md
```
2. Create `.planning/milestones/v[X.Y]-REQUIREMENTS.md`
3. Transform: mark all requirements `[x]` complete, add outcome notes, update traceability table to "Complete", add milestone summary (total shipped, scope changes, drops)
4. Write archive with header:
```markdown
# Requirements Archive: v[X.Y] [Milestone Name]
**Archived:** [DATE]
**Status:** ✅ SHIPPED
For current requirements, see `.planning/REQUIREMENTS.md`.
---
[Full REQUIREMENTS.md content with checkboxes marked complete]
---
## Milestone Summary
**Shipped:** [X] of [Y] requirements
**Adjusted:** [list any that changed]
**Dropped:** [list any removed and why]
---
*Archived: [DATE] as part of v[X.Y] milestone completion*
```
5. Delete original:
```bash
rm .planning/REQUIREMENTS.md
```
6. Confirm:
```
✅ Requirements archived to milestones/v[X.Y]-REQUIREMENTS.md
✅ REQUIREMENTS.md deleted (fresh one for next milestone)
```
**Note:** Next milestone starts with `/gsd:new-milestone` which includes requirements definition. PROJECT.md's Validated section carries cumulative record across milestones.
</step> </step>
<step name="archive_audit"> <step name="reorganize_roadmap_and_delete_originals">
Move audit file to archive (if exists). Use init context or check directly: After `milestone complete` has archived, reorganize ROADMAP.md with milestone groupings, then delete originals:
```bash **Reorganize ROADMAP.md** — group completed milestone phases:
# Check if audit file exists
[ -f ".planning/v[X.Y]-MILESTONE-AUDIT.md" ] && mv .planning/v[X.Y]-MILESTONE-AUDIT.md .planning/milestones/ ```markdown
# Roadmap: [Project Name]
## Milestones
- ✅ **v1.0 MVP** — Phases 1-4 (shipped YYYY-MM-DD)
- 🚧 **v1.1 Security** — Phases 5-6 (in progress)
## Phases
<details>
<summary>✅ v1.0 MVP (Phases 1-4) — SHIPPED YYYY-MM-DD</summary>
- [x] Phase 1: Foundation (2/2 plans) — completed YYYY-MM-DD
- [x] Phase 2: Authentication (2/2 plans) — completed YYYY-MM-DD
</details>
``` ```
Confirm: `✅ Audit archived to milestones/v[X.Y]-MILESTONE-AUDIT.md` **Then delete originals:**
Skip silently if no audit file — audit is optional. ```bash
rm .planning/ROADMAP.md
rm .planning/REQUIREMENTS.md
```
</step> </step>
<step name="update_state"> <step name="update_state">
Update STATE.md for milestone completion. Most STATE.md updates were handled by `milestone complete`, but verify and update remaining fields:
**Project Reference:** **Project Reference:**
@@ -479,17 +418,6 @@ See: .planning/PROJECT.md (updated [today])
**Current focus:** [Next milestone or "Planning next milestone"] **Current focus:** [Next milestone or "Planning next milestone"]
``` ```
**Current Position:**
```markdown
Phase: [Next phase] of [Total] ([Phase name])
Plan: Not started
Status: Ready to plan
Last activity: [today] — v[X.Y] milestone complete
Progress: [updated progress bar]
```
**Accumulated Context:** **Accumulated Context:**
- Clear decisions summary (full log in PROJECT.md) - Clear decisions summary (full log in PROJECT.md)
- Clear resolved blockers - Clear resolved blockers

View File

@@ -17,31 +17,17 @@ Example: `/gsd:insert-phase 72 Fix critical auth bug`
-> after = 72 -> after = 72
-> description = "Fix critical auth bug" -> description = "Fix critical auth bug"
Validation: If arguments missing:
```bash ```
if [ $# -lt 2 ]; then ERROR: Both phase number and description required
echo "ERROR: Both phase number and description required" Usage: /gsd:insert-phase <after> <description>
echo "Usage: /gsd:insert-phase <after> <description>" Example: /gsd:insert-phase 72 Fix critical auth bug
echo "Example: /gsd:insert-phase 72 Fix critical auth bug"
exit 1
fi
``` ```
Parse first argument as integer: Exit.
```bash
after_phase=$1
shift
description="$*"
# Validate after_phase is an integer
if ! [[ "$after_phase" =~ ^[0-9]+$ ]]; then
echo "ERROR: Phase number must be an integer"
exit 1
fi
```
Validate first argument is an integer.
</step> </step>
<step name="init_context"> <step name="init_context">
@@ -56,111 +42,35 @@ Check `roadmap_exists` from init JSON. If false:
ERROR: No roadmap found (.planning/ROADMAP.md) ERROR: No roadmap found (.planning/ROADMAP.md)
``` ```
Exit. Exit.
Read roadmap content for parsing.
</step> </step>
<step name="verify_target_phase"> <step name="insert_phase">
Verify that the target phase exists in the roadmap: **Delegate the phase insertion to gsd-tools:**
1. Search for "### Phase {after_phase}:" heading
2. If not found:
```
ERROR: Phase {after_phase} not found in roadmap
Available phases: [list phase numbers]
```
Exit.
3. Verify phase is in current milestone (not completed/archived)
</step>
<step name="find_existing_decimals">
Calculate next decimal phase number:
```bash ```bash
DECIMAL_INFO=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase next-decimal "${after_phase}") RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase insert "${after_phase}" "${description}")
``` ```
Extract from JSON: The CLI handles:
- `next`: The next available decimal (e.g., "06.1", "06.3") - Verifying target phase exists in ROADMAP.md
- `existing`: Array of existing decimals (e.g., ["06.1", "06.2"]) - Calculating next decimal phase number (checking existing decimals on disk)
- `base_phase`: Normalized base phase (e.g., "06") - Generating slug from description
- Creating the phase directory (`.planning/phases/{N.M}-{slug}/`)
- Inserting the phase entry into ROADMAP.md after the target phase with (INSERTED) marker
Store the result: Extract from result: `phase_number`, `after_phase`, `name`, `slug`, `directory`.
```bash
decimal_phase=$(echo "$DECIMAL_INFO" | jq -r '.next')
```
Examples:
- Phase 72 with no decimals -> next is 72.1
- Phase 72 with 72.1 -> next is 72.2
- Phase 72 with 72.1, 72.2 -> next is 72.3
</step>
<step name="generate_slug">
Convert the phase description to a kebab-case slug.
Use `generate-slug` command (init phase-op provides `phase_slug` for existing phase, but this is a new phase):
```bash
slug=$(node ~/.claude/get-shit-done/bin/gsd-tools.js generate-slug "$description" --raw)
```
Phase directory name: `{decimal-phase}-{slug}`
Example: `06.1-fix-critical-auth-bug` (phase 6 insertion)
</step>
<step name="create_phase_directory">
Create the phase directory structure:
```bash
phase_dir=".planning/phases/${decimal_phase}-${slug}"
mkdir -p "$phase_dir"
```
Confirm: "Created directory: $phase_dir"
</step>
<step name="update_roadmap">
Insert the new phase entry into the roadmap:
1. Find insertion point: immediately after Phase {after_phase}'s content (before next phase heading or "---")
2. Insert new phase heading with (INSERTED) marker:
```
### Phase {decimal_phase}: {Description} (INSERTED)
**Goal:** [Urgent work - to be planned]
**Depends on:** Phase {after_phase}
**Plans:** 0 plans
Plans:
- [ ] TBD (run /gsd:plan-phase {decimal_phase} to break down)
**Details:**
[To be added during planning]
```
3. Write updated roadmap back to file
The "(INSERTED)" marker helps identify decimal phases as urgent insertions.
Preserve all other content exactly (formatting, spacing, other phases).
</step> </step>
<step name="update_project_state"> <step name="update_project_state">
Update STATE.md to reflect the inserted phase: Update STATE.md to reflect the inserted phase:
1. Read `.planning/STATE.md` 1. Read `.planning/STATE.md`
2. Under "## Accumulated Context" -> "### Roadmap Evolution" add entry: 2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry:
``` ```
- Phase {decimal_phase} inserted after Phase {after_phase}: {description} (URGENT) - Phase {decimal_phase} inserted after Phase {after_phase}: {description} (URGENT)
``` ```
If "Roadmap Evolution" section doesn't exist, create it. If "Roadmap Evolution" section doesn't exist, create it.
Add note about insertion reason if appropriate.
</step> </step>
<step name="completion"> <step name="completion">
@@ -173,7 +83,7 @@ Phase {decimal_phase} inserted after Phase {after_phase}:
- Status: Not planned yet - Status: Not planned yet
- Marker: (INSERTED) - indicates urgent work - Marker: (INSERTED) - indicates urgent work
Roadmap updated: {roadmap-path} Roadmap updated: .planning/ROADMAP.md
Project state updated: .planning/STATE.md Project state updated: .planning/STATE.md
--- ---
@@ -211,10 +121,9 @@ Project state updated: .planning/STATE.md
<success_criteria> <success_criteria>
Phase insertion is complete when: Phase insertion is complete when:
- [ ] Phase directory created: `.planning/phases/{N.M}-{slug}/` - [ ] `gsd-tools phase insert` executed successfully
- [ ] Phase directory created
- [ ] Roadmap updated with new phase entry (includes "(INSERTED)" marker) - [ ] Roadmap updated with new phase entry (includes "(INSERTED)" marker)
- [ ] Phase inserted in correct position (after target phase, before next integer phase)
- [ ] STATE.md updated with roadmap evolution note - [ ] STATE.md updated with roadmap evolution note
- [ ] Decimal number calculated correctly (based on existing decimals)
- [ ] User informed of next steps and dependency implications - [ ] User informed of next steps and dependency implications
</success_criteria> </success_criteria>

View File

@@ -50,42 +50,68 @@ All file contents are already loaded via `--include` in init_context step:
No additional file reads needed. No additional file reads needed.
</step> </step>
<step name="analyze_roadmap">
**Get comprehensive roadmap analysis (replaces manual parsing):**
```bash
ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze)
```
This returns structured JSON with:
- All phases with disk status (complete/partial/planned/empty/no_directory)
- Goal and dependencies per phase
- Plan and summary counts per phase
- Aggregated stats: total plans, summaries, progress percent
- Current and next phase identification
Use this instead of manually reading/parsing ROADMAP.md.
</step>
<step name="recent"> <step name="recent">
**Gather recent work context:** **Gather recent work context:**
- Find the 2-3 most recent SUMMARY.md files - Find the 2-3 most recent SUMMARY.md files
- Extract from each: what was accomplished, key decisions, any issues logged - Use `summary-extract` for efficient parsing:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.js summary-extract <path> --fields one_liner
```
- This shows "what we've been working on" - This shows "what we've been working on"
</step> </step>
<step name="position"> <step name="position">
**Parse current position from init context:** **Parse current position from init context and roadmap analysis:**
- Use `current_phase` and `next_phase` from init for position - Use `current_phase` and `next_phase` from roadmap analyze
- Use `phases` array for plan counts per phase - Use phase-level `has_context` and `has_research` flags from analyze
- Note `paused_at` if work was paused - Note `paused_at` if work was paused (from init context)
- Check for CONTEXT.md: For phases without PLAN.md files, check if `{phase}-CONTEXT.md` exists in phase directory
- Count pending todos: use `init todos` or `list-todos` - Count pending todos: use `init todos` or `list-todos`
- Check for active debug sessions: `ls .planning/debug/*.md 2>/dev/null | grep -v resolved | wc -l` - Check for active debug sessions: `ls .planning/debug/*.md 2>/dev/null | grep -v resolved | wc -l`
</step> </step>
<step name="report"> <step name="report">
**Present rich status report:** **Generate progress bar from gsd-tools, then present rich status report:**
```bash
# Get formatted progress bar
PROGRESS_BAR=$(node ~/.claude/get-shit-done/bin/gsd-tools.js progress bar --raw)
```
Present:
``` ```
# [Project Name] # [Project Name]
**Progress:** [████████░░] 8/10 plans complete **Progress:** {PROGRESS_BAR}
**Profile:** [quality/balanced/budget] **Profile:** [quality/balanced/budget]
## Recent Work ## Recent Work
- [Phase X, Plan Y]: [what was accomplished - 1 line] - [Phase X, Plan Y]: [what was accomplished - 1 line from summary-extract]
- [Phase X, Plan Z]: [what was accomplished - 1 line] - [Phase X, Plan Z]: [what was accomplished - 1 line from summary-extract]
## Current Position ## Current Position
Phase [N] of [total]: [phase-name] Phase [N] of [total]: [phase-name]
Plan [M] of [phase-total]: [status] Plan [M] of [phase-total]: [status]
CONTEXT: [✓ if CONTEXT.md exists | - if not] CONTEXT: [✓ if has_context | - if not]
## Key Decisions Made ## Key Decisions Made
- [decision 1 from STATE.md] - [decision 1 from STATE.md]
@@ -102,7 +128,7 @@ CONTEXT: [✓ if CONTEXT.md exists | - if not]
(Only show this section if count > 0) (Only show this section if count > 0)
## What's Next ## What's Next
[Next phase/plan objective from ROADMAP] [Next phase/plan objective from roadmap analyze]
``` ```
</step> </step>

View File

@@ -37,20 +37,6 @@ Extract: `phase_found`, `phase_dir`, `phase_number`, `commit_docs`, `roadmap_exi
Also read STATE.md and ROADMAP.md content for parsing current position. Also read STATE.md and ROADMAP.md content for parsing current position.
</step> </step>
<step name="validate_phase_exists">
Verify the target phase exists in ROADMAP.md:
1. Search for `### Phase {target}:` heading
2. If not found:
```
ERROR: Phase {target} not found in roadmap
Available phases: [list phase numbers]
```
Exit.
</step>
<step name="validate_future_phase"> <step name="validate_future_phase">
Verify the phase is a future phase (not started): Verify the phase is a future phase (not started):
@@ -70,46 +56,6 @@ To abandon current work, use /gsd:pause-work instead.
``` ```
Exit. Exit.
3. Check for SUMMARY.md files in phase directory:
```bash
ls .planning/phases/{target}-*/*-SUMMARY.md 2>/dev/null
```
If any SUMMARY.md files exist:
```
ERROR: Phase {target} has completed work
Found executed plans:
- {list of SUMMARY.md files}
Cannot remove phases with completed work.
```
Exit.
</step>
<step name="gather_phase_info">
Collect information about the phase being removed:
1. Extract phase name from ROADMAP.md heading: `### Phase {target}: {Name}`
2. Find phase directory: `.planning/phases/{target}-{slug}/`
3. Find all subsequent phases (integer and decimal) that need renumbering
**Subsequent phase detection:**
For integer phase removal (e.g., 17):
- Find all phases > 17 (integers: 18, 19, 20...)
- Find all decimal phases >= 17.0 and < 18.0 (17.1, 17.2...) → these become 16.x
- Find all decimal phases for subsequent integers (18.1, 19.1...) → renumber with their parent
For decimal phase removal (e.g., 17.1):
- Find all decimal phases > 17.1 and < 18 (17.2, 17.3...) → renumber down
- Integer phases unchanged
List all phases that will be renumbered.
</step> </step>
<step name="confirm_removal"> <step name="confirm_removal">
@@ -120,11 +66,8 @@ Removing Phase {target}: {Name}
This will: This will:
- Delete: .planning/phases/{target}-{slug}/ - Delete: .planning/phases/{target}-{slug}/
- Renumber {N} subsequent phases: - Renumber all subsequent phases
- Phase 18 → Phase 17 - Update: ROADMAP.md, STATE.md
- Phase 18.1 → Phase 17.1
- Phase 19 → Phase 18
[etc.]
Proceed? (y/n) Proceed? (y/n)
``` ```
@@ -132,110 +75,27 @@ Proceed? (y/n)
Wait for confirmation. Wait for confirmation.
</step> </step>
<step name="delete_phase_directory"> <step name="execute_removal">
Use the init context from earlier. The `phase_dir` from `init phase-op` provides the path. **Delegate the entire removal operation to gsd-tools:**
Delete the target phase directory if it exists:
```bash ```bash
# phase_dir from init phase-op contains the path if found RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase remove "${target}")
if [ -n "$phase_dir" ]; then
rm -rf "$phase_dir"
echo "Deleted: $phase_dir/"
fi
``` ```
If directory doesn't exist (`phase_found=false` from init), note: "No directory to delete (phase not yet created)" If the phase has executed plans (SUMMARY.md files), gsd-tools will error. Use `--force` only if the user confirms:
</step>
<step name="renumber_directories">
Rename all subsequent phase directories:
For each phase directory that needs renumbering (in reverse order to avoid conflicts):
```bash ```bash
# Example: renaming 18-dashboard to 17-dashboard RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase remove "${target}" --force)
mv ".planning/phases/18-dashboard" ".planning/phases/17-dashboard"
``` ```
Process in descending order (20→19, then 19→18, then 18→17) to avoid overwriting. The CLI handles:
- Deleting the phase directory
- Renumbering all subsequent directories (in reverse order to avoid conflicts)
- Renaming all files inside renumbered directories (PLAN.md, SUMMARY.md, etc.)
- Updating ROADMAP.md (removing section, renumbering all phase references, updating dependencies)
- Updating STATE.md (decrementing phase count)
Also rename decimal phase directories: Extract from result: `removed`, `directory_deleted`, `renamed_directories`, `renamed_files`, `roadmap_updated`, `state_updated`.
- `17.1-fix-bug` → `16.1-fix-bug` (if removing integer 17)
- `17.2-hotfix` → `17.1-hotfix` (if removing decimal 17.1)
</step>
<step name="rename_files_in_directories">
Rename plan files inside renumbered directories:
For each renumbered directory, rename files that contain the phase number:
```bash
# Inside 17-dashboard (was 18-dashboard):
mv "18-01-PLAN.md" "17-01-PLAN.md"
mv "18-02-PLAN.md" "17-02-PLAN.md"
mv "18-01-SUMMARY.md" "17-01-SUMMARY.md" # if exists
# etc.
```
Also handle CONTEXT.md and DISCOVERY.md (these don't have phase prefixes, so no rename needed).
</step>
<step name="update_roadmap">
Update ROADMAP.md:
1. **Remove the phase section entirely:**
- Delete from `### Phase {target}:` to the next phase heading (or section end)
2. **Remove from phase list:**
- Delete line `- [ ] **Phase {target}: {Name}**` or similar
3. **Remove from Progress table:**
- Delete the row for Phase {target}
4. **Renumber all subsequent phases:**
- `### Phase 18:` → `### Phase 17:`
- `- [ ] **Phase 18:` → `- [ ] **Phase 17:`
- Table rows: `| 18. Dashboard |` → `| 17. Dashboard |`
- Plan references: `18-01:` → `17-01:`
5. **Update dependency references:**
- `**Depends on:** Phase 18` → `**Depends on:** Phase 17`
- For the phase that depended on the removed phase:
- `**Depends on:** Phase 17` (removed) → `**Depends on:** Phase 16`
6. **Renumber decimal phases:**
- `### Phase 17.1:` → `### Phase 16.1:` (if integer 17 removed)
- Update all references consistently
Write updated ROADMAP.md.
</step>
<step name="update_state">
Update STATE.md:
1. **Update total phase count:**
- `Phase: 16 of 20` → `Phase: 16 of 19`
2. **Recalculate progress percentage:**
- New percentage based on completed plans / new total plans
Do NOT add a "Roadmap Evolution" note - the git commit is the record.
Write updated STATE.md.
</step>
<step name="update_file_contents">
Search for and update phase references inside plan files:
```bash
# Find files that reference the old phase numbers
grep -r "Phase 18" .planning/phases/17-*/ 2>/dev/null
grep -r "Phase 19" .planning/phases/18-*/ 2>/dev/null
# etc.
```
Update any internal references to reflect new numbering.
</step> </step>
<step name="commit"> <step name="commit">
@@ -256,13 +116,10 @@ Phase {target} ({original-name}) removed.
Changes: Changes:
- Deleted: .planning/phases/{target}-{slug}/ - Deleted: .planning/phases/{target}-{slug}/
- Renumbered: Phases {first-renumbered}-{last-old} → {first-renumbered-1}-{last-new} - Renumbered: {N} directories and {M} files
- Updated: ROADMAP.md, STATE.md - Updated: ROADMAP.md, STATE.md
- Committed: chore: remove phase {target} ({original-name}) - Committed: chore: remove phase {target} ({original-name})
Current roadmap: {total-remaining} phases
Current position: Phase {current} of {new-total}
--- ---
## What's Next ## What's Next
@@ -280,47 +137,18 @@ Would you like to:
<anti_patterns> <anti_patterns>
- Don't remove completed phases (have SUMMARY.md files) - Don't remove completed phases (have SUMMARY.md files) without --force
- Don't remove current or past phases - Don't remove current or past phases
- Don't leave gaps in numbering - always renumber - Don't manually renumber — use `gsd-tools phase remove` which handles all renumbering
- Don't add "removed phase" notes to STATE.md - git commit is the record - Don't add "removed phase" notes to STATE.md — git commit is the record
- Don't ask about each decimal phase - just renumber them
- Don't modify completed phase directories - Don't modify completed phase directories
</anti_patterns> </anti_patterns>
<edge_cases>
**Removing a decimal phase (e.g., 17.1):**
- Only affects other decimals in same series (17.2 → 17.1, 17.3 → 17.2)
- Integer phases unchanged
- Simpler operation
**No subsequent phases to renumber:**
- Removing the last phase (e.g., Phase 20 when that's the end)
- Just delete and update ROADMAP.md, no renumbering needed
**Phase directory doesn't exist:**
- Phase may be in ROADMAP.md but directory not created yet
- Skip directory deletion, proceed with ROADMAP.md updates
**Decimal phases under removed integer:**
- Removing Phase 17 when 17.1, 17.2 exist
- 17.1 → 16.1, 17.2 → 16.2
- They maintain their position in execution order (after current last integer)
</edge_cases>
<success_criteria> <success_criteria>
Phase removal is complete when: Phase removal is complete when:
- [ ] Target phase validated as future/unstarted - [ ] Target phase validated as future/unstarted
- [ ] Phase directory deleted (if existed) - [ ] `gsd-tools phase remove` executed successfully
- [ ] All subsequent phase directories renumbered
- [ ] Files inside directories renamed ({old}-01-PLAN.md → {new}-01-PLAN.md)
- [ ] ROADMAP.md updated (section removed, all references renumbered)
- [ ] STATE.md updated (phase count, progress percentage)
- [ ] Dependency references updated in subsequent phases
- [ ] Changes committed with descriptive message - [ ] Changes committed with descriptive message
- [ ] No gaps in phase numbering
- [ ] User informed of changes - [ ] User informed of changes
</success_criteria> </success_criteria>

View File

@@ -118,39 +118,22 @@ If found, delete them — phase is complete, handoffs are stale.
</step> </step>
<step name="update_roadmap"> <step name="update_roadmap_and_state">
Update the roadmap file: **Delegate ROADMAP.md and STATE.md updates to gsd-tools:**
```bash ```bash
ROADMAP_FILE=".planning/ROADMAP.md" TRANSITION=$(node ~/.claude/get-shit-done/bin/gsd-tools.js phase complete "${current_phase}")
``` ```
Update the file: The CLI handles:
- Marking the phase checkbox as `[x]` complete with today's date
- Updating plan count to final (e.g., "3/3 plans complete")
- Updating the Progress table (Status → Complete, adding date)
- Advancing STATE.md to next phase (Current Phase, Status → Ready to plan, Current Plan → Not started)
- Detecting if this is the last phase in the milestone
- Mark current phase: `[x] Complete` Extract from result: `completed_phase`, `plans_executed`, `next_phase`, `next_phase_name`, `is_last_phase`.
- Add completion date
- Update plan count to final (e.g., "3/3 plans complete")
- Update Progress table
- Keep next phase as `[ ] Not started`
**Example:**
```markdown
## Phases
- [x] Phase 1: Foundation (completed 2025-01-15)
- [ ] Phase 2: Authentication ← Next
- [ ] Phase 3: Core Features
## Progress
| Phase | Plans Complete | Status | Completed |
| ----------------- | -------------- | ----------- | ---------- |
| 1. Foundation | 3/3 | Complete | 2025-01-15 |
| 2. Authentication | 0/2 | Not started | - |
| 3. Core Features | 0/1 | Not started | - |
```
</step> </step>
@@ -250,61 +233,21 @@ After (Phase 2 shipped JWT auth, discovered rate limiting needed):
<step name="update_current_position_after_transition"> <step name="update_current_position_after_transition">
Update Current Position section in STATE.md to reflect phase completion and transition. **Note:** Basic position updates (Current Phase, Status, Current Plan, Last Activity) were already handled by `gsd-tools phase complete` in the update_roadmap_and_state step.
**Format:** Verify the updates are correct by reading STATE.md. If the progress bar needs updating, use:
```markdown ```bash
Phase: [next] of [total] ([Next phase name]) PROGRESS=$(node ~/.claude/get-shit-done/bin/gsd-tools.js progress bar --raw)
Plan: Not started
Status: Ready to plan
Last activity: [today] — Phase [X] complete, transitioned to Phase [X+1]
Progress: [updated progress bar]
``` ```
**Instructions:** Update the progress bar line in STATE.md with the result.
- Increment phase number to next phase
- Reset plan to "Not started"
- Set status to "Ready to plan"
- Update last activity to describe transition
- Recalculate progress bar based on completed plans
**Example — transitioning from Phase 2 to Phase 3:**
Before:
```markdown
## Current Position
Phase: 2 of 4 (Authentication)
Plan: 2 of 2 in current phase
Status: Phase complete
Last activity: 2025-01-20 — Completed 02-02-PLAN.md
Progress: ███████░░░ 60%
```
After:
```markdown
## Current Position
Phase: 3 of 4 (Core Features)
Plan: Not started
Status: Ready to plan
Last activity: 2025-01-20 — Phase 2 complete, transitioned to Phase 3
Progress: ███████░░░ 60%
```
**Step complete when:** **Step complete when:**
- [ ] Phase number incremented to next phase - [ ] Phase number incremented to next phase (done by phase complete)
- [ ] Plan status reset to "Not started" - [ ] Plan status reset to "Not started" (done by phase complete)
- [ ] Status shows "Ready to plan" - [ ] Status shows "Ready to plan" (done by phase complete)
- [ ] Last activity describes the transition
- [ ] Progress bar reflects total completed plans - [ ] Progress bar reflects total completed plans
</step> </step>
@@ -394,26 +337,20 @@ Resume file: None
**MANDATORY: Verify milestone status before presenting next steps.** **MANDATORY: Verify milestone status before presenting next steps.**
**Step 1: Read ROADMAP.md and identify phases in current milestone** **Use the transition result from `gsd-tools phase complete`:**
Read the ROADMAP.md file and extract: The `is_last_phase` field from the phase complete result tells you directly:
1. Current phase number (the phase just transitioned from) - `is_last_phase: false` → More phases remain → Go to **Route A**
2. All phase numbers in the current milestone section - `is_last_phase: true` → Milestone complete → Go to **Route B**
To find phases, look for: The `next_phase` and `next_phase_name` fields give you the next phase details.
- Phase headers: lines starting with `### Phase` or `#### Phase`
- Phase list items: lines like `- [ ] **Phase X:` or `- [x] **Phase X:`
Count total phases and identify the highest phase number in the milestone. If you need additional context, use:
```bash
ROADMAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap analyze)
```
State: "Current phase is {X}. Milestone has {N} phases (highest: {Y})." This returns all phases with goals, disk status, and completion info.
**Step 2: Route based on milestone status**
| Condition | Meaning | Action |
|-----------|---------|--------|
| current phase < highest phase | More phases remain | Go to **Route A** |
| current phase = highest phase | Milestone complete | Go to **Route B** |
--- ---