gsd-opencode 1.20.3 → 1.22.0

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 (114) hide show
  1. package/agents/gsd-codebase-mapper.md +9 -1
  2. package/agents/gsd-debugger.md +66 -10
  3. package/agents/gsd-executor.md +36 -16
  4. package/agents/gsd-integration-checker.md +2 -0
  5. package/agents/gsd-nyquist-auditor.md +178 -0
  6. package/agents/gsd-phase-researcher.md +28 -34
  7. package/agents/gsd-plan-checker.md +42 -78
  8. package/agents/gsd-planner.md +139 -24
  9. package/agents/gsd-project-researcher.md +11 -1
  10. package/agents/gsd-research-synthesizer.md +13 -3
  11. package/agents/gsd-roadmapper.md +25 -15
  12. package/agents/gsd-verifier.md +29 -6
  13. package/bin/dm/lib/constants.js +6 -1
  14. package/bin/dm/src/services/file-ops.js +14 -1
  15. package/commands/gsd/gsd-add-phase.md +6 -6
  16. package/commands/gsd/gsd-add-tests.md +41 -0
  17. package/commands/gsd/gsd-add-todo.md +7 -7
  18. package/commands/gsd/gsd-audit-milestone.md +9 -9
  19. package/commands/gsd/gsd-check-profile.md +3 -3
  20. package/commands/gsd/gsd-check-todos.md +7 -7
  21. package/commands/gsd/gsd-cleanup.md +2 -2
  22. package/commands/gsd/gsd-complete-milestone.md +6 -6
  23. package/commands/gsd/gsd-debug.md +11 -7
  24. package/commands/gsd/gsd-discuss-phase.md +26 -19
  25. package/commands/gsd/gsd-execute-phase.md +13 -13
  26. package/commands/gsd/gsd-health.md +7 -7
  27. package/commands/gsd/gsd-help.md +2 -2
  28. package/commands/gsd/gsd-insert-phase.md +6 -6
  29. package/commands/gsd/gsd-join-discord.md +1 -1
  30. package/commands/gsd/gsd-list-phase-assumptions.md +6 -6
  31. package/commands/gsd/gsd-map-codebase.md +8 -8
  32. package/commands/gsd/gsd-new-milestone.md +12 -12
  33. package/commands/gsd/gsd-new-project.md +12 -12
  34. package/commands/gsd/gsd-pause-work.md +6 -6
  35. package/commands/gsd/gsd-plan-milestone-gaps.md +9 -9
  36. package/commands/gsd/gsd-plan-phase.md +14 -13
  37. package/commands/gsd/gsd-progress.md +8 -8
  38. package/commands/gsd/gsd-quick.md +17 -13
  39. package/commands/gsd/gsd-reapply-patches.md +19 -11
  40. package/commands/gsd/gsd-remove-phase.md +7 -7
  41. package/commands/gsd/gsd-research-phase.md +12 -11
  42. package/commands/gsd/gsd-resume-work.md +8 -8
  43. package/commands/gsd/gsd-set-profile.md +6 -6
  44. package/commands/gsd/gsd-settings.md +7 -7
  45. package/commands/gsd/gsd-update.md +5 -5
  46. package/commands/gsd/gsd-validate-phase.md +35 -0
  47. package/commands/gsd/gsd-verify-work.md +11 -11
  48. package/get-shit-done/bin/gsd-oc-commands/allow-read-config.cjs +235 -0
  49. package/get-shit-done/bin/gsd-oc-tools.cjs +11 -5
  50. package/get-shit-done/bin/gsd-tools.cjs +45 -6
  51. package/get-shit-done/bin/lib/commands.cjs +11 -19
  52. package/get-shit-done/bin/lib/config.cjs +8 -1
  53. package/get-shit-done/bin/lib/core.cjs +131 -16
  54. package/get-shit-done/bin/lib/init.cjs +28 -12
  55. package/get-shit-done/bin/lib/milestone.cjs +34 -8
  56. package/get-shit-done/bin/lib/phase.cjs +74 -50
  57. package/get-shit-done/bin/lib/roadmap.cjs +7 -7
  58. package/get-shit-done/bin/lib/state.cjs +294 -63
  59. package/get-shit-done/bin/lib/template.cjs +3 -3
  60. package/get-shit-done/bin/lib/verify.cjs +56 -8
  61. package/get-shit-done/bin/test/allow-read-config.test.cjs +262 -0
  62. package/get-shit-done/references/checkpoints.md +1 -1
  63. package/get-shit-done/references/decimal-phase-calculation.md +6 -6
  64. package/get-shit-done/references/git-integration.md +3 -3
  65. package/get-shit-done/references/git-planning-commit.md +2 -2
  66. package/get-shit-done/references/model-profile-resolution.md +1 -1
  67. package/get-shit-done/references/model-profiles.md +1 -0
  68. package/get-shit-done/references/phase-argument-parsing.md +4 -4
  69. package/get-shit-done/references/planning-config.md +10 -6
  70. package/get-shit-done/references/questioning.md +17 -0
  71. package/get-shit-done/references/verification-patterns.md +1 -1
  72. package/get-shit-done/templates/DEBUG.md +7 -2
  73. package/get-shit-done/templates/VALIDATION.md +18 -46
  74. package/get-shit-done/templates/codebase/structure.md +3 -3
  75. package/get-shit-done/templates/config.json +2 -2
  76. package/get-shit-done/templates/context.md +14 -0
  77. package/get-shit-done/templates/phase-prompt.md +10 -10
  78. package/get-shit-done/templates/retrospective.md +54 -0
  79. package/get-shit-done/templates/roadmap.md +1 -1
  80. package/get-shit-done/workflows/add-phase.md +3 -2
  81. package/get-shit-done/workflows/add-tests.md +351 -0
  82. package/get-shit-done/workflows/add-todo.md +4 -3
  83. package/get-shit-done/workflows/audit-milestone.md +40 -5
  84. package/get-shit-done/workflows/check-todos.md +3 -2
  85. package/get-shit-done/workflows/cleanup.md +1 -1
  86. package/get-shit-done/workflows/complete-milestone.md +69 -5
  87. package/get-shit-done/workflows/diagnose-issues.md +2 -2
  88. package/get-shit-done/workflows/discovery-phase.md +6 -6
  89. package/get-shit-done/workflows/discuss-phase.md +194 -58
  90. package/get-shit-done/workflows/execute-phase.md +29 -23
  91. package/get-shit-done/workflows/execute-plan.md +22 -18
  92. package/get-shit-done/workflows/health.md +5 -2
  93. package/get-shit-done/workflows/help.md +4 -1
  94. package/get-shit-done/workflows/insert-phase.md +3 -2
  95. package/get-shit-done/workflows/map-codebase.md +3 -2
  96. package/get-shit-done/workflows/new-milestone.md +12 -10
  97. package/get-shit-done/workflows/new-project.md +44 -49
  98. package/get-shit-done/workflows/oc-set-profile.md +24 -0
  99. package/get-shit-done/workflows/pause-work.md +2 -2
  100. package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
  101. package/get-shit-done/workflows/plan-phase.md +155 -73
  102. package/get-shit-done/workflows/progress.md +8 -7
  103. package/get-shit-done/workflows/quick.md +158 -10
  104. package/get-shit-done/workflows/remove-phase.md +5 -4
  105. package/get-shit-done/workflows/research-phase.md +5 -4
  106. package/get-shit-done/workflows/resume-project.md +3 -2
  107. package/get-shit-done/workflows/set-profile.md +3 -2
  108. package/get-shit-done/workflows/settings.md +6 -6
  109. package/get-shit-done/workflows/transition.md +5 -5
  110. package/get-shit-done/workflows/update.md +45 -19
  111. package/get-shit-done/workflows/validate-phase.md +167 -0
  112. package/get-shit-done/workflows/verify-phase.md +10 -9
  113. package/get-shit-done/workflows/verify-work.md +18 -4
  114. package/package.json +1 -1
@@ -6,6 +6,13 @@ 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 = {
@@ -20,6 +27,7 @@ const MODEL_PROFILES = {
20
27
  'gsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
21
28
  'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
22
29
  'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
30
+ 'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
23
31
  };
24
32
 
25
33
  // ─── Output helpers ───────────────────────────────────────────────────────────
@@ -69,6 +77,7 @@ function loadConfig(cwd) {
69
77
  research: true,
70
78
  plan_checker: true,
71
79
  verifier: true,
80
+ nyquist_validation: true,
72
81
  parallelization: true,
73
82
  brave_search: false,
74
83
  };
@@ -77,6 +86,14 @@ function loadConfig(cwd) {
77
86
  const raw = fs.readFileSync(configPath, 'utf-8');
78
87
  const parsed = JSON.parse(raw);
79
88
 
89
+ // Migrate deprecated "depth" key to "granularity" with value mapping
90
+ if ('depth' in parsed && !('granularity' in parsed)) {
91
+ const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
92
+ parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
93
+ delete parsed.depth;
94
+ try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
95
+ }
96
+
80
97
  const get = (key, nested) => {
81
98
  if (parsed[key] !== undefined) return parsed[key];
82
99
  if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
@@ -102,8 +119,10 @@ function loadConfig(cwd) {
102
119
  research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
103
120
  plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
104
121
  verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
122
+ nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
105
123
  parallelization,
106
124
  brave_search: get('brave_search') ?? defaults.brave_search,
125
+ model_overrides: parsed.model_overrides || null,
107
126
  };
108
127
  } catch {
109
128
  return defaults;
@@ -114,7 +133,11 @@ function loadConfig(cwd) {
114
133
 
115
134
  function isGitIgnored(cwd, targetPath) {
116
135
  try {
117
- execSync('git check-ignore -q -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
136
+ // --no-index checks .gitignore rules regardless of whether the file is tracked.
137
+ // Without it, git check-ignore returns "not ignored" for tracked files even when
138
+ // .gitignore explicitly lists them — a common source of confusion when .planning/
139
+ // was committed before being added to .gitignore.
140
+ execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
118
141
  cwd,
119
142
  stdio: 'pipe',
120
143
  });
@@ -147,23 +170,55 @@ function execGit(cwd, args) {
147
170
 
148
171
  // ─── Phase utilities ──────────────────────────────────────────────────────────
149
172
 
173
+ function escapeRegex(value) {
174
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
175
+ }
176
+
150
177
  function normalizePhaseName(phase) {
151
- const match = phase.match(/^(\d+(?:\.\d+)?)/);
178
+ const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
152
179
  if (!match) return phase;
153
- const num = match[1];
154
- const parts = num.split('.');
155
- const padded = parts[0].padStart(2, '0');
156
- return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
180
+ const padded = match[1].padStart(2, '0');
181
+ const letter = match[2] ? match[2].toUpperCase() : '';
182
+ const decimal = match[3] || '';
183
+ return padded + letter + decimal;
184
+ }
185
+
186
+ function comparePhaseNum(a, b) {
187
+ const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
188
+ const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
189
+ if (!pa || !pb) return String(a).localeCompare(String(b));
190
+ const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
191
+ if (intDiff !== 0) return intDiff;
192
+ // No letter sorts before letter: 12 < 12A < 12B
193
+ const la = (pa[2] || '').toUpperCase();
194
+ const lb = (pb[2] || '').toUpperCase();
195
+ if (la !== lb) {
196
+ if (!la) return -1;
197
+ if (!lb) return 1;
198
+ return la < lb ? -1 : 1;
199
+ }
200
+ // Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
201
+ const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
202
+ const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
203
+ const maxLen = Math.max(aDecParts.length, bDecParts.length);
204
+ if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
205
+ if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
206
+ for (let i = 0; i < maxLen; i++) {
207
+ const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
208
+ const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
209
+ if (av !== bv) return av - bv;
210
+ }
211
+ return 0;
157
212
  }
158
213
 
159
214
  function searchPhaseInDir(baseDir, relBase, normalized) {
160
215
  try {
161
216
  const entries = fs.readdirSync(baseDir, { withFileTypes: true });
162
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
217
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
163
218
  const match = dirs.find(d => d.startsWith(normalized));
164
219
  if (!match) return null;
165
220
 
166
- const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
221
+ const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
167
222
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
168
223
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
169
224
  const phaseDir = path.join(baseDir, match);
@@ -185,7 +240,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
185
240
 
186
241
  return {
187
242
  found: true,
188
- directory: path.join(relBase, match),
243
+ directory: toPosixPath(path.join(relBase, match)),
189
244
  phase_number: phaseNumber,
190
245
  phase_name: phaseName,
191
246
  phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
@@ -208,7 +263,7 @@ function findPhaseInternal(cwd, phase) {
208
263
  const normalized = normalizePhaseName(phase);
209
264
 
210
265
  // Search current phases first
211
- const current = searchPhaseInDir(phasesDir, path.join('.planning', 'phases'), normalized);
266
+ const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized);
212
267
  if (current) return current;
213
268
 
214
269
  // Search archived milestone phases (newest first)
@@ -226,7 +281,7 @@ function findPhaseInternal(cwd, phase) {
226
281
  for (const archiveName of archiveDirs) {
227
282
  const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
228
283
  const archivePath = path.join(milestonesDir, archiveName);
229
- const relBase = path.join('.planning', 'milestones', archiveName);
284
+ const relBase = '.planning/milestones/' + archiveName;
230
285
  const result = searchPhaseInDir(archivePath, relBase, normalized);
231
286
  if (result) {
232
287
  result.archived = version;
@@ -257,7 +312,7 @@ function getArchivedPhaseDirs(cwd) {
257
312
  const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
258
313
  const archivePath = path.join(milestonesDir, archiveName);
259
314
  const entries = fs.readdirSync(archivePath, { withFileTypes: true });
260
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
315
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
261
316
 
262
317
  for (const dir of dirs) {
263
318
  results.push({
@@ -282,7 +337,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
282
337
 
283
338
  try {
284
339
  const content = fs.readFileSync(roadmapPath, 'utf-8');
285
- const escapedPhase = phaseNum.toString().replace(/\./g, '\\.');
340
+ const escapedPhase = escapeRegex(phaseNum.toString());
286
341
  const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
287
342
  const headerMatch = content.match(phasePattern);
288
343
  if (!headerMatch) return null;
@@ -346,17 +401,73 @@ function generateSlugInternal(text) {
346
401
  function getMilestoneInfo(cwd) {
347
402
  try {
348
403
  const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
349
- const versionMatch = roadmap.match(/v(\d+\.\d+)/);
350
- const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
404
+
405
+ // First: check for list-format roadmaps using 🚧 (in-progress) marker
406
+ // e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
407
+ const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
408
+ if (inProgressMatch) {
409
+ return {
410
+ version: 'v' + inProgressMatch[1],
411
+ name: inProgressMatch[2].trim(),
412
+ };
413
+ }
414
+
415
+ // Second: heading-format roadmaps — strip shipped milestones in <details> blocks
416
+ const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
417
+ // Extract version and name from the same ## heading for consistency
418
+ const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
419
+ if (headingMatch) {
420
+ return {
421
+ version: 'v' + headingMatch[1],
422
+ name: headingMatch[2].trim(),
423
+ };
424
+ }
425
+ // Fallback: try bare version match
426
+ const versionMatch = cleaned.match(/v(\d+\.\d+)/);
351
427
  return {
352
428
  version: versionMatch ? versionMatch[0] : 'v1.0',
353
- name: nameMatch ? nameMatch[1].trim() : 'milestone',
429
+ name: 'milestone',
354
430
  };
355
431
  } catch {
356
432
  return { version: 'v1.0', name: 'milestone' };
357
433
  }
358
434
  }
359
435
 
436
+ /**
437
+ * Returns a filter function that checks whether a phase directory belongs
438
+ * to the current milestone based on ROADMAP.md phase headings.
439
+ * If no ROADMAP exists or no phases are listed, returns a pass-all filter.
440
+ */
441
+ function getMilestonePhaseFilter(cwd) {
442
+ const milestonePhaseNums = new Set();
443
+ try {
444
+ const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
445
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
446
+ let m;
447
+ while ((m = phasePattern.exec(roadmap)) !== null) {
448
+ milestonePhaseNums.add(m[1]);
449
+ }
450
+ } catch {}
451
+
452
+ if (milestonePhaseNums.size === 0) {
453
+ const passAll = () => true;
454
+ passAll.phaseCount = 0;
455
+ return passAll;
456
+ }
457
+
458
+ const normalized = new Set(
459
+ [...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
460
+ );
461
+
462
+ function isDirInMilestone(dirName) {
463
+ const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
464
+ if (!m) return false;
465
+ return normalized.has(m[1].toLowerCase());
466
+ }
467
+ isDirInMilestone.phaseCount = milestonePhaseNums.size;
468
+ return isDirInMilestone;
469
+ }
470
+
360
471
  module.exports = {
361
472
  MODEL_PROFILES,
362
473
  output,
@@ -365,7 +476,9 @@ module.exports = {
365
476
  loadConfig,
366
477
  isGitIgnored,
367
478
  execGit,
479
+ escapeRegex,
368
480
  normalizePhaseName,
481
+ comparePhaseNum,
369
482
  searchPhaseInDir,
370
483
  findPhaseInternal,
371
484
  getArchivedPhaseDirs,
@@ -374,4 +487,6 @@ module.exports = {
374
487
  pathExistsInternal,
375
488
  generateSlugInternal,
376
489
  getMilestoneInfo,
490
+ getMilestonePhaseFilter,
491
+ toPosixPath,
377
492
  };
@@ -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) {
@@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
16
16
  const phaseInfo = findPhaseInternal(cwd, phase);
17
17
  const milestone = getMilestoneInfo(cwd);
18
18
 
19
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
20
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
21
+ const reqExtracted = reqMatch
22
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
23
+ : null;
24
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
25
+
19
26
  const result = {
20
27
  // Models
21
28
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
@@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
35
42
  phase_number: phaseInfo?.phase_number || null,
36
43
  phase_name: phaseInfo?.phase_name || null,
37
44
  phase_slug: phaseInfo?.phase_slug || null,
45
+ phase_req_ids,
38
46
 
39
47
  // Plan inventory
40
48
  plans: phaseInfo?.plans || [],
@@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
80
88
  const config = loadConfig(cwd);
81
89
  const phaseInfo = findPhaseInternal(cwd, phase);
82
90
 
91
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
92
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
93
+ const reqExtracted = reqMatch
94
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
95
+ : null;
96
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
97
+
83
98
  const result = {
84
99
  // Models
85
100
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
@@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
99
114
  phase_name: phaseInfo?.phase_name || null,
100
115
  phase_slug: phaseInfo?.phase_slug || null,
101
116
  padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
117
+ phase_req_ids,
102
118
 
103
119
  // Existing artifacts
104
120
  has_research: phaseInfo?.has_research || false,
@@ -123,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) {
123
139
  const files = fs.readdirSync(phaseDirFull);
124
140
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
125
141
  if (contextFile) {
126
- result.context_path = path.join(phaseInfo.directory, contextFile);
142
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
127
143
  }
128
144
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
129
145
  if (researchFile) {
130
- result.research_path = path.join(phaseInfo.directory, researchFile);
146
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
131
147
  }
132
148
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
133
149
  if (verificationFile) {
134
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
150
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
135
151
  }
136
152
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
137
153
  if (uatFile) {
138
- result.uat_path = path.join(phaseInfo.directory, uatFile);
154
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
139
155
  }
140
156
  } catch {}
141
157
  }
@@ -406,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) {
406
422
  const files = fs.readdirSync(phaseDirFull);
407
423
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
408
424
  if (contextFile) {
409
- result.context_path = path.join(phaseInfo.directory, contextFile);
425
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
410
426
  }
411
427
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
412
428
  if (researchFile) {
413
- result.research_path = path.join(phaseInfo.directory, researchFile);
429
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
414
430
  }
415
431
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
416
432
  if (verificationFile) {
417
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
433
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
418
434
  }
419
435
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
420
436
  if (uatFile) {
421
- result.uat_path = path.join(phaseInfo.directory, uatFile);
437
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
422
438
  }
423
439
  } catch {}
424
440
  }
@@ -453,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) {
453
469
  created: createdMatch ? createdMatch[1].trim() : 'unknown',
454
470
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
455
471
  area: todoArea,
456
- path: path.join('.planning', 'todos', 'pending', file),
472
+ path: '.planning/todos/pending/' + file,
457
473
  });
458
474
  } catch {}
459
475
  }
@@ -595,7 +611,7 @@ function cmdInitProgress(cwd, raw) {
595
611
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
596
612
 
597
613
  for (const dir of dirs) {
598
- const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
614
+ const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
599
615
  const phaseNumber = match ? match[1] : dir;
600
616
  const phaseName = match && match[2] ? match[2] : null;
601
617
 
@@ -613,7 +629,7 @@ function cmdInitProgress(cwd, raw) {
613
629
  const phaseInfo = {
614
630
  number: phaseNumber,
615
631
  name: phaseName,
616
- directory: path.join('.planning', 'phases', dir),
632
+ directory: '.planning/phases/' + dir,
617
633
  status,
618
634
  plan_count: plans.length,
619
635
  summary_count: summaries.length,
@@ -4,8 +4,9 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { output, error } = require('./core.cjs');
7
+ const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
11
12
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
@@ -36,20 +37,21 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
36
37
 
37
38
  for (const reqId of reqIds) {
38
39
  let found = false;
40
+ const reqEscaped = escapeRegex(reqId);
39
41
 
40
42
  // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
41
- const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi');
43
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
42
44
  if (checkboxPattern.test(reqContent)) {
43
45
  reqContent = reqContent.replace(checkboxPattern, '$1x$2');
44
46
  found = true;
45
47
  }
46
48
 
47
49
  // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
48
- const tablePattern = new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
50
+ const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
49
51
  if (tablePattern.test(reqContent)) {
50
52
  // Re-read since test() advances lastIndex for global regex
51
53
  reqContent = reqContent.replace(
52
- new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
54
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
53
55
  '$1 Complete $2'
54
56
  );
55
57
  found = true;
@@ -91,7 +93,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
91
93
  // Ensure archive directory exists
92
94
  fs.mkdirSync(archiveDir, { recursive: true });
93
95
 
94
- // Gather stats from phases
96
+ // Scope stats and accomplishments to only the phases belonging to the
97
+ // current milestone's ROADMAP. Uses the shared filter from core.cjs
98
+ // (same logic used by cmdPhasesList and other callers).
99
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
100
+
101
+ // Gather stats from phases (scoped to current milestone only)
95
102
  let phaseCount = 0;
96
103
  let totalPlans = 0;
97
104
  let totalTasks = 0;
@@ -102,6 +109,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
102
109
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
103
110
 
104
111
  for (const dir of dirs) {
112
+ if (!isDirInMilestone(dir)) continue;
113
+
105
114
  phaseCount++;
106
115
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
107
116
  const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
@@ -149,7 +158,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
149
158
 
150
159
  if (fs.existsSync(milestonesPath)) {
151
160
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
152
- fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8');
161
+ if (!existing.trim()) {
162
+ // Empty file — treat like new
163
+ fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
164
+ } else {
165
+ // Insert after the header line(s) for reverse chronological order (newest first)
166
+ const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
167
+ if (headerMatch) {
168
+ const header = headerMatch[1];
169
+ const rest = existing.slice(header.length);
170
+ fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
171
+ } else {
172
+ // No recognizable header — prepend the entry
173
+ fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
174
+ }
175
+ }
153
176
  } else {
154
177
  fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
155
178
  }
@@ -169,7 +192,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
169
192
  /(\*\*Last Activity Description:\*\*\s*).*/,
170
193
  `$1${version} milestone completed and archived`
171
194
  );
172
- fs.writeFileSync(statePath, stateContent, 'utf-8');
195
+ writeStateMd(statePath, stateContent, cwd);
173
196
  }
174
197
 
175
198
  // Archive phase directories if requested
@@ -181,10 +204,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
181
204
 
182
205
  const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
183
206
  const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
207
+ let archivedCount = 0;
184
208
  for (const dir of phaseDirNames) {
209
+ if (!isDirInMilestone(dir)) continue;
185
210
  fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
211
+ archivedCount++;
186
212
  }
187
- phasesArchived = phaseDirNames.length > 0;
213
+ phasesArchived = archivedCount > 0;
188
214
  } catch {}
189
215
  }
190
216