gsd-opencode 1.20.4 → 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 (110) 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-tools.cjs +45 -6
  49. package/get-shit-done/bin/lib/commands.cjs +11 -19
  50. package/get-shit-done/bin/lib/config.cjs +8 -1
  51. package/get-shit-done/bin/lib/core.cjs +131 -16
  52. package/get-shit-done/bin/lib/init.cjs +28 -12
  53. package/get-shit-done/bin/lib/milestone.cjs +34 -8
  54. package/get-shit-done/bin/lib/phase.cjs +74 -50
  55. package/get-shit-done/bin/lib/roadmap.cjs +7 -7
  56. package/get-shit-done/bin/lib/state.cjs +294 -63
  57. package/get-shit-done/bin/lib/template.cjs +3 -3
  58. package/get-shit-done/bin/lib/verify.cjs +56 -8
  59. package/get-shit-done/references/checkpoints.md +1 -1
  60. package/get-shit-done/references/decimal-phase-calculation.md +6 -6
  61. package/get-shit-done/references/git-integration.md +3 -3
  62. package/get-shit-done/references/git-planning-commit.md +2 -2
  63. package/get-shit-done/references/model-profile-resolution.md +1 -1
  64. package/get-shit-done/references/model-profiles.md +1 -0
  65. package/get-shit-done/references/phase-argument-parsing.md +4 -4
  66. package/get-shit-done/references/planning-config.md +10 -6
  67. package/get-shit-done/references/questioning.md +17 -0
  68. package/get-shit-done/references/verification-patterns.md +1 -1
  69. package/get-shit-done/templates/DEBUG.md +7 -2
  70. package/get-shit-done/templates/VALIDATION.md +18 -46
  71. package/get-shit-done/templates/codebase/structure.md +3 -3
  72. package/get-shit-done/templates/config.json +2 -2
  73. package/get-shit-done/templates/context.md +14 -0
  74. package/get-shit-done/templates/phase-prompt.md +10 -10
  75. package/get-shit-done/templates/retrospective.md +54 -0
  76. package/get-shit-done/templates/roadmap.md +1 -1
  77. package/get-shit-done/workflows/add-phase.md +3 -2
  78. package/get-shit-done/workflows/add-tests.md +351 -0
  79. package/get-shit-done/workflows/add-todo.md +4 -3
  80. package/get-shit-done/workflows/audit-milestone.md +40 -5
  81. package/get-shit-done/workflows/check-todos.md +3 -2
  82. package/get-shit-done/workflows/cleanup.md +1 -1
  83. package/get-shit-done/workflows/complete-milestone.md +69 -5
  84. package/get-shit-done/workflows/diagnose-issues.md +2 -2
  85. package/get-shit-done/workflows/discovery-phase.md +6 -6
  86. package/get-shit-done/workflows/discuss-phase.md +194 -58
  87. package/get-shit-done/workflows/execute-phase.md +29 -23
  88. package/get-shit-done/workflows/execute-plan.md +22 -18
  89. package/get-shit-done/workflows/health.md +5 -2
  90. package/get-shit-done/workflows/help.md +4 -1
  91. package/get-shit-done/workflows/insert-phase.md +3 -2
  92. package/get-shit-done/workflows/map-codebase.md +3 -2
  93. package/get-shit-done/workflows/new-milestone.md +12 -10
  94. package/get-shit-done/workflows/new-project.md +44 -49
  95. package/get-shit-done/workflows/pause-work.md +2 -2
  96. package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
  97. package/get-shit-done/workflows/plan-phase.md +155 -73
  98. package/get-shit-done/workflows/progress.md +8 -7
  99. package/get-shit-done/workflows/quick.md +158 -10
  100. package/get-shit-done/workflows/remove-phase.md +5 -4
  101. package/get-shit-done/workflows/research-phase.md +5 -4
  102. package/get-shit-done/workflows/resume-project.md +3 -2
  103. package/get-shit-done/workflows/set-profile.md +3 -2
  104. package/get-shit-done/workflows/settings.md +6 -6
  105. package/get-shit-done/workflows/transition.md +5 -5
  106. package/get-shit-done/workflows/update.md +45 -19
  107. package/get-shit-done/workflows/validate-phase.md +167 -0
  108. package/get-shit-done/workflows/verify-phase.md +10 -9
  109. package/get-shit-done/workflows/verify-work.md +18 -4
  110. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: gsd-settings
3
3
  description: Configure GSD workflow toggles and model profile
4
- allowed-tools:
5
- - read
6
- - write
7
- - bash
8
- - question
4
+ permissions:
5
+ read: true
6
+ write: true
7
+ bash: true
8
+ question: true
9
9
  ---
10
10
 
11
11
  <objective>
@@ -20,11 +20,11 @@ Routes to the settings workflow which handles:
20
20
  </objective>
21
21
 
22
22
  <execution_context>
23
- @~/.config/opencode/get-shit-done/workflows/settings.md
23
+ @$HOME/.config/opencode/get-shit-done/workflows/settings.md
24
24
  </execution_context>
25
25
 
26
26
  <process>
27
- **Follow the settings workflow** from `@~/.config/opencode/get-shit-done/workflows/settings.md`.
27
+ **Follow the settings workflow** from `@$HOME/.config/opencode/get-shit-done/workflows/settings.md`.
28
28
 
29
29
  The workflow handles all logic including:
30
30
  1. Config file creation with defaults if missing
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: gsd-update
3
3
  description: Update GSD to latest version with changelog display
4
- allowed-tools:
5
- - bash
6
- - question
4
+ permissions:
5
+ bash: true
6
+ question: true
7
7
  ---
8
8
 
9
9
  <objective>
@@ -19,11 +19,11 @@ Routes to the update workflow which handles:
19
19
  </objective>
20
20
 
21
21
  <execution_context>
22
- @~/.config/opencode/get-shit-done/workflows/update.md
22
+ @$HOME/.config/opencode/get-shit-done/workflows/update.md
23
23
  </execution_context>
24
24
 
25
25
  <process>
26
- **Follow the update workflow** from `@~/.config/opencode/get-shit-done/workflows/update.md`.
26
+ **Follow the update workflow** from `@$HOME/.config/opencode/get-shit-done/workflows/update.md`.
27
27
 
28
28
  The workflow handles all logic including:
29
29
  1. Installed version detection (local/global)
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: gsd-validate-phase
3
+ description: Retroactively audit and fill Nyquist validation gaps for a completed phase
4
+ argument-hint: "[phase number]"
5
+ permissions:
6
+ read: true
7
+ write: true
8
+ edit: true
9
+ bash: true
10
+ glob: true
11
+ grep: true
12
+ task: true
13
+ question: true
14
+ ---
15
+ <objective>
16
+ Audit Nyquist validation coverage for a completed phase. Three states:
17
+ - (A) VALIDATION.md exists — audit and fill gaps
18
+ - (B) No VALIDATION.md, SUMMARY.md exists — reconstruct from artifacts
19
+ - (C) Phase not executed — exit with guidance
20
+
21
+ Output: updated VALIDATION.md + generated test files.
22
+ </objective>
23
+
24
+ <execution_context>
25
+ @$HOME/.config/opencode/get-shit-done/workflows/validate-phase.md
26
+ </execution_context>
27
+
28
+ <context>
29
+ Phase: $ARGUMENTS — optional, defaults to last completed phase.
30
+ </context>
31
+
32
+ <process>
33
+ Execute @$HOME/.config/opencode/get-shit-done/workflows/validate-phase.md.
34
+ Preserve all workflow gates.
35
+ </process>
@@ -2,14 +2,14 @@
2
2
  name: gsd-verify-work
3
3
  description: Validate built features through conversational UAT
4
4
  argument-hint: "[phase number, e.g., '4']"
5
- allowed-tools:
6
- - read
7
- - bash
8
- - glob
9
- - grep
10
- - edit
11
- - write
12
- - task
5
+ permissions:
6
+ read: true
7
+ bash: true
8
+ glob: true
9
+ grep: true
10
+ edit: true
11
+ write: true
12
+ task: true
13
13
  ---
14
14
  <objective>
15
15
  Validate built features through conversational testing with persistent state.
@@ -20,8 +20,8 @@ Output: {phase_num}-UAT.md tracking all test results. If issues found: diagnosed
20
20
  </objective>
21
21
 
22
22
  <execution_context>
23
- @~/.config/opencode/get-shit-done/workflows/verify-work.md
24
- @~/.config/opencode/get-shit-done/templates/UAT.md
23
+ @$HOME/.config/opencode/get-shit-done/workflows/verify-work.md
24
+ @$HOME/.config/opencode/get-shit-done/templates/UAT.md
25
25
  </execution_context>
26
26
 
27
27
  <context>
@@ -33,6 +33,6 @@ Context files are resolved inside the workflow (`init verify-work`) and delegate
33
33
  </context>
34
34
 
35
35
  <process>
36
- Execute the verify-work workflow from @~/.config/opencode/get-shit-done/workflows/verify-work.md end-to-end.
36
+ Execute the verify-work workflow from @$HOME/.config/opencode/get-shit-done/workflows/verify-work.md end-to-end.
37
37
  Preserve all workflow gates (session management, test presentation, diagnosis, fix planning, routing).
38
38
  </process>
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Atomic Commands:
12
12
  * state load Load project config + state
13
+ * state json Output STATE.md frontmatter as JSON
13
14
  * state update <field> <value> Update a STATE.md field
14
15
  * state get [section] Get STATE.md content or section
15
16
  * state patch --field val ... Batch update STATE.md fields
@@ -102,7 +103,9 @@
102
103
  * state update-progress Recalculate progress bar
103
104
  * state add-decision --summary "..." Add decision to STATE.md
104
105
  * [--phase N] [--rationale "..."]
106
+ * [--summary-file path] [--rationale-file path]
105
107
  * state add-blocker --text "..." Add blocker
108
+ * [--text-file path]
106
109
  * state resolve-blocker --text "..." Remove blocker
107
110
  * state record-session Update session continuity
108
111
  * --stopped-at "..."
@@ -123,6 +126,8 @@
123
126
  * init progress All context for progress workflow
124
127
  */
125
128
 
129
+ const fs = require('fs');
130
+ const path = require('path');
126
131
  const { error } = require('./lib/core.cjs');
127
132
  const state = require('./lib/state.cjs');
128
133
  const phase = require('./lib/phase.cjs');
@@ -139,21 +144,43 @@ const frontmatter = require('./lib/frontmatter.cjs');
139
144
 
140
145
  async function main() {
141
146
  const args = process.argv.slice(2);
147
+
148
+ // Optional cwd override for sandboxed subagents running outside project root.
149
+ let cwd = process.cwd();
150
+ const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
151
+ const cwdIdx = args.indexOf('--cwd');
152
+ if (cwdEqArg) {
153
+ const value = cwdEqArg.slice('--cwd='.length).trim();
154
+ if (!value) error('Missing value for --cwd');
155
+ args.splice(args.indexOf(cwdEqArg), 1);
156
+ cwd = path.resolve(value);
157
+ } else if (cwdIdx !== -1) {
158
+ const value = args[cwdIdx + 1];
159
+ if (!value || value.startsWith('--')) error('Missing value for --cwd');
160
+ args.splice(cwdIdx, 2);
161
+ cwd = path.resolve(value);
162
+ }
163
+
164
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
165
+ error(`Invalid --cwd: ${cwd}`);
166
+ }
167
+
142
168
  const rawIndex = args.indexOf('--raw');
143
169
  const raw = rawIndex !== -1;
144
170
  if (rawIndex !== -1) args.splice(rawIndex, 1);
145
171
 
146
172
  const command = args[0];
147
- const cwd = process.cwd();
148
173
 
149
174
  if (!command) {
150
- error('Usage: gsd-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
175
+ error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
151
176
  }
152
177
 
153
178
  switch (command) {
154
179
  case 'state': {
155
180
  const subcommand = args[1];
156
- if (subcommand === 'update') {
181
+ if (subcommand === 'json') {
182
+ state.cmdStateJson(cwd, raw);
183
+ } else if (subcommand === 'update') {
157
184
  state.cmdStateUpdate(cwd, args[2], args[3]);
158
185
  } else if (subcommand === 'get') {
159
186
  state.cmdStateGet(cwd, args[2], raw);
@@ -187,15 +214,23 @@ async function main() {
187
214
  } else if (subcommand === 'add-decision') {
188
215
  const phaseIdx = args.indexOf('--phase');
189
216
  const summaryIdx = args.indexOf('--summary');
217
+ const summaryFileIdx = args.indexOf('--summary-file');
190
218
  const rationaleIdx = args.indexOf('--rationale');
219
+ const rationaleFileIdx = args.indexOf('--rationale-file');
191
220
  state.cmdStateAddDecision(cwd, {
192
221
  phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
193
222
  summary: summaryIdx !== -1 ? args[summaryIdx + 1] : null,
223
+ summary_file: summaryFileIdx !== -1 ? args[summaryFileIdx + 1] : null,
194
224
  rationale: rationaleIdx !== -1 ? args[rationaleIdx + 1] : '',
225
+ rationale_file: rationaleFileIdx !== -1 ? args[rationaleFileIdx + 1] : null,
195
226
  }, raw);
196
227
  } else if (subcommand === 'add-blocker') {
197
228
  const textIdx = args.indexOf('--text');
198
- state.cmdStateAddBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
229
+ const textFileIdx = args.indexOf('--text-file');
230
+ state.cmdStateAddBlocker(cwd, {
231
+ text: textIdx !== -1 ? args[textIdx + 1] : null,
232
+ text_file: textFileIdx !== -1 ? args[textFileIdx + 1] : null,
233
+ }, raw);
199
234
  } else if (subcommand === 'resolve-blocker') {
200
235
  const textIdx = args.indexOf('--text');
201
236
  state.cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
@@ -224,9 +259,13 @@ async function main() {
224
259
 
225
260
  case 'commit': {
226
261
  const amend = args.includes('--amend');
227
- const message = args[1];
228
- // Parse --files flag (collect args after --files, stopping at other flags)
229
262
  const filesIndex = args.indexOf('--files');
263
+ // Collect all positional args between command name and first flag,
264
+ // then join them — handles both quoted ("multi word msg") and
265
+ // unquoted (multi word msg) invocations from different shells
266
+ const endIndex = filesIndex !== -1 ? filesIndex : args.length;
267
+ const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
268
+ const message = messageArgs.join(' ') || undefined;
230
269
  const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
231
270
  commands.cmdCommit(cwd, message, files, raw, amend);
232
271
  break;
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { execSync } = require('child_process');
7
- const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
 
10
10
  function cmdGenerateSlug(text, raw) {
@@ -68,7 +68,7 @@ function cmdListTodos(cwd, area, raw) {
68
68
  created: createdMatch ? createdMatch[1].trim() : 'unknown',
69
69
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
70
70
  area: todoArea,
71
- path: path.join('.planning', 'todos', 'pending', file),
71
+ path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
72
72
  });
73
73
  } catch {}
74
74
  }
@@ -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
 
@@ -304,6 +299,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
304
299
  tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
305
300
  patterns: fm['patterns-established'] || [],
306
301
  decisions: parseDecisions(fm['key-decisions']),
302
+ requirements_completed: fm['requirements-completed'] || [],
307
303
  };
308
304
 
309
305
  // If fields specified, filter to only those fields
@@ -394,14 +390,10 @@ function cmdProgressRender(cwd, format, raw) {
394
390
 
395
391
  try {
396
392
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
397
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => {
398
- const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
399
- const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
400
- return aNum - bNum;
401
- });
393
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
402
394
 
403
395
  for (const dir of dirs) {
404
- const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
396
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
405
397
  const phaseNum = dm ? dm[1] : dir;
406
398
  const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
407
399
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
@@ -421,7 +413,7 @@ function cmdProgressRender(cwd, format, raw) {
421
413
  }
422
414
  } catch {}
423
415
 
424
- const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0;
416
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
425
417
 
426
418
  if (format === 'table') {
427
419
  // Render markdown table
@@ -536,7 +528,7 @@ function cmdScaffold(cwd, type, options, raw) {
536
528
  }
537
529
 
538
530
  fs.writeFileSync(filePath, content, 'utf-8');
539
- const relPath = path.relative(cwd, filePath);
531
+ const relPath = toPosixPath(path.relative(cwd, filePath));
540
532
  output({ created: true, path: relPath }, raw, relPath);
541
533
  }
542
534
 
@@ -37,6 +37,13 @@ function cmdConfigEnsureSection(cwd, raw) {
37
37
  try {
38
38
  if (fs.existsSync(globalDefaultsPath)) {
39
39
  userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
40
+ // Migrate deprecated "depth" key to "granularity"
41
+ if ('depth' in userDefaults && !('granularity' in userDefaults)) {
42
+ const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
43
+ userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth;
44
+ delete userDefaults.depth;
45
+ try { fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8'); } catch {}
46
+ }
40
47
  }
41
48
  } catch (err) {
42
49
  // Ignore malformed global defaults, fall back to hardcoded
@@ -54,7 +61,7 @@ function cmdConfigEnsureSection(cwd, raw) {
54
61
  research: true,
55
62
  plan_check: true,
56
63
  verifier: true,
57
- nyquist_validation: false,
64
+ nyquist_validation: true,
58
65
  },
59
66
  parallelization: true,
60
67
  brave_search: hasBraveSearch,
@@ -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
  };