gsd-opencode 1.22.1 → 1.30.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 (156) hide show
  1. package/agents/gsd-advisor-researcher.md +112 -0
  2. package/agents/gsd-assumptions-analyzer.md +110 -0
  3. package/agents/gsd-codebase-mapper.md +0 -2
  4. package/agents/gsd-debugger.md +118 -2
  5. package/agents/gsd-executor.md +24 -4
  6. package/agents/gsd-integration-checker.md +0 -2
  7. package/agents/gsd-nyquist-auditor.md +0 -2
  8. package/agents/gsd-phase-researcher.md +150 -5
  9. package/agents/gsd-plan-checker.md +70 -5
  10. package/agents/gsd-planner.md +49 -4
  11. package/agents/gsd-project-researcher.md +28 -3
  12. package/agents/gsd-research-synthesizer.md +0 -2
  13. package/agents/gsd-roadmapper.md +29 -2
  14. package/agents/gsd-ui-auditor.md +445 -0
  15. package/agents/gsd-ui-checker.md +305 -0
  16. package/agents/gsd-ui-researcher.md +368 -0
  17. package/agents/gsd-user-profiler.md +173 -0
  18. package/agents/gsd-verifier.md +123 -4
  19. package/commands/gsd/gsd-add-backlog.md +76 -0
  20. package/commands/gsd/gsd-audit-uat.md +24 -0
  21. package/commands/gsd/gsd-autonomous.md +41 -0
  22. package/commands/gsd/gsd-debug.md +5 -0
  23. package/commands/gsd/gsd-discuss-phase.md +10 -36
  24. package/commands/gsd/gsd-do.md +30 -0
  25. package/commands/gsd/gsd-execute-phase.md +20 -2
  26. package/commands/gsd/gsd-fast.md +30 -0
  27. package/commands/gsd/gsd-forensics.md +56 -0
  28. package/commands/gsd/gsd-list-workspaces.md +19 -0
  29. package/commands/gsd/gsd-manager.md +39 -0
  30. package/commands/gsd/gsd-milestone-summary.md +51 -0
  31. package/commands/gsd/gsd-new-workspace.md +44 -0
  32. package/commands/gsd/gsd-next.md +24 -0
  33. package/commands/gsd/gsd-note.md +34 -0
  34. package/commands/gsd/gsd-plan-phase.md +3 -1
  35. package/commands/gsd/gsd-plant-seed.md +28 -0
  36. package/commands/gsd/gsd-pr-branch.md +25 -0
  37. package/commands/gsd/gsd-profile-user.md +46 -0
  38. package/commands/gsd/gsd-quick.md +4 -2
  39. package/commands/gsd/gsd-reapply-patches.md +9 -8
  40. package/commands/gsd/gsd-remove-workspace.md +26 -0
  41. package/commands/gsd/gsd-research-phase.md +5 -0
  42. package/commands/gsd/gsd-review-backlog.md +61 -0
  43. package/commands/gsd/gsd-review.md +37 -0
  44. package/commands/gsd/gsd-session-report.md +19 -0
  45. package/commands/gsd/gsd-set-profile.md +24 -23
  46. package/commands/gsd/gsd-ship.md +23 -0
  47. package/commands/gsd/gsd-stats.md +18 -0
  48. package/commands/gsd/gsd-thread.md +127 -0
  49. package/commands/gsd/gsd-ui-phase.md +34 -0
  50. package/commands/gsd/gsd-ui-review.md +32 -0
  51. package/commands/gsd/gsd-workstreams.md +66 -0
  52. package/get-shit-done/bin/gsd-tools.cjs +410 -84
  53. package/get-shit-done/bin/lib/commands.cjs +429 -18
  54. package/get-shit-done/bin/lib/config.cjs +318 -45
  55. package/get-shit-done/bin/lib/core.cjs +822 -84
  56. package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
  57. package/get-shit-done/bin/lib/init.cjs +836 -104
  58. package/get-shit-done/bin/lib/milestone.cjs +44 -33
  59. package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
  60. package/get-shit-done/bin/lib/phase.cjs +293 -306
  61. package/get-shit-done/bin/lib/profile-output.cjs +952 -0
  62. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  63. package/get-shit-done/bin/lib/roadmap.cjs +55 -24
  64. package/get-shit-done/bin/lib/security.cjs +382 -0
  65. package/get-shit-done/bin/lib/state.cjs +363 -53
  66. package/get-shit-done/bin/lib/template.cjs +2 -2
  67. package/get-shit-done/bin/lib/uat.cjs +282 -0
  68. package/get-shit-done/bin/lib/verify.cjs +104 -36
  69. package/get-shit-done/bin/lib/workstream.cjs +491 -0
  70. package/get-shit-done/references/checkpoints.md +12 -10
  71. package/get-shit-done/references/decimal-phase-calculation.md +2 -3
  72. package/get-shit-done/references/git-integration.md +47 -0
  73. package/get-shit-done/references/model-profile-resolution.md +2 -0
  74. package/get-shit-done/references/model-profiles.md +62 -16
  75. package/get-shit-done/references/phase-argument-parsing.md +2 -2
  76. package/get-shit-done/references/planning-config.md +3 -1
  77. package/get-shit-done/references/user-profiling.md +681 -0
  78. package/get-shit-done/references/workstream-flag.md +58 -0
  79. package/get-shit-done/templates/UAT.md +21 -3
  80. package/get-shit-done/templates/UI-SPEC.md +100 -0
  81. package/get-shit-done/templates/claude-md.md +122 -0
  82. package/get-shit-done/templates/config.json +10 -3
  83. package/get-shit-done/templates/context.md +61 -6
  84. package/get-shit-done/templates/dev-preferences.md +21 -0
  85. package/get-shit-done/templates/discussion-log.md +63 -0
  86. package/get-shit-done/templates/phase-prompt.md +46 -5
  87. package/get-shit-done/templates/project.md +2 -0
  88. package/get-shit-done/templates/state.md +2 -2
  89. package/get-shit-done/templates/user-profile.md +146 -0
  90. package/get-shit-done/workflows/add-phase.md +2 -2
  91. package/get-shit-done/workflows/add-tests.md +4 -4
  92. package/get-shit-done/workflows/add-todo.md +3 -3
  93. package/get-shit-done/workflows/audit-milestone.md +13 -5
  94. package/get-shit-done/workflows/audit-uat.md +109 -0
  95. package/get-shit-done/workflows/autonomous.md +891 -0
  96. package/get-shit-done/workflows/check-todos.md +2 -2
  97. package/get-shit-done/workflows/cleanup.md +4 -4
  98. package/get-shit-done/workflows/complete-milestone.md +9 -6
  99. package/get-shit-done/workflows/diagnose-issues.md +15 -3
  100. package/get-shit-done/workflows/discovery-phase.md +3 -3
  101. package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
  102. package/get-shit-done/workflows/discuss-phase.md +411 -38
  103. package/get-shit-done/workflows/do.md +104 -0
  104. package/get-shit-done/workflows/execute-phase.md +405 -18
  105. package/get-shit-done/workflows/execute-plan.md +77 -12
  106. package/get-shit-done/workflows/fast.md +105 -0
  107. package/get-shit-done/workflows/forensics.md +265 -0
  108. package/get-shit-done/workflows/health.md +28 -6
  109. package/get-shit-done/workflows/help.md +124 -7
  110. package/get-shit-done/workflows/insert-phase.md +2 -2
  111. package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
  112. package/get-shit-done/workflows/list-workspaces.md +56 -0
  113. package/get-shit-done/workflows/manager.md +362 -0
  114. package/get-shit-done/workflows/map-codebase.md +74 -13
  115. package/get-shit-done/workflows/milestone-summary.md +223 -0
  116. package/get-shit-done/workflows/new-milestone.md +120 -18
  117. package/get-shit-done/workflows/new-project.md +178 -39
  118. package/get-shit-done/workflows/new-workspace.md +237 -0
  119. package/get-shit-done/workflows/next.md +97 -0
  120. package/get-shit-done/workflows/node-repair.md +92 -0
  121. package/get-shit-done/workflows/note.md +156 -0
  122. package/get-shit-done/workflows/pause-work.md +62 -8
  123. package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
  124. package/get-shit-done/workflows/plan-phase.md +332 -33
  125. package/get-shit-done/workflows/plant-seed.md +169 -0
  126. package/get-shit-done/workflows/pr-branch.md +129 -0
  127. package/get-shit-done/workflows/profile-user.md +450 -0
  128. package/get-shit-done/workflows/progress.md +145 -20
  129. package/get-shit-done/workflows/quick.md +205 -49
  130. package/get-shit-done/workflows/remove-phase.md +2 -2
  131. package/get-shit-done/workflows/remove-workspace.md +90 -0
  132. package/get-shit-done/workflows/research-phase.md +11 -3
  133. package/get-shit-done/workflows/resume-project.md +35 -16
  134. package/get-shit-done/workflows/review.md +228 -0
  135. package/get-shit-done/workflows/session-report.md +146 -0
  136. package/get-shit-done/workflows/set-profile.md +2 -2
  137. package/get-shit-done/workflows/settings.md +79 -10
  138. package/get-shit-done/workflows/ship.md +228 -0
  139. package/get-shit-done/workflows/stats.md +60 -0
  140. package/get-shit-done/workflows/transition.md +147 -20
  141. package/get-shit-done/workflows/ui-phase.md +302 -0
  142. package/get-shit-done/workflows/ui-review.md +165 -0
  143. package/get-shit-done/workflows/update.md +108 -25
  144. package/get-shit-done/workflows/validate-phase.md +15 -8
  145. package/get-shit-done/workflows/verify-phase.md +16 -5
  146. package/get-shit-done/workflows/verify-work.md +72 -18
  147. package/package.json +1 -1
  148. package/skills/gsd-audit-milestone/SKILL.md +29 -0
  149. package/skills/gsd-cleanup/SKILL.md +19 -0
  150. package/skills/gsd-complete-milestone/SKILL.md +131 -0
  151. package/skills/gsd-discuss-phase/SKILL.md +54 -0
  152. package/skills/gsd-execute-phase/SKILL.md +49 -0
  153. package/skills/gsd-plan-phase/SKILL.md +37 -0
  154. package/skills/gsd-ui-phase/SKILL.md +24 -0
  155. package/skills/gsd-ui-review/SKILL.md +24 -0
  156. package/skills/gsd-verify-work/SKILL.md +30 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * UAT Audit — Cross-phase UAT/VERIFICATION scanner
3
+ *
4
+ * Reads all *-UAT.md and *-VERIFICATION.md files across all phases.
5
+ * Extracts non-passing items. Returns structured JSON for workflow consumption.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { output, error, getMilestonePhaseFilter, planningDir, toPosixPath } = require('./core.cjs');
11
+ const { extractFrontmatter } = require('./frontmatter.cjs');
12
+ const { requireSafePath, sanitizeForDisplay } = require('./security.cjs');
13
+
14
+ function cmdAuditUat(cwd, raw) {
15
+ const phasesDir = path.join(planningDir(cwd), 'phases');
16
+ if (!fs.existsSync(phasesDir)) {
17
+ error('No phases directory found in planning directory');
18
+ }
19
+
20
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
21
+ const results = [];
22
+
23
+ // Scan all phase directories
24
+ const dirs = fs.readdirSync(phasesDir, { withFileTypes: true })
25
+ .filter(e => e.isDirectory())
26
+ .map(e => e.name)
27
+ .filter(isDirInMilestone)
28
+ .sort();
29
+
30
+ for (const dir of dirs) {
31
+ const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
32
+ const phaseNum = phaseMatch ? phaseMatch[1] : dir;
33
+ const phaseDir = path.join(phasesDir, dir);
34
+ const files = fs.readdirSync(phaseDir);
35
+
36
+ // Process UAT files
37
+ for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
38
+ const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
39
+ const items = parseUatItems(content);
40
+ if (items.length > 0) {
41
+ results.push({
42
+ phase: phaseNum,
43
+ phase_dir: dir,
44
+ file,
45
+ file_path: toPosixPath(path.relative(cwd, path.join(phaseDir, file))),
46
+ type: 'uat',
47
+ status: (extractFrontmatter(content).status || 'unknown'),
48
+ items,
49
+ });
50
+ }
51
+ }
52
+
53
+ // Process VERIFICATION files
54
+ for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
55
+ const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
56
+ const status = extractFrontmatter(content).status || 'unknown';
57
+ if (status === 'human_needed' || status === 'gaps_found') {
58
+ const items = parseVerificationItems(content, status);
59
+ if (items.length > 0) {
60
+ results.push({
61
+ phase: phaseNum,
62
+ phase_dir: dir,
63
+ file,
64
+ file_path: toPosixPath(path.relative(cwd, path.join(phaseDir, file))),
65
+ type: 'verification',
66
+ status,
67
+ items,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Compute summary
75
+ const summary = {
76
+ total_files: results.length,
77
+ total_items: results.reduce((sum, r) => sum + r.items.length, 0),
78
+ by_category: {},
79
+ by_phase: {},
80
+ };
81
+
82
+ for (const r of results) {
83
+ if (!summary.by_phase[r.phase]) summary.by_phase[r.phase] = 0;
84
+ for (const item of r.items) {
85
+ summary.by_phase[r.phase]++;
86
+ const cat = item.category || 'unknown';
87
+ summary.by_category[cat] = (summary.by_category[cat] || 0) + 1;
88
+ }
89
+ }
90
+
91
+ output({ results, summary }, raw);
92
+ }
93
+
94
+ function cmdRenderCheckpoint(cwd, options = {}, raw) {
95
+ const filePath = options.file;
96
+ if (!filePath) {
97
+ error('UAT file required: use uat render-checkpoint --file <path>');
98
+ }
99
+
100
+ const resolvedPath = requireSafePath(filePath, cwd, 'UAT file', { allowAbsolute: true });
101
+ if (!fs.existsSync(resolvedPath)) {
102
+ error(`UAT file not found: ${filePath}`);
103
+ }
104
+
105
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
106
+ const currentTest = parseCurrentTest(content);
107
+
108
+ if (currentTest.complete) {
109
+ error('UAT session is already complete; no pending checkpoint to render');
110
+ }
111
+
112
+ const checkpoint = buildCheckpoint(currentTest);
113
+ output({
114
+ file_path: toPosixPath(path.relative(cwd, resolvedPath)),
115
+ test_number: currentTest.number,
116
+ test_name: currentTest.name,
117
+ checkpoint,
118
+ }, raw, checkpoint);
119
+ }
120
+
121
+ function parseCurrentTest(content) {
122
+ const currentTestMatch = content.match(/##\s*Current Test\s*(?:\n<!--[\s\S]*?-->)?\n([\s\S]*?)(?=\n##\s|$)/i);
123
+ if (!currentTestMatch) {
124
+ error('UAT file is missing a Current Test section');
125
+ }
126
+
127
+ const section = currentTestMatch[1].trimEnd();
128
+ if (!section.trim()) {
129
+ error('Current Test section is empty');
130
+ }
131
+
132
+ if (/\[testing complete\]/i.test(section)) {
133
+ return { complete: true };
134
+ }
135
+
136
+ const numberMatch = section.match(/^number:\s*(\d+)\s*$/m);
137
+ const nameMatch = section.match(/^name:\s*(.+)\s*$/m);
138
+ const expectedBlockMatch = section.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
139
+ || section.match(/^expected:\s*\|\n([\s\S]+)/m);
140
+ const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
141
+
142
+ if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
143
+ error('Current Test section is malformed');
144
+ }
145
+
146
+ let expected;
147
+ if (expectedBlockMatch) {
148
+ expected = expectedBlockMatch[1]
149
+ .split('\n')
150
+ .map(line => line.replace(/^ {2}/, ''))
151
+ .join('\n')
152
+ .trim();
153
+ } else {
154
+ expected = expectedInlineMatch[1].trim();
155
+ }
156
+
157
+ return {
158
+ complete: false,
159
+ number: parseInt(numberMatch[1], 10),
160
+ name: sanitizeForDisplay(nameMatch[1].trim()),
161
+ expected: sanitizeForDisplay(expected),
162
+ };
163
+ }
164
+
165
+ function buildCheckpoint(currentTest) {
166
+ return [
167
+ '╔══════════════════════════════════════════════════════════════╗',
168
+ '║ CHECKPOINT: Verification Required ║',
169
+ '╚══════════════════════════════════════════════════════════════╝',
170
+ '',
171
+ `**Test ${currentTest.number}: ${currentTest.name}**`,
172
+ '',
173
+ currentTest.expected,
174
+ '',
175
+ '──────────────────────────────────────────────────────────────',
176
+ 'Type `pass` or describe what\'s wrong.',
177
+ '──────────────────────────────────────────────────────────────',
178
+ ].join('\n');
179
+ }
180
+
181
+ function parseUatItems(content) {
182
+ const items = [];
183
+ // Match test blocks: ### N. Name\nexpected: ...\nresult: ...\n
184
+ const testPattern = /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)(?:\n(?:reported|reason|blocked_by):\s*[^\n]*)?/g;
185
+ let match;
186
+ while ((match = testPattern.exec(content)) !== null) {
187
+ const [, num, name, expected, result] = match;
188
+ if (result === 'pending' || result === 'skipped' || result === 'blocked') {
189
+ // Extract optional fields — limit to current test block (up to next ### or EOF)
190
+ const afterMatch = content.slice(match.index);
191
+ const nextHeading = afterMatch.indexOf('\n###', 1);
192
+ const blockText = nextHeading > 0 ? afterMatch.slice(0, nextHeading) : afterMatch;
193
+ const reasonMatch = blockText.match(/reason:\s*(.+)/);
194
+ const blockedByMatch = blockText.match(/blocked_by:\s*(.+)/);
195
+
196
+ const item = {
197
+ test: parseInt(num, 10),
198
+ name: name.trim(),
199
+ expected: expected.trim(),
200
+ result,
201
+ category: categorizeItem(result, reasonMatch?.[1], blockedByMatch?.[1]),
202
+ };
203
+ if (reasonMatch) item.reason = reasonMatch[1].trim();
204
+ if (blockedByMatch) item.blocked_by = blockedByMatch[1].trim();
205
+ items.push(item);
206
+ }
207
+ }
208
+ return items;
209
+ }
210
+
211
+ function parseVerificationItems(content, status) {
212
+ const items = [];
213
+ if (status === 'human_needed') {
214
+ // Extract from human_verification section — look for numbered items or table rows
215
+ const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i);
216
+ if (hvSection) {
217
+ const lines = hvSection[1].split('\n');
218
+ for (const line of lines) {
219
+ // Match table rows: | N | description | ... |
220
+ const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
221
+ // Match bullet items: - description
222
+ const bulletMatch = line.match(/^[-*]\s+(.+)/);
223
+ // Match numbered items: 1. description
224
+ const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
225
+
226
+ if (tableMatch) {
227
+ items.push({
228
+ test: parseInt(tableMatch[1], 10),
229
+ name: tableMatch[2].trim(),
230
+ result: 'human_needed',
231
+ category: 'human_uat',
232
+ });
233
+ } else if (numberedMatch) {
234
+ items.push({
235
+ test: parseInt(numberedMatch[1], 10),
236
+ name: numberedMatch[2].trim(),
237
+ result: 'human_needed',
238
+ category: 'human_uat',
239
+ });
240
+ } else if (bulletMatch && bulletMatch[1].length > 10) {
241
+ items.push({
242
+ name: bulletMatch[1].trim(),
243
+ result: 'human_needed',
244
+ category: 'human_uat',
245
+ });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ // gaps_found items are already handled by plan-phase --gaps pipeline
251
+ return items;
252
+ }
253
+
254
+ function categorizeItem(result, reason, blockedBy) {
255
+ if (result === 'blocked' || blockedBy) {
256
+ if (blockedBy) {
257
+ if (/server/i.test(blockedBy)) return 'server_blocked';
258
+ if (/device|physical/i.test(blockedBy)) return 'device_needed';
259
+ if (/build|release|preview/i.test(blockedBy)) return 'build_needed';
260
+ if (/third.party|twilio|stripe/i.test(blockedBy)) return 'third_party';
261
+ }
262
+ return 'blocked';
263
+ }
264
+ if (result === 'skipped') {
265
+ if (reason) {
266
+ if (/server|not running|not available/i.test(reason)) return 'server_blocked';
267
+ if (/simulator|physical|device/i.test(reason)) return 'device_needed';
268
+ if (/build|release|preview/i.test(reason)) return 'build_needed';
269
+ }
270
+ return 'skipped_unresolved';
271
+ }
272
+ if (result === 'pending') return 'pending';
273
+ if (result === 'human_needed') return 'human_uat';
274
+ return 'unknown';
275
+ }
276
+
277
+ module.exports = {
278
+ cmdAuditUat,
279
+ cmdRenderCheckpoint,
280
+ parseCurrentTest,
281
+ buildCheckpoint,
282
+ };
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs');
7
+ const os = require('os');
8
+ const { safeReadFile, loadConfig, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, planningDir, planningRoot, output, error, checkAgentsInstalled } = require('./core.cjs');
8
9
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
9
10
  const { writeStateMd } = require('./state.cjs');
10
11
 
@@ -395,8 +396,8 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
395
396
  }
396
397
 
397
398
  function cmdValidateConsistency(cwd, raw) {
398
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
399
- const phasesDir = path.join(cwd, '.planning', 'phases');
399
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
400
+ const phasesDir = path.join(planningDir(cwd), 'phases');
400
401
  const errors = [];
401
402
  const warnings = [];
402
403
 
@@ -407,9 +408,10 @@ function cmdValidateConsistency(cwd, raw) {
407
408
  return;
408
409
  }
409
410
 
410
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
411
+ const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
412
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
411
413
 
412
- // Extract phases from ROADMAP
414
+ // Extract phases from ROADMAP (archived milestones already stripped)
413
415
  const roadmapPhases = new Set();
414
416
  const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
415
417
  let m;
@@ -426,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) {
426
428
  const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
427
429
  if (dm) diskPhases.add(dm[1]);
428
430
  }
429
- } catch {}
431
+ } catch { /* intentionally empty */ }
430
432
 
431
433
  // Check: phases in ROADMAP but not on disk
432
434
  for (const p of roadmapPhases) {
@@ -443,15 +445,18 @@ function cmdValidateConsistency(cwd, raw) {
443
445
  }
444
446
  }
445
447
 
446
- // Check: sequential phase numbers (integers only)
447
- const integerPhases = [...diskPhases]
448
- .filter(p => !p.includes('.'))
449
- .map(p => parseInt(p, 10))
450
- .sort((a, b) => a - b);
448
+ // Check: sequential phase numbers (integers only, skip in custom naming mode)
449
+ const config = loadConfig(cwd);
450
+ if (config.phase_naming !== 'custom') {
451
+ const integerPhases = [...diskPhases]
452
+ .filter(p => !p.includes('.'))
453
+ .map(p => parseInt(p, 10))
454
+ .sort((a, b) => a - b);
451
455
 
452
- for (let i = 1; i < integerPhases.length; i++) {
453
- if (integerPhases[i] !== integerPhases[i - 1] + 1) {
454
- warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
456
+ for (let i = 1; i < integerPhases.length; i++) {
457
+ if (integerPhases[i] !== integerPhases[i - 1] + 1) {
458
+ warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
459
+ }
455
460
  }
456
461
  }
457
462
 
@@ -488,7 +493,7 @@ function cmdValidateConsistency(cwd, raw) {
488
493
  }
489
494
  }
490
495
  }
491
- } catch {}
496
+ } catch { /* intentionally empty */ }
492
497
 
493
498
  // Check: frontmatter in plans has required fields
494
499
  try {
@@ -508,19 +513,33 @@ function cmdValidateConsistency(cwd, raw) {
508
513
  }
509
514
  }
510
515
  }
511
- } catch {}
516
+ } catch { /* intentionally empty */ }
512
517
 
513
518
  const passed = errors.length === 0;
514
519
  output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
515
520
  }
516
521
 
517
522
  function cmdValidateHealth(cwd, options, raw) {
518
- const planningDir = path.join(cwd, '.planning');
519
- const projectPath = path.join(planningDir, 'PROJECT.md');
520
- const roadmapPath = path.join(planningDir, 'ROADMAP.md');
521
- const statePath = path.join(planningDir, 'STATE.md');
522
- const configPath = path.join(planningDir, 'config.json');
523
- const phasesDir = path.join(planningDir, 'phases');
523
+ // Guard: detect if CWD is the home directory (likely accidental)
524
+ const resolved = path.resolve(cwd);
525
+ if (resolved === os.homedir()) {
526
+ output({
527
+ status: 'error',
528
+ errors: [{ code: 'E010', message: `CWD is home directory (${resolved}) — health check would read the wrong .planning/ directory. Run from your project root instead.`, fix: 'cd into your project directory and retry' }],
529
+ warnings: [],
530
+ info: [{ code: 'I010', message: `Resolved CWD: ${resolved}` }],
531
+ repairable_count: 0,
532
+ }, raw);
533
+ return;
534
+ }
535
+
536
+ const planBase = planningDir(cwd);
537
+ const planRoot = planningRoot(cwd);
538
+ const projectPath = path.join(planRoot, 'PROJECT.md');
539
+ const roadmapPath = path.join(planBase, 'ROADMAP.md');
540
+ const statePath = path.join(planBase, 'STATE.md');
541
+ const configPath = path.join(planRoot, 'config.json');
542
+ const phasesDir = path.join(planBase, 'phases');
524
543
 
525
544
  const errors = [];
526
545
  const warnings = [];
@@ -536,7 +555,7 @@ function cmdValidateHealth(cwd, options, raw) {
536
555
  };
537
556
 
538
557
  // ─── Check 1: .planning/ exists ───────────────────────────────────────────
539
- if (!fs.existsSync(planningDir)) {
558
+ if (!fs.existsSync(planBase)) {
540
559
  addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd-new-project to initialize');
541
560
  output({
542
561
  status: 'broken',
@@ -584,15 +603,19 @@ function cmdValidateHealth(cwd, options, raw) {
584
603
  if (m) diskPhases.add(m[1]);
585
604
  }
586
605
  }
587
- } catch {}
606
+ } catch { /* intentionally empty */ }
588
607
  // Check for invalid references
589
608
  for (const ref of phaseRefs) {
590
609
  const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
591
610
  if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
592
611
  // Only warn if phases dir has any content (not just an empty project)
593
612
  if (diskPhases.size > 0) {
594
- addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /gsd-health --repair to regenerate STATE.md', true);
595
- if (!repairs.includes('regenerateState')) repairs.push('regenerateState');
613
+ addIssue(
614
+ 'warning',
615
+ 'W002',
616
+ `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`,
617
+ 'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches'
618
+ );
596
619
  }
597
620
  }
598
621
  }
@@ -607,7 +630,7 @@ function cmdValidateHealth(cwd, options, raw) {
607
630
  const raw = fs.readFileSync(configPath, 'utf-8');
608
631
  const parsed = JSON.parse(raw);
609
632
  // Validate known fields
610
- const validProfiles = ['quality', 'balanced', 'budget'];
633
+ const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
611
634
  if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
612
635
  addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
613
636
  }
@@ -626,7 +649,7 @@ function cmdValidateHealth(cwd, options, raw) {
626
649
  addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd-health --repair to add key', true);
627
650
  if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
628
651
  }
629
- } catch {}
652
+ } catch { /* intentionally empty */ }
630
653
  }
631
654
 
632
655
  // ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
@@ -637,7 +660,7 @@ function cmdValidateHealth(cwd, options, raw) {
637
660
  addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
638
661
  }
639
662
  }
640
- } catch {}
663
+ } catch { /* intentionally empty */ }
641
664
 
642
665
  // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
643
666
  try {
@@ -656,7 +679,7 @@ function cmdValidateHealth(cwd, options, raw) {
656
679
  }
657
680
  }
658
681
  }
659
- } catch {}
682
+ } catch { /* intentionally empty */ }
660
683
 
661
684
  // ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
662
685
  try {
@@ -674,12 +697,31 @@ function cmdValidateHealth(cwd, options, raw) {
674
697
  }
675
698
  }
676
699
  }
677
- } catch {}
700
+ } catch { /* intentionally empty */ }
701
+
702
+ // ─── Check 7c: Agent installation (#1371) ──────────────────────────────────
703
+ // Verify GSD agents are installed. Missing agents cause task(subagent_type=...)
704
+ // to silently fall back to general-purpose, losing specialized instructions.
705
+ try {
706
+ const agentStatus = checkAgentsInstalled();
707
+ if (!agentStatus.agents_installed) {
708
+ if (agentStatus.installed_agents.length === 0) {
709
+ addIssue('warning', 'W010',
710
+ `No GSD agents found in ${agentStatus.agents_dir} — task(subagent_type="gsd-*") will fall back to general-purpose`,
711
+ 'Run the GSD installer: npx gsd-opencode@latest');
712
+ } else {
713
+ addIssue('warning', 'W010',
714
+ `Missing ${agentStatus.missing_agents.length} GSD agents: ${agentStatus.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
715
+ 'Run the GSD installer: npx gsd-opencode@latest');
716
+ }
717
+ }
718
+ } catch { /* intentionally empty — agent check is non-blocking */ }
678
719
 
679
720
  // ─── Check 8: Run existing consistency checks ─────────────────────────────
680
721
  // Inline subset of cmdValidateConsistency
681
722
  if (fs.existsSync(roadmapPath)) {
682
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
723
+ const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
724
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
683
725
  const roadmapPhases = new Set();
684
726
  const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
685
727
  let m;
@@ -696,7 +738,7 @@ function cmdValidateHealth(cwd, options, raw) {
696
738
  if (dm) diskPhases.add(dm[1]);
697
739
  }
698
740
  }
699
- } catch {}
741
+ } catch { /* intentionally empty */ }
700
742
 
701
743
  // Phases in ROADMAP but not on disk
702
744
  for (const p of roadmapPhases) {
@@ -728,10 +770,17 @@ function cmdValidateHealth(cwd, options, raw) {
728
770
  commit_docs: true,
729
771
  search_gitignored: false,
730
772
  branching_strategy: 'none',
731
- research: true,
732
- plan_checker: true,
733
- verifier: true,
773
+ phase_branch_template: 'gsd/phase-{phase}-{slug}',
774
+ milestone_branch_template: 'gsd/{milestone}-{slug}',
775
+ quick_branch_template: null,
776
+ workflow: {
777
+ research: true,
778
+ plan_check: true,
779
+ verifier: true,
780
+ nyquist_validation: true,
781
+ },
734
782
  parallelization: true,
783
+ brave_search: false,
735
784
  };
736
785
  fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
737
786
  repairActions.push({ action: repair, success: true, path: 'config.json' });
@@ -807,6 +856,24 @@ function cmdValidateHealth(cwd, options, raw) {
807
856
  }, raw);
808
857
  }
809
858
 
859
+ /**
860
+ * Validate agent installation status (#1371).
861
+ * Returns detailed information about which agents are installed and which are missing.
862
+ */
863
+ function cmdValidateAgents(cwd, raw) {
864
+ const { MODEL_PROFILES } = require('./model-profiles.cjs');
865
+ const agentStatus = checkAgentsInstalled();
866
+ const expected = Object.keys(MODEL_PROFILES);
867
+
868
+ output({
869
+ agents_dir: agentStatus.agents_dir,
870
+ agents_found: agentStatus.agents_installed,
871
+ installed: agentStatus.installed_agents,
872
+ missing: agentStatus.missing_agents,
873
+ expected,
874
+ }, raw);
875
+ }
876
+
810
877
  module.exports = {
811
878
  cmdVerifySummary,
812
879
  cmdVerifyPlanStructure,
@@ -817,4 +884,5 @@ module.exports = {
817
884
  cmdVerifyKeyLinks,
818
885
  cmdValidateConsistency,
819
886
  cmdValidateHealth,
887
+ cmdValidateAgents,
820
888
  };