diff --git a/commands/gsd/milestone-summary.md b/commands/gsd/milestone-summary.md new file mode 100644 index 000000000..e5210d6e9 --- /dev/null +++ b/commands/gsd/milestone-summary.md @@ -0,0 +1,51 @@ +--- +type: prompt +name: gsd:milestone-summary +description: Generate a comprehensive project summary from milestone artifacts for team onboarding and review +argument-hint: "[version]" +allowed-tools: + - Read + - Write + - Bash + - Grep + - Glob +--- + + +Generate a structured milestone summary for team onboarding and project review. Reads completed milestone artifacts (ROADMAP, REQUIREMENTS, CONTEXT, SUMMARY, VERIFICATION files) and produces a human-friendly overview of what was built, how, and why. + +Purpose: Enable new team members to understand a completed project by reading one document and asking follow-up questions. +Output: MILESTONE_SUMMARY written to `.planning/reports/`, presented inline, optional interactive Q&A. + + + +@~/.claude/get-shit-done/workflows/milestone-summary.md + + + +**Project files:** +- `.planning/ROADMAP.md` +- `.planning/PROJECT.md` +- `.planning/STATE.md` +- `.planning/RETROSPECTIVE.md` +- `.planning/milestones/v{version}-ROADMAP.md` (if archived) +- `.planning/milestones/v{version}-REQUIREMENTS.md` (if archived) +- `.planning/phases/*-*/` (SUMMARY.md, VERIFICATION.md, CONTEXT.md, RESEARCH.md) + +**User input:** +- Version: $ARGUMENTS (optional — defaults to current/latest milestone) + + + +Read and execute the milestone-summary workflow from @~/.claude/get-shit-done/workflows/milestone-summary.md end-to-end. + + + +- Milestone version resolved (from args, STATE.md, or archive scan) +- All available artifacts read (ROADMAP, REQUIREMENTS, CONTEXT, SUMMARY, VERIFICATION, RESEARCH, RETROSPECTIVE) +- Summary document written to `.planning/reports/MILESTONE_SUMMARY-v{version}.md` +- All 7 sections generated (Overview, Architecture, Phases, Decisions, Requirements, Tech Debt, Getting Started) +- Summary presented inline to user +- Interactive Q&A offered +- STATE.md updated + diff --git a/get-shit-done/workflows/milestone-summary.md b/get-shit-done/workflows/milestone-summary.md new file mode 100644 index 000000000..e5ef4b37e --- /dev/null +++ b/get-shit-done/workflows/milestone-summary.md @@ -0,0 +1,223 @@ +# Milestone Summary Workflow + +Generate a comprehensive, human-friendly project summary from completed milestone artifacts. +Designed for team onboarding — a new contributor can read the output and understand the entire project. + +--- + +## Step 1: Resolve Version + +```bash +VERSION="$ARGUMENTS" +``` + +If `$ARGUMENTS` is empty: +1. Check `.planning/STATE.md` for current milestone version +2. Check `.planning/milestones/` for the latest archived version +3. If neither found, check if `.planning/ROADMAP.md` exists (project may be mid-milestone) +4. If nothing found: error "No milestone found. Run /gsd:new-project or /gsd:new-milestone first." + +Set `VERSION` to the resolved version (e.g., "1.0"). + +## Step 2: Locate Artifacts + +Determine whether the milestone is **archived** or **current**: + +**Archived milestone** (`.planning/milestones/v{VERSION}-ROADMAP.md` exists): +``` +ROADMAP_PATH=".planning/milestones/v${VERSION}-ROADMAP.md" +REQUIREMENTS_PATH=".planning/milestones/v${VERSION}-REQUIREMENTS.md" +AUDIT_PATH=".planning/milestones/v${VERSION}-MILESTONE-AUDIT.md" +``` + +**Current/in-progress milestone** (no archive yet): +``` +ROADMAP_PATH=".planning/ROADMAP.md" +REQUIREMENTS_PATH=".planning/REQUIREMENTS.md" +AUDIT_PATH=".planning/v${VERSION}-MILESTONE-AUDIT.md" +``` + +Note: The audit file moves to `.planning/milestones/` on archive (per `complete-milestone` workflow). Check both locations as a fallback. + +**Always available:** +``` +PROJECT_PATH=".planning/PROJECT.md" +RETRO_PATH=".planning/RETROSPECTIVE.md" +STATE_PATH=".planning/STATE.md" +``` + +Read all files that exist. Missing files are fine — the summary adapts to what's available. + +## Step 3: Discover Phase Artifacts + +Find all phase directories: + +```bash +gsd-tools.cjs init progress +``` + +This returns phase metadata. For each phase in the milestone scope: + +- Read `{phase_dir}/{padded}-SUMMARY.md` if it exists — extract `one_liner`, `accomplishments`, `decisions` +- Read `{phase_dir}/{padded}-VERIFICATION.md` if it exists — extract status, gaps, deferred items +- Read `{phase_dir}/{padded}-CONTEXT.md` if it exists — extract key decisions from `` section +- Read `{phase_dir}/{padded}-RESEARCH.md` if it exists — note what was researched + +Track which phases have which artifacts. + +**If no phase directories exist** (empty milestone or pre-build state): skip to Step 5 and generate a minimal summary noting "No phases have been executed yet." Do not error — the summary should still capture PROJECT.md and ROADMAP.md content. + +## Step 4: Gather Git Statistics + +Try each method in order until one succeeds: + +**Method 1 — Tagged milestone** (check first): +```bash +git tag -l "v${VERSION}" | head -1 +``` +If the tag exists: +```bash +git log v${VERSION} --oneline | wc -l +git diff --stat $(git log --format=%H --reverse v${VERSION} | head -1)..v${VERSION} +``` + +**Method 2 — STATE.md date range** (if no tag): +Read STATE.md and extract the `started_at` or earliest session date. Use it as the `--since` boundary: +```bash +git log --oneline --since="" | wc -l +``` + +**Method 3 — Earliest phase commit** (if STATE.md has no date): +Find the earliest `.planning/phases/` commit: +```bash +git log --oneline --diff-filter=A -- ".planning/phases/" | tail -1 +``` +Use that commit's date as the start boundary. + +**Method 4 — Skip stats** (if none of the above work): +Report "Git statistics unavailable — no tag or date range could be determined." This is not an error — the summary continues without the Stats section. + +Extract (when available): +- Total commits in milestone +- Files changed, insertions, deletions +- Timeline (start date → end date) +- Contributors (from git log authors) + +## Step 5: Generate Summary Document + +Write to `.planning/reports/MILESTONE_SUMMARY-v${VERSION}.md`: + +```markdown +# Milestone v{VERSION} — Project Summary + +**Generated:** {date} +**Purpose:** Team onboarding and project review + +--- + +## 1. Project Overview + +{From PROJECT.md: "What This Is", core value proposition, target users} +{If mid-milestone: note which phases are complete vs in-progress} + +## 2. Architecture & Technical Decisions + +{From CONTEXT.md files across phases: key technical choices} +{From SUMMARY.md decisions: patterns, libraries, frameworks chosen} +{From PROJECT.md: tech stack if documented} + +Present as a bulleted list of decisions with brief rationale: +- **Decision:** {what was chosen} + - **Why:** {rationale from CONTEXT.md} + - **Phase:** {which phase made this decision} + +## 3. Phases Delivered + +| Phase | Name | Status | One-Liner | +|-------|------|--------|-----------| +{For each phase: number, name, status (complete/in-progress/planned), one_liner from SUMMARY.md} + +## 4. Requirements Coverage + +{From REQUIREMENTS.md: list each requirement with status} +- ✅ {Requirement met} +- ⚠️ {Requirement partially met — note gap} +- ❌ {Requirement not met — note reason} + +{If MILESTONE-AUDIT.md exists: include audit verdict} + +## 5. Key Decisions Log + +{Aggregate from all CONTEXT.md sections} +{Each decision with: ID, description, phase, rationale} + +## 6. Tech Debt & Deferred Items + +{From VERIFICATION.md files: gaps found, anti-patterns noted} +{From RETROSPECTIVE.md: lessons learned, what to improve} +{From CONTEXT.md sections: ideas parked for later} + +## 7. Getting Started + +{Entry points for new contributors:} +- **Run the project:** {from PROJECT.md or SUMMARY.md} +- **Key directories:** {from codebase structure} +- **Tests:** {test command from PROJECT.md or CLAUDE.md} +- **Where to look first:** {main entry points, core modules} + +--- + +## Stats + +- **Timeline:** {start} → {end} ({duration}) +- **Phases:** {count complete} / {count total} +- **Commits:** {count} +- **Files changed:** {count} (+{insertions} / -{deletions}) +- **Contributors:** {list} +``` + +## Step 6: Write and Commit + +**Overwrite guard:** If `.planning/reports/MILESTONE_SUMMARY-v${VERSION}.md` already exists, ask the user: +> "A milestone summary for v{VERSION} already exists. Overwrite it, or view the existing one?" +If "view": display existing file and skip to Step 8 (interactive mode). If "overwrite": proceed. + +Create the reports directory if needed: +```bash +mkdir -p .planning/reports +``` + +Write the summary, then commit: +```bash +gsd-tools.cjs commit "docs(v${VERSION}): generate milestone summary for onboarding" \ + --files ".planning/reports/MILESTONE_SUMMARY-v${VERSION}.md" +``` + +## Step 7: Present Summary + +Display the full summary document inline. + +## Step 8: Offer Interactive Mode + +After presenting the summary: + +> "Summary written to `.planning/reports/MILESTONE_SUMMARY-v{VERSION}.md`. +> +> I have full context from the build artifacts. Want to ask anything about the project? +> Architecture decisions, specific phases, requirements, tech debt — ask away." + +If the user asks questions: +- Answer from the artifacts already loaded (CONTEXT.md, SUMMARY.md, VERIFICATION.md, etc.) +- Reference specific files and decisions +- Stay grounded in what was actually built (not speculation) + +If the user is done: +- Suggest next steps: `/gsd:new-milestone`, `/gsd:progress`, or sharing the summary with the team + +## Step 9: Update STATE.md + +```bash +gsd-tools.cjs state record-session \ + --stopped-at "Milestone v${VERSION} summary generated" \ + --resume-file ".planning/reports/MILESTONE_SUMMARY-v${VERSION}.md" +``` diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 227ad1631..a08fe43a0 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -620,7 +620,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 54, `expected 54 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 55, `expected 55 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1114,7 +1114,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 54; +const EXPECTED_SKILLS = 55; const EXPECTED_AGENTS = 18; function runCopilotInstall(cwd) { diff --git a/tests/milestone-summary.test.cjs b/tests/milestone-summary.test.cjs new file mode 100644 index 000000000..327f1023c --- /dev/null +++ b/tests/milestone-summary.test.cjs @@ -0,0 +1,330 @@ +/** + * GSD Milestone Summary Tests + * + * Validates the milestone-summary command and workflow files exist + * and follow expected patterns. Tests artifact discovery logic. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const commandPath = path.join(repoRoot, 'commands', 'gsd', 'milestone-summary.md'); +const workflowPath = path.join(repoRoot, 'get-shit-done', 'workflows', 'milestone-summary.md'); + +describe('milestone-summary command', () => { + test('command file exists', () => { + assert.ok(fs.existsSync(commandPath), 'commands/gsd/milestone-summary.md should exist'); + }); + + test('command has correct frontmatter name', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok(content.includes('name: gsd:milestone-summary'), 'should have correct command name'); + }); + + test('command references workflow in execution_context', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes('workflows/milestone-summary.md'), + 'should reference the milestone-summary workflow' + ); + }); + + test('command accepts optional version argument', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok(content.includes('argument-hint'), 'should have argument-hint'); + assert.ok(content.includes('[version]'), 'version should be optional (bracketed)'); + }); +}); + +describe('milestone-summary workflow', () => { + test('workflow file exists', () => { + assert.ok(fs.existsSync(workflowPath), 'workflows/milestone-summary.md should exist'); + }); + + test('workflow reads milestone artifacts', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + const requiredArtifacts = [ + 'ROADMAP.md', + 'REQUIREMENTS.md', + 'PROJECT.md', + 'SUMMARY.md', + 'VERIFICATION.md', + 'CONTEXT.md', + 'RETROSPECTIVE.md', + ]; + for (const artifact of requiredArtifacts) { + assert.ok( + content.includes(artifact), + `workflow should reference ${artifact}` + ); + } + }); + + test('workflow writes to reports directory', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('.planning/reports/MILESTONE_SUMMARY'), + 'should write summary to .planning/reports/' + ); + }); + + test('workflow has interactive Q&A mode', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('Interactive Mode') || content.includes('ask anything'), + 'should offer interactive Q&A after summary' + ); + }); + + test('workflow handles both archived and current milestones', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok(content.includes('Archived milestone'), 'should handle archived milestones'); + assert.ok(content.includes('Current') || content.includes('in-progress'), 'should handle current milestones'); + }); + + test('workflow generates all 7 summary sections', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + const sections = [ + 'Project Overview', + 'Architecture', + 'Phases Delivered', + 'Requirements Coverage', + 'Key Decisions', + 'Tech Debt', + 'Getting Started', + ]; + for (const section of sections) { + assert.ok( + content.includes(section), + `summary should include "${section}" section` + ); + } + }); + + test('workflow updates STATE.md', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('state record-session'), + 'should update STATE.md via gsd-tools' + ); + }); + + test('workflow has overwrite guard for existing summaries', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('already exists'), + 'should check for existing summary before overwriting' + ); + }); + + test('workflow handles empty phase directories gracefully', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('no phase directories') || content.includes('No phases'), + 'should handle case where no phases exist' + ); + }); + + test('workflow checks both audit file locations for archived milestones', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('.planning/milestones/v${VERSION}-MILESTONE-AUDIT.md'), + 'should check milestones/ directory for archived audit file' + ); + }); +}); + +describe('milestone-summary command structure', () => { + test('command has success_criteria section', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes(''), + 'should have success_criteria section (follows complete-milestone pattern)' + ); + }); + + test('command context lists RESEARCH.md', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes('RESEARCH.md'), + 'should list RESEARCH.md in context block' + ); + }); +}); + +describe('milestone-summary artifact path resolution', () => { + const { createTempProject, cleanup } = require('./helpers.cjs'); + let tmpDir; + + test('archived milestone paths point to milestones/ directory', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + // Archived roadmap path should be under milestones/ + assert.ok( + content.includes('.planning/milestones/v${VERSION}-ROADMAP.md'), + 'archived ROADMAP path should be under .planning/milestones/' + ); + assert.ok( + content.includes('.planning/milestones/v${VERSION}-REQUIREMENTS.md'), + 'archived REQUIREMENTS path should be under .planning/milestones/' + ); + assert.ok( + content.includes('.planning/milestones/v${VERSION}-MILESTONE-AUDIT.md'), + 'archived AUDIT path should be under .planning/milestones/' + ); + }); + + test('current milestone paths point to .planning/ root', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + // Current milestone should read from .planning/ root + const lines = content.split('\n'); + const currentSection = lines.slice( + lines.findIndex(l => l.includes('Current/in-progress')), + lines.findIndex(l => l.includes('Current/in-progress')) + 10 + ).join('\n'); + assert.ok( + currentSection.includes('ROADMAP_PATH=".planning/ROADMAP.md"'), + 'current ROADMAP path should be at .planning/ root' + ); + }); +}); + +describe('milestone-summary fixture-based artifact discovery', () => { + const os = require('os'); + let tmpDir; + + function setup() { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ms-test-')); + } + + function teardown() { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + } + + test('discovers artifacts in archived milestone structure', () => { + setup(); + try { + // Create archived milestone structure + const milestonesDir = path.join(tmpDir, '.planning', 'milestones'); + fs.mkdirSync(milestonesDir, { recursive: true }); + fs.writeFileSync(path.join(milestonesDir, 'v1.0-ROADMAP.md'), '# Roadmap v1.0'); + fs.writeFileSync(path.join(milestonesDir, 'v1.0-REQUIREMENTS.md'), '# Reqs v1.0'); + fs.writeFileSync(path.join(milestonesDir, 'v1.0-MILESTONE-AUDIT.md'), '# Audit v1.0'); + + // Verify all 3 archived files are discoverable + const files = fs.readdirSync(milestonesDir); + assert.ok(files.includes('v1.0-ROADMAP.md'), 'archived ROADMAP should exist'); + assert.ok(files.includes('v1.0-REQUIREMENTS.md'), 'archived REQUIREMENTS should exist'); + assert.ok(files.includes('v1.0-MILESTONE-AUDIT.md'), 'archived AUDIT should exist'); + } finally { + teardown(); + } + }); + + test('discovers phase artifacts across multiple phases', () => { + setup(); + try { + // Create phase structure with varying artifact completeness + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-core'); + const phase3 = path.join(tmpDir, '.planning', 'phases', '03-ui'); + fs.mkdirSync(phase1, { recursive: true }); + fs.mkdirSync(phase2, { recursive: true }); + fs.mkdirSync(phase3, { recursive: true }); + + // Phase 1: all artifacts + fs.writeFileSync(path.join(phase1, '01-SUMMARY.md'), 'one_liner: Setup'); + fs.writeFileSync(path.join(phase1, '01-CONTEXT.md'), 'D-01'); + fs.writeFileSync(path.join(phase1, '01-VERIFICATION.md'), 'status: passed'); + fs.writeFileSync(path.join(phase1, '01-RESEARCH.md'), '# Research'); + + // Phase 2: partial artifacts (no RESEARCH, no VERIFICATION) + fs.writeFileSync(path.join(phase2, '02-SUMMARY.md'), 'one_liner: Core'); + fs.writeFileSync(path.join(phase2, '02-CONTEXT.md'), 'D-02'); + + // Phase 3: only SUMMARY + fs.writeFileSync(path.join(phase3, '03-SUMMARY.md'), 'one_liner: UI'); + + // Verify discovery + const phasesDir = path.join(tmpDir, '.planning', 'phases'); + const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true }) + .filter(e => e.isDirectory()) + .map(e => e.name); + assert.strictEqual(phaseDirs.length, 3, 'should find 3 phase directories'); + + // Phase 1 has all 4 artifact types + const p1Files = fs.readdirSync(phase1); + assert.strictEqual(p1Files.length, 4, 'phase 1 should have 4 artifacts'); + + // Phase 2 has 2 artifact types + const p2Files = fs.readdirSync(phase2); + assert.strictEqual(p2Files.length, 2, 'phase 2 should have 2 artifacts'); + + // Phase 3 has 1 artifact type + const p3Files = fs.readdirSync(phase3); + assert.strictEqual(p3Files.length, 1, 'phase 3 should have 1 artifact'); + } finally { + teardown(); + } + }); + + test('handles empty .planning directory without error', () => { + setup(); + try { + const planningDir = path.join(tmpDir, '.planning'); + fs.mkdirSync(planningDir, { recursive: true }); + + // No milestones, no phases — just empty .planning/ + const contents = fs.readdirSync(planningDir); + assert.strictEqual(contents.length, 0, 'empty .planning/ should have no contents'); + + // Should not throw when checking for milestones dir + const milestonesExists = fs.existsSync(path.join(planningDir, 'milestones')); + assert.strictEqual(milestonesExists, false, 'milestones/ should not exist'); + + const phasesExists = fs.existsSync(path.join(planningDir, 'phases')); + assert.strictEqual(phasesExists, false, 'phases/ should not exist'); + } finally { + teardown(); + } + }); + + test('output path pattern produces valid filenames', () => { + const versions = ['1.0', '1.1', '2.0', '0.1']; + for (const v of versions) { + const filename = `MILESTONE_SUMMARY-v${v}.md`; + assert.ok( + /^MILESTONE_SUMMARY-v\d+\.\d+\.md$/.test(filename), + `"${filename}" should be a valid milestone summary filename` + ); + } + }); +}); + +describe('milestone-summary git stats resilience', () => { + test('workflow has fallback methods when tag does not exist', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('Method 1') && content.includes('Method 2'), + 'should have multiple fallback methods for git stats' + ); + }); + + test('workflow can skip stats gracefully', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('Skip stats') || content.includes('statistics unavailable'), + 'should handle case where git stats cannot be gathered' + ); + }); + + test('command has type: prompt in frontmatter', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes('type: prompt'), + 'should have type: prompt for consistency with complete-milestone.md' + ); + }); +});