From 52b2d390ccfc76729bfbe69a0a3c1389cbddafba Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 17 Mar 2026 11:00:24 -0400 Subject: [PATCH] feat(milestones): support safe phase-number resets --- docs/COMMANDS.md | 2 ++ get-shit-done/bin/lib/init.cjs | 32 ++++++++++++++++++++ get-shit-done/workflows/help.md | 2 ++ get-shit-done/workflows/new-milestone.md | 38 +++++++++++++++++++++--- tests/init.test.cjs | 29 ++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..750fca646 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -183,6 +183,7 @@ Start next version cycle. | Argument | Required | Description | |----------|----------|-------------| | `name` | No | Milestone name | +| `--reset-phase-numbers` | No | Restart the new milestone at Phase 1 and archive old phase dirs before roadmapping | **Prerequisites:** Previous milestone completed **Produces:** Updated `PROJECT.md`, new `REQUIREMENTS.md`, new `ROADMAP.md` @@ -190,6 +191,7 @@ Start next version cycle. ```bash /gsd:new-milestone # Interactive /gsd:new-milestone "v2.0 Mobile" # Named milestone +/gsd:new-milestone --reset-phase-numbers "v2.0 Mobile" # Restart milestone numbering at 1 ``` --- diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..fd8d48de1 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -7,6 +7,23 @@ const path = require('path'); const { execSync } = require('child_process'); const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); +function getLatestCompletedMilestone(cwd) { + const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); + if (!fs.existsSync(milestonesPath)) return null; + + try { + const content = fs.readFileSync(milestonesPath, 'utf-8'); + const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m); + if (!match) return null; + return { + version: match[1], + name: match[2].trim(), + }; + } catch { + return null; + } +} + function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { error('phase required for init execute-phase'); @@ -221,6 +238,17 @@ function cmdInitNewProject(cwd, raw) { function cmdInitNewMilestone(cwd, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); + const latestCompleted = getLatestCompletedMilestone(cwd); + const phasesDir = path.join(cwd, '.planning', 'phases'); + let phaseDirCount = 0; + + try { + if (fs.existsSync(phasesDir)) { + phaseDirCount = fs.readdirSync(phasesDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .length; + } + } catch {} const result = { // Models @@ -235,6 +263,10 @@ function cmdInitNewMilestone(cwd, raw) { // Current milestone current_milestone: milestone.version, current_milestone_name: milestone.name, + latest_completed_milestone: latestCompleted?.version || null, + latest_completed_milestone_name: latestCompleted?.name || null, + phase_dir_count: phaseDirCount, + phase_archive_path: latestCompleted ? `.planning/milestones/${latestCompleted.version}-phases` : null, // File existence project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 058d4a810..d1af5c4d6 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -192,10 +192,12 @@ Start a new milestone through unified flow. - Optional domain research (spawns 4 parallel researcher agents) - Requirements definition with scoping - Roadmap creation with phase breakdown +- Optional `--reset-phase-numbers` flag restarts numbering at Phase 1 and archives old phase dirs first for safety Mirrors `/gsd:new-project` flow for brownfield projects (existing PROJECT.md). Usage: `/gsd:new-milestone "v2.0 Features"` +Usage: `/gsd:new-milestone --reset-phase-numbers "v2.0 Features"` **`/gsd:complete-milestone `** Archive completed milestone and prepare for next version. diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 59b894bbb..0b818719a 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -14,6 +14,12 @@ Read all files referenced by the invoking prompt's execution_context before star ## 1. Load Context +Parse `$ARGUMENTS` before doing anything else: +- `--reset-phase-numbers` flag → opt into restarting roadmap phase numbering at `1` +- remaining text → use as milestone name if present + +If the flag is absent, keep the current behavior of continuing phase numbering from the previous milestone. + - Read PROJECT.md (existing project, validated requirements, decisions) - Read MILESTONES.md (what shipped previously) - Read STATE.md (pending todos, blockers) @@ -82,7 +88,27 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`. +Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`. + +## 7.5 Reset-phase safety (only when `--reset-phase-numbers`) + +If `--reset-phase-numbers` is active: + +1. Set starting phase number to `1` for the upcoming roadmap. +2. If `phase_dir_count > 0`, archive the old phase directories before roadmapping so new `01-*` / `02-*` directories cannot collide with stale milestone directories. + +If `phase_dir_count > 0` and `phase_archive_path` is available: + +```bash +mkdir -p "${phase_archive_path}" +find .planning/phases -mindepth 1 -maxdepth 1 -type d -exec mv {} "${phase_archive_path}/" \; +``` + +Then verify `.planning/phases/` no longer contains old milestone directories before continuing. + +If `phase_dir_count > 0` but `phase_archive_path` is missing: +- Stop and explain that reset numbering is unsafe without a completed milestone archive target. +- Tell the user to complete/archive the previous milestone first, then rerun `/gsd:new-milestone --reset-phase-numbers`. ## 8. Research Decision @@ -270,7 +296,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milest ◆ Spawning roadmapper... ``` -**Starting phase number:** Read MILESTONES.md for last phase number. Continue from there (v1.0 ended at phase 5 → v1.1 starts at phase 6). +**Starting phase number:** +- If `--reset-phase-numbers` is active, start at **Phase 1** +- Otherwise, continue from the previous milestone's last phase number (v1.0 ended at phase 5 → v1.1 starts at phase 6) ``` Task(prompt=" @@ -286,7 +314,9 @@ Task(prompt=" Create roadmap for milestone v[X.Y]: -1. Start phase numbering from [N] +1. Respect the selected numbering mode: + - `--reset-phase-numbers` → start at Phase 1 + - default behavior → continue from the previous milestone's last phase number 2. Derive phases from THIS MILESTONE's requirements only 3. Map every requirement to exactly one phase 4. Derive 2-5 success criteria per phase (observable user behaviors) @@ -378,7 +408,7 @@ Also: `/gsd:plan-phase [N]` — skip discussion, plan directly - [ ] gsd-roadmapper spawned with phase numbering context - [ ] Roadmap files written immediately (not draft) - [ ] User feedback incorporated (if any) -- [ ] ROADMAP.md phases continue from previous milestone +- [ ] Phase numbering mode respected (continued or reset) - [ ] All commits made (if planning docs committed) - [ ] User knows next step: `/gsd:discuss-phase [N]` diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 66a645d7d..5e67a7018 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -907,6 +907,35 @@ describe('cmdInitNewMilestone', () => { assert.strictEqual(output2.roadmap_exists, true); assert.strictEqual(output2.project_exists, true); }); + + test('reports latest completed milestone and archive target for reset flow', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'MILESTONES.md'), + '# Milestones\n\n## v1.2 Search Refresh (Shipped: 2026-02-18)\n\n---\n' + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-refine-search'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-polish'), { recursive: true }); + + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, 'v1.2'); + assert.strictEqual(output.latest_completed_milestone_name, 'Search Refresh'); + assert.strictEqual(output.phase_dir_count, 2); + assert.strictEqual(output.phase_archive_path, '.planning/milestones/v1.2-phases'); + }); + + test('reset flow metadata is null-safe when no milestones file exists', () => { + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, null); + assert.strictEqual(output.latest_completed_milestone_name, null); + assert.strictEqual(output.phase_dir_count, 0); + assert.strictEqual(output.phase_archive_path, null); + }); }); // ─────────────────────────────────────────────────────────────────────────────