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