feat(milestones): support safe phase-number resets
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 <version>`**
|
||||
Archive completed milestone and prepare for next version.
|
||||
|
||||
@@ -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="
|
||||
|
||||
<instructions>
|
||||
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]`
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user