gsd-opencode 1.22.0 → 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 (157) 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 +1 -2
  4. package/agents/gsd-debugger.md +119 -2
  5. package/agents/gsd-executor.md +25 -4
  6. package/agents/gsd-integration-checker.md +1 -2
  7. package/agents/gsd-nyquist-auditor.md +1 -2
  8. package/agents/gsd-phase-researcher.md +151 -5
  9. package/agents/gsd-plan-checker.md +71 -5
  10. package/agents/gsd-planner.md +50 -4
  11. package/agents/gsd-project-researcher.md +29 -3
  12. package/agents/gsd-research-synthesizer.md +1 -2
  13. package/agents/gsd-roadmapper.md +30 -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 +124 -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 +10 -6
  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-resume-work.md +1 -1
  43. package/commands/gsd/gsd-review-backlog.md +61 -0
  44. package/commands/gsd/gsd-review.md +37 -0
  45. package/commands/gsd/gsd-session-report.md +19 -0
  46. package/commands/gsd/gsd-set-profile.md +24 -23
  47. package/commands/gsd/gsd-ship.md +23 -0
  48. package/commands/gsd/gsd-stats.md +18 -0
  49. package/commands/gsd/gsd-thread.md +127 -0
  50. package/commands/gsd/gsd-ui-phase.md +34 -0
  51. package/commands/gsd/gsd-ui-review.md +32 -0
  52. package/commands/gsd/gsd-workstreams.md +66 -0
  53. package/get-shit-done/bin/gsd-tools.cjs +410 -84
  54. package/get-shit-done/bin/lib/commands.cjs +429 -18
  55. package/get-shit-done/bin/lib/config.cjs +318 -45
  56. package/get-shit-done/bin/lib/core.cjs +822 -84
  57. package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
  58. package/get-shit-done/bin/lib/init.cjs +836 -104
  59. package/get-shit-done/bin/lib/milestone.cjs +44 -33
  60. package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
  61. package/get-shit-done/bin/lib/phase.cjs +293 -306
  62. package/get-shit-done/bin/lib/profile-output.cjs +952 -0
  63. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  64. package/get-shit-done/bin/lib/roadmap.cjs +55 -24
  65. package/get-shit-done/bin/lib/security.cjs +382 -0
  66. package/get-shit-done/bin/lib/state.cjs +363 -53
  67. package/get-shit-done/bin/lib/template.cjs +2 -2
  68. package/get-shit-done/bin/lib/uat.cjs +282 -0
  69. package/get-shit-done/bin/lib/verify.cjs +104 -36
  70. package/get-shit-done/bin/lib/workstream.cjs +491 -0
  71. package/get-shit-done/references/checkpoints.md +12 -10
  72. package/get-shit-done/references/decimal-phase-calculation.md +2 -3
  73. package/get-shit-done/references/git-integration.md +47 -0
  74. package/get-shit-done/references/model-profile-resolution.md +2 -0
  75. package/get-shit-done/references/model-profiles.md +62 -16
  76. package/get-shit-done/references/phase-argument-parsing.md +2 -2
  77. package/get-shit-done/references/planning-config.md +3 -1
  78. package/get-shit-done/references/user-profiling.md +681 -0
  79. package/get-shit-done/references/workstream-flag.md +58 -0
  80. package/get-shit-done/templates/UAT.md +21 -3
  81. package/get-shit-done/templates/UI-SPEC.md +100 -0
  82. package/get-shit-done/templates/claude-md.md +122 -0
  83. package/get-shit-done/templates/config.json +10 -3
  84. package/get-shit-done/templates/context.md +61 -6
  85. package/get-shit-done/templates/dev-preferences.md +21 -0
  86. package/get-shit-done/templates/discussion-log.md +63 -0
  87. package/get-shit-done/templates/phase-prompt.md +46 -5
  88. package/get-shit-done/templates/project.md +2 -0
  89. package/get-shit-done/templates/state.md +2 -2
  90. package/get-shit-done/templates/user-profile.md +146 -0
  91. package/get-shit-done/workflows/add-phase.md +2 -2
  92. package/get-shit-done/workflows/add-tests.md +4 -4
  93. package/get-shit-done/workflows/add-todo.md +3 -3
  94. package/get-shit-done/workflows/audit-milestone.md +13 -5
  95. package/get-shit-done/workflows/audit-uat.md +109 -0
  96. package/get-shit-done/workflows/autonomous.md +891 -0
  97. package/get-shit-done/workflows/check-todos.md +2 -2
  98. package/get-shit-done/workflows/cleanup.md +4 -4
  99. package/get-shit-done/workflows/complete-milestone.md +9 -6
  100. package/get-shit-done/workflows/diagnose-issues.md +15 -3
  101. package/get-shit-done/workflows/discovery-phase.md +2 -2
  102. package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
  103. package/get-shit-done/workflows/discuss-phase.md +411 -38
  104. package/get-shit-done/workflows/do.md +104 -0
  105. package/get-shit-done/workflows/execute-phase.md +405 -18
  106. package/get-shit-done/workflows/execute-plan.md +77 -12
  107. package/get-shit-done/workflows/fast.md +105 -0
  108. package/get-shit-done/workflows/forensics.md +265 -0
  109. package/get-shit-done/workflows/health.md +28 -6
  110. package/get-shit-done/workflows/help.md +124 -7
  111. package/get-shit-done/workflows/insert-phase.md +2 -2
  112. package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
  113. package/get-shit-done/workflows/list-workspaces.md +56 -0
  114. package/get-shit-done/workflows/manager.md +362 -0
  115. package/get-shit-done/workflows/map-codebase.md +74 -13
  116. package/get-shit-done/workflows/milestone-summary.md +223 -0
  117. package/get-shit-done/workflows/new-milestone.md +120 -18
  118. package/get-shit-done/workflows/new-project.md +178 -39
  119. package/get-shit-done/workflows/new-workspace.md +237 -0
  120. package/get-shit-done/workflows/next.md +97 -0
  121. package/get-shit-done/workflows/node-repair.md +92 -0
  122. package/get-shit-done/workflows/note.md +156 -0
  123. package/get-shit-done/workflows/pause-work.md +62 -8
  124. package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
  125. package/get-shit-done/workflows/plan-phase.md +332 -33
  126. package/get-shit-done/workflows/plant-seed.md +169 -0
  127. package/get-shit-done/workflows/pr-branch.md +129 -0
  128. package/get-shit-done/workflows/profile-user.md +450 -0
  129. package/get-shit-done/workflows/progress.md +145 -20
  130. package/get-shit-done/workflows/quick.md +205 -49
  131. package/get-shit-done/workflows/remove-phase.md +2 -2
  132. package/get-shit-done/workflows/remove-workspace.md +90 -0
  133. package/get-shit-done/workflows/research-phase.md +11 -3
  134. package/get-shit-done/workflows/resume-project.md +35 -16
  135. package/get-shit-done/workflows/review.md +228 -0
  136. package/get-shit-done/workflows/session-report.md +146 -0
  137. package/get-shit-done/workflows/set-profile.md +2 -2
  138. package/get-shit-done/workflows/settings.md +80 -11
  139. package/get-shit-done/workflows/ship.md +228 -0
  140. package/get-shit-done/workflows/stats.md +60 -0
  141. package/get-shit-done/workflows/transition.md +147 -20
  142. package/get-shit-done/workflows/ui-phase.md +302 -0
  143. package/get-shit-done/workflows/ui-review.md +165 -0
  144. package/get-shit-done/workflows/update.md +108 -25
  145. package/get-shit-done/workflows/validate-phase.md +15 -8
  146. package/get-shit-done/workflows/verify-phase.md +16 -5
  147. package/get-shit-done/workflows/verify-work.md +72 -18
  148. package/package.json +1 -1
  149. package/skills/gsd-audit-milestone/SKILL.md +29 -0
  150. package/skills/gsd-cleanup/SKILL.md +19 -0
  151. package/skills/gsd-complete-milestone/SKILL.md +131 -0
  152. package/skills/gsd-discuss-phase/SKILL.md +54 -0
  153. package/skills/gsd-execute-phase/SKILL.md +49 -0
  154. package/skills/gsd-plan-phase/SKILL.md +37 -0
  155. package/skills/gsd-ui-phase/SKILL.md +24 -0
  156. package/skills/gsd-ui-review/SKILL.md +24 -0
  157. package/skills/gsd-verify-work/SKILL.md +30 -0
@@ -5,7 +5,40 @@
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, toPosixPath, output, error } = require('./core.cjs');
8
+ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, planningPaths, planningDir, planningRoot, toPosixPath, output, error, checkAgentsInstalled } = require('./core.cjs');
9
+
10
+ function getLatestCompletedMilestone(cwd) {
11
+ const milestonesPath = path.join(planningRoot(cwd), 'MILESTONES.md');
12
+ if (!fs.existsSync(milestonesPath)) return null;
13
+
14
+ try {
15
+ const content = fs.readFileSync(milestonesPath, 'utf-8');
16
+ const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m);
17
+ if (!match) return null;
18
+ return {
19
+ version: match[1],
20
+ name: match[2].trim(),
21
+ };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Inject `project_root` into an init result object.
29
+ * Workflows use this to prefix `.planning/` paths correctly when OpenCode's CWD
30
+ * differs from the project root (e.g., inside a sub-repo).
31
+ */
32
+ function withProjectRoot(cwd, result) {
33
+ result.project_root = cwd;
34
+ // Inject agent installation status into all init outputs (#1371).
35
+ // Workflows that spawn named subagents use this to detect when agents
36
+ // are missing and would silently fall back to general-purpose.
37
+ const agentStatus = checkAgentsInstalled();
38
+ result.agents_installed = agentStatus.agents_installed;
39
+ result.missing_agents = agentStatus.missing_agents;
40
+ return result;
41
+ }
9
42
 
10
43
  function cmdInitExecutePhase(cwd, phase, raw) {
11
44
  if (!phase) {
@@ -13,10 +46,29 @@ function cmdInitExecutePhase(cwd, phase, raw) {
13
46
  }
14
47
 
15
48
  const config = loadConfig(cwd);
16
- const phaseInfo = findPhaseInternal(cwd, phase);
49
+ let phaseInfo = findPhaseInternal(cwd, phase);
17
50
  const milestone = getMilestoneInfo(cwd);
18
51
 
19
52
  const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
53
+
54
+ // Fallback to ROADMAP.md if no phase directory exists yet
55
+ if (!phaseInfo && roadmapPhase?.found) {
56
+ const phaseName = roadmapPhase.phase_name;
57
+ phaseInfo = {
58
+ found: true,
59
+ directory: null,
60
+ phase_number: roadmapPhase.phase_number,
61
+ phase_name: phaseName,
62
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
63
+ plans: [],
64
+ summaries: [],
65
+ incomplete_plans: [],
66
+ has_research: false,
67
+ has_context: false,
68
+ has_verification: false,
69
+ has_reviews: false,
70
+ };
71
+ }
20
72
  const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
21
73
  const reqExtracted = reqMatch
22
74
  ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
@@ -30,7 +82,9 @@ function cmdInitExecutePhase(cwd, phase, raw) {
30
82
 
31
83
  // Config flags
32
84
  commit_docs: config.commit_docs,
85
+ sub_repos: config.sub_repos,
33
86
  parallelization: config.parallelization,
87
+ context_window: config.context_window,
34
88
  branching_strategy: config.branching_strategy,
35
89
  phase_branch_template: config.phase_branch_template,
36
90
  milestone_branch_template: config.milestone_branch_template,
@@ -68,16 +122,16 @@ function cmdInitExecutePhase(cwd, phase, raw) {
68
122
  milestone_slug: generateSlugInternal(milestone.name),
69
123
 
70
124
  // File existence
71
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
72
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
73
- config_exists: pathExistsInternal(cwd, '.planning/config.json'),
125
+ state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
126
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
127
+ config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
74
128
  // File paths
75
- state_path: '.planning/STATE.md',
76
- roadmap_path: '.planning/ROADMAP.md',
77
- config_path: '.planning/config.json',
129
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
130
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
131
+ config_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'config.json'))),
78
132
  };
79
133
 
80
- output(result, raw);
134
+ output(withProjectRoot(cwd, result), raw);
81
135
  }
82
136
 
83
137
  function cmdInitPlanPhase(cwd, phase, raw) {
@@ -86,9 +140,28 @@ function cmdInitPlanPhase(cwd, phase, raw) {
86
140
  }
87
141
 
88
142
  const config = loadConfig(cwd);
89
- const phaseInfo = findPhaseInternal(cwd, phase);
143
+ let phaseInfo = findPhaseInternal(cwd, phase);
90
144
 
91
145
  const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
146
+
147
+ // Fallback to ROADMAP.md if no phase directory exists yet
148
+ if (!phaseInfo && roadmapPhase?.found) {
149
+ const phaseName = roadmapPhase.phase_name;
150
+ phaseInfo = {
151
+ found: true,
152
+ directory: null,
153
+ phase_number: roadmapPhase.phase_number,
154
+ phase_name: phaseName,
155
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
156
+ plans: [],
157
+ summaries: [],
158
+ incomplete_plans: [],
159
+ has_research: false,
160
+ has_context: false,
161
+ has_verification: false,
162
+ has_reviews: false,
163
+ };
164
+ }
92
165
  const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
93
166
  const reqExtracted = reqMatch
94
167
  ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
@@ -106,6 +179,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
106
179
  plan_checker_enabled: config.plan_checker,
107
180
  nyquist_validation_enabled: config.nyquist_validation,
108
181
  commit_docs: config.commit_docs,
182
+ text_mode: config.text_mode,
109
183
 
110
184
  // Phase info
111
185
  phase_found: !!phaseInfo,
@@ -113,23 +187,24 @@ function cmdInitPlanPhase(cwd, phase, raw) {
113
187
  phase_number: phaseInfo?.phase_number || null,
114
188
  phase_name: phaseInfo?.phase_name || null,
115
189
  phase_slug: phaseInfo?.phase_slug || null,
116
- padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
190
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
117
191
  phase_req_ids,
118
192
 
119
193
  // Existing artifacts
120
194
  has_research: phaseInfo?.has_research || false,
121
195
  has_context: phaseInfo?.has_context || false,
196
+ has_reviews: phaseInfo?.has_reviews || false,
122
197
  has_plans: (phaseInfo?.plans?.length || 0) > 0,
123
198
  plan_count: phaseInfo?.plans?.length || 0,
124
199
 
125
200
  // Environment
126
- planning_exists: pathExistsInternal(cwd, '.planning'),
127
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
201
+ planning_exists: fs.existsSync(planningDir(cwd)),
202
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
128
203
 
129
204
  // File paths
130
- state_path: '.planning/STATE.md',
131
- roadmap_path: '.planning/ROADMAP.md',
132
- requirements_path: '.planning/REQUIREMENTS.md',
205
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
206
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
207
+ requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))),
133
208
  };
134
209
 
135
210
  if (phaseInfo?.directory) {
@@ -153,10 +228,14 @@ function cmdInitPlanPhase(cwd, phase, raw) {
153
228
  if (uatFile) {
154
229
  result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
155
230
  }
156
- } catch {}
231
+ const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
232
+ if (reviewsFile) {
233
+ result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile));
234
+ }
235
+ } catch { /* intentionally empty */ }
157
236
  }
158
237
 
159
- output(result, raw);
238
+ output(withProjectRoot(cwd, result), raw);
160
239
  }
161
240
 
162
241
  function cmdInitNewProject(cwd, raw) {
@@ -167,23 +246,67 @@ function cmdInitNewProject(cwd, raw) {
167
246
  const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
168
247
  const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
169
248
 
170
- // Detect existing code
249
+ // Detect Firecrawl API key availability
250
+ const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
251
+ const hasFirecrawl = !!(process.env.FIRECRAWL_API_KEY || fs.existsSync(firecrawlKeyFile));
252
+
253
+ // Detect Exa API key availability
254
+ const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
255
+ const hasExaSearch = !!(process.env.EXA_API_KEY || fs.existsSync(exaKeyFile));
256
+
257
+ // Detect existing code (cross-platform — no Unix `find` dependency)
171
258
  let hasCode = false;
172
259
  let hasPackageFile = false;
173
260
  try {
174
- const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
175
- cwd,
176
- encoding: 'utf-8',
177
- stdio: ['pipe', 'pipe', 'pipe'],
178
- });
179
- hasCode = files.trim().length > 0;
180
- } catch {}
261
+ const codeExtensions = new Set([
262
+ '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java',
263
+ '.kt', '.kts', // Kotlin (Android, server-side)
264
+ '.c', '.cpp', '.h', // C/C++
265
+ '.cs', // C#
266
+ '.rb', // Ruby
267
+ '.php', // PHP
268
+ '.dart', // Dart (Flutter)
269
+ '.m', '.mm', // Objective-C / Objective-C++
270
+ '.scala', // Scala
271
+ '.groovy', // Groovy (Gradle build scripts)
272
+ '.lua', // Lua
273
+ '.r', '.R', // R
274
+ '.zig', // Zig
275
+ '.ex', '.exs', // Elixir
276
+ '.clj', // Clojure
277
+ ]);
278
+ const skipDirs = new Set(['node_modules', '.git', '.planning', '.OpenCode', '__pycache__', 'target', 'dist', 'build']);
279
+ function findCodeFiles(dir, depth) {
280
+ if (depth > 3) return false;
281
+ let entries;
282
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
283
+ for (const entry of entries) {
284
+ if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
285
+ if (entry.isDirectory() && !skipDirs.has(entry.name)) {
286
+ if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
287
+ }
288
+ }
289
+ return false;
290
+ }
291
+ hasCode = findCodeFiles(cwd, 0);
292
+ } catch { /* intentionally empty — best-effort detection */ }
181
293
 
182
294
  hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
183
295
  pathExistsInternal(cwd, 'requirements.txt') ||
184
296
  pathExistsInternal(cwd, 'Cargo.toml') ||
185
297
  pathExistsInternal(cwd, 'go.mod') ||
186
- pathExistsInternal(cwd, 'Package.swift');
298
+ pathExistsInternal(cwd, 'Package.swift') ||
299
+ pathExistsInternal(cwd, 'build.gradle') ||
300
+ pathExistsInternal(cwd, 'build.gradle.kts') ||
301
+ pathExistsInternal(cwd, 'pom.xml') ||
302
+ pathExistsInternal(cwd, 'Gemfile') ||
303
+ pathExistsInternal(cwd, 'composer.json') ||
304
+ pathExistsInternal(cwd, 'pubspec.yaml') ||
305
+ pathExistsInternal(cwd, 'CMakeLists.txt') ||
306
+ pathExistsInternal(cwd, 'Makefile') ||
307
+ pathExistsInternal(cwd, 'build.zig') ||
308
+ pathExistsInternal(cwd, 'mix.exs') ||
309
+ pathExistsInternal(cwd, 'project.clj');
187
310
 
188
311
  const result = {
189
312
  // Models
@@ -210,17 +333,30 @@ function cmdInitNewProject(cwd, raw) {
210
333
 
211
334
  // Enhanced search
212
335
  brave_search_available: hasBraveSearch,
336
+ firecrawl_available: hasFirecrawl,
337
+ exa_search_available: hasExaSearch,
213
338
 
214
339
  // File paths
215
340
  project_path: '.planning/PROJECT.md',
216
341
  };
217
342
 
218
- output(result, raw);
343
+ output(withProjectRoot(cwd, result), raw);
219
344
  }
220
345
 
221
346
  function cmdInitNewMilestone(cwd, raw) {
222
347
  const config = loadConfig(cwd);
223
348
  const milestone = getMilestoneInfo(cwd);
349
+ const latestCompleted = getLatestCompletedMilestone(cwd);
350
+ const phasesDir = path.join(planningDir(cwd), 'phases');
351
+ let phaseDirCount = 0;
352
+
353
+ try {
354
+ if (fs.existsSync(phasesDir)) {
355
+ phaseDirCount = fs.readdirSync(phasesDir, { withFileTypes: true })
356
+ .filter(entry => entry.isDirectory())
357
+ .length;
358
+ }
359
+ } catch {}
224
360
 
225
361
  const result = {
226
362
  // Models
@@ -235,19 +371,23 @@ function cmdInitNewMilestone(cwd, raw) {
235
371
  // Current milestone
236
372
  current_milestone: milestone.version,
237
373
  current_milestone_name: milestone.name,
374
+ latest_completed_milestone: latestCompleted?.version || null,
375
+ latest_completed_milestone_name: latestCompleted?.name || null,
376
+ phase_dir_count: phaseDirCount,
377
+ phase_archive_path: latestCompleted ? toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))) : null,
238
378
 
239
379
  // File existence
240
380
  project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
241
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
242
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
381
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
382
+ state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
243
383
 
244
384
  // File paths
245
385
  project_path: '.planning/PROJECT.md',
246
- roadmap_path: '.planning/ROADMAP.md',
247
- state_path: '.planning/STATE.md',
386
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
387
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
248
388
  };
249
389
 
250
- output(result, raw);
390
+ output(withProjectRoot(cwd, result), raw);
251
391
  }
252
392
 
253
393
  function cmdInitQuick(cwd, description, raw) {
@@ -255,18 +395,25 @@ function cmdInitQuick(cwd, description, raw) {
255
395
  const now = new Date();
256
396
  const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
257
397
 
258
- // Find next quick task number
259
- const quickDir = path.join(cwd, '.planning', 'quick');
260
- let nextNum = 1;
261
- try {
262
- const existing = fs.readdirSync(quickDir)
263
- .filter(f => /^\d+-/.test(f))
264
- .map(f => parseInt(f.split('-')[0], 10))
265
- .filter(n => !isNaN(n));
266
- if (existing.length > 0) {
267
- nextNum = Math.max(...existing) + 1;
268
- }
269
- } catch {}
398
+ // Generate collision-resistant quick task ID: YYMMDD-xxx
399
+ // xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
400
+ // Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
401
+ // Provides ~2s uniqueness window per user — practically collision-free across a team.
402
+ const yy = String(now.getFullYear()).slice(-2);
403
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
404
+ const dd = String(now.getDate()).padStart(2, '0');
405
+ const dateStr = yy + mm + dd;
406
+ const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
407
+ const timeBlocks = Math.floor(secondsSinceMidnight / 2);
408
+ const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
409
+ const quickId = dateStr + '-' + timeEncoded;
410
+ const branchSlug = slug || 'quick';
411
+ const quickBranchName = config.quick_branch_template
412
+ ? config.quick_branch_template
413
+ .replace('{num}', quickId)
414
+ .replace('{quick}', quickId)
415
+ .replace('{slug}', branchSlug)
416
+ : null;
270
417
 
271
418
  const result = {
272
419
  // Models
@@ -277,9 +424,10 @@ function cmdInitQuick(cwd, description, raw) {
277
424
 
278
425
  // Config
279
426
  commit_docs: config.commit_docs,
427
+ branch_name: quickBranchName,
280
428
 
281
429
  // Quick task info
282
- next_num: nextNum,
430
+ quick_id: quickId,
283
431
  slug: slug,
284
432
  description: description || null,
285
433
 
@@ -289,15 +437,15 @@ function cmdInitQuick(cwd, description, raw) {
289
437
 
290
438
  // Paths
291
439
  quick_dir: '.planning/quick',
292
- task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null,
440
+ task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
293
441
 
294
442
  // File existence
295
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
296
- planning_exists: pathExistsInternal(cwd, '.planning'),
443
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
444
+ planning_exists: fs.existsSync(planningRoot(cwd)),
297
445
 
298
446
  };
299
447
 
300
- output(result, raw);
448
+ output(withProjectRoot(cwd, result), raw);
301
449
  }
302
450
 
303
451
  function cmdInitResume(cwd, raw) {
@@ -306,19 +454,19 @@ function cmdInitResume(cwd, raw) {
306
454
  // Check for interrupted agent
307
455
  let interruptedAgentId = null;
308
456
  try {
309
- interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
310
- } catch {}
457
+ interruptedAgentId = fs.readFileSync(path.join(planningRoot(cwd), 'current-agent-id.txt'), 'utf-8').trim();
458
+ } catch { /* intentionally empty */ }
311
459
 
312
460
  const result = {
313
461
  // File existence
314
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
315
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
462
+ state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
463
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
316
464
  project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
317
- planning_exists: pathExistsInternal(cwd, '.planning'),
465
+ planning_exists: fs.existsSync(planningRoot(cwd)),
318
466
 
319
467
  // File paths
320
- state_path: '.planning/STATE.md',
321
- roadmap_path: '.planning/ROADMAP.md',
468
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
469
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
322
470
  project_path: '.planning/PROJECT.md',
323
471
 
324
472
  // Agent state
@@ -329,7 +477,7 @@ function cmdInitResume(cwd, raw) {
329
477
  commit_docs: config.commit_docs,
330
478
  };
331
479
 
332
- output(result, raw);
480
+ output(withProjectRoot(cwd, result), raw);
333
481
  }
334
482
 
335
483
  function cmdInitVerifyWork(cwd, phase, raw) {
@@ -338,7 +486,28 @@ function cmdInitVerifyWork(cwd, phase, raw) {
338
486
  }
339
487
 
340
488
  const config = loadConfig(cwd);
341
- const phaseInfo = findPhaseInternal(cwd, phase);
489
+ let phaseInfo = findPhaseInternal(cwd, phase);
490
+
491
+ // Fallback to ROADMAP.md if no phase directory exists yet
492
+ if (!phaseInfo) {
493
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
494
+ if (roadmapPhase?.found) {
495
+ const phaseName = roadmapPhase.phase_name;
496
+ phaseInfo = {
497
+ found: true,
498
+ directory: null,
499
+ phase_number: roadmapPhase.phase_number,
500
+ phase_name: phaseName,
501
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
502
+ plans: [],
503
+ summaries: [],
504
+ incomplete_plans: [],
505
+ has_research: false,
506
+ has_context: false,
507
+ has_verification: false,
508
+ };
509
+ }
510
+ }
342
511
 
343
512
  const result = {
344
513
  // Models
@@ -358,13 +527,36 @@ function cmdInitVerifyWork(cwd, phase, raw) {
358
527
  has_verification: phaseInfo?.has_verification || false,
359
528
  };
360
529
 
361
- output(result, raw);
530
+ output(withProjectRoot(cwd, result), raw);
362
531
  }
363
532
 
364
533
  function cmdInitPhaseOp(cwd, phase, raw) {
365
534
  const config = loadConfig(cwd);
366
535
  let phaseInfo = findPhaseInternal(cwd, phase);
367
536
 
537
+ // If the only disk match comes from an archived milestone, prefer the
538
+ // current milestone's ROADMAP entry so discuss-phase and similar flows
539
+ // don't attach to shipped work that reused the same phase number.
540
+ if (phaseInfo?.archived) {
541
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
542
+ if (roadmapPhase?.found) {
543
+ const phaseName = roadmapPhase.phase_name;
544
+ phaseInfo = {
545
+ found: true,
546
+ directory: null,
547
+ phase_number: roadmapPhase.phase_number,
548
+ phase_name: phaseName,
549
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
550
+ plans: [],
551
+ summaries: [],
552
+ incomplete_plans: [],
553
+ has_research: false,
554
+ has_context: false,
555
+ has_verification: false,
556
+ };
557
+ }
558
+ }
559
+
368
560
  // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
369
561
  if (!phaseInfo) {
370
562
  const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
@@ -390,6 +582,8 @@ function cmdInitPhaseOp(cwd, phase, raw) {
390
582
  // Config
391
583
  commit_docs: config.commit_docs,
392
584
  brave_search: config.brave_search,
585
+ firecrawl: config.firecrawl,
586
+ exa_search: config.exa_search,
393
587
 
394
588
  // Phase info
395
589
  phase_found: !!phaseInfo,
@@ -397,23 +591,24 @@ function cmdInitPhaseOp(cwd, phase, raw) {
397
591
  phase_number: phaseInfo?.phase_number || null,
398
592
  phase_name: phaseInfo?.phase_name || null,
399
593
  phase_slug: phaseInfo?.phase_slug || null,
400
- padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
594
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
401
595
 
402
596
  // Existing artifacts
403
597
  has_research: phaseInfo?.has_research || false,
404
598
  has_context: phaseInfo?.has_context || false,
405
599
  has_plans: (phaseInfo?.plans?.length || 0) > 0,
406
600
  has_verification: phaseInfo?.has_verification || false,
601
+ has_reviews: phaseInfo?.has_reviews || false,
407
602
  plan_count: phaseInfo?.plans?.length || 0,
408
603
 
409
604
  // File existence
410
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
411
- planning_exists: pathExistsInternal(cwd, '.planning'),
605
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
606
+ planning_exists: fs.existsSync(planningDir(cwd)),
412
607
 
413
608
  // File paths
414
- state_path: '.planning/STATE.md',
415
- roadmap_path: '.planning/ROADMAP.md',
416
- requirements_path: '.planning/REQUIREMENTS.md',
609
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
610
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
611
+ requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))),
417
612
  };
418
613
 
419
614
  if (phaseInfo?.directory) {
@@ -436,10 +631,14 @@ function cmdInitPhaseOp(cwd, phase, raw) {
436
631
  if (uatFile) {
437
632
  result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
438
633
  }
439
- } catch {}
634
+ const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
635
+ if (reviewsFile) {
636
+ result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile));
637
+ }
638
+ } catch { /* intentionally empty */ }
440
639
  }
441
640
 
442
- output(result, raw);
641
+ output(withProjectRoot(cwd, result), raw);
443
642
  }
444
643
 
445
644
  function cmdInitTodos(cwd, area, raw) {
@@ -447,7 +646,7 @@ function cmdInitTodos(cwd, area, raw) {
447
646
  const now = new Date();
448
647
 
449
648
  // List todos (reuse existing logic)
450
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
649
+ const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
451
650
  let count = 0;
452
651
  const todos = [];
453
652
 
@@ -469,11 +668,11 @@ function cmdInitTodos(cwd, area, raw) {
469
668
  created: createdMatch ? createdMatch[1].trim() : 'unknown',
470
669
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
471
670
  area: todoArea,
472
- path: '.planning/todos/pending/' + file,
671
+ path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'pending', file))),
473
672
  });
474
- } catch {}
673
+ } catch { /* intentionally empty */ }
475
674
  }
476
- } catch {}
675
+ } catch { /* intentionally empty */ }
477
676
 
478
677
  const result = {
479
678
  // Config
@@ -489,16 +688,16 @@ function cmdInitTodos(cwd, area, raw) {
489
688
  area_filter: area || null,
490
689
 
491
690
  // Paths
492
- pending_dir: '.planning/todos/pending',
493
- completed_dir: '.planning/todos/completed',
691
+ pending_dir: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'pending'))),
692
+ completed_dir: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'todos', 'completed'))),
494
693
 
495
694
  // File existence
496
- planning_exists: pathExistsInternal(cwd, '.planning'),
497
- todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'),
498
- pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
695
+ planning_exists: fs.existsSync(planningDir(cwd)),
696
+ todos_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'todos')),
697
+ pending_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'todos', 'pending')),
499
698
  };
500
699
 
501
- output(result, raw);
700
+ output(withProjectRoot(cwd, result), raw);
502
701
  }
503
702
 
504
703
  function cmdInitMilestoneOp(cwd, raw) {
@@ -508,7 +707,7 @@ function cmdInitMilestoneOp(cwd, raw) {
508
707
  // Count phases
509
708
  let phaseCount = 0;
510
709
  let completedPhases = 0;
511
- const phasesDir = path.join(cwd, '.planning', 'phases');
710
+ const phasesDir = path.join(planningDir(cwd), 'phases');
512
711
  try {
513
712
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
514
713
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
@@ -520,18 +719,18 @@ function cmdInitMilestoneOp(cwd, raw) {
520
719
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
521
720
  const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
522
721
  if (hasSummary) completedPhases++;
523
- } catch {}
722
+ } catch { /* intentionally empty */ }
524
723
  }
525
- } catch {}
724
+ } catch { /* intentionally empty */ }
526
725
 
527
726
  // Check archive
528
- const archiveDir = path.join(cwd, '.planning', 'archive');
727
+ const archiveDir = path.join(planningRoot(cwd), 'archive');
529
728
  let archivedMilestones = [];
530
729
  try {
531
730
  archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
532
731
  .filter(e => e.isDirectory())
533
732
  .map(e => e.name);
534
- } catch {}
733
+ } catch { /* intentionally empty */ }
535
734
 
536
735
  const result = {
537
736
  // Config
@@ -553,24 +752,24 @@ function cmdInitMilestoneOp(cwd, raw) {
553
752
 
554
753
  // File existence
555
754
  project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
556
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
557
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
558
- archive_exists: pathExistsInternal(cwd, '.planning/archive'),
559
- phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
755
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
756
+ state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
757
+ archive_exists: fs.existsSync(path.join(planningRoot(cwd), 'archive')),
758
+ phases_dir_exists: fs.existsSync(path.join(planningDir(cwd), 'phases')),
560
759
  };
561
760
 
562
- output(result, raw);
761
+ output(withProjectRoot(cwd, result), raw);
563
762
  }
564
763
 
565
764
  function cmdInitMapCodebase(cwd, raw) {
566
765
  const config = loadConfig(cwd);
567
766
 
568
767
  // Check for existing codebase maps
569
- const codebaseDir = path.join(cwd, '.planning', 'codebase');
768
+ const codebaseDir = path.join(planningRoot(cwd), 'codebase');
570
769
  let existingMaps = [];
571
770
  try {
572
771
  existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
573
- } catch {}
772
+ } catch { /* intentionally empty */ }
574
773
 
575
774
  const result = {
576
775
  // Models
@@ -593,27 +792,300 @@ function cmdInitMapCodebase(cwd, raw) {
593
792
  codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
594
793
  };
595
794
 
596
- output(result, raw);
795
+ output(withProjectRoot(cwd, result), raw);
796
+ }
797
+
798
+ function cmdInitManager(cwd, raw) {
799
+ const config = loadConfig(cwd);
800
+ const milestone = getMilestoneInfo(cwd);
801
+
802
+ // Use planningPaths for forward-compatibility with workstream scoping (#1268)
803
+ const paths = planningPaths(cwd);
804
+
805
+ // Validate prerequisites
806
+ if (!fs.existsSync(paths.roadmap)) {
807
+ error('No ROADMAP.md found. Run /gsd-new-milestone first.');
808
+ }
809
+ if (!fs.existsSync(paths.state)) {
810
+ error('No STATE.md found. Run /gsd-new-milestone first.');
811
+ }
812
+ const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
813
+ const content = extractCurrentMilestone(rawContent, cwd);
814
+ const phasesDir = paths.phases;
815
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
816
+
817
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
818
+ const phases = [];
819
+ let match;
820
+
821
+ while ((match = phasePattern.exec(content)) !== null) {
822
+ const phaseNum = match[1];
823
+ const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
824
+
825
+ const sectionStart = match.index;
826
+ const restOfContent = content.slice(sectionStart);
827
+ const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
828
+ const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
829
+ const section = content.slice(sectionStart, sectionEnd);
830
+
831
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
832
+ const goal = goalMatch ? goalMatch[1].trim() : null;
833
+
834
+ const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
835
+ const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
836
+
837
+ const normalized = normalizePhaseName(phaseNum);
838
+ let diskStatus = 'no_directory';
839
+ let planCount = 0;
840
+ let summaryCount = 0;
841
+ let hasContext = false;
842
+ let hasResearch = false;
843
+ let lastActivity = null;
844
+ let isActive = false;
845
+
846
+ try {
847
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
848
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(isDirInMilestone);
849
+ const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
850
+
851
+ if (dirMatch) {
852
+ const fullDir = path.join(phasesDir, dirMatch);
853
+ const phaseFiles = fs.readdirSync(fullDir);
854
+ planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
855
+ summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
856
+ hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
857
+ hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
858
+
859
+ if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
860
+ else if (summaryCount > 0) diskStatus = 'partial';
861
+ else if (planCount > 0) diskStatus = 'planned';
862
+ else if (hasResearch) diskStatus = 'researched';
863
+ else if (hasContext) diskStatus = 'discussed';
864
+ else diskStatus = 'empty';
865
+
866
+ // Activity detection: check most recent file mtime
867
+ const now = Date.now();
868
+ let newestMtime = 0;
869
+ for (const f of phaseFiles) {
870
+ try {
871
+ const stat = fs.statSync(path.join(fullDir, f));
872
+ if (stat.mtimeMs > newestMtime) newestMtime = stat.mtimeMs;
873
+ } catch { /* intentionally empty */ }
874
+ }
875
+ if (newestMtime > 0) {
876
+ lastActivity = new Date(newestMtime).toISOString();
877
+ isActive = (now - newestMtime) < 300000; // 5 minutes
878
+ }
879
+ }
880
+ } catch { /* intentionally empty */ }
881
+
882
+ // Check ROADMAP checkbox status
883
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:\\s]`, 'i');
884
+ const checkboxMatch = content.match(checkboxPattern);
885
+ const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
886
+ if (roadmapComplete && diskStatus !== 'complete') {
887
+ diskStatus = 'complete';
888
+ }
889
+
890
+ phases.push({
891
+ number: phaseNum,
892
+ name: phaseName,
893
+ goal,
894
+ depends_on,
895
+ disk_status: diskStatus,
896
+ has_context: hasContext,
897
+ has_research: hasResearch,
898
+ plan_count: planCount,
899
+ summary_count: summaryCount,
900
+ roadmap_complete: roadmapComplete,
901
+ last_activity: lastActivity,
902
+ is_active: isActive,
903
+ });
904
+ }
905
+
906
+ // Compute display names: truncate to keep table aligned
907
+ const MAX_NAME_WIDTH = 20;
908
+ for (const phase of phases) {
909
+ if (phase.name.length > MAX_NAME_WIDTH) {
910
+ phase.display_name = phase.name.slice(0, MAX_NAME_WIDTH - 1) + '…';
911
+ } else {
912
+ phase.display_name = phase.name;
913
+ }
914
+ }
915
+
916
+ // Dependency satisfaction: check if all depends_on phases are complete
917
+ const completedNums = new Set(phases.filter(p => p.disk_status === 'complete').map(p => p.number));
918
+ for (const phase of phases) {
919
+ if (!phase.depends_on || /^none$/i.test(phase.depends_on.trim())) {
920
+ phase.deps_satisfied = true;
921
+ } else {
922
+ // Parse "Phase 1, Phase 3" or "1, 3" formats
923
+ const depNums = phase.depends_on.match(/\d+(?:\.\d+)*/g) || [];
924
+ phase.deps_satisfied = depNums.every(n => completedNums.has(n));
925
+ phase.dep_phases = depNums;
926
+ }
927
+ }
928
+
929
+ // Compact dependency display for dashboard
930
+ for (const phase of phases) {
931
+ phase.deps_display = (phase.dep_phases && phase.dep_phases.length > 0)
932
+ ? phase.dep_phases.join(',')
933
+ : '—';
934
+ }
935
+
936
+ // Sliding window: discuss is sequential — only the first undiscussed phase is available
937
+ let foundNextToDiscuss = false;
938
+ for (const phase of phases) {
939
+ if (!foundNextToDiscuss && (phase.disk_status === 'empty' || phase.disk_status === 'no_directory')) {
940
+ phase.is_next_to_discuss = true;
941
+ foundNextToDiscuss = true;
942
+ } else {
943
+ phase.is_next_to_discuss = false;
944
+ }
945
+ }
946
+
947
+ // Check for WAITING.json signal
948
+ let waitingSignal = null;
949
+ try {
950
+ const waitingPath = path.join(cwd, '.planning', 'WAITING.json');
951
+ if (fs.existsSync(waitingPath)) {
952
+ waitingSignal = JSON.parse(fs.readFileSync(waitingPath, 'utf-8'));
953
+ }
954
+ } catch { /* intentionally empty */ }
955
+
956
+ // Compute recommended actions (execute > plan > discuss)
957
+ const recommendedActions = [];
958
+ for (const phase of phases) {
959
+ if (phase.disk_status === 'complete') continue;
960
+
961
+ if (phase.disk_status === 'planned' && phase.deps_satisfied) {
962
+ recommendedActions.push({
963
+ phase: phase.number,
964
+ phase_name: phase.name,
965
+ action: 'execute',
966
+ reason: `${phase.plan_count} plans ready, dependencies met`,
967
+ command: `/gsd-execute-phase ${phase.number}`,
968
+ });
969
+ } else if (phase.disk_status === 'discussed' || phase.disk_status === 'researched') {
970
+ recommendedActions.push({
971
+ phase: phase.number,
972
+ phase_name: phase.name,
973
+ action: 'plan',
974
+ reason: 'Context gathered, ready for planning',
975
+ command: `/gsd-plan-phase ${phase.number}`,
976
+ });
977
+ } else if ((phase.disk_status === 'empty' || phase.disk_status === 'no_directory') && phase.is_next_to_discuss) {
978
+ recommendedActions.push({
979
+ phase: phase.number,
980
+ phase_name: phase.name,
981
+ action: 'discuss',
982
+ reason: 'Unblocked, ready to gather context',
983
+ command: `/gsd-discuss-phase ${phase.number}`,
984
+ });
985
+ }
986
+ }
987
+
988
+ // Filter recommendations: no parallel execute/plan unless phases are independent
989
+ // Two phases are "independent" if neither depends on the other (directly or transitively)
990
+ const phaseMap = new Map(phases.map(p => [p.number, p]));
991
+
992
+ function reaches(from, to, visited = new Set()) {
993
+ if (visited.has(from)) return false;
994
+ visited.add(from);
995
+ const p = phaseMap.get(from);
996
+ if (!p || !p.dep_phases || p.dep_phases.length === 0) return false;
997
+ if (p.dep_phases.includes(to)) return true;
998
+ return p.dep_phases.some(dep => reaches(dep, to, visited));
999
+ }
1000
+
1001
+ function hasDepRelationship(numA, numB) {
1002
+ return reaches(numA, numB) || reaches(numB, numA);
1003
+ }
1004
+
1005
+ // Detect phases with active work (file modified in last 5 min)
1006
+ const activeExecuting = phases.filter(p =>
1007
+ p.disk_status === 'partial' ||
1008
+ (p.disk_status === 'planned' && p.is_active)
1009
+ );
1010
+ const activePlanning = phases.filter(p =>
1011
+ p.is_active && (p.disk_status === 'discussed' || p.disk_status === 'researched')
1012
+ );
1013
+
1014
+ const filteredActions = recommendedActions.filter(action => {
1015
+ if (action.action === 'execute' && activeExecuting.length > 0) {
1016
+ // Only allow if independent of ALL actively-executing phases
1017
+ return activeExecuting.every(active => !hasDepRelationship(action.phase, active.number));
1018
+ }
1019
+ if (action.action === 'plan' && activePlanning.length > 0) {
1020
+ // Only allow if independent of ALL actively-planning phases
1021
+ return activePlanning.every(active => !hasDepRelationship(action.phase, active.number));
1022
+ }
1023
+ return true;
1024
+ });
1025
+
1026
+ const completedCount = phases.filter(p => p.disk_status === 'complete').length;
1027
+ const result = {
1028
+ milestone_version: milestone.version,
1029
+ milestone_name: milestone.name,
1030
+ phases,
1031
+ phase_count: phases.length,
1032
+ completed_count: completedCount,
1033
+ in_progress_count: phases.filter(p => ['partial', 'planned', 'discussed', 'researched'].includes(p.disk_status)).length,
1034
+ recommended_actions: filteredActions,
1035
+ waiting_signal: waitingSignal,
1036
+ all_complete: completedCount === phases.length && phases.length > 0,
1037
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1038
+ roadmap_exists: true,
1039
+ state_exists: true,
1040
+ };
1041
+
1042
+ output(withProjectRoot(cwd, result), raw);
597
1043
  }
598
1044
 
599
1045
  function cmdInitProgress(cwd, raw) {
600
1046
  const config = loadConfig(cwd);
601
1047
  const milestone = getMilestoneInfo(cwd);
602
1048
 
603
- // Analyze phases
604
- const phasesDir = path.join(cwd, '.planning', 'phases');
1049
+ // Analyze phases — filter to current milestone and include ROADMAP-only phases
1050
+ const phasesDir = path.join(planningDir(cwd), 'phases');
605
1051
  const phases = [];
606
1052
  let currentPhase = null;
607
1053
  let nextPhase = null;
608
1054
 
1055
+ // Build set of phases defined in ROADMAP for the current milestone
1056
+ const roadmapPhaseNums = new Set();
1057
+ const roadmapPhaseNames = new Map();
1058
+ try {
1059
+ const roadmapContent = extractCurrentMilestone(
1060
+ fs.readFileSync(path.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd
1061
+ );
1062
+ const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
1063
+ let hm;
1064
+ while ((hm = headingPattern.exec(roadmapContent)) !== null) {
1065
+ roadmapPhaseNums.add(hm[1]);
1066
+ roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
1067
+ }
1068
+ } catch { /* intentionally empty */ }
1069
+
1070
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
1071
+ const seenPhaseNums = new Set();
1072
+
609
1073
  try {
610
1074
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
611
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
1075
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
1076
+ .filter(isDirInMilestone)
1077
+ .sort((a, b) => {
1078
+ const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1079
+ const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1080
+ if (!pa || !pb) return a.localeCompare(b);
1081
+ return parseInt(pa[1], 10) - parseInt(pb[1], 10);
1082
+ });
612
1083
 
613
1084
  for (const dir of dirs) {
614
- const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
1085
+ const match = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
615
1086
  const phaseNumber = match ? match[1] : dir;
616
1087
  const phaseName = match && match[2] ? match[2] : null;
1088
+ seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
617
1089
 
618
1090
  const phasePath = path.join(phasesDir, dir);
619
1091
  const phaseFiles = fs.readdirSync(phasePath);
@@ -629,7 +1101,7 @@ function cmdInitProgress(cwd, raw) {
629
1101
  const phaseInfo = {
630
1102
  number: phaseNumber,
631
1103
  name: phaseName,
632
- directory: '.planning/phases/' + dir,
1104
+ directory: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'phases', dir))),
633
1105
  status,
634
1106
  plan_count: plans.length,
635
1107
  summary_count: summaries.length,
@@ -646,15 +1118,38 @@ function cmdInitProgress(cwd, raw) {
646
1118
  nextPhase = phaseInfo;
647
1119
  }
648
1120
  }
649
- } catch {}
1121
+ } catch { /* intentionally empty */ }
1122
+
1123
+ // Add phases defined in ROADMAP but not yet scaffolded to disk
1124
+ for (const [num, name] of roadmapPhaseNames) {
1125
+ const stripped = num.replace(/^0+/, '') || '0';
1126
+ if (!seenPhaseNums.has(stripped)) {
1127
+ const phaseInfo = {
1128
+ number: num,
1129
+ name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
1130
+ directory: null,
1131
+ status: 'not_started',
1132
+ plan_count: 0,
1133
+ summary_count: 0,
1134
+ has_research: false,
1135
+ };
1136
+ phases.push(phaseInfo);
1137
+ if (!nextPhase && !currentPhase) {
1138
+ nextPhase = phaseInfo;
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ // Re-sort phases by number after adding ROADMAP-only phases
1144
+ phases.sort((a, b) => parseInt(a.number, 10) - parseInt(b.number, 10));
650
1145
 
651
1146
  // Check for paused work
652
1147
  let pausedAt = null;
653
1148
  try {
654
- const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
1149
+ const state = fs.readFileSync(path.join(planningDir(cwd), 'STATE.md'), 'utf-8');
655
1150
  const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
656
1151
  if (pauseMatch) pausedAt = pauseMatch[1].trim();
657
- } catch {}
1152
+ } catch { /* intentionally empty */ }
658
1153
 
659
1154
  const result = {
660
1155
  // Models
@@ -682,18 +1177,248 @@ function cmdInitProgress(cwd, raw) {
682
1177
 
683
1178
  // File existence
684
1179
  project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
685
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
686
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1180
+ roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
1181
+ state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
687
1182
  // File paths
688
- state_path: '.planning/STATE.md',
689
- roadmap_path: '.planning/ROADMAP.md',
1183
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
1184
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
690
1185
  project_path: '.planning/PROJECT.md',
691
- config_path: '.planning/config.json',
1186
+ config_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'config.json'))),
1187
+ };
1188
+
1189
+ output(withProjectRoot(cwd, result), raw);
1190
+ }
1191
+
1192
+ /**
1193
+ * Detect child git repos in a directory (one level deep).
1194
+ * Returns array of { name, path, has_uncommitted } objects.
1195
+ */
1196
+ function detectChildRepos(dir) {
1197
+ const repos = [];
1198
+ let entries;
1199
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return repos; }
1200
+ for (const entry of entries) {
1201
+ if (!entry.isDirectory()) continue;
1202
+ if (entry.name.startsWith('.')) continue;
1203
+ const fullPath = path.join(dir, entry.name);
1204
+ const gitDir = path.join(fullPath, '.git');
1205
+ if (fs.existsSync(gitDir)) {
1206
+ let hasUncommitted = false;
1207
+ try {
1208
+ const status = execSync('git status --porcelain', { cwd: fullPath, encoding: 'utf8', timeout: 5000 });
1209
+ hasUncommitted = status.trim().length > 0;
1210
+ } catch { /* best-effort */ }
1211
+ repos.push({ name: entry.name, path: fullPath, has_uncommitted: hasUncommitted });
1212
+ }
1213
+ }
1214
+ return repos;
1215
+ }
1216
+
1217
+ function cmdInitNewWorkspace(cwd, raw) {
1218
+ const homedir = process.env.HOME || require('os').homedir();
1219
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
1220
+
1221
+ // Detect child git repos for interactive selection
1222
+ const childRepos = detectChildRepos(cwd);
1223
+
1224
+ // Check if git worktree is available
1225
+ let worktreeAvailable = false;
1226
+ try {
1227
+ execSync('git --version', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
1228
+ worktreeAvailable = true;
1229
+ } catch { /* no git at all */ }
1230
+
1231
+ const result = {
1232
+ default_workspace_base: defaultBase,
1233
+ child_repos: childRepos,
1234
+ child_repo_count: childRepos.length,
1235
+ worktree_available: worktreeAvailable,
1236
+ is_git_repo: pathExistsInternal(cwd, '.git'),
1237
+ cwd_repo_name: path.basename(cwd),
1238
+ };
1239
+
1240
+ output(withProjectRoot(cwd, result), raw);
1241
+ }
1242
+
1243
+ function cmdInitListWorkspaces(cwd, raw) {
1244
+ const homedir = process.env.HOME || require('os').homedir();
1245
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
1246
+
1247
+ const workspaces = [];
1248
+ if (fs.existsSync(defaultBase)) {
1249
+ let entries;
1250
+ try { entries = fs.readdirSync(defaultBase, { withFileTypes: true }); } catch { entries = []; }
1251
+ for (const entry of entries) {
1252
+ if (!entry.isDirectory()) continue;
1253
+ const wsPath = path.join(defaultBase, entry.name);
1254
+ const manifestPath = path.join(wsPath, 'WORKSPACE.md');
1255
+ if (!fs.existsSync(manifestPath)) continue;
1256
+
1257
+ let repoCount = 0;
1258
+ let hasProject = false;
1259
+ let strategy = 'unknown';
1260
+ try {
1261
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
1262
+ const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
1263
+ if (strategyMatch) strategy = strategyMatch[1].trim();
1264
+ // Count table rows (lines starting with |, excluding header and separator)
1265
+ const tableRows = manifest.split('\n').filter(l => l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---'));
1266
+ repoCount = tableRows.length;
1267
+ } catch { /* best-effort */ }
1268
+ hasProject = fs.existsSync(path.join(wsPath, '.planning', 'PROJECT.md'));
1269
+
1270
+ workspaces.push({
1271
+ name: entry.name,
1272
+ path: wsPath,
1273
+ repo_count: repoCount,
1274
+ strategy,
1275
+ has_project: hasProject,
1276
+ });
1277
+ }
1278
+ }
1279
+
1280
+ const result = {
1281
+ workspace_base: defaultBase,
1282
+ workspaces,
1283
+ workspace_count: workspaces.length,
1284
+ };
1285
+
1286
+ output(result, raw);
1287
+ }
1288
+
1289
+ function cmdInitRemoveWorkspace(cwd, name, raw) {
1290
+ const homedir = process.env.HOME || require('os').homedir();
1291
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
1292
+
1293
+ if (!name) {
1294
+ error('workspace name required for init remove-workspace');
1295
+ }
1296
+
1297
+ const wsPath = path.join(defaultBase, name);
1298
+ const manifestPath = path.join(wsPath, 'WORKSPACE.md');
1299
+
1300
+ if (!fs.existsSync(wsPath)) {
1301
+ error(`Workspace not found: ${wsPath}`);
1302
+ }
1303
+
1304
+ // Parse manifest for repo info
1305
+ const repos = [];
1306
+ let strategy = 'unknown';
1307
+ if (fs.existsSync(manifestPath)) {
1308
+ try {
1309
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
1310
+ const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
1311
+ if (strategyMatch) strategy = strategyMatch[1].trim();
1312
+
1313
+ // Parse table rows for repo names and source paths
1314
+ const lines = manifest.split('\n');
1315
+ for (const line of lines) {
1316
+ const match = line.match(/^\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|$/);
1317
+ if (match && match[1] !== 'Repo' && !match[1].includes('---')) {
1318
+ repos.push({ name: match[1], source: match[2], branch: match[3], strategy: match[4] });
1319
+ }
1320
+ }
1321
+ } catch { /* best-effort */ }
1322
+ }
1323
+
1324
+ // Check for uncommitted changes in workspace repos
1325
+ const dirtyRepos = [];
1326
+ for (const repo of repos) {
1327
+ const repoPath = path.join(wsPath, repo.name);
1328
+ if (!fs.existsSync(repoPath)) continue;
1329
+ try {
1330
+ const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
1331
+ if (status.trim().length > 0) {
1332
+ dirtyRepos.push(repo.name);
1333
+ }
1334
+ } catch { /* best-effort */ }
1335
+ }
1336
+
1337
+ const result = {
1338
+ workspace_name: name,
1339
+ workspace_path: wsPath,
1340
+ has_manifest: fs.existsSync(manifestPath),
1341
+ strategy,
1342
+ repos,
1343
+ repo_count: repos.length,
1344
+ dirty_repos: dirtyRepos,
1345
+ has_dirty_repos: dirtyRepos.length > 0,
692
1346
  };
693
1347
 
694
1348
  output(result, raw);
695
1349
  }
696
1350
 
1351
+ /**
1352
+ * Build a formatted agent skills block for injection into task() prompts.
1353
+ *
1354
+ * Reads `config.agent_skills[agentType]` and validates each skill path exists
1355
+ * within the project root. Returns a formatted `<agent_skills>` block or empty
1356
+ * string if no skills are configured.
1357
+ *
1358
+ * @param {object} config - Loaded project config
1359
+ * @param {string} agentType - The agent type (e.g., 'gsd-executor', 'gsd-planner')
1360
+ * @param {string} projectRoot - Absolute path to project root (for path validation)
1361
+ * @returns {string} Formatted skills block or empty string
1362
+ */
1363
+ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1364
+ const { validatePath } = require('./security.cjs');
1365
+
1366
+ if (!config || !config.agent_skills || !agentType) return '';
1367
+
1368
+ let skillPaths = config.agent_skills[agentType];
1369
+ if (!skillPaths) return '';
1370
+
1371
+ // Normalize single string to array
1372
+ if (typeof skillPaths === 'string') skillPaths = [skillPaths];
1373
+ if (!Array.isArray(skillPaths) || skillPaths.length === 0) return '';
1374
+
1375
+ const validPaths = [];
1376
+ for (const skillPath of skillPaths) {
1377
+ if (typeof skillPath !== 'string') continue;
1378
+
1379
+ // Validate path safety — must resolve within project root
1380
+ const pathCheck = validatePath(skillPath, projectRoot);
1381
+ if (!pathCheck.safe) {
1382
+ process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck.error}\n`);
1383
+ continue;
1384
+ }
1385
+
1386
+ // Check that the skill directory and SKILL.md exist
1387
+ const skillMdPath = path.join(projectRoot, skillPath, 'SKILL.md');
1388
+ if (!fs.existsSync(skillMdPath)) {
1389
+ process.stderr.write(`[agent-skills] WARNING: skill not found at "${skillPath}/SKILL.md" — skipping\n`);
1390
+ continue;
1391
+ }
1392
+
1393
+ validPaths.push(skillPath);
1394
+ }
1395
+
1396
+ if (validPaths.length === 0) return '';
1397
+
1398
+ const lines = validPaths.map(p => `- @${p}/SKILL.md`).join('\n');
1399
+ return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
1400
+ }
1401
+
1402
+ /**
1403
+ * Command: output the agent skills block for a given agent type.
1404
+ * Used by workflows: SKILLS=$(node "$TOOLS" agent-skills gsd-executor 2>/dev/null)
1405
+ */
1406
+ function cmdAgentSkills(cwd, agentType, raw) {
1407
+ if (!agentType) {
1408
+ // No agent type — output empty string silently
1409
+ output('', raw, '');
1410
+ return;
1411
+ }
1412
+
1413
+ const config = loadConfig(cwd);
1414
+ const block = buildAgentSkillsBlock(config, agentType, cwd);
1415
+ // Output raw text (not JSON) so workflows can embed it directly
1416
+ if (block) {
1417
+ process.stdout.write(block);
1418
+ }
1419
+ process.exit(0);
1420
+ }
1421
+
697
1422
  module.exports = {
698
1423
  cmdInitExecutePhase,
699
1424
  cmdInitPlanPhase,
@@ -707,4 +1432,11 @@ module.exports = {
707
1432
  cmdInitMilestoneOp,
708
1433
  cmdInitMapCodebase,
709
1434
  cmdInitProgress,
1435
+ cmdInitManager,
1436
+ cmdInitNewWorkspace,
1437
+ cmdInitListWorkspaces,
1438
+ cmdInitRemoveWorkspace,
1439
+ detectChildRepos,
1440
+ buildAgentSkillsBlock,
1441
+ cmdAgentSkills,
710
1442
  };