Skip to content
Open
Prev Previous commit
Next Next commit
feat: milestone-scoped commit prefixes, switch-milestone command, sta…
…tusline, and docs (#291)

Phase 5: polish for concurrent milestone execution.
- Milestone-scoped commit prefixes in execute-plan.md (v2.0/08-02)
- cmdMilestoneSwitch warns about in-progress work before switching
- /gsd:switch-milestone workflow and command
- Statusline shows active milestone in cyan [v2.0]
- new-milestone.md calls milestone create for multi-milestone mode
- Help and README updated with switch-milestone and concurrent docs
  • Loading branch information
Ethan Hurst committed Mar 3, 2026
commit 320aa9fa04ee23c1aa0fa70309cb60902ba4ba6b
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ You're never locked in. The system adapts.
| `/gsd:audit-milestone` | Verify milestone achieved its definition of done |
| `/gsd:complete-milestone` | Archive milestone, tag release |
| `/gsd:new-milestone [name]` | Start next version: questions → research → requirements → roadmap |
| `/gsd:switch-milestone <name>` | Switch active milestone for concurrent work |

### Navigation

Expand Down Expand Up @@ -522,6 +523,20 @@ You're never locked in. The system adapts.

<sup>¹ Contributed by reddit user OracleGreyBeard</sup>

### Concurrent Milestones

Work on multiple milestones simultaneously — e.g., v2.0 features + v1.5.1 hotfix:

```
/gsd:new-milestone "v1.5.1 Hotfix" # Creates milestone-scoped directory
/gsd:switch-milestone v2.0-features # Switch back to feature work
/gsd:progress # See status of active milestone
```

Each milestone gets isolated state: `STATE.md`, `ROADMAP.md`, `REQUIREMENTS.md`, `phases/` — all scoped under `.planning/milestones/<name>/`. Switch freely without losing progress.

When no second milestone exists, everything stays in `.planning/` as usual (zero behavioral change).

---

## Configuration
Expand Down
30 changes: 30 additions & 0 deletions commands/gsd/switch-milestone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
type: prompt
name: gsd:switch-milestone
description: Switch active milestone for concurrent work
argument-hint: <milestone-name>
allowed-tools:
- Read
- Bash
---

<objective>
Switch the active milestone to work on a different one concurrently.

Reads available milestones, warns about in-progress work on the current milestone, and updates the ACTIVE_MILESTONE pointer.
</objective>

<execution_context>
**Load these files NOW (before proceeding):**

- @~/.claude/get-shit-done/workflows/switch-milestone.md (main workflow)
</execution_context>

<context>
**User input:**
- Target milestone: {{milestone-name}}
</context>

<process>
Follow switch-milestone.md workflow end-to-end.
</process>
29 changes: 28 additions & 1 deletion get-shit-done/bin/lib/milestone.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,25 @@ function cmdMilestoneSwitch(cwd, name, raw) {
error(`milestone "${name}" not found in .planning/milestones/`);
}

// Check current active milestone for in-progress work
let previousMilestone = null;
let previousStatus = null;
let hasInProgress = false;
try {
previousMilestone = fs.readFileSync(activeMilestonePath, 'utf-8').trim();
if (previousMilestone && previousMilestone !== name) {
const prevStatePath = path.join(planningRoot, 'milestones', previousMilestone, 'STATE.md');
if (fs.existsSync(prevStatePath)) {
const content = fs.readFileSync(prevStatePath, 'utf-8');
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
if (statusMatch) {
previousStatus = statusMatch[1].trim();
hasInProgress = /executing|planning/i.test(previousStatus);
}
}
}
} catch {}

// Write ACTIVE_MILESTONE
fs.writeFileSync(activeMilestonePath, name, 'utf-8');

Expand All @@ -418,7 +437,15 @@ function cmdMilestoneSwitch(cwd, name, raw) {
}

const state_path = '.planning/milestones/' + name + '/STATE.md';
output({ switched: true, name, status, state_path }, raw, `switched to milestone "${name}" (${status})`);
output({
switched: true,
name,
status,
state_path,
previous_milestone: previousMilestone,
previous_status: previousStatus,
has_in_progress: hasInProgress,
}, raw, `switched to milestone "${name}" (${status})`);
}

function cmdMilestoneList(cwd, raw) {
Expand Down
7 changes: 5 additions & 2 deletions get-shit-done/workflows/execute-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ git add src/types/user.ts

**4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.

**Multi-milestone mode** (when `is_multi_milestone` is true from init JSON): prefix scope with milestone name:
`{type}({milestone}/{phase}-{plan}): {description}` — e.g., `feat(v2.0/08-02): create user registration endpoint`

**5. Record hash:**
```bash
TASK_COMMIT=$(git rev-parse --short HEAD)
Expand Down Expand Up @@ -397,15 +400,15 @@ Extract requirement IDs from the plan's frontmatter (e.g., `requirements: [AUTH-
Task code already committed per-task. Commit plan metadata:

```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase}-{plan}): complete [plan-name] plan" --files {phase_dir}/{phase}-{plan}-SUMMARY.md {state_path} {roadmap_path} {planning_base}/REQUIREMENTS.md
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({milestone_prefix}{phase}-{plan}): complete [plan-name] plan" --files {phase_dir}/{phase}-{plan}-SUMMARY.md {state_path} {roadmap_path} {planning_base}/REQUIREMENTS.md
```
</step>

<step name="update_codebase_map">
If .planning/codebase/ doesn't exist: skip.

```bash
FIRST_TASK=$(git log --oneline --grep="feat({phase}-{plan}):" --grep="fix({phase}-{plan}):" --grep="test({phase}-{plan}):" --reverse | head -1 | cut -d' ' -f1)
FIRST_TASK=$(git log --oneline --grep="({phase}-{plan}):" --reverse | head -1 | cut -d' ' -f1)
git diff --name-only ${FIRST_TASK}^..HEAD 2>/dev/null
```

Expand Down
9 changes: 9 additions & 0 deletions get-shit-done/workflows/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ Archive completed milestone and prepare for next version.

Usage: `/gsd:complete-milestone 1.0.0`

**`/gsd:switch-milestone <name>`**
Switch active milestone for concurrent work.

- Warns if current milestone has in-progress work
- Updates ACTIVE_MILESTONE pointer
- Shows status of target milestone

Usage: `/gsd:switch-milestone v1.5-hotfix`

### Progress Tracking

**`/gsd:progress`**
Expand Down
24 changes: 18 additions & 6 deletions get-shit-done/workflows/new-milestone.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ Read all files referenced by the invoking prompt's execution_context before star
- Suggest next version (v1.0 → v1.1, or v2.0 for major)
- Confirm with user

## 3b. Initialize Multi-Milestone (if applicable)

If the project already has an active milestone (check `ACTIVE_MILESTONE` file or milestones/ directory):

```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" milestone create "${VERSION_SLUG}"
```

This creates the milestone directory structure and sets `ACTIVE_MILESTONE`. Subsequent path resolution will automatically scope to the new milestone directory.

If this is the first milestone (no milestones/ directory yet), skip — legacy mode paths are fine.

## 4. Update PROJECT.md

Add/update:
Expand Down Expand Up @@ -342,12 +354,12 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create milest

**Milestone v[X.Y]: [Name]**

| Artifact | Location |
|----------------|-----------------------------|
| Project | `.planning/PROJECT.md` |
| Research | `.planning/research/` |
| Requirements | `.planning/REQUIREMENTS.md` |
| Roadmap | `.planning/ROADMAP.md` |
| Artifact | Location |
|----------------|-----------------------------------|
| Project | `.planning/PROJECT.md` |
| Research | `{planning_base}/research/` |
| Requirements | `{planning_base}/REQUIREMENTS.md` |
| Roadmap | `{planning_base}/ROADMAP.md` |

**[N] phases** | **[X] requirements** | Ready to build ✓

Expand Down
66 changes: 66 additions & 0 deletions get-shit-done/workflows/switch-milestone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<purpose>
Switch the active milestone. Shows available milestones, warns about in-progress work on the current milestone, and updates the active milestone pointer.
</purpose>

<process>

## 0. Initialize

```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init milestone-op)
```

Parse JSON for: `planning_base`.

## 1. List Available Milestones

```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" milestone list
```

Display milestones with their status. If only one milestone exists, inform user there's nothing to switch to.

## 2. Get Target Milestone

If not provided as argument, ask user which milestone to switch to using AskUserQuestion.

## 3. Switch

```bash
RESULT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" milestone switch "${TARGET}")
```

Parse JSON for: `switched`, `name`, `status`, `state_path`, `previous_milestone`, `previous_status`, `has_in_progress`.

**If `has_in_progress` is true:**

Present warning before confirming:
```
## Warning: In-Progress Work

Milestone **{previous_milestone}** has status: {previous_status}

Switching won't lose any work — you can switch back anytime.
```

## 4. Confirm

```
## Switched to: {name}

**Status:** {status}
**State:** {state_path}

---

Run `/gsd:progress` to see where this milestone stands.
```

</process>

<success_criteria>
- [ ] Available milestones shown
- [ ] In-progress warning displayed if applicable
- [ ] ACTIVE_MILESTONE updated
- [ ] User sees new milestone status
</success_criteria>
12 changes: 10 additions & 2 deletions hooks/gsd-statusline.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,20 @@ process.stdin.on('end', () => {
} catch (e) {}
}

// Active milestone (multi-milestone mode)
let milestone = '';
const activeMilestonePath = path.join(dir, '.planning', 'ACTIVE_MILESTONE');
try {
const ms = fs.readFileSync(activeMilestonePath, 'utf-8').trim();
if (ms) milestone = `\x1b[36m[${ms}]\x1b[0m │ `;
} catch {}

// Output
const dirname = path.basename(dir);
if (task) {
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ ${milestone}\x1b[2m${dirname}\x1b[0m${ctx}`);
} else {
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ ${milestone}\x1b[2m${dirname}\x1b[0m${ctx}`);
}
} catch (e) {
// Silent fail - don't break statusline on parse errors
Expand Down