@undeemed/get-shit-done-codex 1.23.2 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +51 -5
  2. package/agents/gsd-debugger.md +8 -56
  3. package/agents/gsd-planner.md +2 -118
  4. package/agents/gsd-project-researcher.md +0 -3
  5. package/agents/gsd-research-synthesizer.md +0 -3
  6. package/bin/install.js +267 -5
  7. package/commands/gsd/add-phase.md +2 -6
  8. package/commands/gsd/add-todo.md +1 -6
  9. package/commands/gsd/check-todos.md +2 -6
  10. package/commands/gsd/debug.md +1 -6
  11. package/commands/gsd/discuss-phase.md +16 -9
  12. package/commands/gsd/execute-phase.md +2 -1
  13. package/commands/gsd/new-milestone.md +8 -1
  14. package/commands/gsd/pause-work.md +1 -4
  15. package/commands/gsd/plan-phase.md +1 -2
  16. package/commands/gsd/research-phase.md +15 -17
  17. package/commands/gsd/verify-work.md +2 -1
  18. package/get-shit-done/bin/gsd-tools.cjs +4951 -121
  19. package/get-shit-done/bin/lib/commands.cjs +4 -9
  20. package/get-shit-done/bin/lib/core.cjs +102 -23
  21. package/get-shit-done/bin/lib/init.cjs +11 -11
  22. package/get-shit-done/bin/lib/milestone.cjs +54 -3
  23. package/get-shit-done/bin/lib/phase.cjs +40 -10
  24. package/get-shit-done/bin/lib/state.cjs +86 -33
  25. package/get-shit-done/references/checkpoints.md +0 -1
  26. package/get-shit-done/references/model-profile-resolution.md +13 -6
  27. package/get-shit-done/references/model-profiles.md +60 -51
  28. package/get-shit-done/templates/context.md +14 -0
  29. package/get-shit-done/templates/phase-prompt.md +0 -2
  30. package/get-shit-done/workflows/audit-milestone.md +8 -63
  31. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  32. package/get-shit-done/workflows/execute-phase.md +9 -54
  33. package/get-shit-done/workflows/execute-plan.md +13 -17
  34. package/get-shit-done/workflows/help.md +3 -3
  35. package/get-shit-done/workflows/map-codebase.md +44 -32
  36. package/get-shit-done/workflows/new-milestone.md +7 -16
  37. package/get-shit-done/workflows/new-project.md +80 -49
  38. package/get-shit-done/workflows/progress.md +26 -14
  39. package/get-shit-done/workflows/quick.md +15 -24
  40. package/get-shit-done/workflows/set-profile.md +12 -8
  41. package/get-shit-done/workflows/settings.md +14 -21
  42. package/get-shit-done/workflows/transition.md +0 -5
  43. package/get-shit-done/workflows/verify-work.md +12 -11
  44. package/hooks/dist/gsd-context-monitor.js +1 -1
  45. package/package.json +3 -2
  46. package/scripts/run-tests.cjs +43 -0
@@ -204,17 +204,12 @@ function cmdResolveModel(cwd, agentType, raw) {
204
204
 
205
205
  const config = loadConfig(cwd);
206
206
  const profile = config.model_profile || 'balanced';
207
+ const model = resolveModelInternal(cwd, agentType);
207
208
 
208
209
  const agentModels = MODEL_PROFILES[agentType];
209
- if (!agentModels) {
210
- const result = { model: 'sonnet', profile, unknown_agent: true };
211
- output(result, raw, 'sonnet');
212
- return;
213
- }
214
-
215
- const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
216
- const model = resolved === 'opus' ? 'inherit' : resolved;
217
- const result = { model, profile };
210
+ const result = agentModels
211
+ ? { model, profile }
212
+ : { model, profile, unknown_agent: true };
218
213
  output(result, raw, model);
219
214
  }
220
215
 
@@ -6,22 +6,32 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execSync } = require('child_process');
8
8
 
9
+ // ─── Path helpers ────────────────────────────────────────────────────────────
10
+
11
+ /** Normalize a relative path to always use forward slashes (cross-platform). */
12
+ function toPosixPath(p) {
13
+ return p.split(path.sep).join('/');
14
+ }
15
+
9
16
  // ─── Model Profile Table ─────────────────────────────────────────────────────
10
17
 
11
18
  const MODEL_PROFILES = {
12
- 'gsd-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
13
- 'gsd-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
14
- 'gsd-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
15
- 'gsd-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
16
- 'gsd-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
17
- 'gsd-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
18
- 'gsd-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
19
- 'gsd-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
20
- 'gsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
21
- 'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
22
- 'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
19
+ // quality balanced budget
20
+ 'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
21
+ 'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
22
+ 'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
23
+ 'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
24
+ 'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
25
+ 'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
26
+ 'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
27
+ 'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
28
+ 'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
29
+ 'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
30
+ 'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
23
31
  };
24
32
 
33
+ const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'high' };
34
+
25
35
  // ─── Output helpers ───────────────────────────────────────────────────────────
26
36
 
27
37
  function output(result, raw, rawValue) {
@@ -29,7 +39,7 @@ function output(result, raw, rawValue) {
29
39
  process.stdout.write(String(rawValue));
30
40
  } else {
31
41
  const json = JSON.stringify(result, null, 2);
32
- // Large payloads exceed Codex Code's Bash tool buffer (~50KB).
42
+ // Large payloads exceed Codex CLI's tool buffer (~50KB).
33
43
  // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
34
44
  if (json.length > 50000) {
35
45
  const tmpPath = path.join(require('os').tmpdir(), `gsd-${Date.now()}.json`);
@@ -69,6 +79,7 @@ function loadConfig(cwd) {
69
79
  research: true,
70
80
  plan_checker: true,
71
81
  verifier: true,
82
+ nyquist_validation: false,
72
83
  parallelization: true,
73
84
  brave_search: false,
74
85
  };
@@ -102,8 +113,10 @@ function loadConfig(cwd) {
102
113
  research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
103
114
  plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
104
115
  verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
116
+ nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
105
117
  parallelization,
106
118
  brave_search: get('brave_search') ?? defaults.brave_search,
119
+ model_overrides: parsed.model_overrides || null,
107
120
  };
108
121
  } catch {
109
122
  return defaults;
@@ -114,7 +127,11 @@ function loadConfig(cwd) {
114
127
 
115
128
  function isGitIgnored(cwd, targetPath) {
116
129
  try {
117
- execSync('git check-ignore -q -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
130
+ // --no-index checks .gitignore rules regardless of whether the file is tracked.
131
+ // Without it, git check-ignore returns "not ignored" for tracked files even when
132
+ // .gitignore explicitly lists them — a common source of confusion when .planning/
133
+ // was committed before being added to .gitignore.
134
+ execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
118
135
  cwd,
119
136
  stdio: 'pipe',
120
137
  });
@@ -217,7 +234,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
217
234
 
218
235
  return {
219
236
  found: true,
220
- directory: path.join(relBase, match),
237
+ directory: toPosixPath(path.join(relBase, match)),
221
238
  phase_number: phaseNumber,
222
239
  phase_name: phaseName,
223
240
  phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
@@ -240,7 +257,7 @@ function findPhaseInternal(cwd, phase) {
240
257
  const normalized = normalizePhaseName(phase);
241
258
 
242
259
  // Search current phases first
243
- const current = searchPhaseInDir(phasesDir, path.join('.planning', 'phases'), normalized);
260
+ const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized);
244
261
  if (current) return current;
245
262
 
246
263
  // Search archived milestone phases (newest first)
@@ -258,7 +275,7 @@ function findPhaseInternal(cwd, phase) {
258
275
  for (const archiveName of archiveDirs) {
259
276
  const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
260
277
  const archivePath = path.join(milestonesDir, archiveName);
261
- const relBase = path.join('.planning', 'milestones', archiveName);
278
+ const relBase = '.planning/milestones/' + archiveName;
262
279
  const result = searchPhaseInDir(archivePath, relBase, normalized);
263
280
  if (result) {
264
281
  result.archived = version;
@@ -347,15 +364,19 @@ function resolveModelInternal(cwd, agentType) {
347
364
  // Check per-agent override first
348
365
  const override = config.model_overrides?.[agentType];
349
366
  if (override) {
350
- return override === 'opus' ? 'inherit' : override;
367
+ // Override can be a string (thinking level) or { m, t } object
368
+ if (typeof override === 'string') {
369
+ return { model: 'inherit', thinking: override === 'xhigh' || override === 'high' || override === 'medium' || override === 'low' ? override : 'high' };
370
+ }
371
+ return { model: 'inherit', thinking: override.t || 'high' };
351
372
  }
352
373
 
353
374
  // Fall back to profile lookup
354
375
  const profile = config.model_profile || 'balanced';
355
376
  const agentModels = MODEL_PROFILES[agentType];
356
- if (!agentModels) return 'sonnet';
357
- const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
358
- return resolved === 'opus' ? 'inherit' : resolved;
377
+ if (!agentModels) return { model: 'inherit', thinking: 'high' };
378
+ const entry = agentModels[profile] || agentModels['balanced'] || DEFAULT_ENTRY;
379
+ return { model: 'inherit', thinking: entry.t };
359
380
  }
360
381
 
361
382
  // ─── Misc utilities ───────────────────────────────────────────────────────────
@@ -378,17 +399,73 @@ function generateSlugInternal(text) {
378
399
  function getMilestoneInfo(cwd) {
379
400
  try {
380
401
  const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
381
- const versionMatch = roadmap.match(/v(\d+\.\d+)/);
382
- const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
402
+
403
+ // First: check for list-format roadmaps using 🚧 (in-progress) marker
404
+ // e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
405
+ const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
406
+ if (inProgressMatch) {
407
+ return {
408
+ version: 'v' + inProgressMatch[1],
409
+ name: inProgressMatch[2].trim(),
410
+ };
411
+ }
412
+
413
+ // Second: heading-format roadmaps — strip shipped milestones in <details> blocks
414
+ const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
415
+ // Extract version and name from the same ## heading for consistency
416
+ const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
417
+ if (headingMatch) {
418
+ return {
419
+ version: 'v' + headingMatch[1],
420
+ name: headingMatch[2].trim(),
421
+ };
422
+ }
423
+ // Fallback: try bare version match
424
+ const versionMatch = cleaned.match(/v(\d+\.\d+)/);
383
425
  return {
384
426
  version: versionMatch ? versionMatch[0] : 'v1.0',
385
- name: nameMatch ? nameMatch[1].trim() : 'milestone',
427
+ name: 'milestone',
386
428
  };
387
429
  } catch {
388
430
  return { version: 'v1.0', name: 'milestone' };
389
431
  }
390
432
  }
391
433
 
434
+ /**
435
+ * Returns a filter function that checks whether a phase directory belongs
436
+ * to the current milestone based on ROADMAP.md phase headings.
437
+ * If no ROADMAP exists or no phases are listed, returns a pass-all filter.
438
+ */
439
+ function getMilestonePhaseFilter(cwd) {
440
+ const milestonePhaseNums = new Set();
441
+ try {
442
+ const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
443
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
444
+ let m;
445
+ while ((m = phasePattern.exec(roadmap)) !== null) {
446
+ milestonePhaseNums.add(m[1]);
447
+ }
448
+ } catch {}
449
+
450
+ if (milestonePhaseNums.size === 0) {
451
+ const passAll = () => true;
452
+ passAll.phaseCount = 0;
453
+ return passAll;
454
+ }
455
+
456
+ const normalized = new Set(
457
+ [...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
458
+ );
459
+
460
+ function isDirInMilestone(dirName) {
461
+ const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
462
+ if (!m) return false;
463
+ return normalized.has(m[1].toLowerCase());
464
+ }
465
+ isDirInMilestone.phaseCount = milestonePhaseNums.size;
466
+ return isDirInMilestone;
467
+ }
468
+
392
469
  module.exports = {
393
470
  MODEL_PROFILES,
394
471
  output,
@@ -408,4 +485,6 @@ module.exports = {
408
485
  pathExistsInternal,
409
486
  generateSlugInternal,
410
487
  getMilestoneInfo,
488
+ getMilestonePhaseFilter,
489
+ toPosixPath,
411
490
  };
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execSync } = require('child_process');
8
- const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, output, error } = require('./core.cjs');
8
+ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
9
9
 
10
10
  function cmdInitExecutePhase(cwd, phase, raw) {
11
11
  if (!phase) {
@@ -139,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) {
139
139
  const files = fs.readdirSync(phaseDirFull);
140
140
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
141
141
  if (contextFile) {
142
- result.context_path = path.join(phaseInfo.directory, contextFile);
142
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
143
143
  }
144
144
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
145
145
  if (researchFile) {
146
- result.research_path = path.join(phaseInfo.directory, researchFile);
146
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
147
147
  }
148
148
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
149
149
  if (verificationFile) {
150
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
150
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
151
151
  }
152
152
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
153
153
  if (uatFile) {
154
- result.uat_path = path.join(phaseInfo.directory, uatFile);
154
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
155
155
  }
156
156
  } catch {}
157
157
  }
@@ -422,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) {
422
422
  const files = fs.readdirSync(phaseDirFull);
423
423
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
424
424
  if (contextFile) {
425
- result.context_path = path.join(phaseInfo.directory, contextFile);
425
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
426
426
  }
427
427
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
428
428
  if (researchFile) {
429
- result.research_path = path.join(phaseInfo.directory, researchFile);
429
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
430
430
  }
431
431
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
432
432
  if (verificationFile) {
433
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
433
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
434
434
  }
435
435
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
436
436
  if (uatFile) {
437
- result.uat_path = path.join(phaseInfo.directory, uatFile);
437
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
438
438
  }
439
439
  } catch {}
440
440
  }
@@ -469,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) {
469
469
  created: createdMatch ? createdMatch[1].trim() : 'unknown',
470
470
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
471
471
  area: todoArea,
472
- path: path.join('.planning', 'todos', 'pending', file),
472
+ path: '.planning/todos/pending/' + file,
473
473
  });
474
474
  } catch {}
475
475
  }
@@ -629,7 +629,7 @@ function cmdInitProgress(cwd, raw) {
629
629
  const phaseInfo = {
630
630
  number: phaseNumber,
631
631
  name: phaseName,
632
- directory: path.join('.planning', 'phases', dir),
632
+ directory: '.planning/phases/' + dir,
633
633
  status,
634
634
  plan_count: plans.length,
635
635
  summary_count: summaries.length,
@@ -92,7 +92,44 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
92
92
  // Ensure archive directory exists
93
93
  fs.mkdirSync(archiveDir, { recursive: true });
94
94
 
95
- // Gather stats from phases
95
+ // Extract milestone phase numbers from ROADMAP.md to scope stats.
96
+ // Only phases listed in the current ROADMAP are counted — phases from
97
+ // prior milestones that remain on disk are excluded.
98
+ //
99
+ // Related upstream PRs (getMilestoneInfo, not milestone complete):
100
+ // #756 — fix(core): detect current milestone correctly in getMilestoneInfo
101
+ // #783 — fix: getMilestoneInfo() returns wrong version after completion
102
+ // Those PRs fix *which* milestone is detected; this fix scopes *stats*
103
+ // and *accomplishments* to only the phases belonging to that milestone.
104
+ const milestonePhaseNums = new Set();
105
+ if (fs.existsSync(roadmapPath)) {
106
+ try {
107
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
108
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
109
+ let phaseMatch;
110
+ while ((phaseMatch = phasePattern.exec(roadmapContent)) !== null) {
111
+ milestonePhaseNums.add(phaseMatch[1]);
112
+ }
113
+ } catch {}
114
+ }
115
+
116
+ // Pre-normalize phase numbers for O(1) lookup — strip leading zeros
117
+ // and lowercase for case-insensitive matching of letter suffixes (e.g. 3A/3a).
118
+ const normalizedPhaseNums = new Set(
119
+ [...milestonePhaseNums].map(num => (num.replace(/^0+/, '') || '0').toLowerCase())
120
+ );
121
+
122
+ // Match a phase directory name to the milestone's phase set.
123
+ // Handles: "01-foo" → "1", "3A-bar" → "3a", "3.1-baz" → "3.1"
124
+ // Returns false for non-phase directories (no leading digit).
125
+ function isDirInMilestone(dirName) {
126
+ if (normalizedPhaseNums.size === 0) return true; // no scoping
127
+ const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
128
+ if (!m) return false; // not a phase directory
129
+ return normalizedPhaseNums.has(m[1].toLowerCase());
130
+ }
131
+
132
+ // Gather stats from phases (scoped to current milestone only)
96
133
  let phaseCount = 0;
97
134
  let totalPlans = 0;
98
135
  let totalTasks = 0;
@@ -103,6 +140,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
103
140
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
104
141
 
105
142
  for (const dir of dirs) {
143
+ if (!isDirInMilestone(dir)) continue;
144
+
106
145
  phaseCount++;
107
146
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
108
147
  const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
@@ -150,7 +189,16 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
150
189
 
151
190
  if (fs.existsSync(milestonesPath)) {
152
191
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
153
- fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8');
192
+ // Insert after the header line(s) for reverse chronological order (newest first)
193
+ const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
194
+ if (headerMatch) {
195
+ const header = headerMatch[1];
196
+ const rest = existing.slice(header.length);
197
+ fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
198
+ } else {
199
+ // No recognizable header — prepend the entry
200
+ fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
201
+ }
154
202
  } else {
155
203
  fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
156
204
  }
@@ -182,10 +230,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
182
230
 
183
231
  const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
184
232
  const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
233
+ let archivedCount = 0;
185
234
  for (const dir of phaseDirNames) {
235
+ if (!isDirInMilestone(dir)) continue;
186
236
  fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
237
+ archivedCount++;
187
238
  }
188
- phasesArchived = phaseDirNames.length > 0;
239
+ phasesArchived = archivedCount > 0;
189
240
  } catch {}
190
241
  }
191
242
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { writeStateMd } = require('./state.cjs');
10
10
 
@@ -193,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) {
193
193
  }
194
194
  }
195
195
 
196
+ function extractObjective(content) {
197
+ const m = content.match(/<objective>\s*\n?\s*(.+)/);
198
+ return m ? m[1].trim() : null;
199
+ }
200
+
196
201
  function cmdPhasePlanIndex(cwd, phase, raw) {
197
202
  if (!phase) {
198
203
  error('phase required for phase-plan-index');
@@ -242,9 +247,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
242
247
  const content = fs.readFileSync(planPath, 'utf-8');
243
248
  const fm = extractFrontmatter(content);
244
249
 
245
- // Count tasks (## Task N patterns)
246
- const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
247
- const taskCount = taskMatches.length;
250
+ // Count tasks: XML <task> tags (canonical) or ## Task N markdown (legacy)
251
+ const xmlTasks = content.match(/<task[\s>]/gi) || [];
252
+ const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
253
+ const taskCount = xmlTasks.length || mdTasks.length;
248
254
 
249
255
  // Parse wave as integer
250
256
  const wave = parseInt(fm.wave, 10) || 1;
@@ -259,10 +265,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
259
265
  hasCheckpoints = true;
260
266
  }
261
267
 
262
- // Parse files-modified
268
+ // Parse files_modified (underscore is canonical; also accept hyphenated for compat)
263
269
  let filesModified = [];
264
- if (fm['files-modified']) {
265
- filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']];
270
+ const fmFiles = fm['files_modified'] || fm['files-modified'];
271
+ if (fmFiles) {
272
+ filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
266
273
  }
267
274
 
268
275
  const hasSummary = completedPlanIds.has(planId);
@@ -274,7 +281,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
274
281
  id: planId,
275
282
  wave,
276
283
  autonomous,
277
- objective: fm.objective || null,
284
+ objective: extractObjective(content) || fm.objective || null,
278
285
  files_modified: filesModified,
279
286
  task_count: taskCount,
280
287
  has_summary: hasSummary,
@@ -776,14 +783,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
776
783
  }
777
784
  }
778
785
 
779
- // Find next phase
786
+ // Find next phase — check both filesystem AND roadmap
787
+ // Phases may be defined in ROADMAP.md but not yet scaffolded to disk,
788
+ // so a filesystem-only scan would incorrectly report is_last_phase:true
780
789
  let nextPhaseNum = null;
781
790
  let nextPhaseName = null;
782
791
  let isLastPhase = true;
783
792
 
784
793
  try {
794
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
785
795
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
786
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
796
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
797
+ .filter(isDirInMilestone)
798
+ .sort((a, b) => comparePhaseNum(a, b));
787
799
 
788
800
  // Find the next phase directory after current
789
801
  for (const dir of dirs) {
@@ -799,6 +811,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
799
811
  }
800
812
  } catch {}
801
813
 
814
+ // Fallback: if filesystem found no next phase, check ROADMAP.md
815
+ // for phases that are defined but not yet planned (no directory on disk)
816
+ if (isLastPhase && fs.existsSync(roadmapPath)) {
817
+ try {
818
+ const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
819
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
820
+ let pm;
821
+ while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
822
+ if (comparePhaseNum(pm[1], phaseNum) > 0) {
823
+ nextPhaseNum = pm[1];
824
+ nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
825
+ isLastPhase = false;
826
+ break;
827
+ }
828
+ }
829
+ } catch {}
830
+ }
831
+
802
832
  // Update STATE.md
803
833
  if (fs.existsSync(statePath)) {
804
834
  let stateContent = fs.readFileSync(statePath, 'utf-8');