diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b5bf9c88..59c18f86e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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//` + - `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 diff --git a/README.md b/README.md index 91332b8cef..7f617abb75 100644 --- a/README.md +++ b/README.md @@ -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 ` | Switch active milestone for concurrent work | ### Navigation @@ -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 [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` | ¹ Contributed by reddit user OracleGreyBeard +### 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//`. Switch freely without losing progress. + +When no second milestone exists, everything stays in `.planning/` as usual (zero behavioral change). + --- ## Configuration diff --git a/commands/gsd/switch-milestone.md b/commands/gsd/switch-milestone.md new file mode 100644 index 0000000000..4ca60c4af9 --- /dev/null +++ b/commands/gsd/switch-milestone.md @@ -0,0 +1,30 @@ +--- +type: prompt +name: gsd:switch-milestone +description: Switch active milestone for concurrent work +argument-hint: +allowed-tools: + - Read + - Bash +--- + + +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. + + + +**Load these files NOW (before proceeding):** + +- @~/.claude/get-shit-done/workflows/switch-milestone.md (main workflow) + + + +**User input:** +- Target milestone: {{milestone-name}} + + + +Follow switch-milestone.md workflow end-to-end. + diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 2d02cafd45..47ce46988b 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -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 ` | Switch active milestone for concurrent work | When working on multiple milestones | ### Navigation @@ -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 ` | Quick profile switch | Change cost/quality tradeoff | +| `/gsd:add-tests [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 | --- @@ -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//`: + +``` +.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 @@ -449,11 +483,8 @@ 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 @@ -461,6 +492,12 @@ For reference, here is what GSD creates in your project: 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 @@ -468,4 +505,17 @@ For reference, here is what GSD creates in your project: 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/ ``` diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index fa404eb491..ddfc5596ab 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -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'); @@ -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); @@ -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; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 829ba993a2..01c495ba14 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -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) { @@ -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 = []; @@ -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 {} } @@ -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 @@ -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 = []; @@ -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)) { @@ -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 }); diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 0d9a9260df..583bdcc6fd 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -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 { @@ -67,7 +69,7 @@ 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); @@ -75,7 +77,7 @@ function cmdConfigEnsureSection(cwd, raw) { } 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 '); @@ -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 '); diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 6ef6ccb2a1..7d26a96341 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const { resolvePlanningPaths } = require('./paths.cjs'); // ─── Path helpers ──────────────────────────────────────────────────────────── @@ -64,8 +65,9 @@ function safeReadFile(filePath) { } } -function loadConfig(cwd) { - const configPath = path.join(cwd, '.planning', 'config.json'); +function loadConfig(cwd, paths) { + const p = paths || resolvePlanningPaths(cwd); + const configPath = p.abs.config; const defaults = { model_profile: 'balanced', commit_docs: true, @@ -243,18 +245,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) { } } -function findPhaseInternal(cwd, phase) { +function findPhaseInternal(cwd, phase, paths) { if (!phase) return null; - const phasesDir = path.join(cwd, '.planning', 'phases'); + const p = paths || resolvePlanningPaths(cwd); + const phasesDir = p.abs.phases; const normalized = normalizePhaseName(phase); // Search current phases first - const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized); + const current = searchPhaseInDir(phasesDir, p.rel.phases, normalized); if (current) return current; // Search archived milestone phases (newest first) - const milestonesDir = path.join(cwd, '.planning', 'milestones'); + const milestonesDir = p.global.abs.milestonesDir; if (!fs.existsSync(milestonesDir)) return null; try { @@ -280,8 +283,9 @@ function findPhaseInternal(cwd, phase) { return null; } -function getArchivedPhaseDirs(cwd) { - const milestonesDir = path.join(cwd, '.planning', 'milestones'); +function getArchivedPhaseDirs(cwd, paths) { + const p = paths || resolvePlanningPaths(cwd); + const milestonesDir = p.global.abs.milestonesDir; const results = []; if (!fs.existsSync(milestonesDir)) return results; @@ -317,9 +321,10 @@ function getArchivedPhaseDirs(cwd) { // ─── Roadmap & model utilities ──────────────────────────────────────────────── -function getRoadmapPhaseInternal(cwd, phaseNum) { +function getRoadmapPhaseInternal(cwd, phaseNum, paths) { if (!phaseNum) return null; - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const p = paths || resolvePlanningPaths(cwd); + const roadmapPath = p.abs.roadmap; if (!fs.existsSync(roadmapPath)) return null; try { @@ -385,9 +390,10 @@ function generateSlugInternal(text) { return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); } -function getMilestoneInfo(cwd) { +function getMilestoneInfo(cwd, paths) { try { - const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'); + const p = paths || resolvePlanningPaths(cwd); + const roadmap = fs.readFileSync(p.abs.roadmap, 'utf-8'); // Strip
...
blocks so shipped milestones don't interfere const cleaned = roadmap.replace(/
[\s\S]*?<\/details>/gi, ''); // Extract version and name from the same ## heading for consistency @@ -429,4 +435,5 @@ module.exports = { generateSlugInternal, getMilestoneInfo, toPosixPath, + resolvePlanningPaths, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 7e551a01fb..d1cd5d68a9 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -6,17 +6,19 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { error('phase required for init execute-phase'); } - const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); - const milestone = getMilestoneInfo(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const phaseInfo = findPhaseInternal(cwd, phase, paths); + const milestone = getMilestoneInfo(cwd, paths); - const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase, paths); const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ') @@ -66,15 +68,18 @@ function cmdInitExecutePhase(cwd, phase, raw) { milestone_version: milestone.version, milestone_name: milestone.name, milestone_slug: generateSlugInternal(milestone.name), + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, // File existence - state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), - config_exists: pathExistsInternal(cwd, '.planning/config.json'), + state_exists: pathExistsInternal(cwd, paths.rel.state), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), + config_exists: pathExistsInternal(cwd, paths.rel.config), // File paths - state_path: '.planning/STATE.md', - roadmap_path: '.planning/ROADMAP.md', - config_path: '.planning/config.json', + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, + config_path: paths.rel.config, }; output(result, raw); @@ -85,10 +90,11 @@ function cmdInitPlanPhase(cwd, phase, raw) { error('phase required for init plan-phase'); } - const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const phaseInfo = findPhaseInternal(cwd, phase, paths); - const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase, paths); const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ') @@ -124,12 +130,17 @@ function cmdInitPlanPhase(cwd, phase, raw) { // Environment planning_exists: pathExistsInternal(cwd, '.planning'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), + + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, // File paths - state_path: '.planning/STATE.md', - roadmap_path: '.planning/ROADMAP.md', - requirements_path: '.planning/REQUIREMENTS.md', + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, + requirements_path: paths.rel.requirements, }; if (phaseInfo?.directory) { @@ -160,7 +171,8 @@ function cmdInitPlanPhase(cwd, phase, raw) { } function cmdInitNewProject(cwd, raw) { - const config = loadConfig(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); // Detect Brave Search API key availability const homedir = require('os').homedir(); @@ -211,6 +223,11 @@ function cmdInitNewProject(cwd, raw) { // Enhanced search brave_search_available: hasBraveSearch, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File paths project_path: '.planning/PROJECT.md', }; @@ -219,8 +236,9 @@ function cmdInitNewProject(cwd, raw) { } function cmdInitNewMilestone(cwd, raw) { - const config = loadConfig(cwd); - const milestone = getMilestoneInfo(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const milestone = getMilestoneInfo(cwd, paths); const result = { // Models @@ -236,22 +254,28 @@ function cmdInitNewMilestone(cwd, raw) { current_milestone: milestone.version, current_milestone_name: milestone.name, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File existence project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), - state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), + state_exists: pathExistsInternal(cwd, paths.rel.state), // File paths project_path: '.planning/PROJECT.md', - roadmap_path: '.planning/ROADMAP.md', - state_path: '.planning/STATE.md', + roadmap_path: paths.rel.roadmap, + state_path: paths.rel.state, }; output(result, raw); } function cmdInitQuick(cwd, description, raw) { - const config = loadConfig(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); const now = new Date(); const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null; @@ -291,8 +315,13 @@ function cmdInitQuick(cwd, description, raw) { quick_dir: '.planning/quick', task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File existence - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), planning_exists: pathExistsInternal(cwd, '.planning'), }; @@ -301,7 +330,8 @@ function cmdInitQuick(cwd, description, raw) { } function cmdInitResume(cwd, raw) { - const config = loadConfig(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); // Check for interrupted agent let interruptedAgentId = null; @@ -311,16 +341,21 @@ function cmdInitResume(cwd, raw) { const result = { // File existence - state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), + state_exists: pathExistsInternal(cwd, paths.rel.state), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), planning_exists: pathExistsInternal(cwd, '.planning'), // File paths - state_path: '.planning/STATE.md', - roadmap_path: '.planning/ROADMAP.md', + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, project_path: '.planning/PROJECT.md', + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // Agent state has_interrupted_agent: !!interruptedAgentId, interrupted_agent_id: interruptedAgentId, @@ -337,8 +372,9 @@ function cmdInitVerifyWork(cwd, phase, raw) { error('phase required for init verify-work'); } - const config = loadConfig(cwd); - const phaseInfo = findPhaseInternal(cwd, phase); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const phaseInfo = findPhaseInternal(cwd, phase, paths); const result = { // Models @@ -354,6 +390,11 @@ function cmdInitVerifyWork(cwd, phase, raw) { phase_number: phaseInfo?.phase_number || null, phase_name: phaseInfo?.phase_name || null, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // Existing artifacts has_verification: phaseInfo?.has_verification || false, }; @@ -362,12 +403,13 @@ function cmdInitVerifyWork(cwd, phase, raw) { } function cmdInitPhaseOp(cwd, phase, raw) { - const config = loadConfig(cwd); - let phaseInfo = findPhaseInternal(cwd, phase); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + let phaseInfo = findPhaseInternal(cwd, phase, paths); // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD) if (!phaseInfo) { - const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase, paths); if (roadmapPhase?.found) { const phaseName = roadmapPhase.phase_name; phaseInfo = { @@ -406,14 +448,19 @@ function cmdInitPhaseOp(cwd, phase, raw) { has_verification: phaseInfo?.has_verification || false, plan_count: phaseInfo?.plans?.length || 0, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File existence - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), planning_exists: pathExistsInternal(cwd, '.planning'), // File paths - state_path: '.planning/STATE.md', - roadmap_path: '.planning/ROADMAP.md', - requirements_path: '.planning/REQUIREMENTS.md', + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, + requirements_path: paths.rel.requirements, }; if (phaseInfo?.directory) { @@ -443,7 +490,8 @@ function cmdInitPhaseOp(cwd, phase, raw) { } function cmdInitTodos(cwd, area, raw) { - const config = loadConfig(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); const now = new Date(); // List todos (reuse existing logic) @@ -492,6 +540,11 @@ function cmdInitTodos(cwd, area, raw) { pending_dir: '.planning/todos/pending', completed_dir: '.planning/todos/completed', + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File existence planning_exists: pathExistsInternal(cwd, '.planning'), todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'), @@ -502,13 +555,14 @@ function cmdInitTodos(cwd, area, raw) { } function cmdInitMilestoneOp(cwd, raw) { - const config = loadConfig(cwd); - const milestone = getMilestoneInfo(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const milestone = getMilestoneInfo(cwd, paths); // Count phases let phaseCount = 0; let completedPhases = 0; - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = paths.abs.phases; try { const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); @@ -542,6 +596,11 @@ function cmdInitMilestoneOp(cwd, raw) { milestone_name: milestone.name, milestone_slug: generateSlugInternal(milestone.name), + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // Phase counts phase_count: phaseCount, completed_phases: completedPhases, @@ -553,17 +612,18 @@ function cmdInitMilestoneOp(cwd, raw) { // File existence project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), - state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), + state_exists: pathExistsInternal(cwd, paths.rel.state), archive_exists: pathExistsInternal(cwd, '.planning/archive'), - phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'), + phases_dir_exists: pathExistsInternal(cwd, paths.rel.base + '/phases'), }; output(result, raw); } function cmdInitMapCodebase(cwd, raw) { - const config = loadConfig(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); // Check for existing codebase maps const codebaseDir = path.join(cwd, '.planning', 'codebase'); @@ -588,6 +648,11 @@ function cmdInitMapCodebase(cwd, raw) { existing_maps: existingMaps, has_maps: existingMaps.length > 0, + // Milestone + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, + // File existence planning_exists: pathExistsInternal(cwd, '.planning'), codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'), @@ -597,11 +662,12 @@ function cmdInitMapCodebase(cwd, raw) { } function cmdInitProgress(cwd, raw) { - const config = loadConfig(cwd); - const milestone = getMilestoneInfo(cwd); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); + const milestone = getMilestoneInfo(cwd, paths); // Analyze phases - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = paths.abs.phases; const phases = []; let currentPhase = null; let nextPhase = null; @@ -629,7 +695,7 @@ function cmdInitProgress(cwd, raw) { const phaseInfo = { number: phaseNumber, name: phaseName, - directory: '.planning/phases/' + dir, + directory: paths.rel.phases + '/' + dir, status, plan_count: plans.length, summary_count: summaries.length, @@ -651,7 +717,7 @@ function cmdInitProgress(cwd, raw) { // Check for paused work let pausedAt = null; try { - const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8'); + const state = fs.readFileSync(paths.abs.state, 'utf-8'); const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/); if (pauseMatch) pausedAt = pauseMatch[1].trim(); } catch {} @@ -667,6 +733,9 @@ function cmdInitProgress(cwd, raw) { // Milestone milestone_version: milestone.version, milestone_name: milestone.name, + milestone: paths.milestone, + is_multi_milestone: paths.isMultiMilestone, + planning_base: paths.rel.base, // Phase overview phases, @@ -682,13 +751,13 @@ function cmdInitProgress(cwd, raw) { // File existence project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), - roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), - state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), + roadmap_exists: pathExistsInternal(cwd, paths.rel.roadmap), + state_exists: pathExistsInternal(cwd, paths.rel.state), // File paths - state_path: '.planning/STATE.md', - roadmap_path: '.planning/ROADMAP.md', + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, project_path: '.planning/PROJECT.md', - config_path: '.planning/config.json', + config_path: paths.rel.config, }; output(result, raw); diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 77625376bc..982fe40c9c 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -7,6 +7,7 @@ const path = require('path'); const { output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) { if (!reqIdsRaw || reqIdsRaw.length === 0) { @@ -25,7 +26,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) { error('no valid requirement IDs found'); } - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); + const reqPath = resolvePlanningPaths(cwd).abs.requirements; if (!fs.existsSync(reqPath)) { output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file'); return; @@ -80,12 +81,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) { error('version required for milestone complete (e.g., v1.0)'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); - const statePath = path.join(cwd, '.planning', 'STATE.md'); - const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); - const archiveDir = path.join(cwd, '.planning', 'milestones'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; + const reqPath = paths.abs.requirements; + const statePath = paths.abs.state; + const milestonesPath = path.join(paths.abs.planningRoot, 'MILESTONES.md'); + const archiveDir = path.join(paths.abs.planningRoot, 'milestones'); + const phasesDir = paths.abs.phases; const today = new Date().toISOString().split('T')[0]; const milestoneName = options.name || version; @@ -178,7 +180,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) { } // Archive audit file if exists - const auditFile = path.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`); + const auditFile = path.join(paths.abs.planningRoot, `${version}-MILESTONE-AUDIT.md`); if (fs.existsSync(auditFile)) { fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)); } @@ -261,7 +263,258 @@ function cmdMilestoneComplete(cwd, version, options, raw) { output(result, raw); } +function cmdMilestoneCreate(cwd, name, raw) { + if (!name) { + error('milestone name required. Usage: milestone create '); + } + + const today = new Date().toISOString().split('T')[0]; + const planningRoot = path.join(cwd, '.planning'); + const milestonesDir = path.join(planningRoot, 'milestones'); + const activeMilestonePath = path.join(planningRoot, 'ACTIVE_MILESTONE'); + const targetDir = path.join(milestonesDir, name); + let migratedFrom = null; + + // Check if this is the first milestone AND legacy mode exists + const legacyStateExists = fs.existsSync(path.join(planningRoot, 'STATE.md')); + let existingMilestones = []; + try { + existingMilestones = fs.readdirSync(milestonesDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && fs.existsSync(path.join(milestonesDir, e.name, 'STATE.md'))) + .map(e => e.name); + } catch {} + + if (existingMilestones.length === 0 && legacyStateExists) { + // Auto-migrate existing global files to a milestone directory + // Determine a name for the current milestone from STATE.md + let currentMilestoneName = 'initial'; + try { + const stateContent = fs.readFileSync(path.join(planningRoot, 'STATE.md'), 'utf-8'); + const milestoneMatch = stateContent.match(/\*\*Milestone:\*\*\s*(.+)/); + if (milestoneMatch) { + currentMilestoneName = milestoneMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'initial'; + } + } catch {} + + const migrationDir = path.join(milestonesDir, currentMilestoneName); + fs.mkdirSync(migrationDir, { recursive: true }); + fs.mkdirSync(path.join(migrationDir, 'phases'), { recursive: true }); + + // Copy legacy files + const filesToMigrate = [ + { src: 'STATE.md', dest: 'STATE.md' }, + { src: 'ROADMAP.md', dest: 'ROADMAP.md' }, + { src: 'REQUIREMENTS.md', dest: 'REQUIREMENTS.md' }, + { src: 'config.json', dest: 'config.json' }, + ]; + for (const { src, dest } of filesToMigrate) { + const srcPath = path.join(planningRoot, src); + if (fs.existsSync(srcPath)) { + fs.copyFileSync(srcPath, path.join(migrationDir, dest)); + } + } + + // Set ACTIVE_MILESTONE to the migrated milestone first + fs.writeFileSync(activeMilestonePath, currentMilestoneName, 'utf-8'); + migratedFrom = currentMilestoneName; + } + + // Create the new milestone directory + fs.mkdirSync(targetDir, { recursive: true }); + fs.mkdirSync(path.join(targetDir, 'phases'), { recursive: true }); + + // STATE.md template + const stateTemplate = `# Session State + +## Position + +**Milestone:** ${name} +**Current Phase:** Not started +**Current Phase Name:** TBD +**Total Phases:** 0 +**Current Plan:** Not started +**Total Plans in Phase:** 0 +**Status:** Ready to plan +**Progress:** [░░░░░░░░░░] 0% +**Last Activity:** ${today} +**Last Activity Description:** Milestone created + +## Decisions Made +None yet. + +## Blockers +None + +## Performance Metrics +| Plan | Duration | Tasks | Files | +|------|----------|-------|-------| +None yet + +## Session +**Last Date:** ${today} +**Stopped At:** N/A +**Resume File:** None +`; + fs.writeFileSync(path.join(targetDir, 'STATE.md'), stateTemplate, 'utf-8'); + + // ROADMAP.md template + const roadmapTemplate = `# ${name} Roadmap + +> Milestone roadmap — run /gsd:new-milestone to populate + +## Progress Overview + +| Phase | Plans | Status | Completed | +|-------|-------|--------|-----------| + +## Phase Summary + +(No phases yet — run /gsd:new-milestone to create roadmap) +`; + fs.writeFileSync(path.join(targetDir, 'ROADMAP.md'), roadmapTemplate, 'utf-8'); + + // config.json — copy from current milestone or use defaults + let configContent = JSON.stringify({ commit_docs: true, research: true, verifier: true, plan_checker: true, nyquist_validation: true, parallelization: false, branching_strategy: 'none' }, null, 2); + try { + const currentPaths = resolvePlanningPaths(cwd); + if (fs.existsSync(currentPaths.abs.config)) { + configContent = fs.readFileSync(currentPaths.abs.config, 'utf-8'); + } + } catch {} + fs.writeFileSync(path.join(targetDir, 'config.json'), configContent, 'utf-8'); + + // Set ACTIVE_MILESTONE to the new milestone + fs.writeFileSync(activeMilestonePath, name, 'utf-8'); + + const directory = '.planning/milestones/' + name; + output({ created: true, name, directory, migrated_from: migratedFrom }, raw, `milestone "${name}" created at ${directory}`); +} + +function cmdMilestoneSwitch(cwd, name, raw) { + if (!name) { + error('milestone name required. Usage: milestone switch '); + } + + const planningRoot = path.join(cwd, '.planning'); + const milestoneDir = path.join(planningRoot, 'milestones', name); + const activeMilestonePath = path.join(planningRoot, 'ACTIVE_MILESTONE'); + + if (!fs.existsSync(milestoneDir)) { + 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'); + + // Read status from target milestone's STATE.md + let status = 'unknown'; + const statePath = path.join(milestoneDir, 'STATE.md'); + if (fs.existsSync(statePath)) { + try { + const content = fs.readFileSync(statePath, 'utf-8'); + const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/); + if (statusMatch) status = statusMatch[1].trim(); + } catch {} + } + + const state_path = '.planning/milestones/' + name + '/STATE.md'; + 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) { + const planningRoot = path.join(cwd, '.planning'); + const milestonesDir = path.join(planningRoot, 'milestones'); + const activeMilestonePath = path.join(planningRoot, 'ACTIVE_MILESTONE'); + + // Read active milestone + let active = null; + try { + const content = fs.readFileSync(activeMilestonePath, 'utf-8').trim(); + if (content) active = content; + } catch {} + + // List milestone directories that contain STATE.md (skip archived v*-phases dirs) + const milestones = []; + try { + const entries = fs.readdirSync(milestonesDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + // Skip archived phase directories (e.g. v1.0-phases) + if (/^v[\d.]+-phases$/.test(entry.name)) continue; + const stateFile = path.join(milestonesDir, entry.name, 'STATE.md'); + if (!fs.existsSync(stateFile)) continue; + + let status = 'unknown'; + try { + const content = fs.readFileSync(stateFile, 'utf-8'); + const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/); + if (statusMatch) status = statusMatch[1].trim(); + } catch {} + + milestones.push({ + name: entry.name, + status, + active: entry.name === active, + }); + } + } catch {} + + output({ milestones, active, count: milestones.length }, raw, `${milestones.length} milestone(s) found`); +} + +function cmdMilestoneStatus(cwd, raw) { + const planningRoot = path.join(cwd, '.planning'); + const activeMilestonePath = path.join(planningRoot, 'ACTIVE_MILESTONE'); + + let active = null; + try { + const content = fs.readFileSync(activeMilestonePath, 'utf-8').trim(); + if (content) active = content; + } catch {} + + const isMultiMilestone = !!active; + const paths = resolvePlanningPaths(cwd); + + output({ + active, + is_multi_milestone: isMultiMilestone, + state_path: paths.rel.state, + roadmap_path: paths.rel.roadmap, + }, raw, active ? `active milestone: ${active}` : 'legacy mode (no active milestone)'); +} + module.exports = { cmdRequirementsMarkComplete, cmdMilestoneComplete, + cmdMilestoneCreate, + cmdMilestoneSwitch, + cmdMilestoneList, + cmdMilestoneStatus, }; diff --git a/get-shit-done/bin/lib/paths.cjs b/get-shit-done/bin/lib/paths.cjs new file mode 100644 index 0000000000..081d615684 --- /dev/null +++ b/get-shit-done/bin/lib/paths.cjs @@ -0,0 +1,100 @@ +/** + * Paths — Centralized planning path resolution for milestone-scoped directories + * + * Resolution order: milestoneOverride arg > module-level override > ACTIVE_MILESTONE file > legacy fallback + * When no active milestone is detected, returns identical paths to legacy hardcoded `.planning/` paths. + */ + +const fs = require('fs'); +const path = require('path'); + +// ─── Module-level milestone override (set by CLI --milestone flag) ─────────── + +let _milestoneOverride = null; + +function setMilestoneOverride(milestone) { + _milestoneOverride = milestone || null; +} + +function getMilestoneOverride() { + return _milestoneOverride; +} + +// ─── Path resolution ───────────────────────────────────────────────────────── + +/** + * Resolve all planning paths for the given working directory. + * + * @param {string} cwd - Project root directory + * @param {string|null} [milestoneOverride] - Explicit milestone name (from --milestone flag) + * @returns {object} Resolved paths object with abs, rel, global, milestone, isMultiMilestone + */ +function resolvePlanningPaths(cwd, milestoneOverride) { + const planningRoot = path.join(cwd, '.planning'); + const activeMilestonePath = path.join(planningRoot, 'ACTIVE_MILESTONE'); + + // Determine active milestone: explicit override > module override > file > null + let milestone = milestoneOverride || _milestoneOverride || null; + if (!milestone) { + try { + const content = fs.readFileSync(activeMilestonePath, 'utf-8').trim(); + if (content) milestone = content; + } catch {} + } + + const isMultiMilestone = !!milestone; + + // Determine base directory for milestone-scoped files + const absBase = isMultiMilestone + ? path.join(planningRoot, 'milestones', milestone) + : planningRoot; + + const relBase = isMultiMilestone + ? '.planning/milestones/' + milestone + : '.planning'; + + return { + abs: { + planningRoot, + base: absBase, + state: path.join(absBase, 'STATE.md'), + roadmap: path.join(absBase, 'ROADMAP.md'), + requirements: path.join(absBase, 'REQUIREMENTS.md'), + config: path.join(absBase, 'config.json'), + phases: path.join(absBase, 'phases'), + research: path.join(absBase, 'research'), + codebase: path.join(planningRoot, 'codebase'), + }, + rel: { + base: relBase, + state: relBase + '/STATE.md', + roadmap: relBase + '/ROADMAP.md', + requirements: relBase + '/REQUIREMENTS.md', + config: relBase + '/config.json', + phases: relBase + '/phases', + research: relBase + '/research', + }, + global: { + abs: { + project: path.join(planningRoot, 'PROJECT.md'), + milestones: path.join(planningRoot, 'MILESTONES.md'), + activeMilestone: activeMilestonePath, + codebase: path.join(planningRoot, 'codebase'), + milestonesDir: path.join(planningRoot, 'milestones'), + }, + rel: { + project: '.planning/PROJECT.md', + milestones: '.planning/MILESTONES.md', + activeMilestone: '.planning/ACTIVE_MILESTONE', + }, + }, + milestone, + isMultiMilestone, + }; +} + +module.exports = { + resolvePlanningPaths, + setMilestoneOverride, + getMilestoneOverride, +}; diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 4e4cbff609..efa78f736a 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -7,9 +7,10 @@ const path = require('path'); const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); function cmdPhasesList(cwd, options, raw) { - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = resolvePlanningPaths(cwd).abs.phases; const { type, phase, includeArchived } = options; // If no phases directory, return empty @@ -85,7 +86,7 @@ function cmdPhasesList(cwd, options, raw) { } function cmdPhaseNextDecimal(cwd, basePhase, raw) { - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = resolvePlanningPaths(cwd).abs.phases; const normalized = normalizePhaseName(basePhase); // Check if phases directory exists @@ -154,7 +155,8 @@ function cmdFindPhase(cwd, phase, raw) { error('phase identifier required'); } - const phasesDir = path.join(cwd, '.planning', 'phases'); + const paths = resolvePlanningPaths(cwd); + const phasesDir = paths.abs.phases; const normalized = normalizePhaseName(phase); const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] }; @@ -180,7 +182,7 @@ function cmdFindPhase(cwd, phase, raw) { const result = { found: true, - directory: path.join('.planning', 'phases', match), + directory: paths.rel.phases + '/' + match, phase_number: phaseNumber, phase_name: phaseName, plans, @@ -203,7 +205,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) { error('phase required for phase-plan-index'); } - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = resolvePlanningPaths(cwd).abs.phases; const normalized = normalizePhaseName(phase); // Find phase directory @@ -313,7 +315,8 @@ function cmdPhaseAdd(cwd, description, raw) { error('description required for phase add'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; if (!fs.existsSync(roadmapPath)) { error('ROADMAP.md not found'); } @@ -333,7 +336,7 @@ function cmdPhaseAdd(cwd, description, raw) { const newPhaseNum = maxPhase + 1; const paddedNum = String(newPhaseNum).padStart(2, '0'); const dirName = `${paddedNum}-${slug}`; - const dirPath = path.join(cwd, '.planning', 'phases', dirName); + const dirPath = path.join(paths.abs.phases, dirName); // Create directory with .gitkeep so git tracks empty folders fs.mkdirSync(dirPath, { recursive: true }); @@ -358,7 +361,7 @@ function cmdPhaseAdd(cwd, description, raw) { padded: paddedNum, name: description, slug, - directory: `.planning/phases/${dirName}`, + directory: `${paths.rel.phases}/${dirName}`, }; output(result, raw, paddedNum); @@ -369,7 +372,8 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { error('after-phase and description required for phase insert'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; if (!fs.existsSync(roadmapPath)) { error('ROADMAP.md not found'); } @@ -387,7 +391,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { } // Calculate next decimal using existing logic - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = paths.abs.phases; const normalizedBase = normalizePhaseName(afterPhase); let existingDecimals = []; @@ -404,7 +408,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1; const decimalPhase = `${normalizedBase}.${nextDecimal}`; const dirName = `${decimalPhase}-${slug}`; - const dirPath = path.join(cwd, '.planning', 'phases', dirName); + const dirPath = path.join(phasesDir, dirName); // Create directory with .gitkeep so git tracks empty folders fs.mkdirSync(dirPath, { recursive: true }); @@ -439,7 +443,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { after_phase: afterPhase, name: description, slug, - directory: `.planning/phases/${dirName}`, + directory: `${paths.rel.phases}/${dirName}`, }; output(result, raw, decimalPhase); @@ -450,8 +454,9 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { error('phase number required for phase remove'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; + const phasesDir = paths.abs.phases; const force = options.force || false; if (!fs.existsSync(roadmapPath)) { @@ -666,7 +671,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); // Update STATE.md phase count - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = paths.abs.state; if (fs.existsSync(statePath)) { let stateContent = fs.readFileSync(statePath, 'utf-8'); // Update "Total Phases" field @@ -703,9 +708,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { error('phase number required for phase complete'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const statePath = path.join(cwd, '.planning', 'STATE.md'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; + const statePath = paths.abs.state; + const phasesDir = paths.abs.phases; const normalized = normalizePhaseName(phaseNum); const today = new Date().toISOString().split('T')[0]; @@ -753,7 +759,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); // Update REQUIREMENTS.md traceability for this phase's requirements - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); + const reqPath = paths.abs.requirements; if (fs.existsSync(reqPath)) { // Extract Requirements line from roadmap for this phase const reqMatch = roadmapContent.match( diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 9717b9aae0..fc4bf595d1 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -5,9 +5,11 @@ const fs = require('fs'); const path = require('path'); const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); function cmdRoadmapGetPhase(cwd, phaseNum, raw) { - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; if (!fs.existsSync(roadmapPath)) { output({ found: false, error: 'ROADMAP.md not found' }, raw, ''); @@ -91,7 +93,8 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) { } function cmdRoadmapAnalyze(cwd, raw) { - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; if (!fs.existsSync(roadmapPath)) { output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw); @@ -99,7 +102,7 @@ function cmdRoadmapAnalyze(cwd, raw) { } const content = fs.readFileSync(roadmapPath, 'utf-8'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = paths.abs.phases; // Extract all phase headings: ## Phase N: Name or ### Phase N: Name const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; @@ -222,7 +225,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { error('phase number required for roadmap update-plan-progress'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const roadmapPath = resolvePlanningPaths(cwd).abs.roadmap; const phaseInfo = findPhaseInternal(cwd, phaseNum); if (!phaseInfo) { diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 915f51c452..eefb48f453 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -5,19 +5,20 @@ const fs = require('fs'); const path = require('path'); const { loadConfig, getMilestoneInfo, output, error } = require('./core.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs'); function cmdStateLoad(cwd, raw) { - const config = loadConfig(cwd); - const planningDir = path.join(cwd, '.planning'); + const paths = resolvePlanningPaths(cwd); + const config = loadConfig(cwd, paths); let stateRaw = ''; try { - stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8'); + stateRaw = fs.readFileSync(paths.abs.state, 'utf-8'); } catch {} - const configExists = fs.existsSync(path.join(planningDir, 'config.json')); - const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md')); + const configExists = fs.existsSync(paths.abs.config); + const roadmapExists = fs.existsSync(paths.abs.roadmap); const stateExists = stateRaw.length > 0; const result = { @@ -53,7 +54,7 @@ function cmdStateLoad(cwd, raw) { } function cmdStateGet(cwd, section, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; try { const content = fs.readFileSync(statePath, 'utf-8'); @@ -99,7 +100,7 @@ function readTextArgOrFile(cwd, value, filePath, label) { } function cmdStatePatch(cwd, patches, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; try { let content = fs.readFileSync(statePath, 'utf-8'); const results = { updated: [], failed: [] }; @@ -131,7 +132,7 @@ function cmdStateUpdate(cwd, field, value) { error('field and value required for state update'); } - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; try { let content = fs.readFileSync(statePath, 'utf-8'); const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -167,7 +168,7 @@ function stateReplaceField(content, fieldName, newValue) { } function cmdStateAdvancePlan(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -196,7 +197,7 @@ function cmdStateAdvancePlan(cwd, raw) { } function cmdStateRecordMetric(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -230,13 +231,14 @@ function cmdStateRecordMetric(cwd, options, raw) { } function cmdStateUpdateProgress(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const paths = resolvePlanningPaths(cwd); + const statePath = paths.abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); // Count summaries across all phases - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = paths.abs.phases; let totalPlans = 0; let totalSummaries = 0; @@ -267,7 +269,7 @@ function cmdStateUpdateProgress(cwd, raw) { } function cmdStateAddDecision(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } const { phase, summary, summary_file, rationale, rationale_file } = options; @@ -305,7 +307,7 @@ function cmdStateAddDecision(cwd, options, raw) { } function cmdStateAddBlocker(cwd, text, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } const blockerOptions = typeof text === 'object' && text !== null ? text : { text }; let blockerText = null; @@ -338,7 +340,7 @@ function cmdStateAddBlocker(cwd, text, raw) { } function cmdStateResolveBlocker(cwd, text, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } if (!text) { output({ error: 'text required' }, raw); return; } @@ -370,7 +372,7 @@ function cmdStateResolveBlocker(cwd, text, raw) { } function cmdStateRecordSession(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -405,7 +407,7 @@ function cmdStateRecordSession(cwd, options, raw) { } function cmdStateSnapshot(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); @@ -547,7 +549,8 @@ function buildStateFrontmatter(bodyContent, cwd) { if (cwd) { try { - const phasesDir = path.join(cwd, '.planning', 'phases'); + const _p = require('./paths.cjs').resolvePlanningPaths(cwd); + const phasesDir = _p.abs.phases; if (fs.existsSync(phasesDir)) { const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true }) .filter(e => e.isDirectory()).map(e => e.name); @@ -641,7 +644,7 @@ function writeStateMd(statePath, content, cwd) { } function cmdStateJson(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = resolvePlanningPaths(cwd).abs.state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw, 'STATE.md not found'); return; diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 2e0d5db6a5..bec3738510 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -7,6 +7,7 @@ const path = require('path'); const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs'); const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); +const { resolvePlanningPaths } = require('./paths.cjs'); function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) { if (!summaryPath) { @@ -395,8 +396,9 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) { } function cmdValidateConsistency(cwd, raw) { - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const paths = resolvePlanningPaths(cwd); + const roadmapPath = paths.abs.roadmap; + const phasesDir = paths.abs.phases; const errors = []; const warnings = []; @@ -515,12 +517,13 @@ function cmdValidateConsistency(cwd, raw) { } function cmdValidateHealth(cwd, options, raw) { - const planningDir = path.join(cwd, '.planning'); + const paths = resolvePlanningPaths(cwd); + const planningDir = paths.abs.planningRoot; const projectPath = path.join(planningDir, 'PROJECT.md'); - const roadmapPath = path.join(planningDir, 'ROADMAP.md'); - const statePath = path.join(planningDir, 'STATE.md'); - const configPath = path.join(planningDir, 'config.json'); - const phasesDir = path.join(planningDir, 'phases'); + const roadmapPath = paths.abs.roadmap; + const statePath = paths.abs.state; + const configPath = paths.abs.config; + const phasesDir = paths.abs.phases; const errors = []; const warnings = []; diff --git a/get-shit-done/templates/planner-subagent-prompt.md b/get-shit-done/templates/planner-subagent-prompt.md index bcaa68d275..3f34235f51 100644 --- a/get-shit-done/templates/planner-subagent-prompt.md +++ b/get-shit-done/templates/planner-subagent-prompt.md @@ -13,23 +13,23 @@ Template for spawning gsd-planner agent. The agent contains all planning experti **Mode:** {standard | gap_closure} **Project State:** -@.planning/STATE.md +@{state_path} **Roadmap:** -@.planning/ROADMAP.md +@{roadmap_path} **Requirements (if exists):** -@.planning/REQUIREMENTS.md +@{requirements_path} **Phase Context (if exists):** -@.planning/phases/{phase_dir}/{phase_num}-CONTEXT.md +@{phase_dir}/{phase_num}-CONTEXT.md **Research (if exists):** -@.planning/phases/{phase_dir}/{phase_num}-RESEARCH.md +@{phase_dir}/{phase_num}-RESEARCH.md **Gap Closure (if --gaps mode):** -@.planning/phases/{phase_dir}/{phase_num}-VERIFICATION.md -@.planning/phases/{phase_dir}/{phase_num}-UAT.md +@{phase_dir}/{phase_num}-VERIFICATION.md +@{phase_dir}/{phase_num}-UAT.md @@ -98,8 +98,8 @@ Continue planning for Phase {phase_number}: {phase_name} -Phase directory: @.planning/phases/{phase_dir}/ -Existing plans: @.planning/phases/{phase_dir}/*-PLAN.md +Phase directory: @{phase_dir}/ +Existing plans: @{phase_dir}/*-PLAN.md diff --git a/get-shit-done/workflows/add-phase.md b/get-shit-done/workflows/add-phase.md index 56eb418684..cc46558d05 100644 --- a/get-shit-done/workflows/add-phase.md +++ b/get-shit-done/workflows/add-phase.md @@ -32,6 +32,8 @@ Load phase operation context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "0") ``` +Extract from init JSON: `state_path`, `roadmap_path`, `phase_dir`, `planning_base`. + Check `roadmap_exists` from init JSON. If false: ``` ERROR: No roadmap found (.planning/ROADMAP.md) @@ -51,7 +53,7 @@ The CLI handles: - Finding the highest existing integer phase number - Calculating next phase number (max + 1) - Generating slug from description -- Creating the phase directory (`.planning/phases/{NN}-{slug}/`) +- Creating the phase directory (`{planning_base}/phases/{NN}-{slug}/`) - Inserting the phase entry into ROADMAP.md with Goal, Depends on, and Plans sections Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory`. @@ -60,7 +62,7 @@ Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory`. Update STATE.md to reflect the new phase: -1. Read `.planning/STATE.md` +1. Read `{state_path}` 2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry: ``` - Phase {N} added: {description} @@ -75,10 +77,10 @@ Present completion summary: ``` Phase {N} added to current milestone: - Description: {description} -- Directory: .planning/phases/{phase-num}-{slug}/ +- Directory: {planning_base}/phases/{phase-num}-{slug}/ - Status: Not planned yet -Roadmap updated: .planning/ROADMAP.md +Roadmap updated: {roadmap_path} --- diff --git a/get-shit-done/workflows/add-tests.md b/get-shit-done/workflows/add-tests.md index 6dfc03bd6a..fb86d2bf6f 100644 --- a/get-shit-done/workflows/add-tests.md +++ b/get-shit-done/workflows/add-tests.md @@ -36,12 +36,12 @@ Load phase operation context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}") ``` -Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`. +Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`, `planning_base`. Verify the phase directory exists. If not: ``` ERROR: Phase directory not found for phase ${PHASE_ARG} -Ensure the phase exists in .planning/phases/ +Ensure the phase exists in {planning_base}/phases/ ``` Exit. diff --git a/get-shit-done/workflows/add-todo.md b/get-shit-done/workflows/add-todo.md index cd15cc8a91..4163abc20d 100644 --- a/get-shit-done/workflows/add-todo.md +++ b/get-shit-done/workflows/add-todo.md @@ -15,11 +15,11 @@ Load todo context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init todos) ``` -Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos`, `pending_dir`, `todos_dir_exists`. +Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos`, `pending_dir`, `completed_dir`, `planning_base`, `todos_dir_exists`. Ensure directories exist: ```bash -mkdir -p .planning/todos/pending .planning/todos/done +mkdir -p "${pending_dir}" "${completed_dir}" ``` Note existing areas from the todos array for consistency in infer_area step. @@ -62,7 +62,7 @@ Use existing area from step 2 if similar match exists. ```bash # Search for key words from title in existing todos -grep -l -i "[key words from title]" .planning/todos/pending/*.md 2>/dev/null +grep -l -i "[key words from title]" ${pending_dir}/*.md 2>/dev/null ``` If potential duplicate found: @@ -86,7 +86,7 @@ Generate slug for the title: slug=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$title" --raw) ``` -Write to `.planning/todos/pending/${date}-${slug}.md`: +Write to `${pending_dir}/${date}-${slug}.md`: ```markdown --- @@ -108,7 +108,7 @@ files: -If `.planning/STATE.md` exists: +If `{planning_base}/STATE.md` exists: 1. Use `todo_count` from init context (or re-run `init todos` if count changed) 2. Update "### Pending Todos" under "## Accumulated Context" @@ -118,7 +118,7 @@ If `.planning/STATE.md` exists: Commit the todo and any updated state: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: capture todo - [title]" --files .planning/todos/pending/[filename] .planning/STATE.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: capture todo - [title]" --files ${pending_dir}/[filename] {planning_base}/STATE.md ``` Tool respects `commit_docs` config and gitignore automatically. @@ -128,7 +128,7 @@ Confirm: "Committed: docs: capture todo - [title]" ``` -Todo saved: .planning/todos/pending/[filename] +Todo saved: ${pending_dir}/[filename] [title] Area: [area] diff --git a/get-shit-done/workflows/audit-milestone.md b/get-shit-done/workflows/audit-milestone.md index 7eee93975b..a2c1f4970a 100644 --- a/get-shit-done/workflows/audit-milestone.md +++ b/get-shit-done/workflows/audit-milestone.md @@ -14,7 +14,7 @@ Read all files referenced by the invoking prompt's execution_context before star INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init milestone-op) ``` -Extract from init JSON: `milestone_version`, `milestone_name`, `phase_count`, `completed_phases`, `commit_docs`. +Extract from init JSON: `milestone_version`, `milestone_name`, `phase_count`, `completed_phases`, `commit_docs`, `planning_base`. Resolve integration checker model: ```bash @@ -103,7 +103,7 @@ For each phase's VERIFICATION.md, extract the expanded requirements table: For each phase's SUMMARY.md, extract `requirements-completed` from YAML frontmatter: ```bash -for summary in .planning/phases/*-*/*-SUMMARY.md; do +for summary in ${planning_base}/phases/*-*/*-SUMMARY.md; do node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" summary-extract "$summary" --fields requirements_completed | jq -r '.requirements_completed' done ``` @@ -129,7 +129,7 @@ For each REQ-ID, determine status using all three sources: ## 6. Aggregate into v{version}-MILESTONE-AUDIT.md -Create `.planning/v{version}-v{version}-MILESTONE-AUDIT.md` with: +Create `{planning_base}/v{version}-MILESTONE-AUDIT.md` with: ```yaml --- @@ -186,7 +186,7 @@ Output this markdown directly (not as a code block). Route based on status: ## ✓ Milestone {version} — Audit Passed **Score:** {N}/{M} requirements satisfied -**Report:** .planning/v{version}-MILESTONE-AUDIT.md +**Report:** {planning_base}/v{version}-MILESTONE-AUDIT.md All requirements covered. Cross-phase integration verified. E2E flows complete. @@ -209,7 +209,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete. ## ⚠ Milestone {version} — Gaps Found **Score:** {N}/{M} requirements satisfied -**Report:** .planning/v{version}-MILESTONE-AUDIT.md +**Report:** {planning_base}/v{version}-MILESTONE-AUDIT.md ### Unsatisfied Requirements @@ -240,7 +240,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete. ─────────────────────────────────────────────────────────────── **Also available:** -- cat .planning/v{version}-MILESTONE-AUDIT.md — see full report +- cat {planning_base}/v{version}-MILESTONE-AUDIT.md — see full report - /gsd:complete-milestone {version} — proceed anyway (accept tech debt) ─────────────────────────────────────────────────────────────── @@ -252,7 +252,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete. ## ⚡ Milestone {version} — Tech Debt Review **Score:** {N}/{M} requirements satisfied -**Report:** .planning/v{version}-MILESTONE-AUDIT.md +**Report:** {planning_base}/v{version}-MILESTONE-AUDIT.md All requirements met. No critical blockers. Accumulated tech debt needs review. @@ -291,7 +291,7 @@ All requirements met. No critical blockers. Accumulated tech debt needs review. - [ ] Orphaned requirements detected (in traceability but absent from all VERIFICATIONs) - [ ] Tech debt and deferred gaps aggregated - [ ] Integration checker spawned with milestone requirement IDs -- [ ] v{version}-MILESTONE-AUDIT.md created with structured requirement gap objects +- [ ] {planning_base}/v{version}-MILESTONE-AUDIT.md created with structured requirement gap objects - [ ] FAIL gate enforced — any unsatisfied requirement forces gaps_found status - [ ] Results presented with actionable next steps diff --git a/get-shit-done/workflows/check-todos.md b/get-shit-done/workflows/check-todos.md index 43598fc719..51b23c40ca 100644 --- a/get-shit-done/workflows/check-todos.md +++ b/get-shit-done/workflows/check-todos.md @@ -15,7 +15,7 @@ Load todo context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init todos) ``` -Extract from init JSON: `todo_count`, `todos`, `pending_dir`. +Extract from init JSON: `todo_count`, `todos`, `pending_dir`, `completed_dir`, `planning_base`. If `todo_count` is 0: ``` @@ -92,7 +92,7 @@ If `files` field has entries, read and briefly summarize each. Check for roadmap (can use init progress or directly check file existence): -If `.planning/ROADMAP.md` exists: +If `{planning_base}/ROADMAP.md` exists: 1. Check if todo's area matches an upcoming phase 2. Check if todo's files overlap with a phase's scope 3. Note any match for action options @@ -125,7 +125,7 @@ Use AskUserQuestion: **Work on it now:** ```bash -mv ".planning/todos/pending/[filename]" ".planning/todos/done/" +mv "${pending_dir}/[filename]" "${completed_dir}/" ``` Update STATE.md todo count. Present problem/solution context. Begin work or ask how to proceed. @@ -153,8 +153,8 @@ Re-run `init todos` to get updated count, then update STATE.md "### Pending Todo If todo was moved to done/, commit the change: ```bash -git rm --cached .planning/todos/pending/[filename] 2>/dev/null || true -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start work on todo - [title]" --files .planning/todos/done/[filename] .planning/STATE.md +git rm --cached ${pending_dir}/[filename] 2>/dev/null || true +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start work on todo - [title]" --files ${completed_dir}/[filename] {planning_base}/STATE.md ``` Tool respects `commit_docs` config and gitignore automatically. diff --git a/get-shit-done/workflows/cleanup.md b/get-shit-done/workflows/cleanup.md index c1f772e774..2ed3fb0b36 100644 --- a/get-shit-done/workflows/cleanup.md +++ b/get-shit-done/workflows/cleanup.md @@ -16,6 +16,14 @@ Archive accumulated phase directories from completed milestones into `.planning/ +**Load milestone-aware paths:** + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init milestone-op) +``` + +Extract from init JSON: `planning_base`. + Read `.planning/MILESTONES.md` to identify completed milestones and their versions. ```bash @@ -55,7 +63,7 @@ Extract phase numbers and names from the archived roadmap (e.g., Phase 1: Founda Check which of those phase directories still exist in `.planning/phases/`: ```bash -ls -d .planning/phases/*/ 2>/dev/null +ls -d {planning_base}/phases/*/ 2>/dev/null ``` Match phase directories to milestone membership. Only include directories that still exist in `.planning/phases/`. @@ -110,7 +118,7 @@ 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/ +mv {planning_base}/phases/{dir} .planning/milestones/v{X.Y}-phases/ ``` Repeat for all milestones in the cleanup set. @@ -122,7 +130,7 @@ Repeat for all milestones in the cleanup set. Commit the changes: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: archive phase directories from completed milestones" --files .planning/milestones/ .planning/phases/ +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: archive phase directories from completed milestones" --files .planning/milestones/ {planning_base}/phases/ ``` diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md index d1a77b6c53..bd36b877bb 100644 --- a/get-shit-done/workflows/complete-milestone.md +++ b/get-shit-done/workflows/complete-milestone.md @@ -37,6 +37,14 @@ When a milestone completes: +**Load milestone-aware paths:** + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init milestone-op) +``` + +Extract from init JSON: `state_path`, `roadmap_path`, `requirements_path`, `config_path`, `planning_base`. + **Use `roadmap analyze` for comprehensive readiness check:** ```bash @@ -88,7 +96,7 @@ If user selects "Proceed anyway": note incomplete requirements in MILESTONES.md ```bash -cat .planning/config.json 2>/dev/null +cat {config_path} 2>/dev/null ``` @@ -107,14 +115,17 @@ Proceed to gather_stats. -``` -Ready to mark this milestone as shipped? -(yes / wait / adjust scope) -``` +Use AskUserQuestion: -Wait for confirmation. -- "adjust scope": Ask which phases to include. -- "wait": Stop, user returns when ready. +- header: "Ship" +- question: "Ready to mark this milestone as shipped?" +- options: + - "Yes — ship it" — Mark milestone as complete + - "Wait" — Not ready yet, I'll come back later + - "Adjust scope" — Change which phases to include + +- "Adjust scope": Ask which phases to include. +- "Wait": Stop, user returns when ready. @@ -153,7 +164,7 @@ Extract one-liners from SUMMARY.md files using summary-extract: ```bash # For each phase in milestone, extract one-liner -for summary in .planning/phases/*-*/*-SUMMARY.md; do +for summary in {planning_base}/phases/*-*/*-SUMMARY.md; do node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" summary-extract "$summary" --fields one_liner | jq -r '.one_liner' done ``` @@ -186,7 +197,7 @@ Full PROJECT.md evolution review at milestone completion. Read all phase summaries: ```bash -cat .planning/phases/*-*/*-SUMMARY.md +cat {planning_base}/phases/*-*/*-SUMMARY.md ``` **Full review checklist:** @@ -389,8 +400,8 @@ AskUserQuestion(header="Archive Phases", question="Archive phase directories to 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/ +# For each phase directory in {planning_base}/phases/: +mv {planning_base}/phases/{phase-dir} .planning/milestones/v[X.Y]-phases/ ``` Verify: `✅ Phase directories archived to .planning/milestones/v[X.Y]-phases/` @@ -432,8 +443,8 @@ After `milestone complete` has archived, reorganize ROADMAP.md with milestone gr **Then delete originals:** ```bash -rm .planning/ROADMAP.md -rm .planning/REQUIREMENTS.md +rm {roadmap_path} +rm {requirements_path} ``` @@ -662,7 +673,13 @@ See .planning/MILESTONES.md for full details." Confirm: "Tagged: v[X.Y]" -Ask: "Push tag to remote? (y/n)" +Use AskUserQuestion: + +- header: "Push tag" +- question: "Push tag v[X.Y] to remote?" +- options: + - "Yes — push tag" — Push v[X.Y] to origin + - "No — keep local" — Tag stays local only If yes: ```bash @@ -676,7 +693,7 @@ git push origin v[X.Y] Commit milestone completion. ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: complete v[X.Y] milestone" --files .planning/milestones/v[X.Y]-ROADMAP.md .planning/milestones/v[X.Y]-REQUIREMENTS.md .planning/milestones/v[X.Y]-MILESTONE-AUDIT.md .planning/MILESTONES.md .planning/PROJECT.md .planning/STATE.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: complete v[X.Y] milestone" --files .planning/milestones/v[X.Y]-ROADMAP.md .planning/milestones/v[X.Y]-REQUIREMENTS.md .planning/milestones/v[X.Y]-MILESTONE-AUDIT.md .planning/MILESTONES.md .planning/PROJECT.md {state_path} ``` ``` diff --git a/get-shit-done/workflows/diagnose-issues.md b/get-shit-done/workflows/diagnose-issues.md index 274b50c57c..250f6a83c3 100644 --- a/get-shit-done/workflows/diagnose-issues.md +++ b/get-shit-done/workflows/diagnose-issues.md @@ -79,7 +79,7 @@ For each gap, fill the debug-subagent-prompt template and spawn: ``` Task( - prompt=filled_debug_subagent_prompt + "\n\n\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n", + prompt=filled_debug_subagent_prompt + "\n\n\n- {phase_dir}/{phase_num}-UAT.md\n- {planning_base}/STATE.md\n", subagent_type="general-purpose", description="Debug: {truth_short}" ) @@ -158,7 +158,7 @@ Update status in frontmatter to "diagnosed". Commit the updated UAT.md: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase_num}): add root causes from diagnosis" --files ".planning/phases/XX-name/{phase_num}-UAT.md" +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase_num}): add root causes from diagnosis" --files "{phase_dir}/{phase_num}-UAT.md" ``` diff --git a/get-shit-done/workflows/discovery-phase.md b/get-shit-done/workflows/discovery-phase.md index 6f71d19421..abccb7cc54 100644 --- a/get-shit-done/workflows/discovery-phase.md +++ b/get-shit-done/workflows/discovery-phase.md @@ -116,7 +116,7 @@ For: Choosing between options, new external integration. 7. Return to plan-phase.md. -**Output:** `.planning/phases/XX-name/DISCOVERY.md` +**Output:** `{phase_dir}/DISCOVERY.md` @@ -169,7 +169,7 @@ For: Architectural decisions, novel problems, high-risk choices. 8. Return to plan-phase.md. -**Output:** `.planning/phases/XX-name/DISCOVERY.md` (comprehensive) +**Output:** `{phase_dir}/DISCOVERY.md` (comprehensive) @@ -203,7 +203,7 @@ Run the discovery: -Write `.planning/phases/XX-name/DISCOVERY.md`: +Write `{phase_dir}/DISCOVERY.md`: - Summary with recommendation - Key findings with sources - Code examples if applicable @@ -224,7 +224,13 @@ Use AskUserQuestion: - "Pause" - I need to think about this If confidence is MEDIUM: -Inline: "Discovery complete (medium confidence). [brief reason]. Proceed to planning?" +Use AskUserQuestion: + +- header: "Med Conf." +- question: "Discovery complete (medium confidence). [brief reason]. Proceed to planning?" +- options: + - "Proceed to planning" — Confidence is good enough + - "Dig deeper" — Do more research first If confidence is HIGH: Proceed directly, just note: "Discovery complete (high confidence)." @@ -233,20 +239,27 @@ Proceed directly, just note: "Discovery complete (high confidence)." If DISCOVERY.md has open_questions: -Present them inline: +Present them as context, then use AskUserQuestion: + "Open questions from discovery: - [Question 1] -- [Question 2] +- [Question 2]" -These may affect implementation. Acknowledge and proceed? (yes / address first)" +Use AskUserQuestion: + +- header: "Questions" +- question: "These open questions may affect implementation. How do you want to proceed?" +- options: + - "Proceed anyway" — Accept uncertainty, address during planning + - "Address first" — Discuss these questions before continuing -If "address first": Gather user input on questions, update discovery. +If "Address first": Gather user input on questions, update discovery. ``` -Discovery complete: .planning/phases/XX-name/DISCOVERY.md +Discovery complete: {phase_dir}/DISCOVERY.md Recommendation: [one-liner] Confidence: [level] diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index 225dd07136..cadc90d6a3 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -116,7 +116,7 @@ Phase number from argument (required). INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}") ``` -Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`. +Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`, `state_path`, `roadmap_path`, `requirements_path`, `phase_dir`, `planning_base`. **If `phase_found` is false:** ``` @@ -368,7 +368,7 @@ Use values from init: `phase_dir`, `phase_slug`, `padded_phase`. If `phase_dir` is null (phase exists in roadmap but no directory): ```bash -mkdir -p ".planning/phases/${padded_phase}-${phase_slug}" +mkdir -p "${planning_base}/phases/${padded_phase}-${phase_slug}" ``` **File location:** `${phase_dir}/${padded_phase}-CONTEXT.md` @@ -504,7 +504,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state record-session \ Commit STATE.md: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(state): record phase ${PHASE} context session" --files .planning/STATE.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(state): record phase ${PHASE} context session" --files "${state_path}" ``` diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 5149594ce0..3871b85623 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -19,7 +19,7 @@ Load all context in one call: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "${PHASE_ARG}") ``` -Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`. +Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`, `state_path`, `roadmap_path`, `config_path`, `planning_base`, `milestone`, `is_multi_milestone`. **If `phase_found` is false:** Error — phase directory not found. **If `plan_count` is 0:** Error — no plans found in phase. @@ -119,8 +119,8 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` Read these files at execution start using the Read tool: - {phase_dir}/{plan_file} (Plan) - - .planning/STATE.md (State) - - .planning/config.json (Config, if exists) + - {state_path} (State) + - {config_path} (Config, if exists) - ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions) - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation) @@ -371,7 +371,7 @@ The CLI handles: Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`. ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): complete phase execution" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): complete phase execution" --files {roadmap_path} {state_path} {planning_base}/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md ``` diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 180dfcc290..7b761ce54a 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -18,7 +18,7 @@ Load execution context (paths only to minimize orchestrator context): INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "${PHASE}") ``` -Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`. +Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `roadmap_path`, `config_path`, `planning_base`, `milestone`, `is_multi_milestone`. If `.planning/` missing: error. @@ -26,8 +26,8 @@ If `.planning/` missing: error. ```bash # Use plans/summaries from INIT JSON, or list files -ls .planning/phases/XX-name/*-PLAN.md 2>/dev/null | sort -ls .planning/phases/XX-name/*-SUMMARY.md 2>/dev/null | sort +ls {phase_dir}/*-PLAN.md 2>/dev/null | sort +ls {phase_dir}/*-SUMMARY.md 2>/dev/null | sort ``` Find first PLAN without matching SUMMARY. Decimal phases supported (`01.1-hotfix/`): @@ -55,7 +55,7 @@ PLAN_START_EPOCH=$(date +%s) ```bash -grep -n "type=\"checkpoint" .planning/phases/XX-name/{phase}-{plan}-PLAN.md +grep -n "type=\"checkpoint" {phase_dir}/{phase}-{plan}-PLAN.md ``` **Routing by checkpoint type:** @@ -115,7 +115,7 @@ Pattern B only (verify-only checkpoints). Skip for A/C. ```bash -cat .planning/phases/XX-name/{phase}-{plan}-PLAN.md +cat {phase_dir}/{phase}-{plan}-PLAN.md ``` This IS the execution instructions. Follow exactly. If plan references CONTEXT.md: honor user's vision throughout. @@ -189,9 +189,17 @@ Why needed: [rationale] Impact: [what this affects] Alternatives: [other approaches] -Proceed with proposed change? (yes / different approach / defer) ``` +Use AskUserQuestion: + +- header: "Deviation" +- question: "Proceed with proposed change?" +- options: + - "Yes — apply proposed change" — Continue with the modification described above + - "Different approach" — Try an alternative approach + - "Defer" — Skip this for now and continue with the plan + **Priority:** Rule 4 (STOP) > Rules 1-3 (auto) > unsure → Rule 4 **Edge cases:** missing validation → R2 | null crash → R1 | new table → R4 | new column → R1/2 **Heuristic:** Affects correctness/security/completion? → R1-3. Maybe? → R4. @@ -253,6 +261,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) @@ -309,14 +320,14 @@ fi ```bash -grep -A 50 "^user_setup:" .planning/phases/XX-name/{phase}-{plan}-PLAN.md | head -50 +grep -A 50 "^user_setup:" {phase_dir}/{phase}-{plan}-PLAN.md | head -50 ``` If user_setup exists: create `{phase}-USER-SETUP.md` using template `~/.claude/get-shit-done/templates/user-setup.md`. Per service: env vars table, account setup checklist, dashboard config, local dev notes, verification commands. Status "Incomplete". Set `USER_SETUP_CREATED=true`. If empty/missing: skip. -Create `{phase}-{plan}-SUMMARY.md` at `.planning/phases/XX-name/`. Use `~/.claude/get-shit-done/templates/summary.md`. +Create `{phase}-{plan}-SUMMARY.md` at `{phase_dir}/`. Use `~/.claude/get-shit-done/templates/summary.md`. **Frontmatter:** phase, plan, subsystem, tags | requires/provides/affects | tech-stack.added/patterns | key-files.created/modified | key-decisions | requirements-completed (**MUST** copy `requirements` array from PLAN.md frontmatter verbatim) | duration ($DURATION), completed ($PLAN_END_TIME date). @@ -397,7 +408,7 @@ 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 .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md .planning/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 ``` @@ -405,7 +416,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase}-{plan} 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 ``` @@ -420,8 +431,8 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "" --files .planning If `USER_SETUP_CREATED=true`: display `⚠️ USER SETUP REQUIRED` with path + env/config tasks at TOP. ```bash -ls -1 .planning/phases/[current-phase-dir]/*-PLAN.md 2>/dev/null | wc -l -ls -1 .planning/phases/[current-phase-dir]/*-SUMMARY.md 2>/dev/null | wc -l +ls -1 {phase_dir}/*-PLAN.md 2>/dev/null | wc -l +ls -1 {phase_dir}/*-SUMMARY.md 2>/dev/null | wc -l ``` | Condition | Route | Action | diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 2991aa18eb..1a24a6f695 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -184,6 +184,15 @@ Archive completed milestone and prepare for next version. Usage: `/gsd:complete-milestone 1.0.0` +**`/gsd:switch-milestone `** +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`** @@ -270,6 +279,20 @@ Validate built features through conversational UAT. Usage: `/gsd:verify-work 3` +### Test Generation + +**`/gsd:add-tests [additional instructions]`** +Generate unit and E2E tests for a completed phase. + +- Reads phase SUMMARY.md, CONTEXT.md, and VERIFICATION.md +- Classifies changed files into TDD (unit), E2E (browser), or Skip +- Presents classification for approval before generating +- Runs tests after generation — flags bugs but doesn't fix them +- Commits passing tests + +Usage: `/gsd:add-tests 3` +Usage: `/gsd:add-tests 3 focus on edge cases for auth` + ### Milestone Auditing **`/gsd:audit-milestone [version]`** @@ -438,6 +461,7 @@ Example config: /gsd:plan-phase 1 # Create plans for first phase /clear /gsd:execute-phase 1 # Execute all plans in phase +/gsd:add-tests 1 # Generate tests for completed phase ``` **Resuming work after a break:** diff --git a/get-shit-done/workflows/insert-phase.md b/get-shit-done/workflows/insert-phase.md index 8f2569c461..16e0b1cdb8 100644 --- a/get-shit-done/workflows/insert-phase.md +++ b/get-shit-done/workflows/insert-phase.md @@ -37,6 +37,8 @@ Load phase operation context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${after_phase}") ``` +Extract from init JSON: `state_path`, `roadmap_path`, `phase_dir`, `planning_base`. + Check `roadmap_exists` from init JSON. If false: ``` ERROR: No roadmap found (.planning/ROADMAP.md) @@ -55,7 +57,7 @@ The CLI handles: - Verifying target phase exists in ROADMAP.md - Calculating next decimal phase number (checking existing decimals on disk) - Generating slug from description -- Creating the phase directory (`.planning/phases/{N.M}-{slug}/`) +- Creating the phase directory (`{planning_base}/phases/{N.M}-{slug}/`) - Inserting the phase entry into ROADMAP.md after the target phase with (INSERTED) marker Extract from result: `phase_number`, `after_phase`, `name`, `slug`, `directory`. @@ -64,7 +66,7 @@ Extract from result: `phase_number`, `after_phase`, `name`, `slug`, `directory`. Update STATE.md to reflect the inserted phase: -1. Read `.planning/STATE.md` +1. Read `{state_path}` 2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry: ``` - Phase {decimal_phase} inserted after Phase {after_phase}: {description} (URGENT) @@ -79,12 +81,12 @@ Present completion summary: ``` Phase {decimal_phase} inserted after Phase {after_phase}: - Description: {description} -- Directory: .planning/phases/{decimal-phase}-{slug}/ +- Directory: {planning_base}/phases/{decimal-phase}-{slug}/ - Status: Not planned yet - Marker: (INSERTED) - indicates urgent work -Roadmap updated: .planning/ROADMAP.md -Project state updated: .planning/STATE.md +Roadmap updated: {roadmap_path} +Project state updated: {state_path} --- diff --git a/get-shit-done/workflows/list-phase-assumptions.md b/get-shit-done/workflows/list-phase-assumptions.md index 3269d28300..fc874fde30 100644 --- a/get-shit-done/workflows/list-phase-assumptions.md +++ b/get-shit-done/workflows/list-phase-assumptions.md @@ -21,10 +21,18 @@ Example: /gsd:list-phase-assumptions 3 Exit workflow. **If argument provided:** +Load context to get milestone-aware paths: + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}") +``` + +Extract from init JSON: `roadmap_path`, `state_path`, `phase_dir`, `requirements_path`, `planning_base`. + Validate phase exists in roadmap: ```bash -cat .planning/ROADMAP.md | grep -i "Phase ${PHASE}" +cat ${roadmap_path} | grep -i "Phase ${PHASE}" ``` **If phase not found:** diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 252694ae4a..5c5a3ca236 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -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: @@ -71,7 +83,7 @@ Keep Accumulated Context section from previous milestone. Delete MILESTONE-CONTEXT.md if exists (consumed). ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start milestone v[X.Y] [Name]" --files .planning/PROJECT.md .planning/STATE.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start milestone v[X.Y] [Name]" --files .planning/PROJECT.md {state_path} ``` ## 7. Load Context and Resolve Models @@ -80,7 +92,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start milesto INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone) ``` -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`, `project_path`, `roadmap_path`, `state_path`, `planning_base`. ## 8. Research Decision @@ -110,7 +122,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow.researc ``` ```bash -mkdir -p .planning/research +mkdir -p {planning_base}/research ``` Spawn 4 parallel gsd-project-researcher agents. Each uses this template with dimension-specific fields: @@ -137,7 +149,7 @@ Focus ONLY on what's needed for the NEW features. {GATES} -Write to: .planning/research/{FILE} +Write to: {planning_base}/research/{FILE} Use template: ~/.claude/get-shit-done/templates/research-project/{FILE} ", subagent_type="gsd-project-researcher", model="{researcher_model}", description="{DIMENSION} research") @@ -160,13 +172,13 @@ Task(prompt=" Synthesize research outputs into SUMMARY.md. -- .planning/research/STACK.md -- .planning/research/FEATURES.md -- .planning/research/ARCHITECTURE.md -- .planning/research/PITFALLS.md +- {planning_base}/research/STACK.md +- {planning_base}/research/FEATURES.md +- {planning_base}/research/ARCHITECTURE.md +- {planning_base}/research/PITFALLS.md -Write to: .planning/research/SUMMARY.md +Write to: {planning_base}/research/SUMMARY.md Use template: ~/.claude/get-shit-done/templates/research-project/SUMMARY.md Commit after writing. ", subagent_type="gsd-research-synthesizer", model="{synthesizer_model}", description="Synthesize research") @@ -246,14 +258,21 @@ Present FULL requirements list for confirmation: ### [Category 2] - [ ] **CAT2-01**: User can do Z -Does this capture what you're building? (yes / adjust) ``` -If "adjust": Return to scoping. +Use AskUserQuestion: + +- header: "Requirements" +- question: "Does this capture what you're building?" +- options: + - "Yes — looks good" — Proceed with these requirements + - "Adjust" — Return to scoping to refine + +If "Adjust": Return to scoping. **Commit requirements:** ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milestone v[X.Y] requirements" --files .planning/REQUIREMENTS.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milestone v[X.Y] requirements" --files {planning_base}/REQUIREMENTS.md ``` ## 10. Create Roadmap @@ -273,9 +292,9 @@ Task(prompt=" - .planning/PROJECT.md -- .planning/REQUIREMENTS.md -- .planning/research/SUMMARY.md (if exists) -- .planning/config.json +- {planning_base}/REQUIREMENTS.md +- {planning_base}/research/SUMMARY.md (if exists) +- {planning_base}/config.json - .planning/MILESTONES.md @@ -330,7 +349,7 @@ Success criteria: **Commit roadmap** (after approval): ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create milestone v[X.Y] roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create milestone v[X.Y] roadmap ([N] phases)" --files {roadmap_path} {state_path} {planning_base}/REQUIREMENTS.md ``` ## 11. Done @@ -342,12 +361,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 ✓ diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index e7c56a4a9f..92872df684 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -49,7 +49,7 @@ The document should describe what you want to build. INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-project) ``` -Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`. +Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`, `planning_base`. **If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`. @@ -523,7 +523,7 @@ Researching [domain] ecosystem... Create research directory: ```bash -mkdir -p .planning/research +mkdir -p {planning_base}/research ``` **Determine milestone context:** @@ -579,7 +579,7 @@ Your STACK.md feeds into roadmap creation. Be prescriptive: -Write to: .planning/research/STACK.md +Write to: {planning_base}/research/STACK.md Use template: ~/.claude/get-shit-done/templates/research-project/STACK.md ", subagent_type="general-purpose", model="{researcher_model}", description="Stack research") @@ -619,7 +619,7 @@ Your FEATURES.md feeds into requirements definition. Categorize clearly: -Write to: .planning/research/FEATURES.md +Write to: {planning_base}/research/FEATURES.md Use template: ~/.claude/get-shit-done/templates/research-project/FEATURES.md ", subagent_type="general-purpose", model="{researcher_model}", description="Features research") @@ -659,7 +659,7 @@ Your ARCHITECTURE.md informs phase structure in roadmap. Include: -Write to: .planning/research/ARCHITECTURE.md +Write to: {planning_base}/research/ARCHITECTURE.md Use template: ~/.claude/get-shit-done/templates/research-project/ARCHITECTURE.md ", subagent_type="general-purpose", model="{researcher_model}", description="Architecture research") @@ -699,7 +699,7 @@ Your PITFALLS.md prevents mistakes in roadmap/planning. For each pitfall: -Write to: .planning/research/PITFALLS.md +Write to: {planning_base}/research/PITFALLS.md Use template: ~/.claude/get-shit-done/templates/research-project/PITFALLS.md ", subagent_type="general-purpose", model="{researcher_model}", description="Pitfalls research") @@ -714,14 +714,14 @@ Synthesize research outputs into SUMMARY.md. -- .planning/research/STACK.md -- .planning/research/FEATURES.md -- .planning/research/ARCHITECTURE.md -- .planning/research/PITFALLS.md +- {planning_base}/research/STACK.md +- {planning_base}/research/FEATURES.md +- {planning_base}/research/ARCHITECTURE.md +- {planning_base}/research/PITFALLS.md -Write to: .planning/research/SUMMARY.md +Write to: {planning_base}/research/SUMMARY.md Use template: ~/.claude/get-shit-done/templates/research-project/SUMMARY.md Commit after writing. @@ -839,7 +839,7 @@ Cross-check requirements against Core Value from PROJECT.md. If gaps detected, s **Generate REQUIREMENTS.md:** -Create `.planning/REQUIREMENTS.md` with: +Create `{planning_base}/REQUIREMENTS.md` with: - v1 Requirements grouped by category (checkboxes, REQ-IDs) - v2 Requirements (deferred) - Out of Scope (explicit exclusions with reasoning) @@ -879,15 +879,22 @@ Show every requirement (not counts) for user confirmation: --- -Does this capture what you're building? (yes / adjust) ``` -If "adjust": Return to scoping. +Use AskUserQuestion: + +- header: "Requirements" +- question: "Does this capture what you're building?" +- options: + - "Yes — looks good" — Proceed with these requirements + - "Adjust" — Return to scoping to refine + +If "Adjust": Return to scoping. **Commit requirements:** ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define v1 requirements" --files .planning/REQUIREMENTS.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define v1 requirements" --files {planning_base}/REQUIREMENTS.md ``` ## 8. Create Roadmap @@ -909,9 +916,9 @@ Task(prompt=" - .planning/PROJECT.md (Project context) -- .planning/REQUIREMENTS.md (v1 Requirements) -- .planning/research/SUMMARY.md (Research findings - if exists) -- .planning/config.json (Depth and mode settings) +- {planning_base}/REQUIREMENTS.md (v1 Requirements) +- {planning_base}/research/SUMMARY.md (Research findings - if exists) +- {planning_base}/config.json (Depth and mode settings) @@ -1001,7 +1008,7 @@ Use AskUserQuestion: [user's notes] - - .planning/ROADMAP.md (Current roadmap to revise) + - {planning_base}/ROADMAP.md (Current roadmap to revise) Update the roadmap based on feedback. Edit files in place. @@ -1012,12 +1019,12 @@ Use AskUserQuestion: - Present revised roadmap - Loop until user approves -**If "Review full file":** Display raw `cat .planning/ROADMAP.md`, then re-ask. +**If "Review full file":** Display raw `cat {planning_base}/ROADMAP.md`, then re-ask. **Commit roadmap (after approval or auto mode):** ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files {planning_base}/ROADMAP.md {planning_base}/STATE.md {planning_base}/REQUIREMENTS.md ``` ## 9. Done diff --git a/get-shit-done/workflows/plan-milestone-gaps.md b/get-shit-done/workflows/plan-milestone-gaps.md index 373147bd28..71c4099bb7 100644 --- a/get-shit-done/workflows/plan-milestone-gaps.md +++ b/get-shit-done/workflows/plan-milestone-gaps.md @@ -8,6 +8,14 @@ Read all files referenced by the invoking prompt's execution_context before star +## 0. Initialize + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init progress) +``` + +Parse JSON for: `planning_base`, `roadmap_path`, `config_path`. + ## 1. Load Audit Results ```bash @@ -104,9 +112,17 @@ These gaps are optional. Include them? --- -Create these {X} phases? (yes / adjust / defer all optional) ``` +Use AskUserQuestion: + +- header: "Create" +- question: "Create these {X} phases?" +- options: + - "Yes — create all" — Add all proposed phases to the roadmap + - "Adjust" — Modify which phases to create + - "Defer optional" — Only create required phases, defer optional ones + Wait for user confirmation. ## 6. Update ROADMAP.md @@ -135,19 +151,19 @@ Reset checked-off requirements the audit found unsatisfied: ```bash # Verify traceability table reflects gap closure assignments -grep -c "Pending" .planning/REQUIREMENTS.md +grep -c "Pending" "${planning_base}/REQUIREMENTS.md" ``` ## 8. Create Phase Directories ```bash -mkdir -p ".planning/phases/{NN}-{name}" +mkdir -p "${planning_base}/phases/{NN}-{name}" ``` ## 9. Commit Roadmap and Requirements Update ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(roadmap): add gap closure phases {N}-{M}" --files .planning/ROADMAP.md .planning/REQUIREMENTS.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(roadmap): add gap closure phases {N}-{M}" --files "${planning_base}/ROADMAP.md" "${planning_base}/REQUIREMENTS.md" ``` ## 10. Offer Next Steps diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 7bf31efba3..9b17def810 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -34,7 +34,7 @@ Extract `--prd ` from $ARGUMENTS. If present, set PRD_FILE to the file **If `phase_found` is false:** Validate phase exists in ROADMAP.md. If valid, create the directory using `phase_slug` and `padded_phase` from init: ```bash -mkdir -p ".planning/phases/${padded_phase}-${phase_slug}" +mkdir -p "${planning_base}/phases/${padded_phase}-${phase_slug}" ``` **Existing artifacts from init:** `has_research`, `has_plans`, `plan_count`. diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index e1dcc2eb1c..7f3e2c5945 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -15,7 +15,7 @@ Read all files referenced by the invoking prompt's execution_context before star INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init progress) ``` -Extract from init JSON: `project_exists`, `roadmap_exists`, `state_exists`, `phases`, `current_phase`, `next_phase`, `milestone_version`, `completed_count`, `phase_count`, `paused_at`, `state_path`, `roadmap_path`, `project_path`, `config_path`. +Extract from init JSON: `project_exists`, `roadmap_exists`, `state_exists`, `phases`, `current_phase`, `next_phase`, `milestone_version`, `completed_count`, `phase_count`, `paused_at`, `state_path`, `roadmap_path`, `project_path`, `config_path`, `planning_base`. If `project_exists` is false (no `.planning/` directory): @@ -137,9 +137,9 @@ CONTEXT: [✓ if has_context | - if not] List files in the current phase directory: ```bash -ls -1 .planning/phases/[current-phase-dir]/*-PLAN.md 2>/dev/null | wc -l -ls -1 .planning/phases/[current-phase-dir]/*-SUMMARY.md 2>/dev/null | wc -l -ls -1 .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null | wc -l +ls -1 {planning_base}/phases/[current-phase-dir]/*-PLAN.md 2>/dev/null | wc -l +ls -1 {planning_base}/phases/[current-phase-dir]/*-SUMMARY.md 2>/dev/null | wc -l +ls -1 {planning_base}/phases/[current-phase-dir]/*-UAT.md 2>/dev/null | wc -l ``` State: "This phase has {X} plans, {Y} summaries." @@ -150,7 +150,7 @@ Check for UAT.md files with status "diagnosed" (has gaps needing fixes). ```bash # Check for diagnosed UAT with gaps -grep -l "status: diagnosed" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null +grep -l "status: diagnosed" {planning_base}/phases/[current-phase-dir]/*-UAT.md 2>/dev/null ``` Track: diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index e68f140437..2c84efa24c 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -46,7 +46,7 @@ If `$FULL_MODE`: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init quick "$DESCRIPTION") ``` -Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `next_num`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. +Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `next_num`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `planning_base`, `roadmap_exists`, `planning_exists`. **If `roadmap_exists` is false:** Error — Quick mode requires an active project with ROADMAP.md. Run `/gsd:new-project` first. @@ -67,7 +67,7 @@ mkdir -p "${task_dir}" Create the directory for this quick task: ```bash -QUICK_DIR=".planning/quick/${next_num}-${slug}" +QUICK_DIR="${quick_dir}/${next_num}-${slug}" mkdir -p "$QUICK_DIR" ``` @@ -97,7 +97,7 @@ Task( **Description:** ${DESCRIPTION} -- .planning/STATE.md (Project State) +- {planning_base}/STATE.md (Project State) - ./CLAUDE.md (if exists — follow project-specific guidelines) @@ -250,7 +250,7 @@ Execute quick task ${next_num}. - ${QUICK_DIR}/${next_num}-PLAN.md (Plan) -- .planning/STATE.md (Project state) +- {planning_base}/STATE.md (Project state) - ./CLAUDE.md (Project instructions, if exists) - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation) @@ -388,7 +388,7 @@ Stage and commit quick task artifacts: Build file list: - `${QUICK_DIR}/${next_num}-PLAN.md` - `${QUICK_DIR}/${next_num}-SUMMARY.md` -- `.planning/STATE.md` +- `{planning_base}/STATE.md` - If `$FULL_MODE` and verification file exists: `${QUICK_DIR}/${next_num}-VERIFICATION.md` ```bash @@ -443,7 +443,7 @@ Ready for next task: /gsd:quick - [ ] `--full` flag parsed from arguments when present - [ ] Slug generated (lowercase, hyphens, max 40 chars) - [ ] Next number calculated (001, 002, 003...) -- [ ] Directory created at `.planning/quick/NNN-slug/` +- [ ] Directory created at `${quick_dir}/NNN-slug/` - [ ] `${next_num}-PLAN.md` created by planner - [ ] (--full) Plan checker validates plan, revision loop capped at 2 - [ ] `${next_num}-SUMMARY.md` created by executor diff --git a/get-shit-done/workflows/remove-phase.md b/get-shit-done/workflows/remove-phase.md index cd168cd2a1..e80ed98dac 100644 --- a/get-shit-done/workflows/remove-phase.md +++ b/get-shit-done/workflows/remove-phase.md @@ -32,9 +32,9 @@ Load phase operation context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${target}") ``` -Extract: `phase_found`, `phase_dir`, `phase_number`, `commit_docs`, `roadmap_exists`. +Extract: `phase_found`, `phase_dir`, `phase_number`, `commit_docs`, `roadmap_exists`, `state_path`, `roadmap_path`, `planning_base`. -Also read STATE.md and ROADMAP.md content for parsing current position. +Also read `{state_path}` and `{roadmap_path}` content for parsing current position. @@ -65,13 +65,20 @@ Present removal summary and confirm: Removing Phase {target}: {Name} This will: -- Delete: .planning/phases/{target}-{slug}/ +- Delete: {planning_base}/phases/{target}-{slug}/ - Renumber all subsequent phases - Update: ROADMAP.md, STATE.md -Proceed? (y/n) ``` +Use AskUserQuestion: + +- header: "Confirm" +- question: "Proceed with removing Phase {target}: {Name}?" +- options: + - "Yes — remove it" — Delete the phase and renumber subsequent phases + - "Cancel" — Keep the phase, no changes + Wait for confirmation. @@ -102,7 +109,7 @@ Extract from result: `removed`, `directory_deleted`, `renamed_directories`, `ren Stage and commit the removal: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: remove phase {target} ({original-phase-name})" --files .planning/ +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: remove phase {target} ({original-phase-name})" --files {planning_base}/ ``` The commit message preserves the historical record of what was removed. @@ -115,7 +122,7 @@ Present completion summary: Phase {target} ({original-name}) removed. Changes: -- Deleted: .planning/phases/{target}-{slug}/ +- Deleted: {planning_base}/phases/{target}-{slug}/ - Renumbered: {N} directories and {M} files - Updated: ROADMAP.md, STATE.md - Committed: chore: remove phase {target} ({original-name}) diff --git a/get-shit-done/workflows/research-phase.md b/get-shit-done/workflows/research-phase.md index f527e8f841..6acf2a9fc9 100644 --- a/get-shit-done/workflows/research-phase.md +++ b/get-shit-done/workflows/research-phase.md @@ -25,8 +25,9 @@ If `found` is false: Error and exit. ## Step 2: Check Existing Research +Use `directory` from `PHASE_INFO` (step 1): ```bash -ls .planning/phases/${PHASE}-*/RESEARCH.md 2>/dev/null +ls ${PHASE_INFO.directory}/RESEARCH.md 2>/dev/null ``` If exists: Offer update/view/skip options. @@ -35,7 +36,7 @@ If exists: Offer update/view/skip options. ```bash INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}") -# Extract: phase_dir, padded_phase, phase_number, state_path, requirements_path, context_path +# Extract: phase_dir, padded_phase, phase_number, state_path, requirements_path, context_path, planning_base ``` ## Step 4: Spawn Researcher @@ -57,7 +58,7 @@ Phase description: {description} -Write to: .planning/phases/${PHASE}-{slug}/${PHASE}-RESEARCH.md +Write to: ${phase_dir}/${PHASE}-RESEARCH.md ", subagent_type="gsd-phase-researcher", model="{researcher_model}" diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index f71cadbfa2..3ed1b5d467 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -23,7 +23,7 @@ Load all context in one call: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init resume) ``` -Parse JSON for: `state_exists`, `roadmap_exists`, `project_exists`, `planning_exists`, `has_interrupted_agent`, `interrupted_agent_id`, `commit_docs`. +Parse JSON for: `state_exists`, `roadmap_exists`, `project_exists`, `planning_exists`, `has_interrupted_agent`, `interrupted_agent_id`, `commit_docs`, `state_path`, `roadmap_path`, `project_path`, `planning_base`. **If `state_exists` is true:** Proceed to load_state **If `state_exists` is false but `roadmap_exists` or `project_exists` is true:** Offer to reconstruct STATE.md @@ -35,8 +35,8 @@ Parse JSON for: `state_exists`, `roadmap_exists`, `project_exists`, `planning_ex Read and parse STATE.md, then PROJECT.md: ```bash -cat .planning/STATE.md -cat .planning/PROJECT.md +cat "${state_path}" +cat "${project_path}" ``` **From STATE.md extract:** @@ -63,10 +63,10 @@ Look for incomplete work that needs attention: ```bash # Check for continue-here files (mid-plan resumption) -ls .planning/phases/*/.continue-here*.md 2>/dev/null +ls "${planning_base}"/phases/*/.continue-here*.md 2>/dev/null # Check for plans without summaries (incomplete execution) -for plan in .planning/phases/*/*-PLAN.md; do +for plan in "${planning_base}"/phases/*/*-PLAN.md; do summary="${plan/PLAN/SUMMARY}" [ ! -f "$summary" ] && echo "Incomplete: $plan" done 2>/dev/null @@ -196,7 +196,7 @@ What would you like to do? **Note:** When offering phase planning, check for CONTEXT.md existence first: ```bash -ls .planning/phases/XX-name/*-CONTEXT.md 2>/dev/null +ls "${planning_base}"/phases/XX-name/*-CONTEXT.md 2>/dev/null ``` If missing, suggest discuss-phase before plan. If exists, offer plan directly. diff --git a/get-shit-done/workflows/set-profile.md b/get-shit-done/workflows/set-profile.md index 00c2f5d3fa..97f1924abd 100644 --- a/get-shit-done/workflows/set-profile.md +++ b/get-shit-done/workflows/set-profile.md @@ -27,7 +27,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-ensure-section INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) ``` -This creates `.planning/config.json` with defaults if missing and loads current config. +This creates `{planning_base}/config.json` with defaults if missing and loads current config. + +Extract `planning_base` from init JSON (or derive from `state load` response). @@ -40,7 +42,7 @@ Update `model_profile` field: } ``` -Write updated config back to `.planning/config.json`. +Write updated config back to `{planning_base}/config.json`. diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 9677001db0..35cb41c39b 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -16,12 +16,14 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-ensure-section INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) ``` -Creates `.planning/config.json` with defaults if missing and loads current config values. +Creates `{planning_base}/config.json` with defaults if missing and loads current config values. + +Extract `planning_base` from init JSON (or derive from `state load` response). ```bash -cat .planning/config.json +cat {planning_base}/config.json ``` Parse current values (default to `true` if not present): @@ -127,7 +129,7 @@ Merge new settings into existing config.json: } ``` -Write updated config to `.planning/config.json`. +Write updated config to `{planning_base}/config.json`. diff --git a/get-shit-done/workflows/switch-milestone.md b/get-shit-done/workflows/switch-milestone.md new file mode 100644 index 0000000000..0cd49e3c3a --- /dev/null +++ b/get-shit-done/workflows/switch-milestone.md @@ -0,0 +1,66 @@ + +Switch the active milestone. Shows available milestones, warns about in-progress work on the current milestone, and updates the active milestone pointer. + + + + +## 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. +``` + + + + +- [ ] Available milestones shown +- [ ] In-progress warning displayed if applicable +- [ ] ACTIVE_MILESTONE updated +- [ ] User sees new milestone status + diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 553fc193b5..c87e471337 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -22,10 +22,18 @@ Mark current phase complete and advance to next. This is the natural point where +**Load milestone-aware paths:** + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init transition) +``` + +Extract from init JSON: `state_path`, `roadmap_path`, `config_path`, `planning_base`. + Before transition, read project state: ```bash -cat .planning/STATE.md 2>/dev/null +cat {state_path} 2>/dev/null cat .planning/PROJECT.md 2>/dev/null ``` @@ -39,8 +47,8 @@ Note accumulated context that may need updating after transition. Check current phase has all plan summaries: ```bash -ls .planning/phases/XX-current/*-PLAN.md 2>/dev/null | sort -ls .planning/phases/XX-current/*-SUMMARY.md 2>/dev/null | sort +ls {planning_base}/phases/XX-current/*-PLAN.md 2>/dev/null | sort +ls {planning_base}/phases/XX-current/*-SUMMARY.md 2>/dev/null | sort ``` **Verification logic:** @@ -53,7 +61,7 @@ ls .planning/phases/XX-current/*-SUMMARY.md 2>/dev/null | sort ```bash -cat .planning/config.json 2>/dev/null +cat {config_path} 2>/dev/null ``` @@ -111,7 +119,7 @@ Wait for user decision. Check for lingering handoffs: ```bash -ls .planning/phases/XX-current/.continue-here*.md 2>/dev/null +ls {planning_base}/phases/XX-current/.continue-here*.md 2>/dev/null ``` If found, delete them — phase is complete, handoffs are stale. @@ -151,7 +159,7 @@ Evolve PROJECT.md to reflect learnings from completed phase. **Read phase summaries:** ```bash -cat .planning/phases/XX-current/*-SUMMARY.md +cat {planning_base}/phases/XX-current/*-SUMMARY.md ``` **Assess requirement changes:** @@ -361,7 +369,7 @@ Read ROADMAP.md to get the next phase's name and goal. **Check if next phase has CONTEXT.md:** ```bash -ls .planning/phases/*[X+1]*/*-CONTEXT.md 2>/dev/null +ls {planning_base}/phases/*[X+1]*/*-CONTEXT.md 2>/dev/null ``` **If next phase exists:** diff --git a/get-shit-done/workflows/verify-phase.md b/get-shit-done/workflows/verify-phase.md index 33efb834c5..ebfdadf24b 100644 --- a/get-shit-done/workflows/verify-phase.md +++ b/get-shit-done/workflows/verify-phase.md @@ -31,12 +31,12 @@ Load phase operation context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}") ``` -Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`, `has_plans`, `plan_count`. +Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`, `has_plans`, `plan_count`, `state_path`, `roadmap_path`, `requirements_path`, `planning_base`. Then load phase details and list plans/summaries: ```bash node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${phase_number}" -grep -E "^| ${phase_number}" .planning/REQUIREMENTS.md 2>/dev/null +grep -E "^| ${phase_number}" "${requirements_path}" 2>/dev/null ls "$phase_dir"/*-SUMMARY.md "$phase_dir"/*-PLAN.md 2>/dev/null ``` @@ -159,7 +159,7 @@ Record status and evidence for each key link. If REQUIREMENTS.md exists: ```bash -grep -E "Phase ${PHASE_NUM}" .planning/REQUIREMENTS.md 2>/dev/null +grep -E "Phase ${PHASE_NUM}" "${requirements_path}" 2>/dev/null ``` For each requirement: parse description → identify supporting truths/artifacts → status: ✓ SATISFIED / ✗ BLOCKED / ? NEEDS HUMAN. diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index 466a80c4ff..6d85bdd409 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -27,14 +27,14 @@ If $ARGUMENTS contains a phase number, load context: INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init verify-work "${PHASE_ARG}") ``` -Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`. +Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `planning_base`. **First: Check for active UAT sessions** ```bash -find .planning/phases -name "*-UAT.md" -type f 2>/dev/null | head -5 +find "${planning_base}/phases" -name "*-UAT.md" -type f 2>/dev/null | head -5 ``` **If active sessions exist AND no $ARGUMENTS provided:** @@ -292,7 +292,7 @@ Clear Current Test section: Commit the UAT file: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "test({phase_num}): complete UAT - {passed} passed, {issues} issues" --files ".planning/phases/XX-name/{phase_num}-UAT.md" +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "test({phase_num}): complete UAT - {passed} passed, {issues} issues" --files "${phase_dir}/{phase_num}-UAT.md" ``` Present summary: @@ -367,8 +367,8 @@ Task( - {phase_dir}/{phase_num}-UAT.md (UAT with diagnoses) -- .planning/STATE.md (Project State) -- .planning/ROADMAP.md (Roadmap) +- {planning_base}/STATE.md (Project State) +- {planning_base}/ROADMAP.md (Roadmap) diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index 29185d6835..77718a4ad0 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -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 diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index e2694ceaf7..c49caceef6 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -605,6 +605,39 @@ describe('requirements mark-complete command', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// milestone create auto-migration +// ───────────────────────────────────────────────────────────────────────────── + +describe('milestone create auto-migration', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('migrates legacy STATE.md to milestone directory on first create', () => { + // Write legacy STATE.md with content + const legacyStatePath = path.join(tmpDir, '.planning', 'STATE.md'); + fs.writeFileSync(legacyStatePath, '---\nphase: 3\n---\n# State\n\n**Status:** Executing Phase 3\n'); + + // Create first milestone + const result = runGsdTools('milestone create v1.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + + // Check milestone STATE.md exists + const msStatePath = path.join(tmpDir, '.planning', 'milestones', 'v1.0', 'STATE.md'); + assert.ok(fs.existsSync(msStatePath)); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // validate consistency command // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/paths.test.cjs b/tests/paths.test.cjs new file mode 100644 index 0000000000..8fe227f2b6 --- /dev/null +++ b/tests/paths.test.cjs @@ -0,0 +1,450 @@ +/** + * GSD Tools Tests - paths.cjs + * + * Tests for resolvePlanningPaths, setMilestoneOverride, and milestone CLI commands. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +const { + resolvePlanningPaths, + setMilestoneOverride, + getMilestoneOverride, +} = require('../get-shit-done/bin/lib/paths.cjs'); + +// ─── resolvePlanningPaths — legacy mode ───────────────────────────────────── + +describe('resolvePlanningPaths — legacy mode', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + setMilestoneOverride(null); + }); + + afterEach(() => { + setMilestoneOverride(null); + cleanup(tmpDir); + }); + + test('returns .planning/STATE.md for rel.state when no ACTIVE_MILESTONE', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.rel.state, '.planning/STATE.md'); + }); + + test('isMultiMilestone is false', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.isMultiMilestone, false); + }); + + test('milestone is null', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.milestone, null); + }); + + test('abs.state ends with .planning/STATE.md', () => { + const p = resolvePlanningPaths(tmpDir); + assert.ok(p.abs.state.endsWith(path.join('.planning', 'STATE.md')), + `expected abs.state to end with .planning/STATE.md, got ${p.abs.state}`); + }); + + test('global.abs.project ends with .planning/PROJECT.md', () => { + const p = resolvePlanningPaths(tmpDir); + assert.ok(p.global.abs.project.endsWith(path.join('.planning', 'PROJECT.md')), + `expected global.abs.project to end with .planning/PROJECT.md, got ${p.global.abs.project}`); + }); +}); + +// ─── resolvePlanningPaths — multi-milestone mode ──────────────────────────── + +describe('resolvePlanningPaths — multi-milestone mode', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + setMilestoneOverride(null); + // Create ACTIVE_MILESTONE file with content "v2.0" + fs.writeFileSync(path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'v2.0', 'utf-8'); + }); + + afterEach(() => { + setMilestoneOverride(null); + cleanup(tmpDir); + }); + + test('returns .planning/milestones/v2.0/STATE.md for rel.state', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.rel.state, '.planning/milestones/v2.0/STATE.md'); + }); + + test('isMultiMilestone is true', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.isMultiMilestone, true); + }); + + test('milestone is "v2.0"', () => { + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.milestone, 'v2.0'); + }); + + test('abs.phases ends with .planning/milestones/v2.0/phases', () => { + const p = resolvePlanningPaths(tmpDir); + assert.ok( + p.abs.phases.endsWith(path.join('.planning', 'milestones', 'v2.0', 'phases')), + `expected abs.phases to end with .planning/milestones/v2.0/phases, got ${p.abs.phases}` + ); + }); + + test('global.abs.project still points to .planning/ root', () => { + const p = resolvePlanningPaths(tmpDir); + assert.ok( + p.global.abs.project.endsWith(path.join('.planning', 'PROJECT.md')), + `expected global.abs.project to end with .planning/PROJECT.md, got ${p.global.abs.project}` + ); + // Should NOT contain milestones subpath + assert.ok( + !p.global.abs.project.includes('milestones'), + 'global.abs.project should not contain milestones' + ); + }); + + test('global.abs.milestones still points to .planning/ root', () => { + const p = resolvePlanningPaths(tmpDir); + assert.ok( + p.global.abs.milestones.endsWith(path.join('.planning', 'MILESTONES.md')), + `expected global.abs.milestones to end with .planning/MILESTONES.md, got ${p.global.abs.milestones}` + ); + }); +}); + +// ─── resolvePlanningPaths — explicit override ─────────────────────────────── + +describe('resolvePlanningPaths — explicit override', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + setMilestoneOverride(null); + }); + + afterEach(() => { + setMilestoneOverride(null); + cleanup(tmpDir); + }); + + test('milestoneOverride="hotfix" returns milestone-scoped paths regardless of ACTIVE_MILESTONE', () => { + // Write a different ACTIVE_MILESTONE to prove override takes precedence + fs.writeFileSync(path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'v2.0', 'utf-8'); + + const p = resolvePlanningPaths(tmpDir, 'hotfix'); + assert.strictEqual(p.rel.state, '.planning/milestones/hotfix/STATE.md'); + assert.strictEqual(p.milestone, 'hotfix'); + assert.strictEqual(p.isMultiMilestone, true); + }); + + test('milestoneOverride="hotfix" works when no ACTIVE_MILESTONE file exists', () => { + const p = resolvePlanningPaths(tmpDir, 'hotfix'); + assert.strictEqual(p.rel.state, '.planning/milestones/hotfix/STATE.md'); + assert.strictEqual(p.milestone, 'hotfix'); + assert.strictEqual(p.isMultiMilestone, true); + }); +}); + +// ─── setMilestoneOverride ─────────────────────────────────────────────────── + +describe('setMilestoneOverride', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + setMilestoneOverride(null); + }); + + afterEach(() => { + setMilestoneOverride(null); + cleanup(tmpDir); + }); + + test('setting override causes resolvePlanningPaths to return milestone-scoped paths', () => { + setMilestoneOverride('v3.0'); + const p = resolvePlanningPaths(tmpDir); + assert.strictEqual(p.milestone, 'v3.0'); + assert.strictEqual(p.isMultiMilestone, true); + assert.strictEqual(p.rel.state, '.planning/milestones/v3.0/STATE.md'); + setMilestoneOverride(null); + }); + + test('clearing override restores legacy paths', () => { + setMilestoneOverride('v3.0'); + const p1 = resolvePlanningPaths(tmpDir); + assert.strictEqual(p1.milestone, 'v3.0'); + + setMilestoneOverride(null); + const p2 = resolvePlanningPaths(tmpDir); + assert.strictEqual(p2.milestone, null); + assert.strictEqual(p2.isMultiMilestone, false); + assert.strictEqual(p2.rel.state, '.planning/STATE.md'); + }); + + test('getMilestoneOverride reflects current value', () => { + assert.strictEqual(getMilestoneOverride(), null); + setMilestoneOverride('v3.0'); + assert.strictEqual(getMilestoneOverride(), 'v3.0'); + setMilestoneOverride(null); + assert.strictEqual(getMilestoneOverride(), null); + }); +}); + +// ─── milestone create command (via CLI) ───────────────────────────────────── + +describe('milestone create command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('creates milestone directory with STATE.md, ROADMAP.md, config.json, phases/', () => { + const result = runGsdTools('milestone create v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + assert.strictEqual(output.name, 'v2.0'); + + const milestoneDir = path.join(tmpDir, '.planning', 'milestones', 'v2.0'); + assert.ok(fs.existsSync(path.join(milestoneDir, 'STATE.md')), 'STATE.md should exist'); + assert.ok(fs.existsSync(path.join(milestoneDir, 'ROADMAP.md')), 'ROADMAP.md should exist'); + assert.ok(fs.existsSync(path.join(milestoneDir, 'config.json')), 'config.json should exist'); + assert.ok(fs.existsSync(path.join(milestoneDir, 'phases')), 'phases/ should exist'); + }); + + test('writes ACTIVE_MILESTONE file', () => { + runGsdTools('milestone create v2.0', tmpDir); + + const activeMilestone = fs.readFileSync( + path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'utf-8' + ).trim(); + assert.strictEqual(activeMilestone, 'v2.0'); + }); +}); + +// ─── milestone switch command ─────────────────────────────────────────────── + +describe('milestone switch command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create two milestones + runGsdTools('milestone create v1.0', tmpDir); + runGsdTools('milestone create v2.0', tmpDir); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('switches between milestones and updates ACTIVE_MILESTONE', () => { + // After creating v2.0, it should be active + let active = fs.readFileSync( + path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'utf-8' + ).trim(); + assert.strictEqual(active, 'v2.0'); + + // Switch to v1.0 + const result = runGsdTools('milestone switch v1.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.switched, true); + assert.strictEqual(output.name, 'v1.0'); + + active = fs.readFileSync( + path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'utf-8' + ).trim(); + assert.strictEqual(active, 'v1.0'); + }); + + test('switch back to second milestone', () => { + runGsdTools('milestone switch v1.0', tmpDir); + const result = runGsdTools('milestone switch v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const active = fs.readFileSync( + path.join(tmpDir, '.planning', 'ACTIVE_MILESTONE'), 'utf-8' + ).trim(); + assert.strictEqual(active, 'v2.0'); + }); + + test('switch warns when current milestone has in-progress work', () => { + // beforeEach already created v1.0 and v2.0; recreate to get fresh STATE.md + runGsdTools('milestone create v1.0', tmpDir); + + // Set v1.0 STATE.md to have in-progress status + const v1StatePath = path.join(tmpDir, '.planning', 'milestones', 'v1.0', 'STATE.md'); + let stateContent = fs.readFileSync(v1StatePath, 'utf-8'); + stateContent = stateContent.replace('**Status:** Ready to plan', '**Status:** Executing Phase 2'); + fs.writeFileSync(v1StatePath, stateContent); + + // Recreate second milestone (makes v2.0 active) + runGsdTools('milestone create v2.0', tmpDir); + + // Switch back to v1.0 first (so v1.0 is active) + runGsdTools('milestone switch v1.0', tmpDir); + + // Now set v1.0 to executing again and switch to v2.0 + stateContent = fs.readFileSync(v1StatePath, 'utf-8'); + stateContent = stateContent.replace(/\*\*Status:\*\*.*/, '**Status:** Executing Phase 3'); + fs.writeFileSync(v1StatePath, stateContent); + + const result = runGsdTools('milestone switch v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.switched, true); + assert.strictEqual(output.has_in_progress, true); + assert.strictEqual(output.previous_milestone, 'v1.0'); + assert.ok(output.previous_status.includes('Executing')); + }); + + test('switch has no warning when current milestone is idle', () => { + // beforeEach created v1.0 then v2.0; v2.0 is active with "Ready to plan" status + // Switch to v1.0: previous=v2.0 with "Ready to plan" — not in-progress + const result = runGsdTools('milestone switch v1.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.switched, true); + assert.strictEqual(output.has_in_progress, false); + assert.strictEqual(output.previous_milestone, 'v2.0'); + }); + + test('switch to same milestone has no in-progress warning', () => { + // beforeEach created v1.0 then v2.0; v2.0 is active + // Switch to v2.0 (same as current): previousMilestone === name, so no in-progress check + const result = runGsdTools('milestone switch v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.switched, true); + assert.strictEqual(output.has_in_progress, false); + }); +}); + +// ─── milestone list command ───────────────────────────────────────────────── + +describe('milestone list command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + runGsdTools('milestone create v1.0', tmpDir); + runGsdTools('milestone create v2.0', tmpDir); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('lists both milestones with correct active flag', () => { + const result = runGsdTools('milestone list', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.count, 2, 'should have 2 milestones'); + + // Find milestones by name (note: may also include auto-migrated 'initial') + const names = output.milestones.map(m => m.name); + assert.ok(names.includes('v1.0'), 'v1.0 should be listed'); + assert.ok(names.includes('v2.0'), 'v2.0 should be listed'); + + // v2.0 was created last, so it should be active + const v2 = output.milestones.find(m => m.name === 'v2.0'); + assert.strictEqual(v2.active, true, 'v2.0 should be active'); + + const v1 = output.milestones.find(m => m.name === 'v1.0'); + assert.strictEqual(v1.active, false, 'v1.0 should not be active'); + }); +}); + +// ─── milestone status command ─────────────────────────────────────────────── + +describe('milestone status command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('reports legacy mode when no active milestone', () => { + const result = runGsdTools('milestone status', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.active, null); + assert.strictEqual(output.is_multi_milestone, false); + assert.strictEqual(output.state_path, '.planning/STATE.md'); + }); + + test('reports active milestone name when one is set', () => { + runGsdTools('milestone create v2.0', tmpDir); + + const result = runGsdTools('milestone status', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.active, 'v2.0'); + assert.strictEqual(output.is_multi_milestone, true); + assert.strictEqual(output.state_path, '.planning/milestones/v2.0/STATE.md'); + }); +}); + +// ─── --milestone flag integration ─────────────────────────────────────────── + +describe('--milestone flag integration', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create a milestone so the directory exists + runGsdTools('milestone create v2.0', tmpDir); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('state load --milestone v2.0 reads from milestone-scoped directory', () => { + const result = runGsdTools('state load --milestone v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // The state should be loaded (state_exists depends on whether the milestone has a STATE.md) + // Since milestone create writes STATE.md, it should exist + assert.strictEqual(output.state_exists, true, 'state should exist in milestone directory'); + }); + + test('milestone status --milestone v2.0 shows v2.0 paths', () => { + // Switch away from v2.0 first + runGsdTools('milestone create v3.0', tmpDir); + + const result = runGsdTools('milestone status --milestone v2.0', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.state_path, '.planning/milestones/v2.0/STATE.md'); + }); +});