Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Analysis paralysis guard in agents to prevent over-deliberation during planning
- Exhaustive cross-check and task-level TDD patterns in agent workflows
- Code-aware discuss phase with codebase scouting — `/gsd:discuss-phase` now analyzes relevant source files before asking questions
- Concurrent milestone execution: work on multiple milestones in parallel with isolated state (#291)
- Milestone-scoped directories under `.planning/milestones/<name>/`
- `ACTIVE_MILESTONE` pointer file for switching context
- `/gsd:switch-milestone` command with in-progress work warnings
- `--milestone` CLI flag for explicit milestone targeting
- Statusline shows active milestone in multi-milestone mode
- All 28 workflow files updated for milestone-aware paths
- Zero behavioral change for single-milestone projects (legacy mode)

### Fixed
- Update checker clears both cache paths to prevent stale version notifications
Expand Down
16 changes: 16 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 @@ -516,11 +517,26 @@ You're never locked in. The system adapts.
| `/gsd:add-todo [desc]` | Capture idea for later |
| `/gsd:check-todos` | List pending todos |
| `/gsd:debug [desc]` | Systematic debugging with persistent state |
| `/gsd:add-tests <N> [instructions]` | Generate unit and E2E tests for completed phase |
| `/gsd:quick [--full]` | Execute ad-hoc task with GSD guarantees (`--full` adds plan-checking and verification) |
| `/gsd:health [--repair]` | Validate `.planning/` directory integrity, auto-repair with `--repair` |

<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>
60 changes: 55 additions & 5 deletions docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ rapid prototyping phases where test infrastructure isn't the focus.
| `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone |
| `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified |
| `/gsd:new-milestone [name]` | Start next version cycle | After completing a milestone |
| `/gsd:switch-milestone <name>` | Switch active milestone for concurrent work | When working on multiple milestones |

### Navigation

Expand Down Expand Up @@ -202,6 +203,7 @@ rapid prototyping phases where test infrastructure isn't the focus.
| `/gsd:check-todos` | List pending todos | Review captured ideas |
| `/gsd:settings` | Configure workflow toggles and model profile | Change model, toggle agents |
| `/gsd:set-profile <profile>` | Quick profile switch | Change cost/quality tradeoff |
| `/gsd:add-tests <N> [instructions]` | Generate unit and E2E tests for completed phase | After execution, before milestone completion |
| `/gsd:reapply-patches` | Restore local modifications after update | After `/gsd:update` if you had local edits |

---
Expand Down Expand Up @@ -380,6 +382,38 @@ claude --dangerously-skip-permissions
/gsd:remove-phase 7 # Descope phase 7 and renumber
```

### 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 under `.planning/milestones/<name>/`:

```
.planning/
├── PROJECT.md # Global (shared)
├── MILESTONES.md # Global (shared)
├── ACTIVE_MILESTONE # Pointer: "v2.0"
├── milestones/
│ ├── v2.0/
│ │ ├── STATE.md
│ │ ├── ROADMAP.md
│ │ ├── REQUIREMENTS.md
│ │ ├── config.json
│ │ └── phases/
│ └── v1.5.1-hotfix/
│ ├── STATE.md
│ ├── ROADMAP.md
│ └── phases/
```

When no second milestone exists, everything stays in `.planning/` as usual.

---

## Troubleshooting
Expand Down Expand Up @@ -449,23 +483,39 @@ For reference, here is what GSD creates in your project:
```
.planning/
PROJECT.md # Project vision and context (always loaded)
REQUIREMENTS.md # Scoped v1/v2 requirements with IDs
ROADMAP.md # Phase breakdown with status tracking
STATE.md # Decisions, blockers, session memory
config.json # Workflow configuration
MILESTONES.md # Completed milestone archive
MILESTONES.md # Completed milestone archive (global, shared)
ACTIVE_MILESTONE # Active milestone pointer (multi-milestone mode only)
research/ # Domain research from /gsd:new-project
todos/
pending/ # Captured ideas awaiting work
done/ # Completed todos
debug/ # Active debug sessions
resolved/ # Archived debug sessions
codebase/ # Brownfield codebase mapping (from /gsd:map-codebase)

# Single-milestone layout (default):
REQUIREMENTS.md # Scoped v1/v2 requirements with IDs
ROADMAP.md # Phase breakdown with status tracking
STATE.md # Decisions, blockers, session memory
config.json # Workflow configuration
phases/
XX-phase-name/
XX-YY-PLAN.md # Atomic execution plans
XX-YY-SUMMARY.md # Execution outcomes and decisions
CONTEXT.md # Your implementation preferences
RESEARCH.md # Ecosystem research findings
VERIFICATION.md # Post-execution verification results

# Multi-milestone layout (when concurrent milestones exist):
milestones/
v2.0/
STATE.md
ROADMAP.md
REQUIREMENTS.md
config.json
phases/
v1.5.1-hotfix/
STATE.md
ROADMAP.md
phases/
```
26 changes: 25 additions & 1 deletion get-shit-done/bin/gsd-tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
const fs = require('fs');
const path = require('path');
const { error } = require('./lib/core.cjs');
const { setMilestoneOverride } = require('./lib/paths.cjs');
const state = require('./lib/state.cjs');
const phase = require('./lib/phase.cjs');
const roadmap = require('./lib/roadmap.cjs');
Expand Down Expand Up @@ -165,6 +166,21 @@ async function main() {
error(`Invalid --cwd: ${cwd}`);
}

// Optional --milestone override for multi-milestone support
const msEqArg = args.find(arg => arg.startsWith('--milestone='));
const msIdx = args.indexOf('--milestone');
if (msEqArg) {
const value = msEqArg.slice('--milestone='.length).trim();
if (!value) error('Missing value for --milestone');
args.splice(args.indexOf(msEqArg), 1);
setMilestoneOverride(value);
} else if (msIdx !== -1) {
const value = args[msIdx + 1];
if (!value || value.startsWith('--')) error('Missing value for --milestone');
args.splice(msIdx, 2);
setMilestoneOverride(value);
}

const rawIndex = args.indexOf('--raw');
const raw = rawIndex !== -1;
if (rawIndex !== -1) args.splice(rawIndex, 1);
Expand Down Expand Up @@ -459,8 +475,16 @@ async function main() {
milestoneName = nameArgs.join(' ') || null;
}
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases }, raw);
} else if (subcommand === 'create') {
milestone.cmdMilestoneCreate(cwd, args[2], raw);
} else if (subcommand === 'switch') {
milestone.cmdMilestoneSwitch(cwd, args[2], raw);
} else if (subcommand === 'list') {
milestone.cmdMilestoneList(cwd, raw);
} else if (subcommand === 'status') {
milestone.cmdMilestoneStatus(cwd, raw);
} else {
error('Unknown milestone subcommand. Available: complete');
error('Unknown milestone subcommand. Available: complete, create, switch, list, status');
}
break;
}
Expand Down
20 changes: 12 additions & 8 deletions get-shit-done/bin/lib/commands.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const path = require('path');
const { execSync } = require('child_process');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, output, error, findPhaseInternal } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { resolvePlanningPaths } = require('./paths.cjs');

function cmdGenerateSlug(text, raw) {
if (!text) {
Expand Down Expand Up @@ -42,7 +43,8 @@ function cmdCurrentTimestamp(format, raw) {
}

function cmdListTodos(cwd, area, raw) {
const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
const paths = resolvePlanningPaths(cwd);
const pendingDir = path.join(paths.abs.planningRoot, 'todos', 'pending');

let count = 0;
const todos = [];
Expand All @@ -68,7 +70,7 @@ function cmdListTodos(cwd, area, raw) {
created: createdMatch ? createdMatch[1].trim() : 'unknown',
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
area: todoArea,
path: path.join('.planning', 'todos', 'pending', file),
path: '.planning/todos/pending/' + file,
});
} catch {}
}
Expand Down Expand Up @@ -97,7 +99,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
}

function cmdHistoryDigest(cwd, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = resolvePlanningPaths(cwd).abs.phases;
const digest = { phases: {}, decisions: [], tech_stack: new Set() };

// Collect all phase directories: archived + current
Expand Down Expand Up @@ -380,8 +382,9 @@ async function cmdWebsearch(query, options, raw) {
}

function cmdProgressRender(cwd, format, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const paths = resolvePlanningPaths(cwd);
const phasesDir = paths.abs.phases;
const roadmapPath = paths.abs.roadmap;
const milestone = getMilestoneInfo(cwd);

const phases = [];
Expand Down Expand Up @@ -452,8 +455,9 @@ function cmdTodoComplete(cwd, filename, raw) {
error('filename required for todo complete');
}

const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
const completedDir = path.join(cwd, '.planning', 'todos', 'completed');
const planningRoot = resolvePlanningPaths(cwd).abs.planningRoot;
const pendingDir = path.join(planningRoot, 'todos', 'pending');
const completedDir = path.join(planningRoot, 'todos', 'completed');
const sourcePath = path.join(pendingDir, filename);

if (!fs.existsSync(sourcePath)) {
Expand Down Expand Up @@ -511,7 +515,7 @@ function cmdScaffold(cwd, type, options, raw) {
}
const slug = generateSlugInternal(name);
const dirName = `${padded}-${slug}`;
const phasesParent = path.join(cwd, '.planning', 'phases');
const phasesParent = resolvePlanningPaths(cwd).abs.phases;
fs.mkdirSync(phasesParent, { recursive: true });
const dirPath = path.join(phasesParent, dirName);
fs.mkdirSync(dirPath, { recursive: true });
Expand Down
12 changes: 7 additions & 5 deletions get-shit-done/bin/lib/config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
const fs = require('fs');
const path = require('path');
const { output, error } = require('./core.cjs');
const { resolvePlanningPaths } = require('./paths.cjs');

function cmdConfigEnsureSection(cwd, raw) {
const configPath = path.join(cwd, '.planning', 'config.json');
const planningDir = path.join(cwd, '.planning');
const paths = resolvePlanningPaths(cwd);
const configPath = paths.abs.config;
const planningDir = paths.abs.base;

// Ensure .planning directory exists
try {
Expand Down Expand Up @@ -67,15 +69,15 @@ function cmdConfigEnsureSection(cwd, raw) {

try {
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
const result = { created: true, path: '.planning/config.json' };
const result = { created: true, path: paths.rel.config };
output(result, raw, 'created');
} catch (err) {
error('Failed to create config.json: ' + err.message);
}
}

function cmdConfigSet(cwd, keyPath, value, raw) {
const configPath = path.join(cwd, '.planning', 'config.json');
const configPath = resolvePlanningPaths(cwd).abs.config;

if (!keyPath) {
error('Usage: config-set <key.path> <value>');
Expand Down Expand Up @@ -120,7 +122,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
}

function cmdConfigGet(cwd, keyPath, raw) {
const configPath = path.join(cwd, '.planning', 'config.json');
const configPath = resolvePlanningPaths(cwd).abs.config;

if (!keyPath) {
error('Usage: config-get <key.path>');
Expand Down
Loading