diff --git a/commands/gsd/cleanup.md b/commands/gsd/cleanup.md new file mode 100644 index 000000000..c95b2af1b --- /dev/null +++ b/commands/gsd/cleanup.md @@ -0,0 +1,18 @@ +--- +name: gsd:cleanup +description: Archive accumulated phase directories from completed milestones +--- + +Archive phase directories from completed milestones into `.planning/milestones/v{X.Y}-phases/`. + +Use when `.planning/phases/` has accumulated directories from past milestones. + + + +@~/.claude/get-shit-done/workflows/cleanup.md + + + +Follow the cleanup workflow at @~/.claude/get-shit-done/workflows/cleanup.md. +Identify completed milestones, show a dry-run summary, and archive on confirmation. + diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index a3f225a76..0eef32dbf 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -44,6 +44,7 @@ * Milestone Operations: * milestone complete Archive milestone, create MILESTONES.md * [--name ] + * [--archive-phases] Move phase dirs to milestones/vX.Y-phases/ * * Validation: * validate consistency Check phase numbering, disk/roadmap sync @@ -694,20 +695,36 @@ function cmdHistoryDigest(cwd, raw) { const phasesDir = path.join(cwd, '.planning', 'phases'); const digest = { phases: {}, decisions: [], tech_stack: new Set() }; - if (!fs.existsSync(phasesDir)) { + // Collect all phase directories: archived + current + const allPhaseDirs = []; + + // Add archived phases first (oldest milestones first) + const archived = getArchivedPhaseDirs(cwd); + for (const a of archived) { + allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone }); + } + + // Add current phases + if (fs.existsSync(phasesDir)) { + try { + const currentDirs = fs.readdirSync(phasesDir, { withFileTypes: true }) + .filter(e => e.isDirectory()) + .map(e => e.name) + .sort(); + for (const dir of currentDirs) { + allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null }); + } + } catch {} + } + + if (allPhaseDirs.length === 0) { digest.tech_stack = []; output(digest, raw); return; } try { - const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true }) - .filter(e => e.isDirectory()) - .map(e => e.name) - .sort(); - - for (const dir of phaseDirs) { - const dirPath = path.join(phasesDir, dir); + for (const { name: dir, fullPath: dirPath } of allPhaseDirs) { const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); for (const summary of summaries) { @@ -777,7 +794,7 @@ function cmdHistoryDigest(cwd, raw) { function cmdPhasesList(cwd, options, raw) { const phasesDir = path.join(cwd, '.planning', 'phases'); - const { type, phase } = options; + const { type, phase, includeArchived } = options; // If no phases directory, return empty if (!fs.existsSync(phasesDir)) { @@ -794,6 +811,14 @@ function cmdPhasesList(cwd, options, raw) { const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); let dirs = entries.filter(e => e.isDirectory()).map(e => e.name); + // Include archived phases if requested + if (includeArchived) { + const archived = getArchivedPhaseDirs(cwd); + for (const a of archived) { + dirs.push(`${a.name} [${a.milestone}]`); + } + } + // Sort numerically (handles decimals: 01, 02, 02.1, 02.2, 03) dirs.sort((a, b) => { const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0'); @@ -3338,6 +3363,22 @@ function cmdMilestoneComplete(cwd, version, options, raw) { fs.writeFileSync(statePath, stateContent, 'utf-8'); } + // Archive phase directories if requested + let phasesArchived = false; + if (options.archivePhases) { + try { + const phaseArchiveDir = path.join(archiveDir, `${version}-phases`); + fs.mkdirSync(phaseArchiveDir, { recursive: true }); + + const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name); + for (const dir of phaseDirNames) { + fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir)); + } + phasesArchived = phaseDirNames.length > 0; + } catch {} + } + const result = { version, name: milestoneName, @@ -3350,6 +3391,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) { roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)), requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)), audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)), + phases: phasesArchived, }, milestones_updated: true, state_updated: fs.existsSync(statePath), @@ -3662,14 +3704,44 @@ function resolveModelInternal(cwd, agentType) { return resolved === 'opus' ? 'inherit' : resolved; } -function findPhaseInternal(cwd, phase) { - if (!phase) return null; +function getArchivedPhaseDirs(cwd) { + const milestonesDir = path.join(cwd, '.planning', 'milestones'); + const results = []; - const phasesDir = path.join(cwd, '.planning', 'phases'); - const normalized = normalizePhaseName(phase); + if (!fs.existsSync(milestonesDir)) return results; try { - const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true }); + // Find v*-phases directories, sort newest first + const phaseDirs = milestoneEntries + .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name)) + .map(e => e.name) + .sort() + .reverse(); + + for (const archiveName of phaseDirs) { + const version = archiveName.match(/^(v[\d.]+)-phases$/)[1]; + const archivePath = path.join(milestonesDir, archiveName); + const entries = fs.readdirSync(archivePath, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); + + for (const dir of dirs) { + results.push({ + name: dir, + milestone: version, + basePath: path.join('.planning', 'milestones', archiveName), + fullPath: path.join(archivePath, dir), + }); + } + } + } catch {} + + return results; +} + +function searchPhaseInDir(baseDir, relBase, normalized) { + try { + const entries = fs.readdirSync(baseDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort(); const match = dirs.find(d => d.startsWith(normalized)); if (!match) return null; @@ -3677,7 +3749,7 @@ function findPhaseInternal(cwd, phase) { const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/); const phaseNumber = dirMatch ? dirMatch[1] : normalized; const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null; - const phaseDir = path.join(phasesDir, match); + const phaseDir = path.join(baseDir, match); const phaseFiles = fs.readdirSync(phaseDir); const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort(); @@ -3686,7 +3758,6 @@ function findPhaseInternal(cwd, phase) { const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); const hasVerification = phaseFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); - // Determine incomplete plans (plans without matching summaries) const completedPlanIds = new Set( summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '')) ); @@ -3697,7 +3768,7 @@ function findPhaseInternal(cwd, phase) { return { found: true, - directory: path.join('.planning', 'phases', match), + directory: path.join(relBase, match), phase_number: phaseNumber, phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, @@ -3713,6 +3784,43 @@ function findPhaseInternal(cwd, phase) { } } +function findPhaseInternal(cwd, phase) { + if (!phase) return null; + + const phasesDir = path.join(cwd, '.planning', 'phases'); + const normalized = normalizePhaseName(phase); + + // Search current phases first + const current = searchPhaseInDir(phasesDir, path.join('.planning', 'phases'), normalized); + if (current) return current; + + // Search archived milestone phases (newest first) + const milestonesDir = path.join(cwd, '.planning', 'milestones'); + if (!fs.existsSync(milestonesDir)) return null; + + try { + const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true }); + const archiveDirs = milestoneEntries + .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name)) + .map(e => e.name) + .sort() + .reverse(); + + for (const archiveName of archiveDirs) { + const version = archiveName.match(/^(v[\d.]+)-phases$/)[1]; + const archivePath = path.join(milestonesDir, archiveName); + const relBase = path.join('.planning', 'milestones', archiveName); + const result = searchPhaseInDir(archivePath, relBase, normalized); + if (result) { + result.archived = version; + return result; + } + } + } catch {} + + return null; +} + function getRoadmapPhaseInternal(cwd, phaseNum) { if (!phaseNum) return null; const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); @@ -4660,6 +4768,7 @@ async function main() { const options = { type: typeIndex !== -1 ? args[typeIndex + 1] : null, phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null, + includeArchived: args.includes('--include-archived'), }; cmdPhasesList(cwd, options, raw); } else { @@ -4705,8 +4814,18 @@ async function main() { const subcommand = args[1]; if (subcommand === 'complete') { const nameIndex = args.indexOf('--name'); - const milestoneName = nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null; - cmdMilestoneComplete(cwd, args[2], { name: milestoneName }, raw); + const archivePhases = args.includes('--archive-phases'); + // Collect --name value (everything after --name until next flag or end) + let milestoneName = null; + if (nameIndex !== -1) { + const nameArgs = []; + for (let i = nameIndex + 1; i < args.length; i++) { + if (args[i].startsWith('--')) break; + nameArgs.push(args[i]); + } + milestoneName = nameArgs.join(' ') || null; + } + cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases }, raw); } else { error('Unknown milestone subcommand. Available: complete'); } diff --git a/get-shit-done/workflows/audit-milestone.md b/get-shit-done/workflows/audit-milestone.md index e10c23fa1..9dcc3cabf 100644 --- a/get-shit-done/workflows/audit-milestone.md +++ b/get-shit-done/workflows/audit-milestone.md @@ -38,9 +38,10 @@ node ~/.claude/get-shit-done/bin/gsd-tools.cjs phases list For each phase directory, read the VERIFICATION.md: ```bash -cat .planning/phases/01-*/*-VERIFICATION.md -cat .planning/phases/02-*/*-VERIFICATION.md -# etc. +# For each phase, use find-phase to resolve the directory (handles archived phases) +PHASE_INFO=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs find-phase 01 --raw) +# Extract directory from JSON, then read VERIFICATION.md from that directory +# Repeat for each phase number from ROADMAP.md ``` From each VERIFICATION.md, extract: diff --git a/get-shit-done/workflows/cleanup.md b/get-shit-done/workflows/cleanup.md new file mode 100644 index 000000000..670fb93f4 --- /dev/null +++ b/get-shit-done/workflows/cleanup.md @@ -0,0 +1,152 @@ + + +Archive accumulated phase directories from completed milestones into `.planning/milestones/v{X.Y}-phases/`. Identifies which phases belong to each completed milestone, shows a dry-run summary, and moves directories on confirmation. + + + + + +1. `.planning/MILESTONES.md` +2. `.planning/milestones/` directory listing +3. `.planning/phases/` directory listing + + + + + + + +Read `.planning/MILESTONES.md` to identify completed milestones and their versions. + +```bash +cat .planning/MILESTONES.md +``` + +Extract each milestone version (e.g., v1.0, v1.1, v2.0). + +Check which milestone archive dirs already exist: + +```bash +ls -d .planning/milestones/v*-phases 2>/dev/null +``` + +Filter to milestones that do NOT already have a `-phases` archive directory. + +If all milestones already have phase archives: + +``` +All completed milestones already have phase directories archived. Nothing to clean up. +``` + +Stop here. + + + + + +For each completed milestone without a `-phases` archive, read the archived ROADMAP snapshot to determine which phases belong to it: + +```bash +cat .planning/milestones/v{X.Y}-ROADMAP.md +``` + +Extract phase numbers and names from the archived roadmap (e.g., Phase 1: Foundation, Phase 2: Auth). + +Check which of those phase directories still exist in `.planning/phases/`: + +```bash +ls -d .planning/phases/*/ 2>/dev/null +``` + +Match phase directories to milestone membership. Only include directories that still exist in `.planning/phases/`. + + + + + +Present a dry-run summary for each milestone: + +``` +## Cleanup Summary + +### v{X.Y} — {Milestone Name} +These phase directories will be archived: +- 01-foundation/ +- 02-auth/ +- 03-core-features/ + +Destination: .planning/milestones/v{X.Y}-phases/ + +### v{X.Z} — {Milestone Name} +These phase directories will be archived: +- 04-security/ +- 05-hardening/ + +Destination: .planning/milestones/v{X.Z}-phases/ +``` + +If no phase directories remain to archive (all already moved or deleted): + +``` +No phase directories found to archive. Phases may have been removed or archived previously. +``` + +Stop here. + +AskUserQuestion: "Proceed with archiving?" with options: "Yes — archive listed phases" | "Cancel" + +If "Cancel": Stop. + + + + + +For each milestone, move phase directories: + +```bash +mkdir -p .planning/milestones/v{X.Y}-phases +``` + +For each phase directory belonging to this milestone: + +```bash +mv .planning/phases/{dir} .planning/milestones/v{X.Y}-phases/ +``` + +Repeat for all milestones in the cleanup set. + + + + + +Commit the changes: + +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "chore: archive phase directories from completed milestones" --files .planning/milestones/ .planning/phases/ +``` + + + + + +``` +Archived: +{For each milestone} +- v{X.Y}: {N} phase directories → .planning/milestones/v{X.Y}-phases/ + +.planning/phases/ cleaned up. +``` + + + + + + + +- [ ] All completed milestones without existing phase archives identified +- [ ] Phase membership determined from archived ROADMAP snapshots +- [ ] Dry-run summary shown and user confirmed +- [ ] Phase directories moved to `.planning/milestones/v{X.Y}-phases/` +- [ ] Changes committed + + diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md index 4abfd64f0..e7ca51df0 100644 --- a/get-shit-done/workflows/complete-milestone.md +++ b/get-shit-done/workflows/complete-milestone.md @@ -359,7 +359,19 @@ Extract from result: `version`, `date`, `phases`, `plans`, `tasks`, `accomplishm Verify: `✅ Milestone archived to .planning/milestones/` -**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.). +**Phase archival (optional):** After archival completes, ask the user: + +AskUserQuestion(header="Archive Phases", question="Archive phase directories to milestones/?", options: "Yes — move to milestones/v[X.Y]-phases/" | "Skip — keep phases in place") + +If "Yes": move phase directories to the milestone archive: +```bash +mkdir -p .planning/milestones/v[X.Y]-phases +# For each phase directory in .planning/phases/: +mv .planning/phases/{phase-dir} .planning/milestones/v[X.Y]-phases/ +``` +Verify: `✅ Phase directories archived to .planning/milestones/v[X.Y]-phases/` + +If "Skip": Phase directories remain in `.planning/phases/` as raw execution history. Use `/gsd:cleanup` later to archive retroactively. After archival, the AI still handles: - Reorganizing ROADMAP.md with milestone grouping (requires judgment) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 6a5390a7c..89a227434 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -241,7 +241,8 @@ fi **2. Find parent UAT file:** ```bash -find .planning/phases -path "*${PARENT_PHASE}*/*-UAT.md" -type f 2>/dev/null +PARENT_INFO=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs find-phase "${PARENT_PHASE}" --raw) +# Extract directory from PARENT_INFO JSON, then find UAT file in that directory ``` **If no parent UAT found:** Skip this step (gap-closure may have been triggered by VERIFICATION.md instead). diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index b6bfcc16d..f8ea940d6 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -15,15 +15,7 @@ Read config.json for planning behavior settings. Load execution context (uses `init execute-phase` for full context, including file contents): ```bash -INIT_RAW=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs init execute-phase "${PHASE}" --include state,config) -# Large payloads are written to a tmpfile — output starts with @file:/path -if [[ "$INIT_RAW" == @file:* ]]; then - INIT_FILE="${INIT_RAW#@file:}" - INIT=$(cat "$INIT_FILE") - rm -f "$INIT_FILE" -else - INIT="$INIT_RAW" -fi +INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs init execute-phase "${PHASE}" --include state,config) ``` Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`. @@ -136,7 +128,8 @@ This IS the execution instructions. Follow exactly. If plan references CONTEXT.m ```bash -ls .planning/phases/*/SUMMARY.md 2>/dev/null | sort -r | head -2 | tail -1 +node ~/.claude/get-shit-done/bin/gsd-tools.cjs phases list --type summaries --raw +# Extract the second-to-last summary from the JSON result ``` If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness" blockers: AskUserQuestion(header="Previous Issues", options: "Proceed anyway" | "Address first" | "Review previous"). diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 46921ad54..b327f5803 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -312,6 +312,16 @@ Usage: `/gsd:set-profile budget` ### Utility Commands +**`/gsd:cleanup`** +Archive accumulated phase directories from completed milestones. + +- Identifies phases from completed milestones still in `.planning/phases/` +- Shows dry-run summary before moving anything +- Moves phase dirs to `.planning/milestones/v{X.Y}-phases/` +- Use after multiple milestones to reduce `.planning/phases/` clutter + +Usage: `/gsd:cleanup` + **`/gsd:help`** Show this command reference. @@ -347,6 +357,12 @@ Usage: `/gsd:join-discord` │ └── done/ # Completed todos ├── debug/ # Active debug sessions │ └── resolved/ # Archived resolved issues +├── milestones/ +│ ├── v1.0-ROADMAP.md # Archived roadmap snapshot +│ ├── v1.0-REQUIREMENTS.md # Archived requirements +│ └── v1.0-phases/ # Archived phase dirs (via /gsd:cleanup or --archive-phases) +│ ├── 01-foundation/ +│ └── 02-core-features/ ├── codebase/ # Codebase map (brownfield projects) │ ├── STACK.md # Languages, frameworks, dependencies │ ├── ARCHITECTURE.md # Patterns, layers, data flow