@bhargavvc/sdd-cc 1.30.1 → 1.35.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 (242) hide show
  1. package/README.ja-JP.md +144 -110
  2. package/README.ko-KR.md +143 -107
  3. package/README.md +183 -112
  4. package/README.pt-BR.md +90 -52
  5. package/README.zh-CN.md +141 -101
  6. package/agents/sdd-advisor-researcher.md +23 -0
  7. package/agents/sdd-ai-researcher.md +133 -0
  8. package/agents/sdd-code-fixer.md +516 -0
  9. package/agents/sdd-code-reviewer.md +355 -0
  10. package/agents/sdd-codebase-mapper.md +3 -3
  11. package/agents/sdd-debugger.md +17 -5
  12. package/agents/sdd-doc-verifier.md +201 -0
  13. package/agents/sdd-doc-writer.md +602 -0
  14. package/agents/sdd-domain-researcher.md +153 -0
  15. package/agents/sdd-eval-auditor.md +164 -0
  16. package/agents/sdd-eval-planner.md +154 -0
  17. package/agents/sdd-executor.md +87 -4
  18. package/agents/sdd-framework-selector.md +160 -0
  19. package/agents/sdd-intel-updater.md +314 -0
  20. package/agents/sdd-nyquist-auditor.md +1 -1
  21. package/agents/sdd-phase-researcher.md +71 -4
  22. package/agents/sdd-plan-checker.md +100 -6
  23. package/agents/sdd-planner.md +145 -206
  24. package/agents/sdd-project-researcher.md +25 -2
  25. package/agents/sdd-research-synthesizer.md +3 -3
  26. package/agents/sdd-roadmapper.md +6 -6
  27. package/agents/sdd-security-auditor.md +128 -0
  28. package/agents/sdd-ui-auditor.md +43 -3
  29. package/agents/sdd-ui-checker.md +5 -5
  30. package/agents/sdd-ui-researcher.md +27 -4
  31. package/agents/sdd-user-profiler.md +2 -2
  32. package/agents/sdd-verifier.md +142 -22
  33. package/bin/install.js +2145 -545
  34. package/commands/sdd/add-backlog.md +5 -5
  35. package/commands/sdd/add-tests.md +2 -2
  36. package/commands/sdd/ai-integration-phase.md +36 -0
  37. package/commands/sdd/analyze-dependencies.md +34 -0
  38. package/commands/sdd/audit-fix.md +33 -0
  39. package/commands/sdd/autonomous.md +7 -2
  40. package/commands/sdd/cleanup.md +5 -0
  41. package/commands/sdd/code-review-fix.md +52 -0
  42. package/commands/sdd/code-review.md +55 -0
  43. package/commands/sdd/complete-milestone.md +6 -6
  44. package/commands/sdd/debug.md +22 -9
  45. package/commands/sdd/discuss-phase.md +7 -2
  46. package/commands/sdd/do.md +1 -1
  47. package/commands/sdd/docs-update.md +48 -0
  48. package/commands/sdd/eval-review.md +32 -0
  49. package/commands/sdd/execute-phase.md +4 -0
  50. package/commands/sdd/explore.md +27 -0
  51. package/commands/sdd/fast.md +2 -2
  52. package/commands/sdd/from-sdd2.md +45 -0
  53. package/commands/sdd/help.md +2 -0
  54. package/commands/sdd/import.md +36 -0
  55. package/commands/sdd/intel.md +179 -0
  56. package/commands/sdd/join-discord.md +2 -1
  57. package/commands/sdd/manager.md +1 -0
  58. package/commands/sdd/map-codebase.md +3 -3
  59. package/commands/sdd/new-milestone.md +1 -1
  60. package/commands/sdd/new-project.md +5 -1
  61. package/commands/sdd/new-workspace.md +1 -1
  62. package/commands/sdd/next.md +2 -0
  63. package/commands/sdd/plan-milestone-gaps.md +2 -2
  64. package/commands/sdd/plan-phase.md +6 -1
  65. package/commands/sdd/plant-seed.md +1 -1
  66. package/commands/sdd/profile-user.md +1 -1
  67. package/commands/sdd/quick.md +5 -3
  68. package/commands/sdd/reapply-patches.md +230 -42
  69. package/commands/sdd/research-phase.md +3 -3
  70. package/commands/sdd/review-backlog.md +1 -0
  71. package/commands/sdd/review.md +6 -3
  72. package/commands/sdd/scan.md +26 -0
  73. package/commands/sdd/secure-phase.md +35 -0
  74. package/commands/sdd/ship.md +1 -1
  75. package/commands/sdd/thread.md +5 -5
  76. package/commands/sdd/undo.md +34 -0
  77. package/commands/sdd/verify-work.md +1 -1
  78. package/commands/sdd/workstreams.md +17 -11
  79. package/hooks/dist/sdd-check-update.js +33 -8
  80. package/hooks/dist/sdd-context-monitor.js +17 -8
  81. package/hooks/dist/sdd-phase-boundary.sh +27 -0
  82. package/hooks/dist/sdd-prompt-guard.js +1 -0
  83. package/hooks/dist/sdd-read-guard.js +82 -0
  84. package/hooks/dist/sdd-session-state.sh +33 -0
  85. package/hooks/dist/sdd-statusline.js +137 -15
  86. package/hooks/dist/sdd-validate-commit.sh +47 -0
  87. package/hooks/dist/sdd-workflow-guard.js +4 -4
  88. package/hooks/sdd-check-update.js +139 -0
  89. package/hooks/sdd-context-monitor.js +165 -0
  90. package/hooks/sdd-phase-boundary.sh +27 -0
  91. package/hooks/sdd-prompt-guard.js +97 -0
  92. package/hooks/sdd-read-guard.js +82 -0
  93. package/hooks/sdd-session-state.sh +33 -0
  94. package/hooks/sdd-statusline.js +241 -0
  95. package/hooks/sdd-validate-commit.sh +47 -0
  96. package/hooks/sdd-workflow-guard.js +94 -0
  97. package/package.json +3 -3
  98. package/scripts/build-hooks.js +18 -7
  99. package/scripts/prompt-injection-scan.sh +1 -0
  100. package/scripts/rebrand-gsd-to-sdd.sh +221 -220
  101. package/scripts/run-tests.cjs +5 -1
  102. package/scripts/sync-upstream.sh +1 -1
  103. package/sdd/bin/lib/commands.cjs +79 -17
  104. package/sdd/bin/lib/config.cjs +90 -48
  105. package/sdd/bin/lib/core.cjs +452 -87
  106. package/sdd/bin/lib/docs.cjs +267 -0
  107. package/sdd/bin/lib/frontmatter.cjs +381 -336
  108. package/sdd/bin/lib/init.cjs +110 -16
  109. package/sdd/bin/lib/intel.cjs +660 -0
  110. package/sdd/bin/lib/learnings.cjs +378 -0
  111. package/sdd/bin/lib/milestone.cjs +42 -11
  112. package/sdd/bin/lib/model-profiles.cjs +17 -15
  113. package/sdd/bin/lib/phase.cjs +367 -288
  114. package/sdd/bin/lib/profile-output.cjs +106 -10
  115. package/sdd/bin/lib/roadmap.cjs +146 -115
  116. package/sdd/bin/lib/schema-detect.cjs +238 -0
  117. package/sdd/bin/lib/sdd2-import.cjs +511 -0
  118. package/sdd/bin/lib/security.cjs +124 -3
  119. package/sdd/bin/lib/state.cjs +648 -264
  120. package/sdd/bin/lib/template.cjs +8 -4
  121. package/sdd/bin/lib/verify.cjs +209 -28
  122. package/sdd/bin/lib/workstream.cjs +7 -3
  123. package/sdd/bin/sdd-tools.cjs +184 -12
  124. package/sdd/contexts/dev.md +21 -0
  125. package/sdd/contexts/research.md +22 -0
  126. package/sdd/contexts/review.md +22 -0
  127. package/sdd/references/agent-contracts.md +79 -0
  128. package/sdd/references/ai-evals.md +156 -0
  129. package/sdd/references/ai-frameworks.md +186 -0
  130. package/sdd/references/artifact-types.md +113 -0
  131. package/sdd/references/common-bug-patterns.md +114 -0
  132. package/sdd/references/context-budget.md +49 -0
  133. package/sdd/references/continuation-format.md +25 -25
  134. package/sdd/references/domain-probes.md +125 -0
  135. package/sdd/references/few-shot-examples/plan-checker.md +73 -0
  136. package/sdd/references/few-shot-examples/verifier.md +109 -0
  137. package/sdd/references/gate-prompts.md +100 -0
  138. package/sdd/references/gates.md +70 -0
  139. package/sdd/references/git-integration.md +1 -1
  140. package/sdd/references/ios-scaffold.md +123 -0
  141. package/sdd/references/model-profile-resolution.md +2 -0
  142. package/sdd/references/model-profiles.md +24 -18
  143. package/sdd/references/planner-gap-closure.md +62 -0
  144. package/sdd/references/planner-reviews.md +39 -0
  145. package/sdd/references/planner-revision.md +87 -0
  146. package/sdd/references/planning-config.md +252 -0
  147. package/sdd/references/revision-loop.md +97 -0
  148. package/sdd/references/thinking-models-debug.md +44 -0
  149. package/sdd/references/thinking-models-execution.md +50 -0
  150. package/sdd/references/thinking-models-planning.md +62 -0
  151. package/sdd/references/thinking-models-research.md +50 -0
  152. package/sdd/references/thinking-models-verification.md +55 -0
  153. package/sdd/references/thinking-partner.md +96 -0
  154. package/sdd/references/ui-brand.md +4 -4
  155. package/sdd/references/universal-anti-patterns.md +63 -0
  156. package/sdd/references/verification-overrides.md +227 -0
  157. package/sdd/references/workstream-flag.md +56 -3
  158. package/sdd/templates/AI-SPEC.md +246 -0
  159. package/sdd/templates/DEBUG.md +1 -1
  160. package/sdd/templates/SECURITY.md +61 -0
  161. package/sdd/templates/UAT.md +4 -4
  162. package/sdd/templates/VALIDATION.md +4 -4
  163. package/sdd/templates/claude-md.md +32 -9
  164. package/sdd/templates/config.json +4 -0
  165. package/sdd/templates/debug-subagent-prompt.md +1 -1
  166. package/sdd/templates/dev-preferences.md +1 -1
  167. package/sdd/templates/discovery.md +2 -2
  168. package/sdd/templates/phase-prompt.md +1 -1
  169. package/sdd/templates/planner-subagent-prompt.md +3 -3
  170. package/sdd/templates/project.md +1 -1
  171. package/sdd/templates/research.md +1 -1
  172. package/sdd/templates/state.md +2 -2
  173. package/sdd/workflows/add-phase.md +8 -8
  174. package/sdd/workflows/add-tests.md +12 -9
  175. package/sdd/workflows/add-todo.md +5 -3
  176. package/sdd/workflows/ai-integration-phase.md +284 -0
  177. package/sdd/workflows/analyze-dependencies.md +96 -0
  178. package/sdd/workflows/audit-fix.md +157 -0
  179. package/sdd/workflows/audit-milestone.md +11 -11
  180. package/sdd/workflows/audit-uat.md +2 -2
  181. package/sdd/workflows/autonomous.md +195 -27
  182. package/sdd/workflows/check-todos.md +12 -10
  183. package/sdd/workflows/cleanup.md +2 -0
  184. package/sdd/workflows/code-review-fix.md +497 -0
  185. package/sdd/workflows/code-review.md +515 -0
  186. package/sdd/workflows/complete-milestone.md +56 -22
  187. package/sdd/workflows/diagnose-issues.md +10 -3
  188. package/sdd/workflows/discovery-phase.md +5 -3
  189. package/sdd/workflows/discuss-phase-assumptions.md +24 -6
  190. package/sdd/workflows/discuss-phase-power.md +291 -0
  191. package/sdd/workflows/discuss-phase.md +173 -21
  192. package/sdd/workflows/do.md +23 -21
  193. package/sdd/workflows/docs-update.md +1155 -0
  194. package/sdd/workflows/eval-review.md +155 -0
  195. package/sdd/workflows/execute-phase.md +594 -38
  196. package/sdd/workflows/execute-plan.md +67 -96
  197. package/sdd/workflows/explore.md +139 -0
  198. package/sdd/workflows/fast.md +5 -5
  199. package/sdd/workflows/forensics.md +2 -2
  200. package/sdd/workflows/health.md +4 -4
  201. package/sdd/workflows/help.md +122 -119
  202. package/sdd/workflows/import.md +276 -0
  203. package/sdd/workflows/inbox.md +387 -0
  204. package/sdd/workflows/insert-phase.md +7 -7
  205. package/sdd/workflows/list-phase-assumptions.md +4 -4
  206. package/sdd/workflows/list-workspaces.md +2 -2
  207. package/sdd/workflows/manager.md +35 -32
  208. package/sdd/workflows/map-codebase.md +7 -5
  209. package/sdd/workflows/milestone-summary.md +2 -2
  210. package/sdd/workflows/new-milestone.md +17 -9
  211. package/sdd/workflows/new-project.md +50 -25
  212. package/sdd/workflows/new-workspace.md +7 -5
  213. package/sdd/workflows/next.md +67 -11
  214. package/sdd/workflows/note.md +9 -7
  215. package/sdd/workflows/pause-work.md +75 -12
  216. package/sdd/workflows/plan-milestone-gaps.md +8 -8
  217. package/sdd/workflows/plan-phase.md +294 -42
  218. package/sdd/workflows/plant-seed.md +6 -3
  219. package/sdd/workflows/pr-branch.md +42 -14
  220. package/sdd/workflows/profile-user.md +9 -7
  221. package/sdd/workflows/progress.md +45 -45
  222. package/sdd/workflows/quick.md +195 -47
  223. package/sdd/workflows/remove-phase.md +6 -6
  224. package/sdd/workflows/remove-workspace.md +3 -1
  225. package/sdd/workflows/research-phase.md +2 -2
  226. package/sdd/workflows/resume-project.md +12 -12
  227. package/sdd/workflows/review.md +109 -9
  228. package/sdd/workflows/scan.md +102 -0
  229. package/sdd/workflows/secure-phase.md +166 -0
  230. package/sdd/workflows/session-report.md +2 -2
  231. package/sdd/workflows/settings.md +38 -12
  232. package/sdd/workflows/ship.md +21 -9
  233. package/sdd/workflows/stats.md +1 -1
  234. package/sdd/workflows/transition.md +23 -23
  235. package/sdd/workflows/ui-phase.md +15 -7
  236. package/sdd/workflows/ui-review.md +29 -4
  237. package/sdd/workflows/undo.md +314 -0
  238. package/sdd/workflows/update.md +171 -20
  239. package/sdd/workflows/validate-phase.md +6 -4
  240. package/sdd/workflows/verify-phase.md +210 -6
  241. package/sdd/workflows/verify-work.md +83 -9
  242. package/sdd/commands/sdd/workstreams.md +0 -63
package/bin/install.js CHANGED
@@ -62,12 +62,17 @@ const hasLocal = args.includes('--local') || args.includes('-l');
62
62
  const hasOpencode = args.includes('--opencode');
63
63
  const hasClaude = args.includes('--claude');
64
64
  const hasGemini = args.includes('--gemini');
65
+ const hasKilo = args.includes('--kilo');
65
66
  const hasCodex = args.includes('--codex');
66
67
  const hasCopilot = args.includes('--copilot');
67
68
  const hasAntigravity = args.includes('--antigravity');
68
69
  const hasCursor = args.includes('--cursor');
69
70
  const hasWindsurf = args.includes('--windsurf');
70
- const hasSdk = args.includes('--sdk');
71
+ const hasAugment = args.includes('--augment');
72
+ const hasTrae = args.includes('--trae');
73
+ const hasQwen = args.includes('--qwen');
74
+ const hasCodebuddy = args.includes('--codebuddy');
75
+ const hasCline = args.includes('--cline');
71
76
  const hasBoth = args.includes('--both'); // Legacy flag, keeps working
72
77
  const hasAll = args.includes('--all');
73
78
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
@@ -75,18 +80,24 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
75
80
  // Runtime selection - can be set by flags or interactive prompt
76
81
  let selectedRuntimes = [];
77
82
  if (hasAll) {
78
- selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
83
+ selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', 'cline'];
79
84
  } else if (hasBoth) {
80
85
  selectedRuntimes = ['claude', 'opencode'];
81
86
  } else {
82
- if (hasOpencode) selectedRuntimes.push('opencode');
83
87
  if (hasClaude) selectedRuntimes.push('claude');
88
+ if (hasOpencode) selectedRuntimes.push('opencode');
84
89
  if (hasGemini) selectedRuntimes.push('gemini');
90
+ if (hasKilo) selectedRuntimes.push('kilo');
85
91
  if (hasCodex) selectedRuntimes.push('codex');
86
92
  if (hasCopilot) selectedRuntimes.push('copilot');
87
93
  if (hasAntigravity) selectedRuntimes.push('antigravity');
88
94
  if (hasCursor) selectedRuntimes.push('cursor');
89
95
  if (hasWindsurf) selectedRuntimes.push('windsurf');
96
+ if (hasAugment) selectedRuntimes.push('augment');
97
+ if (hasTrae) selectedRuntimes.push('trae');
98
+ if (hasQwen) selectedRuntimes.push('qwen');
99
+ if (hasCodebuddy) selectedRuntimes.push('codebuddy');
100
+ if (hasCline) selectedRuntimes.push('cline');
90
101
  }
91
102
 
92
103
  // WSL + Windows Node.js detection
@@ -128,10 +139,16 @@ function getDirName(runtime) {
128
139
  if (runtime === 'copilot') return '.github';
129
140
  if (runtime === 'opencode') return '.opencode';
130
141
  if (runtime === 'gemini') return '.gemini';
142
+ if (runtime === 'kilo') return '.kilo';
131
143
  if (runtime === 'codex') return '.codex';
132
144
  if (runtime === 'antigravity') return '.agent';
133
145
  if (runtime === 'cursor') return '.cursor';
134
146
  if (runtime === 'windsurf') return '.windsurf';
147
+ if (runtime === 'augment') return '.augment';
148
+ if (runtime === 'trae') return '.trae';
149
+ if (runtime === 'qwen') return '.qwen';
150
+ if (runtime === 'codebuddy') return '.codebuddy';
151
+ if (runtime === 'cline') return '.cline';
135
152
  return '.claude';
136
153
  }
137
154
 
@@ -154,6 +171,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
154
171
  return "'.config', 'opencode'";
155
172
  }
156
173
  if (runtime === 'gemini') return "'.gemini'";
174
+ if (runtime === 'kilo') return "'.config', 'kilo'";
157
175
  if (runtime === 'codex') return "'.codex'";
158
176
  if (runtime === 'antigravity') {
159
177
  if (!isGlobal) return "'.agent'";
@@ -161,6 +179,11 @@ function getConfigDirFromHome(runtime, isGlobal) {
161
179
  }
162
180
  if (runtime === 'cursor') return "'.cursor'";
163
181
  if (runtime === 'windsurf') return "'.windsurf'";
182
+ if (runtime === 'augment') return "'.augment'";
183
+ if (runtime === 'trae') return "'.trae'";
184
+ if (runtime === 'qwen') return "'.qwen'";
185
+ if (runtime === 'codebuddy') return "'.codebuddy'";
186
+ if (runtime === 'cline') return "'.cline'";
164
187
  return "'.claude'";
165
188
  }
166
189
 
@@ -174,21 +197,46 @@ function getOpencodeGlobalDir() {
174
197
  if (process.env.OPENCODE_CONFIG_DIR) {
175
198
  return expandTilde(process.env.OPENCODE_CONFIG_DIR);
176
199
  }
177
-
200
+
178
201
  // 2. OPENCODE_CONFIG env var (use its directory)
179
202
  if (process.env.OPENCODE_CONFIG) {
180
203
  return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
181
204
  }
182
-
205
+
183
206
  // 3. XDG_CONFIG_HOME/opencode
184
207
  if (process.env.XDG_CONFIG_HOME) {
185
208
  return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
186
209
  }
187
-
210
+
188
211
  // 4. Default: ~/.config/opencode (XDG default)
189
212
  return path.join(os.homedir(), '.config', 'opencode');
190
213
  }
191
214
 
215
+ /**
216
+ * Get the global config directory for Kilo
217
+ * Kilo follows XDG Base Directory spec and uses ~/.config/kilo/
218
+ * Priority: KILO_CONFIG_DIR > dirname(KILO_CONFIG) > XDG_CONFIG_HOME/kilo > ~/.config/kilo
219
+ */
220
+ function getKiloGlobalDir() {
221
+ // 1. Explicit KILO_CONFIG_DIR env var
222
+ if (process.env.KILO_CONFIG_DIR) {
223
+ return expandTilde(process.env.KILO_CONFIG_DIR);
224
+ }
225
+
226
+ // 2. KILO_CONFIG env var (use its directory)
227
+ if (process.env.KILO_CONFIG) {
228
+ return path.dirname(expandTilde(process.env.KILO_CONFIG));
229
+ }
230
+
231
+ // 3. XDG_CONFIG_HOME/kilo
232
+ if (process.env.XDG_CONFIG_HOME) {
233
+ return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo');
234
+ }
235
+
236
+ // 4. Default: ~/.config/kilo (XDG default)
237
+ return path.join(os.homedir(), '.config', 'kilo');
238
+ }
239
+
192
240
  /**
193
241
  * Get the global config directory for a runtime
194
242
  * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
@@ -202,7 +250,15 @@ function getGlobalDir(runtime, explicitDir = null) {
202
250
  }
203
251
  return getOpencodeGlobalDir();
204
252
  }
205
-
253
+
254
+ if (runtime === 'kilo') {
255
+ // For Kilo, --config-dir overrides env vars
256
+ if (explicitDir) {
257
+ return expandTilde(explicitDir);
258
+ }
259
+ return getKiloGlobalDir();
260
+ }
261
+
206
262
  if (runtime === 'gemini') {
207
263
  // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
208
264
  if (explicitDir) {
@@ -259,16 +315,68 @@ function getGlobalDir(runtime, explicitDir = null) {
259
315
  }
260
316
 
261
317
  if (runtime === 'windsurf') {
262
- // Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.windsurf
318
+ // Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.codeium/windsurf
263
319
  if (explicitDir) {
264
320
  return expandTilde(explicitDir);
265
321
  }
266
322
  if (process.env.WINDSURF_CONFIG_DIR) {
267
323
  return expandTilde(process.env.WINDSURF_CONFIG_DIR);
268
324
  }
269
- return path.join(os.homedir(), '.windsurf');
325
+ return path.join(os.homedir(), '.codeium', 'windsurf');
326
+ }
327
+
328
+ if (runtime === 'augment') {
329
+ // Augment: --config-dir > AUGMENT_CONFIG_DIR > ~/.augment
330
+ if (explicitDir) {
331
+ return expandTilde(explicitDir);
332
+ }
333
+ if (process.env.AUGMENT_CONFIG_DIR) {
334
+ return expandTilde(process.env.AUGMENT_CONFIG_DIR);
335
+ }
336
+ return path.join(os.homedir(), '.augment');
337
+ }
338
+ if (runtime === 'trae') {
339
+ // Trae: --config-dir > TRAE_CONFIG_DIR > ~/.trae
340
+ if (explicitDir) {
341
+ return expandTilde(explicitDir);
342
+ }
343
+ if (process.env.TRAE_CONFIG_DIR) {
344
+ return expandTilde(process.env.TRAE_CONFIG_DIR);
345
+ }
346
+ return path.join(os.homedir(), '.trae');
347
+ }
348
+
349
+ if (runtime === 'qwen') {
350
+ if (explicitDir) {
351
+ return expandTilde(explicitDir);
352
+ }
353
+ if (process.env.QWEN_CONFIG_DIR) {
354
+ return expandTilde(process.env.QWEN_CONFIG_DIR);
355
+ }
356
+ return path.join(os.homedir(), '.qwen');
357
+ }
358
+
359
+ if (runtime === 'codebuddy') {
360
+ // CodeBuddy: --config-dir > CODEBUDDY_CONFIG_DIR > ~/.codebuddy
361
+ if (explicitDir) {
362
+ return expandTilde(explicitDir);
363
+ }
364
+ if (process.env.CODEBUDDY_CONFIG_DIR) {
365
+ return expandTilde(process.env.CODEBUDDY_CONFIG_DIR);
366
+ }
367
+ return path.join(os.homedir(), '.codebuddy');
270
368
  }
271
369
 
370
+ if (runtime === 'cline') {
371
+ // Cline: --config-dir > CLINE_CONFIG_DIR > ~/.cline
372
+ if (explicitDir) {
373
+ return expandTilde(explicitDir);
374
+ }
375
+ if (process.env.CLINE_CONFIG_DIR) {
376
+ return expandTilde(process.env.CLINE_CONFIG_DIR);
377
+ }
378
+ return path.join(os.homedir(), '.cline');
379
+ }
272
380
 
273
381
  // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
274
382
  if (explicitDir) {
@@ -290,7 +398,7 @@ const banner = '\n' +
290
398
  '\n' +
291
399
  ' Spec-Driven Development ' + dim + 'v' + pkg.version + reset + '\n' +
292
400
  ' A meta-prompting, context engineering and spec-driven\n' +
293
- ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, Cursor, and Windsurf by TÂCHES.\n';
401
+ ' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Cline and CodeBuddy by TÂCHES.\n';
294
402
 
295
403
  // Parse --config-dir argument
296
404
  function parseConfigDirArg() {
@@ -328,7 +436,7 @@ if (hasUninstall) {
328
436
 
329
437
  // Show help if requested
330
438
  if (hasHelp) {
331
- console.log(` ${yellow}Usage:${reset} npx @bhargavvc/sdd-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}--sdk${reset} Also install SDD SDK CLI (sdd-sdk)\n ${cyan}-u, --uninstall${reset} Uninstall SDD (remove all SDD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx @bhargavvc/sdd-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx @bhargavvc/sdd-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @bhargavvc/sdd-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx @bhargavvc/sdd-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx @bhargavvc/sdd-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx @bhargavvc/sdd-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx @bhargavvc/sdd-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx @bhargavvc/sdd-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx @bhargavvc/sdd-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx @bhargavvc/sdd-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx @bhargavvc/sdd-cc --windsurf --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx @bhargavvc/sdd-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @bhargavvc/sdd-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx @bhargavvc/sdd-cc --claude --local\n\n ${dim}# Uninstall SDD from Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR environment variables.\n`);
439
+ console.log(` ${yellow}Usage:${reset} npx @bhargavvc/sdd-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall SDD (remove all SDD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx @bhargavvc/sdd-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx @bhargavvc/sdd-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @bhargavvc/sdd-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx @bhargavvc/sdd-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx @bhargavvc/sdd-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx @bhargavvc/sdd-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx @bhargavvc/sdd-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx @bhargavvc/sdd-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx @bhargavvc/sdd-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx @bhargavvc/sdd-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx @bhargavvc/sdd-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx @bhargavvc/sdd-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx @bhargavvc/sdd-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx @bhargavvc/sdd-cc --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx @bhargavvc/sdd-cc --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx @bhargavvc/sdd-cc --trae --local\n\n ${dim}# Install for Cline locally${reset}\n npx @bhargavvc/sdd-cc --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx @bhargavvc/sdd-cc --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx @bhargavvc/sdd-cc --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx @bhargavvc/sdd-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @bhargavvc/sdd-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx @bhargavvc/sdd-cc --claude --local\n\n ${dim}# Uninstall SDD from Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`);
332
440
  process.exit(0);
333
441
  }
334
442
 
@@ -349,7 +457,13 @@ function expandTilde(filePath) {
349
457
  function buildHookCommand(configDir, hookName) {
350
458
  // Use forward slashes for Node.js compatibility on all platforms
351
459
  const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
352
- return `node "${hooksPath}"`;
460
+ // .sh hooks use bash; .js hooks use node. Both wrap the path in double quotes
461
+ // so that paths with spaces (e.g. Windows "C:/Users/First Last/") work correctly
462
+ // (fixes #2045). Routing .sh hooks through this function also ensures they always
463
+ // receive an absolute path rather than the bare relative string that the old manual
464
+ // concatenation produced (fixes #2046).
465
+ const runner = hookName.endsWith('.sh') ? 'bash' : 'node';
466
+ return `${runner} "${hooksPath}"`;
353
467
  }
354
468
 
355
469
  /**
@@ -364,14 +478,89 @@ function resolveOpencodeConfigPath(configDir) {
364
478
  }
365
479
 
366
480
  /**
367
- * Read and parse settings.json, returning empty object if it doesn't exist
481
+ * Resolve the Kilo config file path, preferring .jsonc if it exists.
482
+ */
483
+ function resolveKiloConfigPath(configDir) {
484
+ const jsoncPath = path.join(configDir, 'kilo.jsonc');
485
+ if (fs.existsSync(jsoncPath)) {
486
+ return jsoncPath;
487
+ }
488
+ return path.join(configDir, 'kilo.json');
489
+ }
490
+
491
+ /**
492
+ * Strip JSONC comments (// and /* *​/) from a string to produce valid JSON.
493
+ * Handles comments inside strings correctly (does not strip them).
494
+ */
495
+ function stripJsonComments(text) {
496
+ let result = '';
497
+ let i = 0;
498
+ let inString = false;
499
+ let stringChar = '';
500
+ while (i < text.length) {
501
+ // Handle string literals — don't strip comments inside strings
502
+ if (inString) {
503
+ if (text[i] === '\\') {
504
+ result += text[i] + (text[i + 1] || '');
505
+ i += 2;
506
+ continue;
507
+ }
508
+ if (text[i] === stringChar) {
509
+ inString = false;
510
+ }
511
+ result += text[i];
512
+ i++;
513
+ continue;
514
+ }
515
+ // Start of string
516
+ if (text[i] === '"' || text[i] === "'") {
517
+ inString = true;
518
+ stringChar = text[i];
519
+ result += text[i];
520
+ i++;
521
+ continue;
522
+ }
523
+ // Line comment
524
+ if (text[i] === '/' && text[i + 1] === '/') {
525
+ // Skip to end of line
526
+ while (i < text.length && text[i] !== '\n') i++;
527
+ continue;
528
+ }
529
+ // Block comment
530
+ if (text[i] === '/' && text[i + 1] === '*') {
531
+ i += 2;
532
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
533
+ i += 2; // skip closing */
534
+ continue;
535
+ }
536
+ result += text[i];
537
+ i++;
538
+ }
539
+ // Remove trailing commas before } or ] (common in JSONC)
540
+ return result.replace(/,\s*([}\]])/g, '$1');
541
+ }
542
+
543
+ /**
544
+ * Read and parse settings.json, returning empty object if it doesn't exist.
545
+ * Supports JSONC (JSON with comments) — many CLI tools allow comments in
546
+ * their settings files, so we strip them before parsing to avoid silent
547
+ * data loss from JSON.parse failures.
368
548
  */
369
549
  function readSettings(settingsPath) {
370
550
  if (fs.existsSync(settingsPath)) {
371
551
  try {
372
- return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
552
+ const raw = fs.readFileSync(settingsPath, 'utf8');
553
+ // Try standard JSON first (fast path)
554
+ try {
555
+ return JSON.parse(raw);
556
+ } catch {
557
+ // Fall back to JSONC stripping
558
+ return JSON.parse(stripJsonComments(raw));
559
+ }
373
560
  } catch (e) {
374
- return {};
561
+ // If even JSONC stripping fails, warn instead of silently returning {}
562
+ console.warn(' ' + yellow + '⚠' + reset + ' Warning: Could not parse ' + settingsPath + ' — file may be malformed. Existing settings preserved.');
563
+ return null;
375
564
  }
376
565
  }
377
566
  return {};
@@ -400,13 +589,16 @@ function getCommitAttribution(runtime) {
400
589
 
401
590
  let result;
402
591
 
403
- if (runtime === 'opencode') {
404
- const config = readSettings(resolveOpencodeConfigPath(getGlobalDir('opencode', null)));
405
- result = config.disable_ai_attribution === true ? null : undefined;
592
+ if (runtime === 'opencode' || runtime === 'kilo') {
593
+ const resolveConfigPath = runtime === 'opencode'
594
+ ? resolveOpencodeConfigPath
595
+ : resolveKiloConfigPath;
596
+ const config = readSettings(resolveConfigPath(getGlobalDir(runtime, null)));
597
+ result = (config && config.disable_ai_attribution === true) ? null : undefined;
406
598
  } else if (runtime === 'gemini') {
407
599
  // Gemini: check gemini settings.json for attribution config
408
600
  const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
409
- if (!settings.attribution || settings.attribution.commit === undefined) {
601
+ if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
410
602
  result = undefined;
411
603
  } else if (settings.attribution.commit === '') {
412
604
  result = null;
@@ -416,7 +608,7 @@ function getCommitAttribution(runtime) {
416
608
  } else if (runtime === 'claude') {
417
609
  // Claude Code
418
610
  const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
419
- if (!settings.attribution || settings.attribution.commit === undefined) {
611
+ if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
420
612
  result = undefined;
421
613
  } else if (settings.attribution.commit === '') {
422
614
  result = null;
@@ -542,6 +734,72 @@ function convertGeminiToolName(claudeTool) {
542
734
  return claudeTool.toLowerCase();
543
735
  }
544
736
 
737
+ const claudeToKiloAgentPermissions = {
738
+ Read: 'read',
739
+ Write: 'edit',
740
+ Edit: 'edit',
741
+ Bash: 'bash',
742
+ Grep: 'grep',
743
+ Glob: 'glob',
744
+ Task: 'task',
745
+ WebFetch: 'webfetch',
746
+ WebSearch: 'websearch',
747
+ TodoWrite: 'todowrite',
748
+ AskUserQuestion: 'question',
749
+ SlashCommand: 'skill',
750
+ };
751
+
752
+ const kiloAgentPermissionOrder = [
753
+ 'read',
754
+ 'edit',
755
+ 'bash',
756
+ 'grep',
757
+ 'glob',
758
+ 'task',
759
+ 'webfetch',
760
+ 'websearch',
761
+ 'skill',
762
+ 'question',
763
+ 'todowrite',
764
+ 'list',
765
+ 'codesearch',
766
+ 'lsp',
767
+ ];
768
+
769
+ function convertClaudeToKiloPermissionTool(claudeTool) {
770
+ return claudeToKiloAgentPermissions[claudeTool] || null;
771
+ }
772
+
773
+ function buildKiloAgentPermissionBlock(claudeTools) {
774
+ const allowedPermissions = new Set();
775
+
776
+ for (const tool of claudeTools) {
777
+ const mapped = convertClaudeToKiloPermissionTool(tool);
778
+ if (mapped) {
779
+ allowedPermissions.add(mapped);
780
+ }
781
+ }
782
+
783
+ const lines = ['permission:'];
784
+ for (const permission of kiloAgentPermissionOrder) {
785
+ lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`);
786
+ }
787
+
788
+ return lines;
789
+ }
790
+
791
+ function escapeRegExp(value) {
792
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
793
+ }
794
+
795
+ function replaceRelativePathReference(content, fromPath, toPath) {
796
+ const escapedPath = escapeRegExp(fromPath);
797
+ return content.replace(
798
+ new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'),
799
+ (_, prefix) => `${prefix}${toPath}`,
800
+ );
801
+ }
802
+
545
803
  /**
546
804
  * Convert a Claude Code tool name to GitHub Copilot format.
547
805
  * - Applies explicit mapping from claudeToCopilotTools
@@ -579,6 +837,7 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
579
837
  } else {
580
838
  c = c.replace(/\$HOME\/\.claude\//g, '.github/');
581
839
  c = c.replace(/~\/\.claude\//g, '.github/');
840
+ c = c.replace(/~\/\.claude\n/g, '.github/');
582
841
  }
583
842
  c = c.replace(/\.\/\.claude\//g, './.github/');
584
843
  c = c.replace(/\.claude\//g, '.github/');
@@ -623,6 +882,39 @@ function convertClaudeCommandToCopilotSkill(content, skillName, isGlobal = false
623
882
  return `${fm}\n${body}`;
624
883
  }
625
884
 
885
+ /**
886
+ * Convert a Claude command (.md) to a Claude skill (SKILL.md).
887
+ * Claude Code is the native format, so minimal conversion needed —
888
+ * preserve allowed-tools as YAML multiline list, preserve argument-hint,
889
+ * convert name from sdd:xxx to sdd-xxx format.
890
+ */
891
+ function convertClaudeCommandToClaudeSkill(content, skillName) {
892
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
893
+ if (!frontmatter) return content;
894
+
895
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
896
+ const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
897
+ const agent = extractFrontmatterField(frontmatter, 'agent');
898
+
899
+ // Preserve allowed-tools as YAML multiline list (Claude native format)
900
+ const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
901
+ let toolsBlock = '';
902
+ if (toolsMatch) {
903
+ toolsBlock = 'allowed-tools:\n' + toolsMatch[1];
904
+ // Ensure trailing newline
905
+ if (!toolsBlock.endsWith('\n')) toolsBlock += '\n';
906
+ }
907
+
908
+ // Reconstruct frontmatter in Claude skill format
909
+ let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
910
+ if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
911
+ if (agent) fm += `agent: ${agent}\n`;
912
+ if (toolsBlock) fm += toolsBlock;
913
+ fm += '---';
914
+
915
+ return `${fm}\n${body}`;
916
+ }
917
+
626
918
  /**
627
919
  * Convert a Claude agent (.md) to a Copilot agent (.agent.md).
628
920
  * Applies tool mapping + deduplication, formats tools as JSON array.
@@ -878,8 +1170,8 @@ function convertClaudeAgentToCursorAgent(content) {
878
1170
  }
879
1171
 
880
1172
  // --- Windsurf converters ---
881
- // Windsurf (by Codeium) uses a tool set similar to Cursor (both VS Code-based).
882
- // Config lives in .windsurf/ (local) and ~/.windsurf/ (global).
1173
+ // Windsurf uses a tool set similar to Cursor.
1174
+ // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
883
1175
 
884
1176
  // Tool name mapping from Claude Code to Windsurf Cascade
885
1177
  const claudeToWindsurfTools = {
@@ -920,10 +1212,10 @@ function convertClaudeToWindsurfMarkdown(content) {
920
1212
  converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
921
1213
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
922
1214
  // Replace project-level Claude conventions with Windsurf equivalents
923
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules/`');
924
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules/');
925
- converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules/`');
926
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules/');
1215
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
1216
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
1217
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
1218
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
927
1219
  converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
928
1220
  // Remove Claude Code-specific bug workarounds before brand replacement
929
1221
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
@@ -995,67 +1287,98 @@ function convertClaudeAgentToWindsurfAgent(content) {
995
1287
  return `${cleanFrontmatter}\n${body}`;
996
1288
  }
997
1289
 
998
- function convertSlashCommandsToCodexSkillMentions(content) {
999
- let converted = content.replace(/\/sdd:([a-z0-9-]+)/gi, (_, commandName) => {
1000
- return `$sdd-${String(commandName).toLowerCase()}`;
1001
- });
1002
- converted = converted.replace(/\/sdd-help\b/g, '$sdd-help');
1003
- return converted;
1290
+ // --- Augment converters ---
1291
+ // Augment uses a tool set similar to Cursor/Windsurf.
1292
+ // Config lives in .augment/ (local) and ~/.augment/ (global).
1293
+
1294
+ const claudeToAugmentTools = {
1295
+ Bash: 'launch-process',
1296
+ Edit: 'str-replace-editor',
1297
+ AskUserQuestion: null,
1298
+ SlashCommand: null,
1299
+ TodoWrite: 'add_tasks',
1300
+ };
1301
+
1302
+ function convertAugmentToolName(claudeTool) {
1303
+ if (claudeTool in claudeToAugmentTools) {
1304
+ return claudeToAugmentTools[claudeTool];
1305
+ }
1306
+ if (claudeTool.startsWith('mcp__')) {
1307
+ return claudeTool;
1308
+ }
1309
+ const toolMapping = {
1310
+ Read: 'view',
1311
+ Write: 'save-file',
1312
+ Glob: 'view',
1313
+ Grep: 'grep',
1314
+ Task: null,
1315
+ WebSearch: 'web-search',
1316
+ WebFetch: 'web-fetch',
1317
+ };
1318
+ return toolMapping[claudeTool] || claudeTool;
1004
1319
  }
1005
1320
 
1006
- function convertClaudeToCodexMarkdown(content) {
1007
- let converted = convertSlashCommandsToCodexSkillMentions(content);
1321
+ function convertSlashCommandsToAugmentSkillMentions(content) {
1322
+ return content.replace(/sdd:/gi, 'sdd-');
1323
+ }
1324
+
1325
+ function convertClaudeToAugmentMarkdown(content) {
1326
+ let converted = convertSlashCommandsToAugmentSkillMentions(content);
1327
+ converted = converted.replace(/\bBash\(/g, 'launch-process(');
1328
+ converted = converted.replace(/\bEdit\(/g, 'str-replace-editor(');
1329
+ converted = converted.replace(/\bRead\(/g, 'view(');
1330
+ converted = converted.replace(/\bWrite\(/g, 'save-file(');
1331
+ converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks(');
1332
+ converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
1333
+ // Replace subagent_type from Claude to Augment format
1334
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
1008
1335
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
1009
- // Runtime-neutral agent name replacement (#766)
1010
- converted = neutralizeAgentReferences(converted, 'AGENTS.md');
1336
+ // Replace project-level Claude conventions with Augment equivalents
1337
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`');
1338
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/');
1339
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`');
1340
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/');
1341
+ converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/');
1342
+ // Remove Claude Code-specific bug workarounds before brand replacement
1343
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1344
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1345
+ // Replace "Claude Code" brand references with "Augment"
1346
+ converted = converted.replace(/\bClaude Code\b/g, 'Augment');
1011
1347
  return converted;
1012
1348
  }
1013
1349
 
1014
- function getCodexSkillAdapterHeader(skillName) {
1015
- const invocation = `$${skillName}`;
1016
- return `<codex_skill_adapter>
1350
+ function getAugmentSkillAdapterHeader(skillName) {
1351
+ return `<augment_skill_adapter>
1017
1352
  ## A. Skill Invocation
1018
- - This skill is invoked by mentioning \`${invocation}\`.
1019
- - Treat all user text after \`${invocation}\` as \`{{SDD_ARGS}}\`.
1353
+ - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
1354
+ - Treat all user text after the skill mention as \`{{SDD_ARGS}}\`.
1020
1355
  - If no arguments are present, treat \`{{SDD_ARGS}}\` as empty.
1021
1356
 
1022
- ## B. AskUserQuestion → request_user_input Mapping
1023
- SDD workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
1024
-
1025
- Parameter mapping:
1026
- - \`header\` \`header\`
1027
- - \`question\` → \`question\`
1028
- - Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
1029
- - Generate \`id\` from header: lowercase, replace spaces with underscores
1030
-
1031
- Batched calls:
1032
- - \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
1033
-
1034
- Multi-select workaround:
1035
- - Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
1036
-
1037
- Execute mode fallback:
1038
- - When \`request_user_input\` is rejected (Execute mode), present a plain-text numbered list and pick a reasonable default.
1039
-
1040
- ## C. Task() → spawn_agent Mapping
1041
- SDD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
1042
-
1043
- Direct mapping:
1044
- - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1045
- - \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
1046
- - \`fork_context: false\` by default — SDD agents load their own context via \`<files_to_read>\` blocks
1357
+ ## B. User Prompting
1358
+ When the workflow needs user input, prompt the user conversationally:
1359
+ - Present options as a numbered list in your response text
1360
+ - Ask the user to reply with their choice
1361
+ - For multi-select, ask for comma-separated numbers
1047
1362
 
1048
- Parallel fan-out:
1049
- - Spawn multiple agents collect agent IDs → \`wait(ids)\` for all to complete
1363
+ ## C. Tool Usage
1364
+ Use these Augment tools when executing SDD workflows:
1365
+ - \`launch-process\` for running commands (terminal operations)
1366
+ - \`str-replace-editor\` for editing existing files
1367
+ - \`view\` for reading files and listing directories
1368
+ - \`save-file\` for creating new files
1369
+ - \`grep\` for searching code (or use MCP servers for advanced search)
1370
+ - \`web-search\`, \`web-fetch\` for web queries
1371
+ - \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management
1050
1372
 
1051
- Result parsing:
1052
- - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
1053
- - \`close_agent(id)\` after collecting results from each agent
1054
- </codex_skill_adapter>`;
1373
+ ## D. Subagent Spawning
1374
+ When the workflow needs to spawn a subagent:
1375
+ - Use the built-in subagent spawning capability
1376
+ - Define agent prompts in \`.augment/agents/\` directory
1377
+ </augment_skill_adapter>`;
1055
1378
  }
1056
1379
 
1057
- function convertClaudeCommandToCodexSkill(content, skillName) {
1058
- const converted = convertClaudeToCodexMarkdown(content);
1380
+ function convertClaudeCommandToAugmentSkill(content, skillName) {
1381
+ const converted = convertClaudeToAugmentMarkdown(content);
1059
1382
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
1060
1383
  let description = `Run SDD workflow ${skillName}.`;
1061
1384
  if (frontmatter) {
@@ -1066,72 +1389,379 @@ function convertClaudeCommandToCodexSkill(content, skillName) {
1066
1389
  }
1067
1390
  description = toSingleLine(description);
1068
1391
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1069
- const adapter = getCodexSkillAdapterHeader(skillName);
1392
+ const adapter = getAugmentSkillAdapterHeader(skillName);
1070
1393
 
1071
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
1394
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
1072
1395
  }
1073
1396
 
1074
1397
  /**
1075
- * Convert Claude Code agent markdown to Codex agent format.
1076
- * Applies base markdown conversions, then adds a <codex_agent_role> header
1077
- * and cleans up frontmatter (removes tools/color fields).
1398
+ * Convert Claude Code agent markdown to Augment agent format.
1399
+ * Strips frontmatter fields Augment doesn't support (color, skills),
1400
+ * converts tool references, and cleans up for Augment agents.
1078
1401
  */
1079
- function convertClaudeAgentToCodexAgent(content) {
1080
- let converted = convertClaudeToCodexMarkdown(content);
1402
+ function convertClaudeAgentToAugmentAgent(content) {
1403
+ let converted = convertClaudeToAugmentMarkdown(content);
1081
1404
 
1082
1405
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
1083
1406
  if (!frontmatter) return converted;
1084
1407
 
1085
1408
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
1086
1409
  const description = extractFrontmatterField(frontmatter, 'description') || '';
1087
- const tools = extractFrontmatterField(frontmatter, 'tools') || '';
1088
-
1089
- const roleHeader = `<codex_agent_role>
1090
- role: ${name}
1091
- tools: ${tools}
1092
- purpose: ${toSingleLine(description)}
1093
- </codex_agent_role>`;
1094
1410
 
1095
- const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1411
+ const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1096
1412
 
1097
- return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
1413
+ return `${cleanFrontmatter}\n${body}`;
1098
1414
  }
1099
1415
 
1100
1416
  /**
1101
- * Generate a per-agent .toml config file for Codex.
1102
- * Sets required agent metadata, sandbox_mode, and developer_instructions
1103
- * from the agent markdown content.
1417
+ * Copy Claude commands as Augment skills one folder per skill with SKILL.md.
1418
+ * Mirrors copyCommandsAsCursorSkills but uses Augment converters.
1104
1419
  */
1105
- function generateCodexAgentToml(agentName, agentContent) {
1106
- const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
1107
- const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
1108
- const frontmatterText = frontmatter || '';
1109
- const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
1110
- const resolvedDescription = toSingleLine(
1111
- extractFrontmatterField(frontmatterText, 'description') || `SDD agent ${resolvedName}`
1112
- );
1113
- const instructions = body.trim();
1420
+ function copyCommandsAsAugmentSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
1421
+ if (!fs.existsSync(srcDir)) {
1422
+ return;
1423
+ }
1114
1424
 
1115
- const lines = [
1116
- `name = ${JSON.stringify(resolvedName)}`,
1117
- `description = ${JSON.stringify(resolvedDescription)}`,
1118
- `sandbox_mode = "${sandboxMode}"`,
1119
- // Agent prompts contain raw backslashes in regexes and shell snippets.
1120
- // TOML literal multiline strings preserve them without escape parsing.
1121
- `developer_instructions = '''`,
1122
- instructions,
1123
- `'''`,
1124
- ];
1125
- return lines.join('\n') + '\n';
1126
- }
1425
+ fs.mkdirSync(skillsDir, { recursive: true });
1127
1426
 
1128
- /**
1129
- * Generate the SDD config block for Codex config.toml.
1130
- * @param {Array<{name: string, description: string}>} agents
1131
- */
1132
- function generateCodexConfigBlock(agents, targetDir) {
1133
- // Use absolute paths when targetDir is provided — Codex ≥0.116 requires
1134
- // AbsolutePathBuf for config_file and cannot resolve relative paths.
1427
+ // Remove previous SDD Augment skills to avoid stale command skills
1428
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
1429
+ for (const entry of existing) {
1430
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
1431
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
1432
+ }
1433
+ }
1434
+
1435
+ function recurse(currentSrcDir, currentPrefix) {
1436
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
1437
+
1438
+ for (const entry of entries) {
1439
+ const srcPath = path.join(currentSrcDir, entry.name);
1440
+ if (entry.isDirectory()) {
1441
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
1442
+ continue;
1443
+ }
1444
+
1445
+ if (!entry.name.endsWith('.md')) {
1446
+ continue;
1447
+ }
1448
+
1449
+ const baseName = entry.name.replace('.md', '');
1450
+ const skillName = `${currentPrefix}-${baseName}`;
1451
+ const skillDir = path.join(skillsDir, skillName);
1452
+ fs.mkdirSync(skillDir, { recursive: true });
1453
+
1454
+ let content = fs.readFileSync(srcPath, 'utf8');
1455
+ const globalClaudeRegex = /~\/\.claude\//g;
1456
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
1457
+ const localClaudeRegex = /\.\/\.claude\//g;
1458
+ const augmentDirRegex = /~\/\.augment\//g;
1459
+ content = content.replace(globalClaudeRegex, pathPrefix);
1460
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
1461
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
1462
+ content = content.replace(augmentDirRegex, pathPrefix);
1463
+ content = processAttribution(content, getCommitAttribution(runtime));
1464
+ content = convertClaudeCommandToAugmentSkill(content, skillName);
1465
+
1466
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
1467
+ }
1468
+ }
1469
+
1470
+ recurse(srcDir, prefix);
1471
+ }
1472
+
1473
+ function convertSlashCommandsToTraeSkillMentions(content) {
1474
+ return content.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
1475
+ return `/sdd-${commandName}`;
1476
+ });
1477
+ }
1478
+
1479
+ function convertClaudeToTraeMarkdown(content) {
1480
+ let converted = convertSlashCommandsToTraeSkillMentions(content);
1481
+ converted = converted.replace(/\bBash\(/g, 'Shell(');
1482
+ converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
1483
+ // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
1484
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
1485
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
1486
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
1487
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
1488
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
1489
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
1490
+ converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
1491
+ converted = converted.replace(/\.\/\.claude\//g, './.trae/');
1492
+ converted = converted.replace(/\.claude\//g, '.trae/');
1493
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1494
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1495
+ converted = converted.replace(/\bClaude Code\b/g, 'Trae');
1496
+ return converted;
1497
+ }
1498
+
1499
+ function convertClaudeCommandToTraeSkill(content, skillName) {
1500
+ const converted = convertClaudeToTraeMarkdown(content);
1501
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1502
+ let description = `Run SDD workflow ${skillName}.`;
1503
+ if (frontmatter) {
1504
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1505
+ if (maybeDescription) {
1506
+ description = maybeDescription;
1507
+ }
1508
+ }
1509
+ description = toSingleLine(description);
1510
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1511
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${shortDescription}\n---\n${body}`;
1512
+ }
1513
+
1514
+ function convertClaudeAgentToTraeAgent(content) {
1515
+ let converted = convertClaudeToTraeMarkdown(content);
1516
+
1517
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1518
+ if (!frontmatter) return converted;
1519
+
1520
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
1521
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
1522
+
1523
+ const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1524
+
1525
+ return `${cleanFrontmatter}\n${body}`;
1526
+ }
1527
+
1528
+ function convertSlashCommandsToCodebuddySkillMentions(content) {
1529
+ return content.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
1530
+ return `/sdd-${commandName}`;
1531
+ });
1532
+ }
1533
+
1534
+ function convertClaudeToCodebuddyMarkdown(content) {
1535
+ let converted = convertSlashCommandsToCodebuddySkillMentions(content);
1536
+ // CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
1537
+ // No tool name conversion needed
1538
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
1539
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`CODEBUDDY.md`');
1540
+ converted = converted.replace(/\.\/CLAUDE\.md/g, 'CODEBUDDY.md');
1541
+ converted = converted.replace(/`CLAUDE\.md`/g, '`CODEBUDDY.md`');
1542
+ converted = converted.replace(/\bCLAUDE\.md\b/g, 'CODEBUDDY.md');
1543
+ converted = converted.replace(/\.claude\/skills\//g, '.codebuddy/skills/');
1544
+ converted = converted.replace(/\.\/\.claude\//g, './.codebuddy/');
1545
+ converted = converted.replace(/\.claude\//g, '.codebuddy/');
1546
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1547
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1548
+ converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
1549
+ return converted;
1550
+ }
1551
+
1552
+ function convertClaudeCommandToCodebuddySkill(content, skillName) {
1553
+ const converted = convertClaudeToCodebuddyMarkdown(content);
1554
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1555
+ let description = `Run SDD workflow ${skillName}.`;
1556
+ if (frontmatter) {
1557
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1558
+ if (maybeDescription) {
1559
+ description = maybeDescription;
1560
+ }
1561
+ }
1562
+ description = toSingleLine(description);
1563
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1564
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${shortDescription}\n---\n${body}`;
1565
+ }
1566
+
1567
+ function convertClaudeAgentToCodebuddyAgent(content) {
1568
+ let converted = convertClaudeToCodebuddyMarkdown(content);
1569
+
1570
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1571
+ if (!frontmatter) return converted;
1572
+
1573
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
1574
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
1575
+
1576
+ const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1577
+
1578
+ return `${cleanFrontmatter}\n${body}`;
1579
+ }
1580
+
1581
+ // ── Cline converters ────────────────────────────────────────────────────────
1582
+
1583
+ function convertClaudeToCliineMarkdown(content) {
1584
+ let converted = content;
1585
+ // Cline uses the same tool names as Claude Code — no tool name conversion needed
1586
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
1587
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
1588
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`');
1589
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules');
1590
+ converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/');
1591
+ converted = converted.replace(/\.\/\.claude\//g, './.cline/');
1592
+ converted = converted.replace(/\.claude\//g, '.cline/');
1593
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1594
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1595
+ converted = converted.replace(/\bClaude Code\b/g, 'Cline');
1596
+ return converted;
1597
+ }
1598
+
1599
+ function convertClaudeAgentToClineAgent(content) {
1600
+ let converted = convertClaudeToCliineMarkdown(content);
1601
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1602
+ if (!frontmatter) return converted;
1603
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
1604
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
1605
+ const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1606
+ return `${cleanFrontmatter}\n${body}`;
1607
+ }
1608
+
1609
+ // ── End Cline converters ─────────────────────────────────────────────────────
1610
+
1611
+ function convertSlashCommandsToCodexSkillMentions(content) {
1612
+ // Convert colon-style skill invocations to Codex $ prefix
1613
+ let converted = content.replace(/\/sdd:([a-z0-9-]+)/gi, (_, commandName) => {
1614
+ return `$sdd-${String(commandName).toLowerCase()}`;
1615
+ });
1616
+ // Convert hyphen-style command references (workflow output) to Codex $ prefix.
1617
+ // Negative lookbehind excludes file paths like bin/sdd-tools.cjs where
1618
+ // the slash is preceded by a word char, dot, or another slash.
1619
+ converted = converted.replace(/(?<![a-zA-Z0-9./])\/sdd-([a-z0-9-]+)/gi, (_, commandName) => {
1620
+ return `$sdd-${String(commandName).toLowerCase()}`;
1621
+ });
1622
+ return converted;
1623
+ }
1624
+
1625
+ function convertClaudeToCodexMarkdown(content) {
1626
+ let converted = convertSlashCommandsToCodexSkillMentions(content);
1627
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
1628
+ // Remove /clear references — Codex has no equivalent command
1629
+ // Handle backtick-wrapped: `\/clear` then: → (removed)
1630
+ converted = converted.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
1631
+ // Handle bare: /clear then: → (removed)
1632
+ converted = converted.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
1633
+ // Handle standalone /clear on its own line
1634
+ converted = converted.replace(/^\s*`?\/clear`?\s*$/gm, '');
1635
+ // Path replacement: .claude → .codex (#1430)
1636
+ converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
1637
+ converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
1638
+ converted = converted.replace(/\.\/\.claude\//g, './.codex/');
1639
+ // Runtime-neutral agent name replacement (#766)
1640
+ converted = neutralizeAgentReferences(converted, 'AGENTS.md');
1641
+ return converted;
1642
+ }
1643
+
1644
+ function getCodexSkillAdapterHeader(skillName) {
1645
+ const invocation = `$${skillName}`;
1646
+ return `<codex_skill_adapter>
1647
+ ## A. Skill Invocation
1648
+ - This skill is invoked by mentioning \`${invocation}\`.
1649
+ - Treat all user text after \`${invocation}\` as \`{{SDD_ARGS}}\`.
1650
+ - If no arguments are present, treat \`{{SDD_ARGS}}\` as empty.
1651
+
1652
+ ## B. AskUserQuestion → request_user_input Mapping
1653
+ SDD workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
1654
+
1655
+ Parameter mapping:
1656
+ - \`header\` → \`header\`
1657
+ - \`question\` → \`question\`
1658
+ - Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
1659
+ - Generate \`id\` from header: lowercase, replace spaces with underscores
1660
+
1661
+ Batched calls:
1662
+ - \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
1663
+
1664
+ Multi-select workaround:
1665
+ - Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
1666
+
1667
+ Execute mode fallback:
1668
+ - When \`request_user_input\` is rejected (Execute mode), present a plain-text numbered list and pick a reasonable default.
1669
+
1670
+ ## C. Task() → spawn_agent Mapping
1671
+ SDD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
1672
+
1673
+ Direct mapping:
1674
+ - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1675
+ - \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
1676
+ - \`fork_context: false\` by default — SDD agents load their own context via \`<files_to_read>\` blocks
1677
+
1678
+ Parallel fan-out:
1679
+ - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
1680
+
1681
+ Result parsing:
1682
+ - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
1683
+ - \`close_agent(id)\` after collecting results from each agent
1684
+ </codex_skill_adapter>`;
1685
+ }
1686
+
1687
+ function convertClaudeCommandToCodexSkill(content, skillName) {
1688
+ const converted = convertClaudeToCodexMarkdown(content);
1689
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1690
+ let description = `Run SDD workflow ${skillName}.`;
1691
+ if (frontmatter) {
1692
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1693
+ if (maybeDescription) {
1694
+ description = maybeDescription;
1695
+ }
1696
+ }
1697
+ description = toSingleLine(description);
1698
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1699
+ const adapter = getCodexSkillAdapterHeader(skillName);
1700
+
1701
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
1702
+ }
1703
+
1704
+ /**
1705
+ * Convert Claude Code agent markdown to Codex agent format.
1706
+ * Applies base markdown conversions, then adds a <codex_agent_role> header
1707
+ * and cleans up frontmatter (removes tools/color fields).
1708
+ */
1709
+ function convertClaudeAgentToCodexAgent(content) {
1710
+ let converted = convertClaudeToCodexMarkdown(content);
1711
+
1712
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1713
+ if (!frontmatter) return converted;
1714
+
1715
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
1716
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
1717
+ const tools = extractFrontmatterField(frontmatter, 'tools') || '';
1718
+
1719
+ const roleHeader = `<codex_agent_role>
1720
+ role: ${name}
1721
+ tools: ${tools}
1722
+ purpose: ${toSingleLine(description)}
1723
+ </codex_agent_role>`;
1724
+
1725
+ const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1726
+
1727
+ return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
1728
+ }
1729
+
1730
+ /**
1731
+ * Generate a per-agent .toml config file for Codex.
1732
+ * Sets required agent metadata, sandbox_mode, and developer_instructions
1733
+ * from the agent markdown content.
1734
+ */
1735
+ function generateCodexAgentToml(agentName, agentContent) {
1736
+ const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
1737
+ const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
1738
+ const frontmatterText = frontmatter || '';
1739
+ const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
1740
+ const resolvedDescription = toSingleLine(
1741
+ extractFrontmatterField(frontmatterText, 'description') || `SDD agent ${resolvedName}`
1742
+ );
1743
+ const instructions = body.trim();
1744
+
1745
+ const lines = [
1746
+ `name = ${JSON.stringify(resolvedName)}`,
1747
+ `description = ${JSON.stringify(resolvedDescription)}`,
1748
+ `sandbox_mode = "${sandboxMode}"`,
1749
+ // Agent prompts contain raw backslashes in regexes and shell snippets.
1750
+ // TOML literal multiline strings preserve them without escape parsing.
1751
+ `developer_instructions = '''`,
1752
+ instructions,
1753
+ `'''`,
1754
+ ];
1755
+ return lines.join('\n') + '\n';
1756
+ }
1757
+
1758
+ /**
1759
+ * Generate the SDD config block for Codex config.toml.
1760
+ * @param {Array<{name: string, description: string}>} agents
1761
+ */
1762
+ function generateCodexConfigBlock(agents, targetDir) {
1763
+ // Use absolute paths when targetDir is provided — Codex ≥0.116 requires
1764
+ // AbsolutePathBuf for config_file and cannot resolve relative paths.
1135
1765
  const agentsPrefix = targetDir
1136
1766
  ? path.join(targetDir, 'agents').replace(/\\/g, '/')
1137
1767
  : 'agents';
@@ -1150,7 +1780,7 @@ function generateCodexConfigBlock(agents, targetDir) {
1150
1780
  return lines.join('\n');
1151
1781
  }
1152
1782
 
1153
- function stripCodexGsdAgentSections(content) {
1783
+ function stripCodexSddAgentSections(content) {
1154
1784
  return content.replace(/^\[agents\.sdd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
1155
1785
  }
1156
1786
 
@@ -1158,7 +1788,7 @@ function stripCodexGsdAgentSections(content) {
1158
1788
  * Strip SDD sections from Codex config.toml content.
1159
1789
  * Returns cleaned content, or null if file would be empty.
1160
1790
  */
1161
- function stripGsdFromCodexConfig(content) {
1791
+ function stripSddFromCodexConfig(content) {
1162
1792
  const eol = detectLineEnding(content);
1163
1793
  const markerIndex = content.indexOf(SDD_CODEX_MARKER);
1164
1794
  const codexHooksOwnership = getManagedCodexHooksOwnership(content);
@@ -1184,7 +1814,7 @@ function stripGsdFromCodexConfig(content) {
1184
1814
  cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, '');
1185
1815
 
1186
1816
  // Remove [agents.sdd-*] sections (from header to next section or EOF)
1187
- cleaned = stripCodexGsdAgentSections(cleaned);
1817
+ cleaned = stripCodexSddAgentSections(cleaned);
1188
1818
 
1189
1819
  // Remove [features] section if now empty (only header, no keys before next section)
1190
1820
  cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
@@ -1818,7 +2448,7 @@ function setManagedCodexHooksOwnership(content, ownership) {
1818
2448
  remainder;
1819
2449
  }
1820
2450
 
1821
- function isLegacyGsdAgentsSection(body) {
2451
+ function isLegacySddAgentsSection(body) {
1822
2452
  const lineRecords = getTomlLineRecords(body);
1823
2453
  const legacyKeys = new Set(['max_threads', 'max_depth']);
1824
2454
  let sawLegacyKey = false;
@@ -1847,13 +2477,13 @@ function isLegacyGsdAgentsSection(body) {
1847
2477
  return sawLegacyKey;
1848
2478
  }
1849
2479
 
1850
- function stripLeakedGsdCodexSections(content) {
2480
+ function stripLeakedSddCodexSections(content) {
1851
2481
  const leakedSections = getTomlTableSections(content)
1852
2482
  .filter((section) =>
1853
2483
  section.path.startsWith('agents.sdd-') ||
1854
2484
  (
1855
2485
  section.path === 'agents' &&
1856
- isLegacyGsdAgentsSection(content.slice(section.headerEnd, section.end))
2486
+ isLegacySddAgentsSection(content.slice(section.headerEnd, section.end))
1857
2487
  )
1858
2488
  );
1859
2489
 
@@ -2023,7 +2653,7 @@ function mergeCodexConfig(configPath, sddBlock) {
2023
2653
 
2024
2654
  const existing = fs.readFileSync(configPath, 'utf8');
2025
2655
  const eol = detectLineEnding(existing);
2026
- const normalizedGsdBlock = sddBlock.replace(/\r?\n/g, eol);
2656
+ const normalizedSddBlock = sddBlock.replace(/\r?\n/g, eol);
2027
2657
  const markerIndex = existing.indexOf(SDD_CODEX_MARKER);
2028
2658
 
2029
2659
  // Case 2: Has SDD marker — truncate and re-append
@@ -2031,21 +2661,21 @@ function mergeCodexConfig(configPath, sddBlock) {
2031
2661
  let before = existing.substring(0, markerIndex).trimEnd();
2032
2662
  if (before) {
2033
2663
  // Strip any SDD-managed sections that leaked above the marker from previous installs
2034
- before = stripLeakedGsdCodexSections(before).trimEnd();
2664
+ before = stripLeakedSddCodexSections(before).trimEnd();
2035
2665
 
2036
- fs.writeFileSync(configPath, before + eol + eol + normalizedGsdBlock + eol);
2666
+ fs.writeFileSync(configPath, before + eol + eol + normalizedSddBlock + eol);
2037
2667
  } else {
2038
- fs.writeFileSync(configPath, normalizedGsdBlock + eol);
2668
+ fs.writeFileSync(configPath, normalizedSddBlock + eol);
2039
2669
  }
2040
2670
  return;
2041
2671
  }
2042
2672
 
2043
2673
  // Case 3: No marker — append SDD block
2044
- let content = stripLeakedGsdCodexSections(existing).trimEnd();
2674
+ let content = stripLeakedSddCodexSections(existing).trimEnd();
2045
2675
  if (content) {
2046
- content = content + eol + eol + normalizedGsdBlock + eol;
2676
+ content = content + eol + eol + normalizedSddBlock + eol;
2047
2677
  } else {
2048
- content = normalizedGsdBlock + eol;
2678
+ content = normalizedSddBlock + eol;
2049
2679
  }
2050
2680
 
2051
2681
  fs.writeFileSync(configPath, content);
@@ -2303,7 +2933,7 @@ function mergeCopilotInstructions(filePath, sddContent) {
2303
2933
  * @param {string} content - File content
2304
2934
  * @returns {string|null} - Cleaned content or null if empty
2305
2935
  */
2306
- function stripGsdFromCopilotInstructions(content) {
2936
+ function stripSddFromCopilotInstructions(content) {
2307
2937
  const openIndex = content.indexOf(SDD_COPILOT_INSTRUCTIONS_MARKER);
2308
2938
  const closeIndex = content.indexOf(SDD_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
2309
2939
 
@@ -2332,13 +2962,13 @@ function installCodexConfig(targetDir, agentsSrc) {
2332
2962
  const agents = [];
2333
2963
 
2334
2964
  // Compute the Codex SDD install path (absolute, so subagents with empty $HOME work — #820)
2335
- const codexGsdPath = `${path.resolve(targetDir, 'sdd').replace(/\\/g, '/')}/`;
2965
+ const codexSddPath = `${path.resolve(targetDir, 'sdd').replace(/\\/g, '/')}/`;
2336
2966
 
2337
2967
  for (const file of agentEntries) {
2338
2968
  let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
2339
2969
  // Replace full .claude/sdd prefix so path resolves to codex SDD install
2340
- content = content.replace(/~\/\.claude\/sdd\//g, codexGsdPath);
2341
- content = content.replace(/\$HOME\/\.claude\/sdd\//g, codexGsdPath);
2970
+ content = content.replace(/~\/\.claude\/sdd\//g, codexSddPath);
2971
+ content = content.replace(/\$HOME\/\.claude\/sdd\//g, codexSddPath);
2342
2972
  const { frontmatter } = extractFrontmatterAndBody(content);
2343
2973
  const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '');
2344
2974
  const description = extractFrontmatterField(frontmatter, 'description') || '';
@@ -2460,57 +3090,213 @@ function convertClaudeToGeminiAgent(content) {
2460
3090
  continue;
2461
3091
  }
2462
3092
 
2463
- // Collect allowed-tools/tools array items
3093
+ // Collect allowed-tools/tools array items
3094
+ if (inAllowedTools) {
3095
+ if (trimmed.startsWith('- ')) {
3096
+ const mapped = convertGeminiToolName(trimmed.substring(2).trim());
3097
+ if (mapped) tools.push(mapped);
3098
+ continue;
3099
+ } else if (trimmed && !trimmed.startsWith('-')) {
3100
+ inAllowedTools = false;
3101
+ }
3102
+ }
3103
+
3104
+ if (!inAllowedTools) {
3105
+ newLines.push(line);
3106
+ }
3107
+ }
3108
+
3109
+ // Add tools as YAML array (Gemini requires array format)
3110
+ if (tools.length > 0) {
3111
+ newLines.push('tools:');
3112
+ for (const tool of tools) {
3113
+ newLines.push(` - ${tool}`);
3114
+ }
3115
+ }
3116
+
3117
+ const newFrontmatter = newLines.join('\n').trim();
3118
+
3119
+ // Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
3120
+ // Gemini's templateString() treats all ${word} patterns as template variables
3121
+ // and throws "Template validation failed: Missing required input parameters"
3122
+ // when they can't be resolved. SDD agents use ${PHASE}, ${PLAN}, etc. as
3123
+ // shell variables in bash code blocks — convert to $VAR (no braces) which
3124
+ // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
3125
+ const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
3126
+
3127
+ // Runtime-neutral agent name replacement (#766)
3128
+ const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
3129
+ return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
3130
+ }
3131
+
3132
+ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
3133
+ // Replace tool name references in content (applies to all files)
3134
+ let convertedContent = content;
3135
+ convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
3136
+ convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
3137
+ convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
3138
+ // Replace /sdd-command colon variant with /sdd-command for opencode (flat command structure)
3139
+ convertedContent = convertedContent.replace(/\/sdd:/g, '/sdd-');
3140
+ // Replace ~/.claude and $HOME/.claude with OpenCode's config location
3141
+ convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
3142
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
3143
+ // Replace general-purpose subagent type with OpenCode's equivalent "general"
3144
+ convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
3145
+ // Runtime-neutral agent name replacement (#766)
3146
+ convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
3147
+
3148
+ // Check if content has frontmatter
3149
+ if (!convertedContent.startsWith('---')) {
3150
+ return convertedContent;
3151
+ }
3152
+
3153
+ // Find the end of frontmatter
3154
+ const endIndex = convertedContent.indexOf('---', 3);
3155
+ if (endIndex === -1) {
3156
+ return convertedContent;
3157
+ }
3158
+
3159
+ const frontmatter = convertedContent.substring(3, endIndex).trim();
3160
+ const body = convertedContent.substring(endIndex + 3);
3161
+
3162
+ // Parse frontmatter line by line (simple YAML parsing)
3163
+ const lines = frontmatter.split('\n');
3164
+ const newLines = [];
3165
+ let inAllowedTools = false;
3166
+ let inSkippedArray = false;
3167
+ const allowedTools = [];
3168
+
3169
+ for (const line of lines) {
3170
+ const trimmed = line.trim();
3171
+
3172
+ // For agents: skip commented-out lines (e.g. hooks blocks)
3173
+ if (isAgent && trimmed.startsWith('#')) {
3174
+ continue;
3175
+ }
3176
+
3177
+ // Detect start of allowed-tools array
3178
+ if (trimmed.startsWith('allowed-tools:')) {
3179
+ inAllowedTools = true;
3180
+ continue;
3181
+ }
3182
+
3183
+ // Detect inline tools: field (comma-separated string)
3184
+ if (trimmed.startsWith('tools:')) {
3185
+ if (isAgent) {
3186
+ // Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
3187
+ inSkippedArray = true;
3188
+ continue;
3189
+ }
3190
+ const toolsValue = trimmed.substring(6).trim();
3191
+ if (toolsValue) {
3192
+ // Parse comma-separated tools
3193
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
3194
+ allowedTools.push(...tools);
3195
+ }
3196
+ continue;
3197
+ }
3198
+
3199
+ // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
3200
+ if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
3201
+ inSkippedArray = true;
3202
+ continue;
3203
+ }
3204
+
3205
+ // Skip continuation lines of a stripped array/object field
3206
+ if (inSkippedArray) {
3207
+ if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
3208
+ continue;
3209
+ }
3210
+ inSkippedArray = false;
3211
+ }
3212
+
3213
+ // For commands: remove name: field (opencode uses filename for command name)
3214
+ // For agents: keep name: (required by OpenCode agents)
3215
+ if (!isAgent && trimmed.startsWith('name:')) {
3216
+ continue;
3217
+ }
3218
+
3219
+ // Strip model: field — OpenCode doesn't support Claude Code model aliases
3220
+ // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
3221
+ // its configured default model. See #1156.
3222
+ if (trimmed.startsWith('model:')) {
3223
+ continue;
3224
+ }
3225
+
3226
+ // Convert color names to hex for opencode (commands only; agents strip color above)
3227
+ if (trimmed.startsWith('color:')) {
3228
+ const colorValue = trimmed.substring(6).trim().toLowerCase();
3229
+ const hexColor = colorNameToHex[colorValue];
3230
+ if (hexColor) {
3231
+ newLines.push(`color: "${hexColor}"`);
3232
+ } else if (colorValue.startsWith('#')) {
3233
+ // Validate hex color format (#RGB or #RRGGBB)
3234
+ if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
3235
+ // Already hex and valid, keep as is
3236
+ newLines.push(line);
3237
+ }
3238
+ // Skip invalid hex colors
3239
+ }
3240
+ // Skip unknown color names
3241
+ continue;
3242
+ }
3243
+
3244
+ // Collect allowed-tools items
2464
3245
  if (inAllowedTools) {
2465
3246
  if (trimmed.startsWith('- ')) {
2466
- const mapped = convertGeminiToolName(trimmed.substring(2).trim());
2467
- if (mapped) tools.push(mapped);
3247
+ allowedTools.push(trimmed.substring(2).trim());
2468
3248
  continue;
2469
3249
  } else if (trimmed && !trimmed.startsWith('-')) {
3250
+ // End of array, new field started
2470
3251
  inAllowedTools = false;
2471
3252
  }
2472
3253
  }
2473
3254
 
3255
+ // Keep other fields
2474
3256
  if (!inAllowedTools) {
2475
3257
  newLines.push(line);
2476
3258
  }
2477
3259
  }
2478
3260
 
2479
- // Add tools as YAML array (Gemini requires array format)
2480
- if (tools.length > 0) {
3261
+ // For agents: add required OpenCode agent fields
3262
+ // Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
3263
+ // keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
3264
+ // use its default model for subagents. See #1156.
3265
+ if (isAgent) {
3266
+ newLines.push('mode: subagent');
3267
+ }
3268
+
3269
+ // For commands: add tools object if we had allowed-tools or tools
3270
+ if (!isAgent && allowedTools.length > 0) {
2481
3271
  newLines.push('tools:');
2482
- for (const tool of tools) {
2483
- newLines.push(` - ${tool}`);
3272
+ for (const tool of allowedTools) {
3273
+ newLines.push(` ${convertToolName(tool)}: true`);
2484
3274
  }
2485
3275
  }
2486
3276
 
3277
+ // Rebuild frontmatter (body already has tool names converted)
2487
3278
  const newFrontmatter = newLines.join('\n').trim();
2488
-
2489
- // Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
2490
- // Gemini's templateString() treats all ${word} patterns as template variables
2491
- // and throws "Template validation failed: Missing required input parameters"
2492
- // when they can't be resolved. SDD agents use ${PHASE}, ${PLAN}, etc. as
2493
- // shell variables in bash code blocks — convert to $VAR (no braces) which
2494
- // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
2495
- const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
2496
-
2497
- // Runtime-neutral agent name replacement (#766)
2498
- const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
2499
- return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
3279
+ return `---\n${newFrontmatter}\n---${body}`;
2500
3280
  }
2501
3281
 
2502
- function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
3282
+ // Kilo CLI same conversion logic as OpenCode, different config paths.
3283
+ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
2503
3284
  // Replace tool name references in content (applies to all files)
2504
3285
  let convertedContent = content;
2505
3286
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
2506
3287
  convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
2507
3288
  convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
2508
- // Replace /sdd:command with /sdd-command for opencode (flat command structure)
3289
+ // Replace /sdd-command colon variant with /sdd-command for Kilo (flat command structure)
2509
3290
  convertedContent = convertedContent.replace(/\/sdd:/g, '/sdd-');
2510
- // Replace ~/.claude and $HOME/.claude with OpenCode's config location
2511
- convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
2512
- convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
2513
- // Replace general-purpose subagent type with OpenCode's equivalent "general"
3291
+ // Replace ~/.claude and $HOME/.claude with Kilo's config location
3292
+ convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo');
3293
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo');
3294
+ convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/');
3295
+ // Normalize both Claude skill directory variants to Kilo's canonical skills dir.
3296
+ convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/');
3297
+ convertedContent = replaceRelativePathReference(convertedContent, '.agents/skills/', '.kilo/skills/');
3298
+ convertedContent = replaceRelativePathReference(convertedContent, '.claude/agents/', '.kilo/agents/');
3299
+ // Replace general-purpose subagent type with Kilo's equivalent "general"
2514
3300
  convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
2515
3301
  // Runtime-neutral agent name replacement (#766)
2516
3302
  convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
@@ -2533,8 +3319,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
2533
3319
  const lines = frontmatter.split('\n');
2534
3320
  const newLines = [];
2535
3321
  let inAllowedTools = false;
3322
+ let inAgentTools = false;
2536
3323
  let inSkippedArray = false;
2537
3324
  const allowedTools = [];
3325
+ const agentTools = [];
2538
3326
 
2539
3327
  for (const line of lines) {
2540
3328
  const trimmed = line.trim();
@@ -2550,11 +3338,26 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
2550
3338
  continue;
2551
3339
  }
2552
3340
 
3341
+ if (isAgent && inAgentTools) {
3342
+ if (trimmed.startsWith('- ')) {
3343
+ agentTools.push(trimmed.substring(2).trim());
3344
+ continue;
3345
+ }
3346
+ if (trimmed && !trimmed.startsWith('-')) {
3347
+ inAgentTools = false;
3348
+ }
3349
+ }
3350
+
2553
3351
  // Detect inline tools: field (comma-separated string)
2554
3352
  if (trimmed.startsWith('tools:')) {
2555
3353
  if (isAgent) {
2556
- // Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
2557
- inSkippedArray = true;
3354
+ const toolsValue = trimmed.substring(6).trim();
3355
+ if (toolsValue) {
3356
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
3357
+ agentTools.push(...tools);
3358
+ } else {
3359
+ inAgentTools = true;
3360
+ }
2558
3361
  continue;
2559
3362
  }
2560
3363
  const toolsValue = trimmed.substring(6).trim();
@@ -2580,20 +3383,20 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
2580
3383
  inSkippedArray = false;
2581
3384
  }
2582
3385
 
2583
- // For commands: remove name: field (opencode uses filename for command name)
2584
- // For agents: keep name: (required by OpenCode agents)
3386
+ // For commands: remove name: field (Kilo uses filename for command name)
3387
+ // For agents: keep name: (required by Kilo agents)
2585
3388
  if (!isAgent && trimmed.startsWith('name:')) {
2586
3389
  continue;
2587
3390
  }
2588
3391
 
2589
- // Strip model: field — OpenCode doesn't support Claude Code model aliases
2590
- // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
2591
- // its configured default model. See #1156.
3392
+ // Strip model: field — Kilo doesn't support Claude Code model aliases
3393
+ // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use
3394
+ // its configured default model.
2592
3395
  if (trimmed.startsWith('model:')) {
2593
3396
  continue;
2594
3397
  }
2595
3398
 
2596
- // Convert color names to hex for opencode (commands only; agents strip color above)
3399
+ // Convert color names to hex for Kilo (commands only; agents strip color above)
2597
3400
  if (trimmed.startsWith('color:')) {
2598
3401
  const colorValue = trimmed.substring(6).trim().toLowerCase();
2599
3402
  const hexColor = colorNameToHex[colorValue];
@@ -2614,7 +3417,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
2614
3417
  // Collect allowed-tools items
2615
3418
  if (inAllowedTools) {
2616
3419
  if (trimmed.startsWith('- ')) {
2617
- allowedTools.push(trimmed.substring(2).trim());
3420
+ const tool = trimmed.substring(2).trim();
3421
+ if (isAgent) {
3422
+ agentTools.push(tool);
3423
+ } else {
3424
+ allowedTools.push(tool);
3425
+ }
2618
3426
  continue;
2619
3427
  } else if (trimmed && !trimmed.startsWith('-')) {
2620
3428
  // End of array, new field started
@@ -2628,12 +3436,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
2628
3436
  }
2629
3437
  }
2630
3438
 
2631
- // For agents: add required OpenCode agent fields
2632
- // Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
2633
- // keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
2634
- // use its default model for subagents. See #1156.
3439
+ // For agents: add required Kilo agent fields
2635
3440
  if (isAgent) {
2636
3441
  newLines.push('mode: subagent');
3442
+ newLines.push(...buildKiloAgentPermissionBlock(agentTools));
2637
3443
  }
2638
3444
 
2639
3445
  // For commands: add tools object if we had allowed-tools or tools
@@ -2667,7 +3473,7 @@ function convertClaudeToGeminiToml(content) {
2667
3473
 
2668
3474
  const frontmatter = content.substring(3, endIndex).trim();
2669
3475
  const body = content.substring(endIndex + 3).trim();
2670
-
3476
+
2671
3477
  // Extract description from frontmatter
2672
3478
  let description = '';
2673
3479
  const lines = frontmatter.split('\n');
@@ -2684,9 +3490,9 @@ function convertClaudeToGeminiToml(content) {
2684
3490
  if (description) {
2685
3491
  toml += `description = ${JSON.stringify(description)}\n`;
2686
3492
  }
2687
-
3493
+
2688
3494
  toml += `prompt = ${JSON.stringify(body)}\n`;
2689
-
3495
+
2690
3496
  return toml;
2691
3497
  }
2692
3498
 
@@ -2699,74 +3505,240 @@ function convertClaudeToGeminiToml(content) {
2699
3505
  * @param {string} destDir - Destination directory (e.g., command/)
2700
3506
  * @param {string} prefix - Prefix for filenames (e.g., 'sdd')
2701
3507
  * @param {string} pathPrefix - Path prefix for file references
2702
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
3508
+ * @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
2703
3509
  */
2704
3510
  function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
2705
3511
  if (!fs.existsSync(srcDir)) {
2706
3512
  return;
2707
3513
  }
2708
-
3514
+
2709
3515
  // Remove old sdd-*.md files before copying new ones
2710
3516
  if (fs.existsSync(destDir)) {
2711
3517
  for (const file of fs.readdirSync(destDir)) {
2712
3518
  if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
2713
3519
  fs.unlinkSync(path.join(destDir, file));
2714
3520
  }
2715
- }
2716
- } else {
2717
- fs.mkdirSync(destDir, { recursive: true });
2718
- }
2719
-
2720
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
2721
-
2722
- for (const entry of entries) {
2723
- const srcPath = path.join(srcDir, entry.name);
2724
-
2725
- if (entry.isDirectory()) {
2726
- // Recurse into subdirectories, adding to prefix
2727
- // e.g., commands/sdd/debug/start.md -> command/sdd-debug-start.md
2728
- copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
2729
- } else if (entry.name.endsWith('.md')) {
2730
- // Flatten: help.md -> sdd-help.md
3521
+ }
3522
+ } else {
3523
+ fs.mkdirSync(destDir, { recursive: true });
3524
+ }
3525
+
3526
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
3527
+
3528
+ for (const entry of entries) {
3529
+ const srcPath = path.join(srcDir, entry.name);
3530
+
3531
+ if (entry.isDirectory()) {
3532
+ // Recurse into subdirectories, adding to prefix
3533
+ // e.g., commands/sdd/debug/start.md -> command/sdd-debug-start.md
3534
+ copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
3535
+ } else if (entry.name.endsWith('.md')) {
3536
+ // Flatten: help.md -> sdd-help.md
3537
+ const baseName = entry.name.replace('.md', '');
3538
+ const destName = `${prefix}-${baseName}.md`;
3539
+ const destPath = path.join(destDir, destName);
3540
+
3541
+ let content = fs.readFileSync(srcPath, 'utf8');
3542
+ const globalClaudeRegex = /~\/\.claude\//g;
3543
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
3544
+ const localClaudeRegex = /\.\/\.claude\//g;
3545
+ const opencodeDirRegex = /~\/\.opencode\//g;
3546
+ const kiloDirRegex = /~\/\.kilo\//g;
3547
+ content = content.replace(globalClaudeRegex, pathPrefix);
3548
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
3549
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
3550
+ content = content.replace(opencodeDirRegex, pathPrefix);
3551
+ content = content.replace(kiloDirRegex, pathPrefix);
3552
+ content = processAttribution(content, getCommitAttribution(runtime));
3553
+ content = runtime === 'kilo'
3554
+ ? convertClaudeToKiloFrontmatter(content)
3555
+ : convertClaudeToOpencodeFrontmatter(content);
3556
+
3557
+ fs.writeFileSync(destPath, content);
3558
+ }
3559
+ }
3560
+ }
3561
+
3562
+ function listCodexSkillNames(skillsDir, prefix = 'sdd-') {
3563
+ if (!fs.existsSync(skillsDir)) return [];
3564
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
3565
+ return entries
3566
+ .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
3567
+ .filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
3568
+ .map(entry => entry.name)
3569
+ .sort();
3570
+ }
3571
+
3572
+ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3573
+ if (!fs.existsSync(srcDir)) {
3574
+ return;
3575
+ }
3576
+
3577
+ fs.mkdirSync(skillsDir, { recursive: true });
3578
+
3579
+ // Remove previous SDD Codex skills to avoid stale command skills.
3580
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
3581
+ for (const entry of existing) {
3582
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
3583
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
3584
+ }
3585
+ }
3586
+
3587
+ function recurse(currentSrcDir, currentPrefix) {
3588
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
3589
+
3590
+ for (const entry of entries) {
3591
+ const srcPath = path.join(currentSrcDir, entry.name);
3592
+ if (entry.isDirectory()) {
3593
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
3594
+ continue;
3595
+ }
3596
+
3597
+ if (!entry.name.endsWith('.md')) {
3598
+ continue;
3599
+ }
3600
+
3601
+ const baseName = entry.name.replace('.md', '');
3602
+ const skillName = `${currentPrefix}-${baseName}`;
3603
+ const skillDir = path.join(skillsDir, skillName);
3604
+ fs.mkdirSync(skillDir, { recursive: true });
3605
+
3606
+ let content = fs.readFileSync(srcPath, 'utf8');
3607
+ const globalClaudeRegex = /~\/\.claude\//g;
3608
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
3609
+ const localClaudeRegex = /\.\/\.claude\//g;
3610
+ const codexDirRegex = /~\/\.codex\//g;
3611
+ content = content.replace(globalClaudeRegex, pathPrefix);
3612
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
3613
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
3614
+ content = content.replace(codexDirRegex, pathPrefix);
3615
+ content = processAttribution(content, getCommitAttribution(runtime));
3616
+ content = convertClaudeCommandToCodexSkill(content, skillName);
3617
+
3618
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
3619
+ }
3620
+ }
3621
+
3622
+ recurse(srcDir, prefix);
3623
+ }
3624
+
3625
+ function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3626
+ if (!fs.existsSync(srcDir)) {
3627
+ return;
3628
+ }
3629
+
3630
+ fs.mkdirSync(skillsDir, { recursive: true });
3631
+
3632
+ // Remove previous SDD Cursor skills to avoid stale command skills
3633
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
3634
+ for (const entry of existing) {
3635
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
3636
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
3637
+ }
3638
+ }
3639
+
3640
+ function recurse(currentSrcDir, currentPrefix) {
3641
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
3642
+
3643
+ for (const entry of entries) {
3644
+ const srcPath = path.join(currentSrcDir, entry.name);
3645
+ if (entry.isDirectory()) {
3646
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
3647
+ continue;
3648
+ }
3649
+
3650
+ if (!entry.name.endsWith('.md')) {
3651
+ continue;
3652
+ }
3653
+
3654
+ const baseName = entry.name.replace('.md', '');
3655
+ const skillName = `${currentPrefix}-${baseName}`;
3656
+ const skillDir = path.join(skillsDir, skillName);
3657
+ fs.mkdirSync(skillDir, { recursive: true });
3658
+
3659
+ let content = fs.readFileSync(srcPath, 'utf8');
3660
+ const globalClaudeRegex = /~\/\.claude\//g;
3661
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
3662
+ const localClaudeRegex = /\.\/\.claude\//g;
3663
+ const cursorDirRegex = /~\/\.cursor\//g;
3664
+ content = content.replace(globalClaudeRegex, pathPrefix);
3665
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
3666
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
3667
+ content = content.replace(cursorDirRegex, pathPrefix);
3668
+ content = processAttribution(content, getCommitAttribution(runtime));
3669
+ content = convertClaudeCommandToCursorSkill(content, skillName);
3670
+
3671
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
3672
+ }
3673
+ }
3674
+
3675
+ recurse(srcDir, prefix);
3676
+ }
3677
+
3678
+ /**
3679
+ * Copy Claude commands as Windsurf skills — one folder per skill with SKILL.md.
3680
+ * Mirrors copyCommandsAsCursorSkills but uses Windsurf converters.
3681
+ */
3682
+ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3683
+ if (!fs.existsSync(srcDir)) {
3684
+ return;
3685
+ }
3686
+
3687
+ fs.mkdirSync(skillsDir, { recursive: true });
3688
+
3689
+ // Remove previous SDD Windsurf skills to avoid stale command skills
3690
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
3691
+ for (const entry of existing) {
3692
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
3693
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
3694
+ }
3695
+ }
3696
+
3697
+ function recurse(currentSrcDir, currentPrefix) {
3698
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
3699
+
3700
+ for (const entry of entries) {
3701
+ const srcPath = path.join(currentSrcDir, entry.name);
3702
+ if (entry.isDirectory()) {
3703
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
3704
+ continue;
3705
+ }
3706
+
3707
+ if (!entry.name.endsWith('.md')) {
3708
+ continue;
3709
+ }
3710
+
2731
3711
  const baseName = entry.name.replace('.md', '');
2732
- const destName = `${prefix}-${baseName}.md`;
2733
- const destPath = path.join(destDir, destName);
3712
+ const skillName = `${currentPrefix}-${baseName}`;
3713
+ const skillDir = path.join(skillsDir, skillName);
3714
+ fs.mkdirSync(skillDir, { recursive: true });
2734
3715
 
2735
3716
  let content = fs.readFileSync(srcPath, 'utf8');
2736
3717
  const globalClaudeRegex = /~\/\.claude\//g;
2737
3718
  const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
2738
3719
  const localClaudeRegex = /\.\/\.claude\//g;
2739
- const opencodeDirRegex = /~\/\.opencode\//g;
3720
+ const windsurfDirRegex = /~\/\.codeium\/windsurf\//g;
2740
3721
  content = content.replace(globalClaudeRegex, pathPrefix);
2741
3722
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
2742
3723
  content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
2743
- content = content.replace(opencodeDirRegex, pathPrefix);
3724
+ content = content.replace(windsurfDirRegex, pathPrefix);
2744
3725
  content = processAttribution(content, getCommitAttribution(runtime));
2745
- content = convertClaudeToOpencodeFrontmatter(content);
3726
+ content = convertClaudeCommandToWindsurfSkill(content, skillName);
2746
3727
 
2747
- fs.writeFileSync(destPath, content);
3728
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
2748
3729
  }
2749
3730
  }
2750
- }
2751
3731
 
2752
- function listCodexSkillNames(skillsDir, prefix = 'sdd-') {
2753
- if (!fs.existsSync(skillsDir)) return [];
2754
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
2755
- return entries
2756
- .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
2757
- .filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
2758
- .map(entry => entry.name)
2759
- .sort();
3732
+ recurse(srcDir, prefix);
2760
3733
  }
2761
3734
 
2762
- function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3735
+ function copyCommandsAsTraeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
2763
3736
  if (!fs.existsSync(srcDir)) {
2764
3737
  return;
2765
3738
  }
2766
3739
 
2767
3740
  fs.mkdirSync(skillsDir, { recursive: true });
2768
3741
 
2769
- // Remove previous SDD Codex skills to avoid stale command skills.
2770
3742
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
2771
3743
  for (const entry of existing) {
2772
3744
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
@@ -2797,13 +3769,20 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
2797
3769
  const globalClaudeRegex = /~\/\.claude\//g;
2798
3770
  const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
2799
3771
  const localClaudeRegex = /\.\/\.claude\//g;
2800
- const codexDirRegex = /~\/\.codex\//g;
3772
+ const bareGlobalClaudeRegex = /~\/\.claude\b/g;
3773
+ const bareGlobalClaudeHomeRegex = /\$HOME\/\.claude\b/g;
3774
+ const bareLocalClaudeRegex = /\.\/\.claude\b/g;
3775
+ const traeDirRegex = /~\/\.trae\//g;
3776
+ const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
2801
3777
  content = content.replace(globalClaudeRegex, pathPrefix);
2802
3778
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
2803
3779
  content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
2804
- content = content.replace(codexDirRegex, pathPrefix);
3780
+ content = content.replace(bareGlobalClaudeRegex, normalizedPathPrefix);
3781
+ content = content.replace(bareGlobalClaudeHomeRegex, normalizedPathPrefix);
3782
+ content = content.replace(bareLocalClaudeRegex, `./${getDirName(runtime)}`);
3783
+ content = content.replace(traeDirRegex, pathPrefix);
2805
3784
  content = processAttribution(content, getCommitAttribution(runtime));
2806
- content = convertClaudeCommandToCodexSkill(content, skillName);
3785
+ content = convertClaudeCommandToTraeSkill(content, skillName);
2807
3786
 
2808
3787
  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
2809
3788
  }
@@ -2812,14 +3791,17 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
2812
3791
  recurse(srcDir, prefix);
2813
3792
  }
2814
3793
 
2815
- function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3794
+ /**
3795
+ * Copy Claude commands as CodeBuddy skills — one folder per skill with SKILL.md.
3796
+ * CodeBuddy uses the same tool names as Claude Code, but has its own config directory structure.
3797
+ */
3798
+ function copyCommandsAsCodebuddySkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
2816
3799
  if (!fs.existsSync(srcDir)) {
2817
3800
  return;
2818
3801
  }
2819
3802
 
2820
3803
  fs.mkdirSync(skillsDir, { recursive: true });
2821
3804
 
2822
- // Remove previous SDD Cursor skills to avoid stale command skills
2823
3805
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
2824
3806
  for (const entry of existing) {
2825
3807
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
@@ -2850,13 +3832,20 @@ function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runti
2850
3832
  const globalClaudeRegex = /~\/\.claude\//g;
2851
3833
  const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
2852
3834
  const localClaudeRegex = /\.\/\.claude\//g;
2853
- const cursorDirRegex = /~\/\.cursor\//g;
3835
+ const bareGlobalClaudeRegex = /~\/\.claude\b/g;
3836
+ const bareGlobalClaudeHomeRegex = /\$HOME\/\.claude\b/g;
3837
+ const bareLocalClaudeRegex = /\.\/\.claude\b/g;
3838
+ const codebuddyDirRegex = /~\/\.codebuddy\//g;
3839
+ const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
2854
3840
  content = content.replace(globalClaudeRegex, pathPrefix);
2855
3841
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
2856
3842
  content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
2857
- content = content.replace(cursorDirRegex, pathPrefix);
3843
+ content = content.replace(bareGlobalClaudeRegex, normalizedPathPrefix);
3844
+ content = content.replace(bareGlobalClaudeHomeRegex, normalizedPathPrefix);
3845
+ content = content.replace(bareLocalClaudeRegex, `./${getDirName(runtime)}`);
3846
+ content = content.replace(codebuddyDirRegex, pathPrefix);
2858
3847
  content = processAttribution(content, getCommitAttribution(runtime));
2859
- content = convertClaudeCommandToCursorSkill(content, skillName);
3848
+ content = convertClaudeCommandToCodebuddySkill(content, skillName);
2860
3849
 
2861
3850
  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
2862
3851
  }
@@ -2866,17 +3855,17 @@ function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runti
2866
3855
  }
2867
3856
 
2868
3857
  /**
2869
- * Copy Claude commands as Windsurf skills — one folder per skill with SKILL.md.
2870
- * Mirrors copyCommandsAsCursorSkills but uses Windsurf converters.
3858
+ * Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
3859
+ * Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
2871
3860
  */
2872
- function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
3861
+ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false) {
2873
3862
  if (!fs.existsSync(srcDir)) {
2874
3863
  return;
2875
3864
  }
2876
3865
 
2877
3866
  fs.mkdirSync(skillsDir, { recursive: true });
2878
3867
 
2879
- // Remove previous SDD Windsurf skills to avoid stale command skills
3868
+ // Remove previous SDD Copilot skills
2880
3869
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
2881
3870
  for (const entry of existing) {
2882
3871
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
@@ -2904,16 +3893,8 @@ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, run
2904
3893
  fs.mkdirSync(skillDir, { recursive: true });
2905
3894
 
2906
3895
  let content = fs.readFileSync(srcPath, 'utf8');
2907
- const globalClaudeRegex = /~\/\.claude\//g;
2908
- const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
2909
- const localClaudeRegex = /\.\/\.claude\//g;
2910
- const windsurfDirRegex = /~\/\.windsurf\//g;
2911
- content = content.replace(globalClaudeRegex, pathPrefix);
2912
- content = content.replace(globalClaudeHomeRegex, pathPrefix);
2913
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
2914
- content = content.replace(windsurfDirRegex, pathPrefix);
2915
- content = processAttribution(content, getCommitAttribution(runtime));
2916
- content = convertClaudeCommandToWindsurfSkill(content, skillName);
3896
+ content = convertClaudeCommandToCopilotSkill(content, skillName, isGlobal);
3897
+ content = processAttribution(content, getCommitAttribution('copilot'));
2917
3898
 
2918
3899
  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
2919
3900
  }
@@ -2923,17 +3904,25 @@ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, run
2923
3904
  }
2924
3905
 
2925
3906
  /**
2926
- * Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
2927
- * Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
3907
+ * Copy Claude commands as Claude skills — one folder per skill with SKILL.md.
3908
+ * Claude Code 2.1.88+ uses skills/xxx/SKILL.md instead of commands/sdd/xxx.md.
3909
+ * Claude is the native format so no path replacement is needed — only
3910
+ * frontmatter restructuring via convertClaudeCommandToClaudeSkill.
3911
+ * @param {string} srcDir - Source commands directory
3912
+ * @param {string} skillsDir - Target skills directory
3913
+ * @param {string} prefix - Skill name prefix (e.g. 'sdd')
3914
+ * @param {string} pathPrefix - Path prefix for file references
3915
+ * @param {string} runtime - Target runtime
3916
+ * @param {boolean} isGlobal - Whether this is a global install
2928
3917
  */
2929
- function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false) {
3918
+ function copyCommandsAsClaudeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime, isGlobal = false) {
2930
3919
  if (!fs.existsSync(srcDir)) {
2931
3920
  return;
2932
3921
  }
2933
3922
 
2934
3923
  fs.mkdirSync(skillsDir, { recursive: true });
2935
3924
 
2936
- // Remove previous SDD Copilot skills
3925
+ // Remove previous SDD Claude skills to avoid stale command skills
2937
3926
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
2938
3927
  for (const entry of existing) {
2939
3928
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
@@ -2961,8 +3950,14 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false
2961
3950
  fs.mkdirSync(skillDir, { recursive: true });
2962
3951
 
2963
3952
  let content = fs.readFileSync(srcPath, 'utf8');
2964
- content = convertClaudeCommandToCopilotSkill(content, skillName, isGlobal);
2965
- content = processAttribution(content, getCommitAttribution('copilot'));
3953
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
3954
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3955
+ content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
3956
+ content = content.replace(/~\/\.qwen\//g, pathPrefix);
3957
+ content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
3958
+ content = content.replace(/\.\/\.qwen\//g, `./${getDirName(runtime)}/`);
3959
+ content = processAttribution(content, getCommitAttribution(runtime));
3960
+ content = convertClaudeCommandToClaudeSkill(content, skillName);
2966
3961
 
2967
3962
  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
2968
3963
  }
@@ -3025,6 +4020,42 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
3025
4020
  recurse(srcDir, prefix);
3026
4021
  }
3027
4022
 
4023
+ /**
4024
+ * Save user-generated files from destDir to an in-memory map before a wipe.
4025
+ *
4026
+ * @param {string} destDir - Directory that is about to be wiped
4027
+ * @param {string[]} fileNames - Relative file names (e.g. ['USER-PROFILE.md']) to preserve
4028
+ * @returns {Map<string, string>} Map of fileName → file content (only entries that existed)
4029
+ */
4030
+ function preserveUserArtifacts(destDir, fileNames) {
4031
+ const saved = new Map();
4032
+ for (const name of fileNames) {
4033
+ const fullPath = path.join(destDir, name);
4034
+ if (fs.existsSync(fullPath)) {
4035
+ try {
4036
+ saved.set(name, fs.readFileSync(fullPath, 'utf8'));
4037
+ } catch { /* skip unreadable files */ }
4038
+ }
4039
+ }
4040
+ return saved;
4041
+ }
4042
+
4043
+ /**
4044
+ * Restore user-generated files saved by preserveUserArtifacts after a wipe.
4045
+ *
4046
+ * @param {string} destDir - Directory that was wiped and recreated
4047
+ * @param {Map<string, string>} saved - Map returned by preserveUserArtifacts
4048
+ */
4049
+ function restoreUserArtifacts(destDir, saved) {
4050
+ for (const [name, content] of saved) {
4051
+ const fullPath = path.join(destDir, name);
4052
+ try {
4053
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
4054
+ fs.writeFileSync(fullPath, content, 'utf8');
4055
+ } catch { /* skip unwritable paths */ }
4056
+ }
4057
+ }
4058
+
3028
4059
  /**
3029
4060
  * Recursively copy directory, replacing paths in .md files
3030
4061
  * Deletes existing destDir first to remove orphaned files from previous versions
@@ -3035,11 +4066,16 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
3035
4066
  */
3036
4067
  function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false) {
3037
4068
  const isOpencode = runtime === 'opencode';
4069
+ const isKilo = runtime === 'kilo';
3038
4070
  const isCodex = runtime === 'codex';
3039
4071
  const isCopilot = runtime === 'copilot';
3040
4072
  const isAntigravity = runtime === 'antigravity';
3041
4073
  const isCursor = runtime === 'cursor';
3042
4074
  const isWindsurf = runtime === 'windsurf';
4075
+ const isAugment = runtime === 'augment';
4076
+ const isTrae = runtime === 'trae';
4077
+ const isQwen = runtime === 'qwen';
4078
+ const isCline = runtime === 'cline';
3043
4079
  const dirName = getDirName(runtime);
3044
4080
 
3045
4081
  // Clean install: remove existing destination to prevent orphaned files
@@ -3067,12 +4103,17 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
3067
4103
  content = content.replace(globalClaudeRegex, pathPrefix);
3068
4104
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
3069
4105
  content = content.replace(localClaudeRegex, `./${dirName}/`);
4106
+ content = content.replace(/~\/\.qwen\//g, pathPrefix);
4107
+ content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
4108
+ content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
3070
4109
  }
3071
4110
  content = processAttribution(content, getCommitAttribution(runtime));
3072
4111
 
3073
4112
  // Convert frontmatter for opencode compatibility
3074
- if (isOpencode) {
3075
- content = convertClaudeToOpencodeFrontmatter(content);
4113
+ if (isOpencode || isKilo) {
4114
+ content = isKilo
4115
+ ? convertClaudeToKiloFrontmatter(content)
4116
+ : convertClaudeToOpencodeFrontmatter(content);
3076
4117
  fs.writeFileSync(destPath, content);
3077
4118
  } else if (runtime === 'gemini') {
3078
4119
  if (isCommand) {
@@ -3102,6 +4143,12 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
3102
4143
  } else if (isWindsurf) {
3103
4144
  content = convertClaudeToWindsurfMarkdown(content);
3104
4145
  fs.writeFileSync(destPath, content);
4146
+ } else if (isTrae) {
4147
+ content = convertClaudeToTraeMarkdown(content);
4148
+ fs.writeFileSync(destPath, content);
4149
+ } else if (isCline) {
4150
+ content = convertClaudeToCliineMarkdown(content);
4151
+ fs.writeFileSync(destPath, content);
3105
4152
  } else {
3106
4153
  fs.writeFileSync(destPath, content);
3107
4154
  }
@@ -3128,9 +4175,24 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
3128
4175
  let jsContent = fs.readFileSync(srcPath, 'utf8');
3129
4176
  jsContent = jsContent.replace(/sdd:/gi, 'sdd-');
3130
4177
  jsContent = jsContent.replace(/\.claude\/skills\//g, '.windsurf/skills/');
3131
- jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules/');
4178
+ jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules');
3132
4179
  jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
3133
4180
  fs.writeFileSync(destPath, jsContent);
4181
+ } else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
4182
+ let jsContent = fs.readFileSync(srcPath, 'utf8');
4183
+ jsContent = jsContent.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
4184
+ return `/sdd-${commandName}`;
4185
+ });
4186
+ jsContent = jsContent.replace(/\.claude\/skills\//g, '.trae/skills/');
4187
+ jsContent = jsContent.replace(/CLAUDE\.md/g, '.trae/rules/');
4188
+ jsContent = jsContent.replace(/\bClaude Code\b/g, 'Trae');
4189
+ fs.writeFileSync(destPath, jsContent);
4190
+ } else if (isCline && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
4191
+ let jsContent = fs.readFileSync(srcPath, 'utf8');
4192
+ jsContent = jsContent.replace(/\.claude\/skills\//g, '.cline/skills/');
4193
+ jsContent = jsContent.replace(/CLAUDE\.md/g, '.clinerules');
4194
+ jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cline');
4195
+ fs.writeFileSync(destPath, jsContent);
3134
4196
  } else {
3135
4197
  fs.copyFileSync(srcPath, destPath);
3136
4198
  }
@@ -3201,7 +4263,7 @@ function cleanupOrphanedHooks(settings) {
3201
4263
  // Only match the specific old SDD path pattern (hooks/statusline.js),
3202
4264
  // not third-party statusline scripts that happen to contain 'statusline.js'
3203
4265
  if (settings.statusLine && settings.statusLine.command &&
3204
- /hooks[\/\\]statusline\.js/.test(settings.statusLine.command)) {
4266
+ /hooks[\/\\]statusline\.js/.test(settings.statusLine.command)) {
3205
4267
  settings.statusLine.command = settings.statusLine.command.replace(
3206
4268
  /hooks([\/\\])statusline\.js/,
3207
4269
  'hooks$1sdd-statusline.js'
@@ -3299,11 +4361,17 @@ function validateHookFields(settings) {
3299
4361
  */
3300
4362
  function uninstall(isGlobal, runtime = 'claude') {
3301
4363
  const isOpencode = runtime === 'opencode';
4364
+ const isKilo = runtime === 'kilo';
4365
+ const isGemini = runtime === 'gemini';
3302
4366
  const isCodex = runtime === 'codex';
3303
4367
  const isCopilot = runtime === 'copilot';
3304
4368
  const isAntigravity = runtime === 'antigravity';
3305
4369
  const isCursor = runtime === 'cursor';
3306
4370
  const isWindsurf = runtime === 'windsurf';
4371
+ const isAugment = runtime === 'augment';
4372
+ const isTrae = runtime === 'trae';
4373
+ const isQwen = runtime === 'qwen';
4374
+ const isCodebuddy = runtime === 'codebuddy';
3307
4375
  const dirName = getDirName(runtime);
3308
4376
 
3309
4377
  // Get the target directory based on runtime and install type
@@ -3318,11 +4386,16 @@ function uninstall(isGlobal, runtime = 'claude') {
3318
4386
  let runtimeLabel = 'Claude Code';
3319
4387
  if (runtime === 'opencode') runtimeLabel = 'OpenCode';
3320
4388
  if (runtime === 'gemini') runtimeLabel = 'Gemini';
4389
+ if (runtime === 'kilo') runtimeLabel = 'Kilo';
3321
4390
  if (runtime === 'codex') runtimeLabel = 'Codex';
3322
4391
  if (runtime === 'copilot') runtimeLabel = 'Copilot';
3323
4392
  if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
3324
4393
  if (runtime === 'cursor') runtimeLabel = 'Cursor';
3325
4394
  if (runtime === 'windsurf') runtimeLabel = 'Windsurf';
4395
+ if (runtime === 'augment') runtimeLabel = 'Augment';
4396
+ if (runtime === 'trae') runtimeLabel = 'Trae';
4397
+ if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
4398
+ if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy';
3326
4399
 
3327
4400
  console.log(` Uninstalling SDD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
3328
4401
 
@@ -3336,8 +4409,8 @@ function uninstall(isGlobal, runtime = 'claude') {
3336
4409
  let removedCount = 0;
3337
4410
 
3338
4411
  // 1. Remove SDD commands/skills
3339
- if (isOpencode) {
3340
- // OpenCode: remove command/sdd-*.md files
4412
+ if (isOpencode || isKilo) {
4413
+ // OpenCode/Kilo: remove command/sdd-*.md files
3341
4414
  const commandDir = path.join(targetDir, 'command');
3342
4415
  if (fs.existsSync(commandDir)) {
3343
4416
  const files = fs.readdirSync(commandDir);
@@ -3349,8 +4422,8 @@ function uninstall(isGlobal, runtime = 'claude') {
3349
4422
  }
3350
4423
  console.log(` ${green}✓${reset} Removed SDD commands from command/`);
3351
4424
  }
3352
- } else if (isCodex || isCursor || isWindsurf) {
3353
- // Codex/Cursor/Windsurf: remove skills/sdd-*/SKILL.md skill directories
4425
+ } else if (isCodex || isCursor || isWindsurf || isTrae || isCodebuddy) {
4426
+ // Codex/Cursor/Windsurf/Trae/CodeBuddy: remove skills/sdd-*/SKILL.md skill directories
3354
4427
  const skillsDir = path.join(targetDir, 'skills');
3355
4428
  if (fs.existsSync(skillsDir)) {
3356
4429
  let skillCount = 0;
@@ -3369,39 +4442,39 @@ function uninstall(isGlobal, runtime = 'claude') {
3369
4442
 
3370
4443
  // Codex-only: remove SDD agent .toml config files and config.toml sections
3371
4444
  if (isCodex) {
3372
- const codexAgentsDir = path.join(targetDir, 'agents');
3373
- if (fs.existsSync(codexAgentsDir)) {
3374
- const tomlFiles = fs.readdirSync(codexAgentsDir);
3375
- let tomlCount = 0;
3376
- for (const file of tomlFiles) {
3377
- if (file.startsWith('sdd-') && file.endsWith('.toml')) {
3378
- fs.unlinkSync(path.join(codexAgentsDir, file));
3379
- tomlCount++;
4445
+ const codexAgentsDir = path.join(targetDir, 'agents');
4446
+ if (fs.existsSync(codexAgentsDir)) {
4447
+ const tomlFiles = fs.readdirSync(codexAgentsDir);
4448
+ let tomlCount = 0;
4449
+ for (const file of tomlFiles) {
4450
+ if (file.startsWith('sdd-') && file.endsWith('.toml')) {
4451
+ fs.unlinkSync(path.join(codexAgentsDir, file));
4452
+ tomlCount++;
4453
+ }
4454
+ }
4455
+ if (tomlCount > 0) {
4456
+ removedCount++;
4457
+ console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
3380
4458
  }
3381
4459
  }
3382
- if (tomlCount > 0) {
3383
- removedCount++;
3384
- console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
3385
- }
3386
- }
3387
4460
 
3388
- // Codex: clean SDD sections from config.toml
3389
- const configPath = path.join(targetDir, 'config.toml');
3390
- if (fs.existsSync(configPath)) {
3391
- const content = fs.readFileSync(configPath, 'utf8');
3392
- const cleaned = stripGsdFromCodexConfig(content);
3393
- if (cleaned === null) {
3394
- // File is empty after stripping — delete it
3395
- fs.unlinkSync(configPath);
3396
- removedCount++;
3397
- console.log(` ${green}✓${reset} Removed config.toml (was SDD-only)`);
3398
- } else if (cleaned !== content) {
3399
- fs.writeFileSync(configPath, cleaned);
3400
- removedCount++;
3401
- console.log(` ${green}✓${reset} Cleaned SDD sections from config.toml`);
4461
+ // Codex: clean SDD sections from config.toml
4462
+ const configPath = path.join(targetDir, 'config.toml');
4463
+ if (fs.existsSync(configPath)) {
4464
+ const content = fs.readFileSync(configPath, 'utf8');
4465
+ const cleaned = stripSddFromCodexConfig(content);
4466
+ if (cleaned === null) {
4467
+ // File is empty after stripping — delete it
4468
+ fs.unlinkSync(configPath);
4469
+ removedCount++;
4470
+ console.log(` ${green}✓${reset} Removed config.toml (was SDD-only)`);
4471
+ } else if (cleaned !== content) {
4472
+ fs.writeFileSync(configPath, cleaned);
4473
+ removedCount++;
4474
+ console.log(` ${green}✓${reset} Cleaned SDD sections from config.toml`);
4475
+ }
3402
4476
  }
3403
4477
  }
3404
- }
3405
4478
  } else if (isCopilot) {
3406
4479
  // Copilot: remove skills/sdd-*/ directories (same layout as Codex skills)
3407
4480
  const skillsDir = path.join(targetDir, 'skills');
@@ -3424,7 +4497,7 @@ function uninstall(isGlobal, runtime = 'claude') {
3424
4497
  const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
3425
4498
  if (fs.existsSync(instructionsPath)) {
3426
4499
  const content = fs.readFileSync(instructionsPath, 'utf8');
3427
- const cleaned = stripGsdFromCopilotInstructions(content);
4500
+ const cleaned = stripSddFromCopilotInstructions(content);
3428
4501
  if (cleaned === null) {
3429
4502
  fs.unlinkSync(instructionsPath);
3430
4503
  removedCount++;
@@ -3452,8 +4525,7 @@ function uninstall(isGlobal, runtime = 'claude') {
3452
4525
  console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
3453
4526
  }
3454
4527
  }
3455
- } else if (isCursor) {
3456
- // Cursor: remove skills/sdd-*/ directories (same layout as Codex skills)
4528
+ } else if (isQwen) {
3457
4529
  const skillsDir = path.join(targetDir, 'skills');
3458
4530
  if (fs.existsSync(skillsDir)) {
3459
4531
  let skillCount = 0;
@@ -3466,11 +4538,45 @@ function uninstall(isGlobal, runtime = 'claude') {
3466
4538
  }
3467
4539
  if (skillCount > 0) {
3468
4540
  removedCount++;
3469
- console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`);
4541
+ console.log(` ${green}✓${reset} Removed ${skillCount} Qwen Code skills`);
3470
4542
  }
3471
4543
  }
3472
- } else if (isWindsurf) {
3473
- // Windsurf: remove skills/sdd-*/ directories (same layout as Cursor skills)
4544
+
4545
+ const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
4546
+ if (fs.existsSync(legacyCommandsDir)) {
4547
+ const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
4548
+ fs.rmSync(legacyCommandsDir, { recursive: true });
4549
+ removedCount++;
4550
+ console.log(` ${green}✓${reset} Removed legacy commands/sdd/`);
4551
+ restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
4552
+ }
4553
+ } else if (isGemini) {
4554
+ // Gemini: still uses commands/sdd/
4555
+ const sddCommandsDir = path.join(targetDir, 'commands', 'sdd');
4556
+ if (fs.existsSync(sddCommandsDir)) {
4557
+ // Preserve user-generated files before wipe (#1423)
4558
+ // Note: if more user files are added, consider a naming convention (e.g., USER-*.md)
4559
+ // and preserve all matching files instead of listing each one individually.
4560
+ const devPrefsPath = path.join(sddCommandsDir, 'dev-preferences.md');
4561
+ const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
4562
+
4563
+ fs.rmSync(sddCommandsDir, { recursive: true });
4564
+ removedCount++;
4565
+ console.log(` ${green}✓${reset} Removed commands/sdd/`);
4566
+
4567
+ // Restore user-generated files
4568
+ if (preservedDevPrefs) {
4569
+ try {
4570
+ fs.mkdirSync(sddCommandsDir, { recursive: true });
4571
+ fs.writeFileSync(devPrefsPath, preservedDevPrefs);
4572
+ console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
4573
+ } catch (err) {
4574
+ console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
4575
+ }
4576
+ }
4577
+ }
4578
+ } else if (isGlobal) {
4579
+ // Claude Code global: remove skills/sdd-*/ directories (primary global install location)
3474
4580
  const skillsDir = path.join(targetDir, 'skills');
3475
4581
  if (fs.existsSync(skillsDir)) {
3476
4582
  let skillCount = 0;
@@ -3483,24 +4589,76 @@ function uninstall(isGlobal, runtime = 'claude') {
3483
4589
  }
3484
4590
  if (skillCount > 0) {
3485
4591
  removedCount++;
3486
- console.log(` ${green}✓${reset} Removed ${skillCount} Windsurf skills`);
4592
+ console.log(` ${green}✓${reset} Removed ${skillCount} Claude Code skills`);
4593
+ }
4594
+ }
4595
+
4596
+ // Also clean up legacy commands/sdd/ from older global installs
4597
+ const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
4598
+ if (fs.existsSync(legacyCommandsDir)) {
4599
+ // Preserve user-generated files before legacy wipe (#1423)
4600
+ const devPrefsPath = path.join(legacyCommandsDir, 'dev-preferences.md');
4601
+ const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
4602
+
4603
+ fs.rmSync(legacyCommandsDir, { recursive: true });
4604
+ removedCount++;
4605
+ console.log(` ${green}✓${reset} Removed legacy commands/sdd/`);
4606
+
4607
+ if (preservedDevPrefs) {
4608
+ try {
4609
+ fs.mkdirSync(legacyCommandsDir, { recursive: true });
4610
+ fs.writeFileSync(devPrefsPath, preservedDevPrefs);
4611
+ console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
4612
+ } catch (err) {
4613
+ console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
4614
+ }
3487
4615
  }
3488
4616
  }
3489
4617
  } else {
4618
+ // Claude Code local: remove commands/sdd/ (primary local install location since #1736)
3490
4619
  const sddCommandsDir = path.join(targetDir, 'commands', 'sdd');
3491
4620
  if (fs.existsSync(sddCommandsDir)) {
4621
+ // Preserve user-generated files before wipe (#1423)
4622
+ const devPrefsPath = path.join(sddCommandsDir, 'dev-preferences.md');
4623
+ const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
4624
+
3492
4625
  fs.rmSync(sddCommandsDir, { recursive: true });
3493
4626
  removedCount++;
3494
4627
  console.log(` ${green}✓${reset} Removed commands/sdd/`);
4628
+
4629
+ if (preservedDevPrefs) {
4630
+ try {
4631
+ fs.mkdirSync(sddCommandsDir, { recursive: true });
4632
+ fs.writeFileSync(devPrefsPath, preservedDevPrefs);
4633
+ console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
4634
+ } catch (err) {
4635
+ console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
4636
+ }
4637
+ }
3495
4638
  }
3496
4639
  }
3497
4640
 
3498
4641
  // 2. Remove sdd directory
3499
4642
  const sddDir = path.join(targetDir, 'sdd');
3500
4643
  if (fs.existsSync(sddDir)) {
4644
+ // Preserve user-generated files before wipe (#1423)
4645
+ const userProfilePath = path.join(sddDir, 'USER-PROFILE.md');
4646
+ const preservedProfile = fs.existsSync(userProfilePath) ? fs.readFileSync(userProfilePath, 'utf-8') : null;
4647
+
3501
4648
  fs.rmSync(sddDir, { recursive: true });
3502
4649
  removedCount++;
3503
4650
  console.log(` ${green}✓${reset} Removed sdd/`);
4651
+
4652
+ // Restore user-generated files
4653
+ if (preservedProfile) {
4654
+ try {
4655
+ fs.mkdirSync(sddDir, { recursive: true });
4656
+ fs.writeFileSync(userProfilePath, preservedProfile);
4657
+ console.log(` ${green}✓${reset} Preserved sdd/USER-PROFILE.md`);
4658
+ } catch (err) {
4659
+ console.error(` ${red}✗${reset} Failed to restore USER-PROFILE.md: ${err.message}`);
4660
+ }
4661
+ }
3504
4662
  }
3505
4663
 
3506
4664
  // 3. Remove SDD agents (sdd-*.md files only)
@@ -3523,7 +4681,7 @@ function uninstall(isGlobal, runtime = 'claude') {
3523
4681
  // 4. Remove SDD hooks
3524
4682
  const hooksDir = path.join(targetDir, 'hooks');
3525
4683
  if (fs.existsSync(hooksDir)) {
3526
- const sddHooks = ['sdd-statusline.js', 'sdd-check-update.js', 'sdd-check-update.sh', 'sdd-context-monitor.js', 'sdd-prompt-guard.js'];
4684
+ const sddHooks = ['sdd-statusline.js', 'sdd-check-update.js', 'sdd-context-monitor.js', 'sdd-prompt-guard.js', 'sdd-read-guard.js', 'sdd-workflow-guard.js', 'sdd-session-state.sh', 'sdd-validate-commit.sh', 'sdd-phase-boundary.sh'];
3527
4685
  let hookCount = 0;
3528
4686
  for (const hook of sddHooks) {
3529
4687
  const hookPath = path.join(hooksDir, hook);
@@ -3558,83 +4716,50 @@ function uninstall(isGlobal, runtime = 'claude') {
3558
4716
  const settingsPath = path.join(targetDir, 'settings.json');
3559
4717
  if (fs.existsSync(settingsPath)) {
3560
4718
  let settings = readSettings(settingsPath);
4719
+ if (settings === null) {
4720
+ console.log(` ${yellow}i${reset} Skipping settings.json cleanup — file could not be parsed`);
4721
+ settings = {}; // prevent downstream crashes, but don't write back
4722
+ }
3561
4723
  let settingsModified = false;
3562
4724
 
3563
4725
  // Remove SDD statusline if it references our hook
3564
4726
  if (settings.statusLine && settings.statusLine.command &&
3565
- settings.statusLine.command.includes('sdd-statusline')) {
4727
+ settings.statusLine.command.includes('sdd-statusline')) {
3566
4728
  delete settings.statusLine;
3567
4729
  settingsModified = true;
3568
4730
  console.log(` ${green}✓${reset} Removed SDD statusline from settings`);
3569
4731
  }
3570
4732
 
3571
- // Remove SDD hooks from SessionStart
3572
- if (settings.hooks && settings.hooks.SessionStart) {
3573
- const before = settings.hooks.SessionStart.length;
3574
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
3575
- if (entry.hooks && Array.isArray(entry.hooks)) {
3576
- // Filter out SDD hooks
3577
- const hasGsdHook = entry.hooks.some(h =>
3578
- h.command && (h.command.includes('sdd-check-update') || h.command.includes('sdd-statusline'))
3579
- );
3580
- return !hasGsdHook;
3581
- }
3582
- return true;
3583
- });
3584
- if (settings.hooks.SessionStart.length < before) {
3585
- settingsModified = true;
3586
- console.log(` ${green}✓${reset} Removed SDD hooks from settings`);
3587
- }
3588
- // Clean up empty array
3589
- if (settings.hooks.SessionStart.length === 0) {
3590
- delete settings.hooks.SessionStart;
3591
- }
3592
- }
4733
+ // Remove SDD hooks from settings — per-hook granularity to preserve
4734
+ // user hooks that share an entry with a SDD hook (#1755 followup)
4735
+ const isSddHookCommand = (cmd) =>
4736
+ cmd && (cmd.includes('sdd-check-update') || cmd.includes('sdd-statusline') ||
4737
+ cmd.includes('sdd-session-state') || cmd.includes('sdd-context-monitor') ||
4738
+ cmd.includes('sdd-phase-boundary') || cmd.includes('sdd-prompt-guard') ||
4739
+ cmd.includes('sdd-read-guard') || cmd.includes('sdd-validate-commit') ||
4740
+ cmd.includes('sdd-workflow-guard'));
3593
4741
 
3594
- // Remove SDD hooks from PostToolUse and AfterTool (Gemini uses AfterTool)
3595
- for (const eventName of ['PostToolUse', 'AfterTool']) {
4742
+ for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool']) {
3596
4743
  if (settings.hooks && settings.hooks[eventName]) {
3597
- const before = settings.hooks[eventName].length;
3598
- settings.hooks[eventName] = settings.hooks[eventName].filter(entry => {
3599
- if (entry.hooks && Array.isArray(entry.hooks)) {
3600
- const hasGsdHook = entry.hooks.some(h =>
3601
- h.command && h.command.includes('sdd-context-monitor')
3602
- );
3603
- return !hasGsdHook;
3604
- }
3605
- return true;
3606
- });
3607
- if (settings.hooks[eventName].length < before) {
4744
+ const before = JSON.stringify(settings.hooks[eventName]);
4745
+ settings.hooks[eventName] = settings.hooks[eventName]
4746
+ .map(entry => {
4747
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return entry;
4748
+ // Filter out individual SDD hooks, keep user hooks
4749
+ entry.hooks = entry.hooks.filter(h => !isSddHookCommand(h.command));
4750
+ return entry.hooks.length > 0 ? entry : null;
4751
+ })
4752
+ .filter(Boolean);
4753
+ if (JSON.stringify(settings.hooks[eventName]) !== before) {
3608
4754
  settingsModified = true;
3609
- console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
3610
4755
  }
3611
4756
  if (settings.hooks[eventName].length === 0) {
3612
4757
  delete settings.hooks[eventName];
3613
4758
  }
3614
4759
  }
3615
4760
  }
3616
-
3617
- // Remove SDD hooks from PreToolUse and BeforeTool (Gemini uses BeforeTool)
3618
- for (const eventName of ['PreToolUse', 'BeforeTool']) {
3619
- if (settings.hooks && settings.hooks[eventName]) {
3620
- const before = settings.hooks[eventName].length;
3621
- settings.hooks[eventName] = settings.hooks[eventName].filter(entry => {
3622
- if (entry.hooks && Array.isArray(entry.hooks)) {
3623
- const hasGsdHook = entry.hooks.some(h =>
3624
- h.command && h.command.includes('sdd-prompt-guard')
3625
- );
3626
- return !hasGsdHook;
3627
- }
3628
- return true;
3629
- });
3630
- if (settings.hooks[eventName].length < before) {
3631
- settingsModified = true;
3632
- console.log(` ${green}✓${reset} Removed prompt injection guard hook from settings`);
3633
- }
3634
- if (settings.hooks[eventName].length === 0) {
3635
- delete settings.hooks[eventName];
3636
- }
3637
- }
4761
+ if (settingsModified) {
4762
+ console.log(` ${green}✓${reset} Removed SDD hooks from settings`);
3638
4763
  }
3639
4764
 
3640
4765
  // Clean up empty hooks object
@@ -3650,10 +4775,48 @@ function uninstall(isGlobal, runtime = 'claude') {
3650
4775
 
3651
4776
  // 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
3652
4777
  if (isOpencode) {
3653
- const opencodeConfigDir = isGlobal
3654
- ? getOpencodeGlobalDir()
3655
- : path.join(process.cwd(), '.opencode');
3656
- const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
4778
+ const configPath = resolveOpencodeConfigPath(targetDir);
4779
+ if (fs.existsSync(configPath)) {
4780
+ try {
4781
+ const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
4782
+ let modified = false;
4783
+
4784
+ // Remove SDD permission entries
4785
+ if (config.permission) {
4786
+ for (const permType of ['read', 'external_directory']) {
4787
+ if (config.permission[permType]) {
4788
+ const keys = Object.keys(config.permission[permType]);
4789
+ for (const key of keys) {
4790
+ if (key.includes('sdd')) {
4791
+ delete config.permission[permType][key];
4792
+ modified = true;
4793
+ }
4794
+ }
4795
+ // Clean up empty objects
4796
+ if (Object.keys(config.permission[permType]).length === 0) {
4797
+ delete config.permission[permType];
4798
+ }
4799
+ }
4800
+ }
4801
+ if (Object.keys(config.permission).length === 0) {
4802
+ delete config.permission;
4803
+ }
4804
+ }
4805
+
4806
+ if (modified) {
4807
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
4808
+ removedCount++;
4809
+ console.log(` ${green}✓${reset} Removed SDD permissions from ${path.basename(configPath)}`);
4810
+ }
4811
+ } catch (e) {
4812
+ // Ignore JSON parse errors
4813
+ }
4814
+ }
4815
+ }
4816
+
4817
+ // 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc
4818
+ if (isKilo) {
4819
+ const configPath = resolveKiloConfigPath(targetDir);
3657
4820
  if (fs.existsSync(configPath)) {
3658
4821
  try {
3659
4822
  const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
@@ -3692,6 +4855,15 @@ function uninstall(isGlobal, runtime = 'claude') {
3692
4855
  }
3693
4856
  }
3694
4857
 
4858
+ // Remove the file manifest that the installer wrote at install time.
4859
+ // Without this step the metadata file persists after uninstall (#1908).
4860
+ const manifestPath = path.join(targetDir, MANIFEST_NAME);
4861
+ if (fs.existsSync(manifestPath)) {
4862
+ fs.rmSync(manifestPath, { force: true });
4863
+ removedCount++;
4864
+ console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
4865
+ }
4866
+
3695
4867
  if (removedCount === 0) {
3696
4868
  console.log(` ${yellow}⚠${reset} No SDD files found to remove.`);
3697
4869
  }
@@ -3757,27 +4929,108 @@ function parseJsonc(content) {
3757
4929
  }
3758
4930
  }
3759
4931
 
3760
- // Remove trailing commas before } or ]
3761
- result = result.replace(/,(\s*[}\]])/g, '$1');
4932
+ // Remove trailing commas before } or ]
4933
+ result = result.replace(/,(\s*[}\]])/g, '$1');
4934
+
4935
+ return JSON.parse(result);
4936
+ }
4937
+
4938
+ /**
4939
+ * Configure OpenCode permissions to allow reading SDD reference docs
4940
+ * This prevents permission prompts when SDD accesses the sdd directory
4941
+ * @param {boolean} isGlobal - Whether this is a global or local install
4942
+ * @param {string|null} configDir - Resolved config directory when already known
4943
+ */
4944
+ function configureOpencodePermissions(isGlobal = true, configDir = null) {
4945
+ // For local installs, use ./.opencode/
4946
+ // For global installs, use ~/.config/opencode/
4947
+ const opencodeConfigDir = configDir || (isGlobal
4948
+ ? getGlobalDir('opencode', explicitConfigDir)
4949
+ : path.join(process.cwd(), '.opencode'));
4950
+ // Ensure config directory exists
4951
+ fs.mkdirSync(opencodeConfigDir, { recursive: true });
4952
+
4953
+ const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
4954
+
4955
+ // Read existing config or create empty object
4956
+ let config = {};
4957
+ if (fs.existsSync(configPath)) {
4958
+ try {
4959
+ const content = fs.readFileSync(configPath, 'utf8');
4960
+ config = parseJsonc(content);
4961
+ } catch (e) {
4962
+ // Cannot parse - DO NOT overwrite user's config
4963
+ const configFile = path.basename(configPath);
4964
+ console.log(` ${yellow}⚠${reset} Could not parse ${configFile} - skipping permission config`);
4965
+ console.log(` ${dim}Reason: ${e.message}${reset}`);
4966
+ console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
4967
+ return;
4968
+ }
4969
+ }
4970
+
4971
+ // OpenCode also allows a top-level string permission like "allow".
4972
+ // In that case, path-specific permission entries are unnecessary.
4973
+ if (typeof config.permission === 'string') {
4974
+ return;
4975
+ }
4976
+
4977
+ // Ensure permission structure exists
4978
+ if (!config.permission || typeof config.permission !== 'object') {
4979
+ config.permission = {};
4980
+ }
4981
+
4982
+ // Build the SDD path using the actual config directory
4983
+ // Use ~ shorthand if it's in the default location, otherwise use full path
4984
+ const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
4985
+ const sddPath = opencodeConfigDir === defaultConfigDir
4986
+ ? '~/.config/opencode/sdd/*'
4987
+ : `${opencodeConfigDir.replace(/\\/g, '/')}/sdd/*`;
3762
4988
 
3763
- return JSON.parse(result);
4989
+ let modified = false;
4990
+
4991
+ // Configure read permission
4992
+ if (!config.permission.read || typeof config.permission.read !== 'object') {
4993
+ config.permission.read = {};
4994
+ }
4995
+ if (config.permission.read[sddPath] !== 'allow') {
4996
+ config.permission.read[sddPath] = 'allow';
4997
+ modified = true;
4998
+ }
4999
+
5000
+ // Configure external_directory permission (the safety guard for paths outside project)
5001
+ if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
5002
+ config.permission.external_directory = {};
5003
+ }
5004
+ if (config.permission.external_directory[sddPath] !== 'allow') {
5005
+ config.permission.external_directory[sddPath] = 'allow';
5006
+ modified = true;
5007
+ }
5008
+
5009
+ if (!modified) {
5010
+ return; // Already configured
5011
+ }
5012
+
5013
+ // Write config back
5014
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
5015
+ console.log(` ${green}✓${reset} Configured read permission for SDD docs`);
3764
5016
  }
3765
5017
 
3766
5018
  /**
3767
- * Configure OpenCode permissions to allow reading SDD reference docs
5019
+ * Configure Kilo permissions to allow reading SDD reference docs
3768
5020
  * This prevents permission prompts when SDD accesses the sdd directory
3769
5021
  * @param {boolean} isGlobal - Whether this is a global or local install
5022
+ * @param {string|null} configDir - Resolved config directory when already known
3770
5023
  */
3771
- function configureOpencodePermissions(isGlobal = true) {
3772
- // For local installs, use ./.opencode/
3773
- // For global installs, use ~/.config/opencode/
3774
- const opencodeConfigDir = isGlobal
3775
- ? getOpencodeGlobalDir()
3776
- : path.join(process.cwd(), '.opencode');
5024
+ function configureKiloPermissions(isGlobal = true, configDir = null) {
5025
+ // For local installs, use ./.kilo/
5026
+ // For global installs, use ~/.config/kilo/
5027
+ const kiloConfigDir = configDir || (isGlobal
5028
+ ? getGlobalDir('kilo', explicitConfigDir)
5029
+ : path.join(process.cwd(), '.kilo'));
3777
5030
  // Ensure config directory exists
3778
- fs.mkdirSync(opencodeConfigDir, { recursive: true });
5031
+ fs.mkdirSync(kiloConfigDir, { recursive: true });
3779
5032
 
3780
- const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
5033
+ const configPath = resolveKiloConfigPath(kiloConfigDir);
3781
5034
 
3782
5035
  // Read existing config or create empty object
3783
5036
  let config = {};
@@ -3796,17 +5049,17 @@ function configureOpencodePermissions(isGlobal = true) {
3796
5049
  }
3797
5050
 
3798
5051
  // Ensure permission structure exists
3799
- if (!config.permission) {
5052
+ if (!config.permission || typeof config.permission !== 'object') {
3800
5053
  config.permission = {};
3801
5054
  }
3802
5055
 
3803
5056
  // Build the SDD path using the actual config directory
3804
5057
  // Use ~ shorthand if it's in the default location, otherwise use full path
3805
- const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
3806
- const sddPath = opencodeConfigDir === defaultConfigDir
3807
- ? '~/.config/opencode/sdd/*'
3808
- : `${opencodeConfigDir.replace(/\\/g, '/')}/sdd/*`;
3809
-
5058
+ const defaultConfigDir = path.join(os.homedir(), '.config', 'kilo');
5059
+ const sddPath = kiloConfigDir === defaultConfigDir
5060
+ ? '~/.config/kilo/sdd/*'
5061
+ : `${kiloConfigDir.replace(/\\/g, '/')}/sdd/*`;
5062
+
3810
5063
  let modified = false;
3811
5064
 
3812
5065
  // Configure read permission
@@ -3914,11 +5167,15 @@ function generateManifest(dir, baseDir) {
3914
5167
  */
3915
5168
  function writeManifest(configDir, runtime = 'claude') {
3916
5169
  const isOpencode = runtime === 'opencode';
5170
+ const isKilo = runtime === 'kilo';
5171
+ const isGemini = runtime === 'gemini';
3917
5172
  const isCodex = runtime === 'codex';
3918
5173
  const isCopilot = runtime === 'copilot';
3919
5174
  const isAntigravity = runtime === 'antigravity';
3920
5175
  const isCursor = runtime === 'cursor';
3921
5176
  const isWindsurf = runtime === 'windsurf';
5177
+ const isTrae = runtime === 'trae';
5178
+ const isCline = runtime === 'cline';
3922
5179
  const sddDir = path.join(configDir, 'sdd');
3923
5180
  const commandsDir = path.join(configDir, 'commands', 'sdd');
3924
5181
  const opencodeCommandDir = path.join(configDir, 'command');
@@ -3930,20 +5187,20 @@ function writeManifest(configDir, runtime = 'claude') {
3930
5187
  for (const [rel, hash] of Object.entries(sddHashes)) {
3931
5188
  manifest.files['sdd/' + rel] = hash;
3932
5189
  }
3933
- if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && !isWindsurf && fs.existsSync(commandsDir)) {
5190
+ if (isGemini && fs.existsSync(commandsDir)) {
3934
5191
  const cmdHashes = generateManifest(commandsDir);
3935
5192
  for (const [rel, hash] of Object.entries(cmdHashes)) {
3936
5193
  manifest.files['commands/sdd/' + rel] = hash;
3937
5194
  }
3938
5195
  }
3939
- if (isOpencode && fs.existsSync(opencodeCommandDir)) {
5196
+ if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
3940
5197
  for (const file of fs.readdirSync(opencodeCommandDir)) {
3941
5198
  if (file.startsWith('sdd-') && file.endsWith('.md')) {
3942
5199
  manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
3943
5200
  }
3944
5201
  }
3945
5202
  }
3946
- if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf) && fs.existsSync(codexSkillsDir)) {
5203
+ if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
3947
5204
  for (const skillName of listCodexSkillNames(codexSkillsDir)) {
3948
5205
  const skillRoot = path.join(codexSkillsDir, skillName);
3949
5206
  const skillHashes = generateManifest(skillRoot);
@@ -3959,13 +5216,21 @@ function writeManifest(configDir, runtime = 'claude') {
3959
5216
  }
3960
5217
  }
3961
5218
  }
5219
+ // Track .clinerules file in manifest for Cline installs
5220
+ if (isCline) {
5221
+ const clinerulesDest = path.join(configDir, '.clinerules');
5222
+ if (fs.existsSync(clinerulesDest)) {
5223
+ manifest.files['.clinerules'] = fileHash(clinerulesDest);
5224
+ }
5225
+ }
5226
+
3962
5227
  // Track hook files so saveLocalPatches() can detect user modifications
3963
- // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot)
3964
- if (!isCodex && !isCopilot) {
5228
+ // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
5229
+ if (!isCodex && !isCopilot && !isCline) {
3965
5230
  const hooksDir = path.join(configDir, 'hooks');
3966
5231
  if (fs.existsSync(hooksDir)) {
3967
5232
  for (const file of fs.readdirSync(hooksDir)) {
3968
- if (file.startsWith('sdd-') && file.endsWith('.js')) {
5233
+ if (file.startsWith('sdd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
3969
5234
  manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
3970
5235
  }
3971
5236
  }
@@ -3979,6 +5244,8 @@ function writeManifest(configDir, runtime = 'claude') {
3979
5244
  /**
3980
5245
  * Detect user-modified SDD files by comparing against install manifest.
3981
5246
  * Backs up modified files to sdd-local-patches/ for reapply after update.
5247
+ * Also saves pristine copies (from manifest) to sdd-pristine/ to enable
5248
+ * three-way merge during reapply-patches (pristine vs user vs new).
3982
5249
  */
3983
5250
  function saveLocalPatches(configDir) {
3984
5251
  const manifestPath = path.join(configDir, MANIFEST_NAME);
@@ -3988,6 +5255,7 @@ function saveLocalPatches(configDir) {
3988
5255
  try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
3989
5256
 
3990
5257
  const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
5258
+ const pristineDir = path.join(configDir, 'sdd-pristine');
3991
5259
  const modified = [];
3992
5260
 
3993
5261
  for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
@@ -3995,6 +5263,7 @@ function saveLocalPatches(configDir) {
3995
5263
  if (!fs.existsSync(fullPath)) continue;
3996
5264
  const currentHash = fileHash(fullPath);
3997
5265
  if (currentHash !== originalHash) {
5266
+ // Back up the user's modified version
3998
5267
  const backupPath = path.join(patchesDir, relPath);
3999
5268
  fs.mkdirSync(path.dirname(backupPath), { recursive: true });
4000
5269
  fs.copyFileSync(fullPath, backupPath);
@@ -4002,12 +5271,28 @@ function saveLocalPatches(configDir) {
4002
5271
  }
4003
5272
  }
4004
5273
 
5274
+ // Save pristine copies of modified files from the CURRENT install (before wipe)
5275
+ // These represent the original SDD distribution files that the user then modified.
5276
+ // The reapply-patches workflow uses these for three-way merge:
5277
+ // pristine (original) → user's version (what they changed) → new version (after update)
4005
5278
  if (modified.length > 0) {
5279
+ // We need the pristine originals, but the current files on disk are user-modified.
5280
+ // The manifest records SHA-256 hashes but not content. However, we can reconstruct
5281
+ // the pristine version from the npm package cache or git history.
5282
+ // As a practical approach: save the manifest's version info so the reapply workflow
5283
+ // knows which SDD version these files came from, enabling npm-based reconstruction.
4006
5284
  const meta = {
4007
5285
  backed_up_at: new Date().toISOString(),
4008
5286
  from_version: manifest.version,
4009
- files: modified
5287
+ from_manifest_timestamp: manifest.timestamp,
5288
+ files: modified,
5289
+ pristine_hashes: {}
4010
5290
  };
5291
+ // Record the original (pristine) hash for each modified file
5292
+ // This lets the reapply workflow verify reconstructed pristine files
5293
+ for (const relPath of modified) {
5294
+ meta.pristine_hashes[relPath] = manifest.files[relPath];
5295
+ }
4011
5296
  fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
4012
5297
  console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified SDD file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
4013
5298
  for (const f of modified) {
@@ -4029,13 +5314,13 @@ function reportLocalPatches(configDir, runtime = 'claude') {
4029
5314
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
4030
5315
 
4031
5316
  if (meta.files && meta.files.length > 0) {
4032
- const reapplyCommand = (runtime === 'opencode' || runtime === 'copilot')
5317
+ const reapplyCommand = (runtime === 'opencode' || runtime === 'kilo' || runtime === 'copilot')
4033
5318
  ? '/sdd-reapply-patches'
4034
5319
  : runtime === 'codex'
4035
5320
  ? '$sdd-reapply-patches'
4036
5321
  : runtime === 'cursor'
4037
5322
  ? 'sdd-reapply-patches (mention the skill name)'
4038
- : '/sdd:reapply-patches';
5323
+ : '/sdd-reapply-patches';
4039
5324
  console.log('');
4040
5325
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
4041
5326
  for (const f of meta.files) {
@@ -4053,18 +5338,28 @@ function reportLocalPatches(configDir, runtime = 'claude') {
4053
5338
  function install(isGlobal, runtime = 'claude') {
4054
5339
  const isOpencode = runtime === 'opencode';
4055
5340
  const isGemini = runtime === 'gemini';
5341
+ const isKilo = runtime === 'kilo';
4056
5342
  const isCodex = runtime === 'codex';
4057
5343
  const isCopilot = runtime === 'copilot';
4058
5344
  const isAntigravity = runtime === 'antigravity';
4059
5345
  const isCursor = runtime === 'cursor';
4060
5346
  const isWindsurf = runtime === 'windsurf';
5347
+ const isAugment = runtime === 'augment';
5348
+ const isTrae = runtime === 'trae';
5349
+ const isQwen = runtime === 'qwen';
5350
+ const isCodebuddy = runtime === 'codebuddy';
5351
+ const isCline = runtime === 'cline';
4061
5352
  const dirName = getDirName(runtime);
4062
5353
  const src = path.join(__dirname, '..');
4063
5354
 
4064
- // Get the target directory based on runtime and install type
5355
+ // Get the target directory based on runtime and install type.
5356
+ // Cline local installs write to the project root (like Claude Code) — .clinerules
5357
+ // lives at the root, not inside a .cline/ subdirectory.
4065
5358
  const targetDir = isGlobal
4066
5359
  ? getGlobalDir(runtime, explicitConfigDir)
4067
- : path.join(process.cwd(), dirName);
5360
+ : isCline
5361
+ ? process.cwd()
5362
+ : path.join(process.cwd(), dirName);
4068
5363
 
4069
5364
  const locationLabel = isGlobal
4070
5365
  ? targetDir.replace(os.homedir(), '~')
@@ -4084,11 +5379,17 @@ function install(isGlobal, runtime = 'claude') {
4084
5379
  let runtimeLabel = 'Claude Code';
4085
5380
  if (isOpencode) runtimeLabel = 'OpenCode';
4086
5381
  if (isGemini) runtimeLabel = 'Gemini';
5382
+ if (isKilo) runtimeLabel = 'Kilo';
4087
5383
  if (isCodex) runtimeLabel = 'Codex';
4088
5384
  if (isCopilot) runtimeLabel = 'Copilot';
4089
5385
  if (isAntigravity) runtimeLabel = 'Antigravity';
4090
5386
  if (isCursor) runtimeLabel = 'Cursor';
4091
5387
  if (isWindsurf) runtimeLabel = 'Windsurf';
5388
+ if (isAugment) runtimeLabel = 'Augment';
5389
+ if (isTrae) runtimeLabel = 'Trae';
5390
+ if (isQwen) runtimeLabel = 'Qwen Code';
5391
+ if (isCodebuddy) runtimeLabel = 'CodeBuddy';
5392
+ if (isCline) runtimeLabel = 'Cline';
4092
5393
 
4093
5394
  console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
4094
5395
 
@@ -4101,12 +5402,12 @@ function install(isGlobal, runtime = 'claude') {
4101
5402
  // Clean up orphaned files from previous versions
4102
5403
  cleanupOrphanedFiles(targetDir);
4103
5404
 
4104
- // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/sdd/
4105
- if (isOpencode) {
4106
- // OpenCode: flat structure in command/ directory
5405
+ // OpenCode/Kilo use command/ (flat), Codex uses skills/, Claude/Gemini use commands/sdd/
5406
+ if (isOpencode || isKilo) {
5407
+ // OpenCode/Kilo: flat structure in command/ directory
4107
5408
  const commandDir = path.join(targetDir, 'command');
4108
5409
  fs.mkdirSync(commandDir, { recursive: true });
4109
-
5410
+
4110
5411
  // Copy commands/sdd/*.md as command/sdd-*.md (flatten structure)
4111
5412
  const sddSrc = path.join(src, 'commands', 'sdd');
4112
5413
  copyFlattenedCommands(sddSrc, commandDir, 'sdd', pathPrefix, runtime);
@@ -4176,11 +5477,66 @@ function install(isGlobal, runtime = 'claude') {
4176
5477
  } else {
4177
5478
  failures.push('skills/sdd-*');
4178
5479
  }
4179
- } else {
4180
- // Claude Code & Gemini: nested structure in commands/ directory
5480
+ } else if (isAugment) {
5481
+ const skillsDir = path.join(targetDir, 'skills');
5482
+ const sddSrc = path.join(src, 'commands', 'sdd');
5483
+ copyCommandsAsAugmentSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
5484
+ const installedSkillNames = listCodexSkillNames(skillsDir);
5485
+ if (installedSkillNames.length > 0) {
5486
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
5487
+ } else {
5488
+ failures.push('skills/sdd-*');
5489
+ }
5490
+ } else if (isTrae) {
5491
+ const skillsDir = path.join(targetDir, 'skills');
5492
+ const sddSrc = path.join(src, 'commands', 'sdd');
5493
+ copyCommandsAsTraeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
5494
+ const installedSkillNames = listCodexSkillNames(skillsDir);
5495
+ if (installedSkillNames.length > 0) {
5496
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
5497
+ } else {
5498
+ failures.push('skills/sdd-*');
5499
+ }
5500
+ } else if (isQwen) {
5501
+ const skillsDir = path.join(targetDir, 'skills');
5502
+ const sddSrc = path.join(src, 'commands', 'sdd');
5503
+ copyCommandsAsClaudeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime, isGlobal);
5504
+ if (fs.existsSync(skillsDir)) {
5505
+ const count = fs.readdirSync(skillsDir, { withFileTypes: true })
5506
+ .filter(e => e.isDirectory() && e.name.startsWith('sdd-')).length;
5507
+ if (count > 0) {
5508
+ console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
5509
+ } else {
5510
+ failures.push('skills/sdd-*');
5511
+ }
5512
+ } else {
5513
+ failures.push('skills/sdd-*');
5514
+ }
5515
+
5516
+ const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
5517
+ if (fs.existsSync(legacyCommandsDir)) {
5518
+ const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
5519
+ fs.rmSync(legacyCommandsDir, { recursive: true });
5520
+ console.log(` ${green}✓${reset} Removed legacy commands/sdd/ directory`);
5521
+ restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
5522
+ }
5523
+ } else if (isCodebuddy) {
5524
+ const skillsDir = path.join(targetDir, 'skills');
5525
+ const sddSrc = path.join(src, 'commands', 'sdd');
5526
+ copyCommandsAsCodebuddySkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
5527
+ const installedSkillNames = listCodexSkillNames(skillsDir);
5528
+ if (installedSkillNames.length > 0) {
5529
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
5530
+ } else {
5531
+ failures.push('skills/sdd-*');
5532
+ }
5533
+ } else if (isCline) {
5534
+ // Cline is rules-based — commands are embedded in .clinerules (generated below).
5535
+ // No skills/commands directory needed. Engine is installed via copyWithPathReplacement.
5536
+ console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
5537
+ } else if (isGemini) {
4181
5538
  const commandsDir = path.join(targetDir, 'commands');
4182
5539
  fs.mkdirSync(commandsDir, { recursive: true });
4183
-
4184
5540
  const sddSrc = path.join(src, 'commands', 'sdd');
4185
5541
  const sddDest = path.join(commandsDir, 'sdd');
4186
5542
  copyWithPathReplacement(sddSrc, sddDest, pathPrefix, runtime, true, isGlobal);
@@ -4189,12 +5545,68 @@ function install(isGlobal, runtime = 'claude') {
4189
5545
  } else {
4190
5546
  failures.push('commands/sdd');
4191
5547
  }
5548
+ } else if (isGlobal) {
5549
+ // Claude Code global: skills/ format (2.1.88+ compatibility)
5550
+ const skillsDir = path.join(targetDir, 'skills');
5551
+ const sddSrc = path.join(src, 'commands', 'sdd');
5552
+ copyCommandsAsClaudeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime, isGlobal);
5553
+ if (fs.existsSync(skillsDir)) {
5554
+ const count = fs.readdirSync(skillsDir, { withFileTypes: true })
5555
+ .filter(e => e.isDirectory() && e.name.startsWith('sdd-')).length;
5556
+ if (count > 0) {
5557
+ console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
5558
+ } else {
5559
+ failures.push('skills/sdd-*');
5560
+ }
5561
+ } else {
5562
+ failures.push('skills/sdd-*');
5563
+ }
5564
+
5565
+ // Clean up legacy commands/sdd/ from previous global installs
5566
+ // Preserve user-generated files (dev-preferences.md) before wiping the directory
5567
+ const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
5568
+ if (fs.existsSync(legacyCommandsDir)) {
5569
+ const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
5570
+ fs.rmSync(legacyCommandsDir, { recursive: true });
5571
+ console.log(` ${green}✓${reset} Removed legacy commands/sdd/ directory`);
5572
+ restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
5573
+ }
5574
+ } else {
5575
+ // Claude Code local: commands/sdd/ format — Claude Code reads local project
5576
+ // commands from .claude/commands/sdd/, not .claude/skills/
5577
+ const commandsDir = path.join(targetDir, 'commands');
5578
+ fs.mkdirSync(commandsDir, { recursive: true });
5579
+ const sddSrc = path.join(src, 'commands', 'sdd');
5580
+ const sddDest = path.join(commandsDir, 'sdd');
5581
+ copyWithPathReplacement(sddSrc, sddDest, pathPrefix, runtime, true, isGlobal);
5582
+ if (verifyInstalled(sddDest, 'commands/sdd')) {
5583
+ const count = fs.readdirSync(sddDest).filter(f => f.endsWith('.md')).length;
5584
+ console.log(` ${green}✓${reset} Installed ${count} commands to commands/sdd/`);
5585
+ } else {
5586
+ failures.push('commands/sdd');
5587
+ }
5588
+
5589
+ // Clean up any stale skills/ from a previous local install
5590
+ const staleSkillsDir = path.join(targetDir, 'skills');
5591
+ if (fs.existsSync(staleSkillsDir)) {
5592
+ const staleGsd = fs.readdirSync(staleSkillsDir, { withFileTypes: true })
5593
+ .filter(e => e.isDirectory() && e.name.startsWith('sdd-'));
5594
+ for (const e of staleGsd) {
5595
+ fs.rmSync(path.join(staleSkillsDir, e.name), { recursive: true });
5596
+ }
5597
+ if (staleGsd.length > 0) {
5598
+ console.log(` ${green}✓${reset} Removed ${staleGsd.length} stale SDD skill(s) from skills/`);
5599
+ }
5600
+ }
4192
5601
  }
4193
5602
 
4194
5603
  // Copy sdd skill with path replacement
5604
+ // Preserve user-generated files before the wipe-and-copy so they survive re-install
4195
5605
  const skillSrc = path.join(src, 'sdd');
4196
5606
  const skillDest = path.join(targetDir, 'sdd');
5607
+ const savedSddArtifacts = preserveUserArtifacts(skillDest, ['USER-PROFILE.md']);
4197
5608
  copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
5609
+ restoreUserArtifacts(skillDest, savedSddArtifacts);
4198
5610
  if (verifyInstalled(skillDest, 'sdd')) {
4199
5611
  console.log(` ${green}✓${reset} Installed sdd`);
4200
5612
  } else {
@@ -4224,14 +5636,21 @@ function install(isGlobal, runtime = 'claude') {
4224
5636
  // Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
4225
5637
  const dirRegex = /~\/\.claude\//g;
4226
5638
  const homeDirRegex = /\$HOME\/\.claude\//g;
5639
+ const bareDirRegex = /~\/\.claude\b/g;
5640
+ const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
5641
+ const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
4227
5642
  if (!isCopilot && !isAntigravity) {
4228
5643
  content = content.replace(dirRegex, pathPrefix);
4229
5644
  content = content.replace(homeDirRegex, pathPrefix);
5645
+ content = content.replace(bareDirRegex, normalizedPathPrefix);
5646
+ content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
4230
5647
  }
4231
5648
  content = processAttribution(content, getCommitAttribution(runtime));
4232
5649
  // Convert frontmatter for runtime compatibility (agents need different handling)
4233
5650
  if (isOpencode) {
4234
5651
  content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true });
5652
+ } else if (isKilo) {
5653
+ content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
4235
5654
  } else if (isGemini) {
4236
5655
  content = convertClaudeToGeminiAgent(content);
4237
5656
  } else if (isCodex) {
@@ -4244,6 +5663,14 @@ function install(isGlobal, runtime = 'claude') {
4244
5663
  content = convertClaudeAgentToCursorAgent(content);
4245
5664
  } else if (isWindsurf) {
4246
5665
  content = convertClaudeAgentToWindsurfAgent(content);
5666
+ } else if (isAugment) {
5667
+ content = convertClaudeAgentToAugmentAgent(content);
5668
+ } else if (isTrae) {
5669
+ content = convertClaudeAgentToTraeAgent(content);
5670
+ } else if (isCodebuddy) {
5671
+ content = convertClaudeAgentToCodebuddyAgent(content);
5672
+ } else if (isCline) {
5673
+ content = convertClaudeAgentToClineAgent(content);
4247
5674
  }
4248
5675
  const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
4249
5676
  fs.writeFileSync(path.join(agentsDest, destName), content);
@@ -4277,7 +5704,7 @@ function install(isGlobal, runtime = 'claude') {
4277
5704
  failures.push('VERSION');
4278
5705
  }
4279
5706
 
4280
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
5707
+ if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
4281
5708
  // Write package.json to force CommonJS mode for SDD scripts
4282
5709
  // Prevents "require is not defined" errors when project has "type": "module"
4283
5710
  // Node.js walks up looking for package.json - this stops inheritance from project
@@ -4308,11 +5735,22 @@ function install(isGlobal, runtime = 'claude') {
4308
5735
  try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
4309
5736
  } else {
4310
5737
  fs.copyFileSync(srcFile, destFile);
5738
+ // Ensure .sh hook files are executable (mirrors chmod in build-hooks.js)
5739
+ if (entry.endsWith('.sh')) {
5740
+ try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
5741
+ }
4311
5742
  }
4312
5743
  }
4313
5744
  }
4314
5745
  if (verifyInstalled(hooksDest, 'hooks')) {
4315
5746
  console.log(` ${green}✓${reset} Installed hooks (bundled)`);
5747
+ // Warn if expected community .sh hooks are missing (non-fatal)
5748
+ const expectedShHooks = ['sdd-session-state.sh', 'sdd-validate-commit.sh', 'sdd-phase-boundary.sh'];
5749
+ for (const sh of expectedShHooks) {
5750
+ if (!fs.existsSync(path.join(hooksDest, sh))) {
5751
+ console.warn(` ${yellow}⚠${reset} Missing expected hook: ${sh}`);
5752
+ }
5753
+ }
4316
5754
  } else {
4317
5755
  failures.push('hooks');
4318
5756
  }
@@ -4320,8 +5758,8 @@ function install(isGlobal, runtime = 'claude') {
4320
5758
  }
4321
5759
 
4322
5760
  // Clear stale update cache so next session re-evaluates hook versions
4323
- // targetDir is e.g. ~/.claude/sdd/, parent is the config dir
4324
- const updateCacheFile = path.join(path.dirname(targetDir), 'cache', 'sdd-update-check.json');
5761
+ // Cache lives at ~/.cache/sdd/ (see hooks/sdd-check-update.js line 35-36)
5762
+ const updateCacheFile = path.join(os.homedir(), '.cache', 'sdd', 'sdd-update-check.json');
4325
5763
  try { fs.unlinkSync(updateCacheFile); } catch (e) { /* cache may not exist yet */ }
4326
5764
 
4327
5765
  if (failures.length > 0) {
@@ -4400,14 +5838,21 @@ function install(isGlobal, runtime = 'claude') {
4400
5838
  configContent = setManagedCodexHooksOwnership(codexHooksFeature.content, codexHooksFeature.ownership);
4401
5839
 
4402
5840
  // Add SessionStart hook for update checking
4403
- const updateCheckScript = path.resolve(targetDir, 'sdd', 'hooks', 'sdd-update-check.js').replace(/\\/g, '/');
5841
+ const updateCheckScript = path.resolve(targetDir, 'hooks', 'sdd-check-update.js').replace(/\\/g, '/');
4404
5842
  const hookBlock =
4405
5843
  `${eol}# SDD Hooks${eol}` +
4406
5844
  `[[hooks]]${eol}` +
4407
5845
  `event = "SessionStart"${eol}` +
4408
5846
  `command = "node ${updateCheckScript}"${eol}`;
4409
5847
 
4410
- if (hasEnabledCodexHooksFeature(configContent) && !configContent.includes('sdd-update-check')) {
5848
+ // Migrate legacy sdd-update-check entries from prior installs (#1755 followup)
5849
+ // Remove stale hook blocks that used the inverted filename or wrong path
5850
+ if (configContent.includes('sdd-update-check')) {
5851
+ configContent = configContent.replace(/\n# SDD Hooks\n\[\[hooks\]\]\nevent = "SessionStart"\ncommand = "node [^\n]*sdd-update-check\.js"\n/g, '\n');
5852
+ configContent = configContent.replace(/\r\n# SDD Hooks\r\n\[\[hooks\]\]\r\nevent = "SessionStart"\r\ncommand = "node [^\r\n]*sdd-update-check\.js"\r\n/g, '\r\n');
5853
+ }
5854
+
5855
+ if (hasEnabledCodexHooksFeature(configContent) && !configContent.includes('sdd-check-update')) {
4411
5856
  configContent += hookBlock;
4412
5857
  }
4413
5858
 
@@ -4417,7 +5862,7 @@ function install(isGlobal, runtime = 'claude') {
4417
5862
  console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
4418
5863
  }
4419
5864
 
4420
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
5865
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
4421
5866
  }
4422
5867
 
4423
5868
  if (isCopilot) {
@@ -4430,36 +5875,72 @@ function install(isGlobal, runtime = 'claude') {
4430
5875
  console.log(` ${green}✓${reset} Generated copilot-instructions.md`);
4431
5876
  }
4432
5877
  // Copilot: no settings.json, no hooks, no statusline (like Codex)
4433
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
5878
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
4434
5879
  }
4435
5880
 
4436
5881
  if (isCursor) {
4437
5882
  // Cursor uses skills — no config.toml, no settings.json hooks needed
4438
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
5883
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
4439
5884
  }
4440
5885
 
4441
5886
  if (isWindsurf) {
4442
5887
  // Windsurf uses skills — no config.toml, no settings.json hooks needed
4443
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
5888
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
5889
+ }
5890
+
5891
+ if (isTrae) {
5892
+ // Trae uses skills — no settings.json hooks needed
5893
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
5894
+ }
5895
+
5896
+ if (isCline) {
5897
+ // Cline uses .clinerules — generate a rules file with SDD system instructions
5898
+ const clinerulesDest = path.join(targetDir, '.clinerules');
5899
+ const clinerules = [
5900
+ '# SDD — Spec-Driven Development',
5901
+ '',
5902
+ '- SDD workflows live in `sdd/workflows/`. Load the relevant workflow when',
5903
+ ' the user runs a `/sdd-*` command.',
5904
+ '- SDD agents live in `agents/`. Use the matching agent when spawning subagents.',
5905
+ '- SDD tools are at `sdd/bin/sdd-tools.cjs`. Run with `node`.',
5906
+ '- Planning artifacts live in `.planning/`. Never edit them outside a SDD workflow.',
5907
+ '- Do not apply SDD workflows unless the user explicitly asks for them.',
5908
+ '- When a SDD command triggers a deliverable (feature, fix, docs), offer the next',
5909
+ ' step to the user using Cline\'s ask_user tool after completing it.',
5910
+ ].join('\n') + '\n';
5911
+ fs.writeFileSync(clinerulesDest, clinerules);
5912
+ console.log(` ${green}✓${reset} Wrote .clinerules`);
5913
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
4444
5914
  }
4445
5915
 
4446
5916
  // Configure statusline and hooks in settings.json
4447
5917
  // Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
4448
5918
  const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
4449
5919
  const settingsPath = path.join(targetDir, 'settings.json');
4450
- const settings = validateHookFields(cleanupOrphanedHooks(readSettings(settingsPath)));
5920
+ const rawSettings = readSettings(settingsPath);
5921
+ if (rawSettings === null) {
5922
+ console.log(' ' + yellow + 'i' + reset + ' Skipping settings.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
5923
+ return;
5924
+ }
5925
+ const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
5926
+ // Local installs anchor paths to $CLAUDE_PROJECT_DIR so hooks resolve
5927
+ // correctly regardless of the shell's current working directory (#1906).
5928
+ const localPrefix = '"$CLAUDE_PROJECT_DIR"/' + dirName;
4451
5929
  const statuslineCommand = isGlobal
4452
5930
  ? buildHookCommand(targetDir, 'sdd-statusline.js')
4453
- : 'node ' + dirName + '/hooks/sdd-statusline.js';
5931
+ : 'node ' + localPrefix + '/hooks/sdd-statusline.js';
4454
5932
  const updateCheckCommand = isGlobal
4455
5933
  ? buildHookCommand(targetDir, 'sdd-check-update.js')
4456
- : 'node ' + dirName + '/hooks/sdd-check-update.js';
5934
+ : 'node ' + localPrefix + '/hooks/sdd-check-update.js';
4457
5935
  const contextMonitorCommand = isGlobal
4458
5936
  ? buildHookCommand(targetDir, 'sdd-context-monitor.js')
4459
- : 'node ' + dirName + '/hooks/sdd-context-monitor.js';
5937
+ : 'node ' + localPrefix + '/hooks/sdd-context-monitor.js';
4460
5938
  const promptGuardCommand = isGlobal
4461
5939
  ? buildHookCommand(targetDir, 'sdd-prompt-guard.js')
4462
- : 'node ' + dirName + '/hooks/sdd-prompt-guard.js';
5940
+ : 'node ' + localPrefix + '/hooks/sdd-prompt-guard.js';
5941
+ const readGuardCommand = isGlobal
5942
+ ? buildHookCommand(targetDir, 'sdd-read-guard.js')
5943
+ : 'node ' + localPrefix + '/hooks/sdd-read-guard.js';
4463
5944
 
4464
5945
  // Enable experimental agents for Gemini CLI (required for custom sub-agents)
4465
5946
  if (isGemini) {
@@ -4473,7 +5954,7 @@ function install(isGlobal, runtime = 'claude') {
4473
5954
  }
4474
5955
 
4475
5956
  // Configure SessionStart hook for update checking (skip for opencode)
4476
- if (!isOpencode) {
5957
+ if (!isOpencode && !isKilo) {
4477
5958
  if (!settings.hooks) {
4478
5959
  settings.hooks = {};
4479
5960
  }
@@ -4481,11 +5962,16 @@ function install(isGlobal, runtime = 'claude') {
4481
5962
  settings.hooks.SessionStart = [];
4482
5963
  }
4483
5964
 
4484
- const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
5965
+ const hasSddUpdateHook = settings.hooks.SessionStart.some(entry =>
4485
5966
  entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-check-update'))
4486
5967
  );
4487
5968
 
4488
- if (!hasGsdUpdateHook) {
5969
+ // Guard: only register if the hook file was actually installed (#1754).
5970
+ // When hooks/dist/ is missing from the npm package (as in v1.32.0), the
5971
+ // copy step produces no files but the registration step ran unconditionally,
5972
+ // causing "hook error" on every tool invocation.
5973
+ const checkUpdateFile = path.join(targetDir, 'hooks', 'sdd-check-update.js');
5974
+ if (!hasSddUpdateHook && fs.existsSync(checkUpdateFile)) {
4489
5975
  settings.hooks.SessionStart.push({
4490
5976
  hooks: [
4491
5977
  {
@@ -4495,6 +5981,8 @@ function install(isGlobal, runtime = 'claude') {
4495
5981
  ]
4496
5982
  });
4497
5983
  console.log(` ${green}✓${reset} Configured update check hook`);
5984
+ } else if (!hasSddUpdateHook && !fs.existsSync(checkUpdateFile)) {
5985
+ console.warn(` ${yellow}⚠${reset} Skipped update check hook — sdd-check-update.js not found at target`);
4498
5986
  }
4499
5987
 
4500
5988
  // Configure post-tool hook for context window monitoring
@@ -4506,7 +5994,8 @@ function install(isGlobal, runtime = 'claude') {
4506
5994
  entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-context-monitor'))
4507
5995
  );
4508
5996
 
4509
- if (!hasContextMonitorHook) {
5997
+ const contextMonitorFile = path.join(targetDir, 'hooks', 'sdd-context-monitor.js');
5998
+ if (!hasContextMonitorHook && fs.existsSync(contextMonitorFile)) {
4510
5999
  settings.hooks[postToolEvent].push({
4511
6000
  matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task',
4512
6001
  hooks: [
@@ -4518,6 +6007,8 @@ function install(isGlobal, runtime = 'claude') {
4518
6007
  ]
4519
6008
  });
4520
6009
  console.log(` ${green}✓${reset} Configured context window monitor hook`);
6010
+ } else if (!hasContextMonitorHook && !fs.existsSync(contextMonitorFile)) {
6011
+ console.warn(` ${yellow}⚠${reset} Skipped context monitor hook — sdd-context-monitor.js not found at target`);
4521
6012
  } else {
4522
6013
  // Migrate existing context monitor hooks: add matcher and timeout if missing
4523
6014
  for (const entry of settings.hooks[postToolEvent]) {
@@ -4551,7 +6042,8 @@ function install(isGlobal, runtime = 'claude') {
4551
6042
  entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-prompt-guard'))
4552
6043
  );
4553
6044
 
4554
- if (!hasPromptGuardHook) {
6045
+ const promptGuardFile = path.join(targetDir, 'hooks', 'sdd-prompt-guard.js');
6046
+ if (!hasPromptGuardHook && fs.existsSync(promptGuardFile)) {
4555
6047
  settings.hooks[preToolEvent].push({
4556
6048
  matcher: 'Write|Edit',
4557
6049
  hooks: [
@@ -4563,23 +6055,157 @@ function install(isGlobal, runtime = 'claude') {
4563
6055
  ]
4564
6056
  });
4565
6057
  console.log(` ${green}✓${reset} Configured prompt injection guard hook`);
6058
+ } else if (!hasPromptGuardHook && !fs.existsSync(promptGuardFile)) {
6059
+ console.warn(` ${yellow}⚠${reset} Skipped prompt guard hook — sdd-prompt-guard.js not found at target`);
6060
+ }
6061
+
6062
+ // Configure PreToolUse hook for read-before-edit guidance (#1628)
6063
+ // Prevents infinite retry loops when non-Claude models attempt to edit
6064
+ // files without reading them first. Advisory-only — does not block.
6065
+ const hasReadGuardHook = settings.hooks[preToolEvent].some(entry =>
6066
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-read-guard'))
6067
+ );
6068
+
6069
+ const readGuardFile = path.join(targetDir, 'hooks', 'sdd-read-guard.js');
6070
+ if (!hasReadGuardHook && fs.existsSync(readGuardFile)) {
6071
+ settings.hooks[preToolEvent].push({
6072
+ matcher: 'Write|Edit',
6073
+ hooks: [
6074
+ {
6075
+ type: 'command',
6076
+ command: readGuardCommand,
6077
+ timeout: 5
6078
+ }
6079
+ ]
6080
+ });
6081
+ console.log(` ${green}✓${reset} Configured read-before-edit guard hook`);
6082
+ } else if (!hasReadGuardHook && !fs.existsSync(readGuardFile)) {
6083
+ console.warn(` ${yellow}⚠${reset} Skipped read guard hook — sdd-read-guard.js not found at target`);
6084
+ }
6085
+
6086
+ // Community hooks — registered on install but opt-in at runtime.
6087
+ // Each hook checks .planning/config.json for hooks.community: true
6088
+ // and exits silently (no-op) if not enabled. This lets users enable
6089
+ // them per-project by adding: "hooks": { "community": true }
6090
+
6091
+ // Configure workflow guard hook (opt-in via hooks.workflow_guard: true)
6092
+ // Detects file edits outside SDD workflow context and advises using
6093
+ // /sdd-quick or /sdd-fast for state-tracked changes. Advisory only.
6094
+ const workflowGuardCommand = isGlobal
6095
+ ? buildHookCommand(targetDir, 'sdd-workflow-guard.js')
6096
+ : 'node ' + localPrefix + '/hooks/sdd-workflow-guard.js';
6097
+ const hasWorkflowGuardHook = settings.hooks[preToolEvent].some(entry =>
6098
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-workflow-guard'))
6099
+ );
6100
+
6101
+ const workflowGuardFile = path.join(targetDir, 'hooks', 'sdd-workflow-guard.js');
6102
+ if (!hasWorkflowGuardHook && fs.existsSync(workflowGuardFile)) {
6103
+ settings.hooks[preToolEvent].push({
6104
+ matcher: 'Write|Edit',
6105
+ hooks: [
6106
+ {
6107
+ type: 'command',
6108
+ command: workflowGuardCommand,
6109
+ timeout: 5
6110
+ }
6111
+ ]
6112
+ });
6113
+ console.log(` ${green}✓${reset} Configured workflow guard hook (opt-in via hooks.workflow_guard)`);
6114
+ } else if (!hasWorkflowGuardHook && !fs.existsSync(workflowGuardFile)) {
6115
+ console.warn(` ${yellow}⚠${reset} Skipped workflow guard hook — sdd-workflow-guard.js not found at target`);
6116
+ }
6117
+
6118
+ // Configure commit validation hook (Conventional Commits enforcement, opt-in)
6119
+ const validateCommitCommand = isGlobal
6120
+ ? buildHookCommand(targetDir, 'sdd-validate-commit.sh')
6121
+ : 'bash ' + localPrefix + '/hooks/sdd-validate-commit.sh';
6122
+ const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
6123
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-validate-commit'))
6124
+ );
6125
+ // Guard: only register if the .sh file was actually installed. If the npm package
6126
+ // omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
6127
+ // causes a hook error on every Bash tool invocation.
6128
+ const validateCommitFile = path.join(targetDir, 'hooks', 'sdd-validate-commit.sh');
6129
+ if (!hasValidateCommitHook && fs.existsSync(validateCommitFile)) {
6130
+ settings.hooks[preToolEvent].push({
6131
+ matcher: 'Bash',
6132
+ hooks: [
6133
+ {
6134
+ type: 'command',
6135
+ command: validateCommitCommand,
6136
+ timeout: 5
6137
+ }
6138
+ ]
6139
+ });
6140
+ console.log(` ${green}✓${reset} Configured commit validation hook (opt-in via config)`);
6141
+ } else if (!hasValidateCommitHook && !fs.existsSync(validateCommitFile)) {
6142
+ console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — sdd-validate-commit.sh not found at target`);
6143
+ }
6144
+
6145
+ // Configure session state orientation hook (opt-in)
6146
+ const sessionStateCommand = isGlobal
6147
+ ? buildHookCommand(targetDir, 'sdd-session-state.sh')
6148
+ : 'bash ' + localPrefix + '/hooks/sdd-session-state.sh';
6149
+ const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
6150
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-session-state'))
6151
+ );
6152
+ const sessionStateFile = path.join(targetDir, 'hooks', 'sdd-session-state.sh');
6153
+ if (!hasSessionStateHook && fs.existsSync(sessionStateFile)) {
6154
+ settings.hooks.SessionStart.push({
6155
+ hooks: [
6156
+ {
6157
+ type: 'command',
6158
+ command: sessionStateCommand
6159
+ }
6160
+ ]
6161
+ });
6162
+ console.log(` ${green}✓${reset} Configured session state orientation hook (opt-in via config)`);
6163
+ } else if (!hasSessionStateHook && !fs.existsSync(sessionStateFile)) {
6164
+ console.warn(` ${yellow}⚠${reset} Skipped session state hook — sdd-session-state.sh not found at target`);
6165
+ }
6166
+
6167
+ // Configure phase boundary detection hook (opt-in)
6168
+ const phaseBoundaryCommand = isGlobal
6169
+ ? buildHookCommand(targetDir, 'sdd-phase-boundary.sh')
6170
+ : 'bash ' + localPrefix + '/hooks/sdd-phase-boundary.sh';
6171
+ const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
6172
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-phase-boundary'))
6173
+ );
6174
+ const phaseBoundaryFile = path.join(targetDir, 'hooks', 'sdd-phase-boundary.sh');
6175
+ if (!hasPhaseBoundaryHook && fs.existsSync(phaseBoundaryFile)) {
6176
+ settings.hooks[postToolEvent].push({
6177
+ matcher: 'Write|Edit',
6178
+ hooks: [
6179
+ {
6180
+ type: 'command',
6181
+ command: phaseBoundaryCommand,
6182
+ timeout: 5
6183
+ }
6184
+ ]
6185
+ });
6186
+ console.log(` ${green}✓${reset} Configured phase boundary detection hook (opt-in via config)`);
6187
+ } else if (!hasPhaseBoundaryHook && !fs.existsSync(phaseBoundaryFile)) {
6188
+ console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — sdd-phase-boundary.sh not found at target`);
4566
6189
  }
4567
6190
  }
4568
6191
 
4569
- return { settingsPath, settings, statuslineCommand, runtime };
6192
+ return { settingsPath, settings, statuslineCommand, runtime, configDir: targetDir };
4570
6193
  }
4571
6194
 
4572
6195
  /**
4573
6196
  * Apply statusline config, then print completion message
4574
6197
  */
4575
- function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
6198
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null) {
4576
6199
  const isOpencode = runtime === 'opencode';
6200
+ const isKilo = runtime === 'kilo';
4577
6201
  const isCodex = runtime === 'codex';
4578
6202
  const isCopilot = runtime === 'copilot';
4579
6203
  const isCursor = runtime === 'cursor';
4580
6204
  const isWindsurf = runtime === 'windsurf';
6205
+ const isTrae = runtime === 'trae';
6206
+ const isCline = runtime === 'cline';
4581
6207
 
4582
- if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor && !isWindsurf) {
6208
+ if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
4583
6209
  settings.statusLine = {
4584
6210
  type: 'command',
4585
6211
  command: statuslineCommand
@@ -4588,13 +6214,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
4588
6214
  }
4589
6215
 
4590
6216
  // Write settings when runtime supports settings.json
4591
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
6217
+ if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) {
4592
6218
  writeSettings(settingsPath, settings);
4593
6219
  }
4594
6220
 
4595
6221
  // Configure OpenCode permissions
4596
6222
  if (isOpencode) {
4597
- configureOpencodePermissions(isGlobal);
6223
+ configureOpencodePermissions(isGlobal, configDir);
6224
+ }
6225
+
6226
+ // Configure Kilo permissions
6227
+ if (isKilo) {
6228
+ configureKiloPermissions(isGlobal, configDir);
4598
6229
  }
4599
6230
 
4600
6231
  // For non-Claude runtimes, set resolve_model_ids: "omit" in ~/.sdd/defaults.json
@@ -4621,21 +6252,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
4621
6252
  let program = 'Claude Code';
4622
6253
  if (runtime === 'opencode') program = 'OpenCode';
4623
6254
  if (runtime === 'gemini') program = 'Gemini';
6255
+ if (runtime === 'kilo') program = 'Kilo';
4624
6256
  if (runtime === 'codex') program = 'Codex';
4625
6257
  if (runtime === 'copilot') program = 'Copilot';
4626
6258
  if (runtime === 'antigravity') program = 'Antigravity';
4627
6259
  if (runtime === 'cursor') program = 'Cursor';
6260
+ if (runtime === 'windsurf') program = 'Windsurf';
6261
+ if (runtime === 'augment') program = 'Augment';
6262
+ if (runtime === 'trae') program = 'Trae';
6263
+ if (runtime === 'cline') program = 'Cline';
4628
6264
 
4629
- let command = '/sdd:new-project';
6265
+ let command = '/sdd-new-project';
4630
6266
  if (runtime === 'opencode') command = '/sdd-new-project';
6267
+ if (runtime === 'kilo') command = '/sdd-new-project';
4631
6268
  if (runtime === 'codex') command = '$sdd-new-project';
4632
6269
  if (runtime === 'copilot') command = '/sdd-new-project';
4633
6270
  if (runtime === 'antigravity') command = '/sdd-new-project';
4634
6271
  if (runtime === 'cursor') command = 'sdd-new-project (mention the skill name)';
6272
+ if (runtime === 'windsurf') command = '/sdd-new-project';
6273
+ if (runtime === 'augment') command = '/sdd-new-project';
6274
+ if (runtime === 'trae') command = '/sdd-new-project';
6275
+ if (runtime === 'cline') command = '/sdd-new-project';
4635
6276
  console.log(`
4636
6277
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
4637
6278
 
4638
- ${cyan}Join the community:${reset} https://discord.gg/sdd
6279
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
4639
6280
  `);
4640
6281
  }
4641
6282
 
@@ -4690,71 +6331,6 @@ function handleStatusline(settings, isInteractive, callback) {
4690
6331
  });
4691
6332
  }
4692
6333
 
4693
- /**
4694
- * Install the SDD SDK globally via npm.
4695
- * @returns {boolean} true if install succeeded
4696
- */
4697
- function installSdk() {
4698
- const sdkVersion = pkg.version;
4699
- const sdkPkg = `@gsd-build/sdk@${sdkVersion}`;
4700
- console.log(`\n ${cyan}Installing SDD SDK...${reset}`);
4701
- console.log(` ${dim}npm install -g ${sdkPkg}${reset}\n`);
4702
- try {
4703
- require('child_process').execSync(`npm install -g ${sdkPkg}`, { stdio: 'inherit' });
4704
- console.log(`\n ${green}✓${reset} SDD SDK installed (${cyan}sdd-sdk${reset} command available)`);
4705
- return true;
4706
- } catch (e) {
4707
- console.log(`\n ${yellow}⚠${reset} SDK install failed: ${e.message}`);
4708
- console.log(` ${dim}You can install it manually: npm install -g ${sdkPkg}${reset}`);
4709
- return false;
4710
- }
4711
- }
4712
-
4713
- /**
4714
- * Prompt the user to optionally install the SDD SDK.
4715
- * Called after runtime installation completes.
4716
- * @param {Function} callback - called with true/false
4717
- */
4718
- function promptSdk(callback) {
4719
- if (!process.stdin.isTTY) {
4720
- callback(false);
4721
- return;
4722
- }
4723
-
4724
- const rl = readline.createInterface({
4725
- input: process.stdin,
4726
- output: process.stdout
4727
- });
4728
-
4729
- let answered = false;
4730
-
4731
- rl.on('close', () => {
4732
- if (!answered) {
4733
- answered = true;
4734
- callback(false);
4735
- }
4736
- });
4737
-
4738
- console.log(`
4739
- ${yellow}Also install the SDD SDK?${reset}
4740
-
4741
- The SDK provides a standalone CLI for autonomous execution:
4742
- ${dim}sdd-sdk init @prd.md${reset} Bootstrap a project from a PRD
4743
- ${dim}sdd-sdk auto${reset} Run full autonomous lifecycle
4744
- ${dim}sdd-sdk run "prompt"${reset} Execute a milestone from text
4745
-
4746
- ${cyan}1${reset}) No
4747
- ${cyan}2${reset}) Yes ${dim}(runs: npm install -g @gsd-build/sdk)${reset}
4748
- `);
4749
-
4750
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
4751
- answered = true;
4752
- rl.close();
4753
- const choice = answer.trim() || '1';
4754
- callback(choice === '2');
4755
- });
4756
- }
4757
-
4758
6334
  /**
4759
6335
  * Prompt for runtime selection
4760
6336
  */
@@ -4776,27 +6352,39 @@ function promptRuntime(callback) {
4776
6352
 
4777
6353
  const runtimeMap = {
4778
6354
  '1': 'claude',
4779
- '2': 'opencode',
4780
- '3': 'gemini',
4781
- '4': 'codex',
4782
- '5': 'copilot',
4783
- '6': 'antigravity',
4784
- '7': 'cursor',
4785
- '8': 'windsurf'
6355
+ '2': 'antigravity',
6356
+ '3': 'augment',
6357
+ '4': 'cline',
6358
+ '5': 'codebuddy',
6359
+ '6': 'codex',
6360
+ '7': 'copilot',
6361
+ '8': 'cursor',
6362
+ '9': 'gemini',
6363
+ '10': 'kilo',
6364
+ '11': 'opencode',
6365
+ '12': 'qwen',
6366
+ '13': 'trae',
6367
+ '14': 'windsurf'
4786
6368
  };
4787
- const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
6369
+ const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
4788
6370
 
4789
6371
  console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
4790
- ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
4791
- ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
4792
- ${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
4793
- ${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
4794
- ${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
4795
- ${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset}
4796
- ${cyan}8${reset}) Windsurf ${dim}(~/.windsurf)${reset}
4797
- ${cyan}9${reset}) All
4798
-
4799
- ${dim}Select multiple: 1,4,6 or 1 4 6${reset}
6372
+ ${cyan}2${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
6373
+ ${cyan}3${reset}) Augment ${dim}(~/.augment)${reset}
6374
+ ${cyan}4${reset}) Cline ${dim}(.clinerules)${reset}
6375
+ ${cyan}5${reset}) CodeBuddy ${dim}(~/.codebuddy)${reset}
6376
+ ${cyan}6${reset}) Codex ${dim}(~/.codex)${reset}
6377
+ ${cyan}7${reset}) Copilot ${dim}(~/.copilot)${reset}
6378
+ ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
6379
+ ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset}
6380
+ ${cyan}10${reset}) Kilo ${dim}(~/.config/kilo)${reset}
6381
+ ${cyan}11${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
6382
+ ${cyan}12${reset}) Qwen Code ${dim}(~/.qwen)${reset}
6383
+ ${cyan}13${reset}) Trae ${dim}(~/.trae)${reset}
6384
+ ${cyan}14${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
6385
+ ${cyan}15${reset}) All
6386
+
6387
+ ${dim}Select multiple: 1,2,6 or 1 2 6${reset}
4800
6388
  `);
4801
6389
 
4802
6390
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
@@ -4805,7 +6393,7 @@ function promptRuntime(callback) {
4805
6393
  const input = answer.trim() || '1';
4806
6394
 
4807
6395
  // "All" shortcut
4808
- if (input === '9') {
6396
+ if (input === '15') {
4809
6397
  callback(allRuntimes);
4810
6398
  return;
4811
6399
  }
@@ -4894,23 +6482,13 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
4894
6482
  result.statuslineCommand,
4895
6483
  useStatusline,
4896
6484
  result.runtime,
4897
- isGlobal
6485
+ isGlobal,
6486
+ result.configDir
4898
6487
  );
4899
6488
  }
4900
6489
  };
4901
6490
 
4902
- if (hasSdk) {
4903
- // --sdk flag: install without prompting
4904
- installSdk();
4905
- printSummaries();
4906
- } else if (isInteractive) {
4907
- promptSdk((wantsSdk) => {
4908
- if (wantsSdk) installSdk();
4909
- printSummaries();
4910
- });
4911
- } else {
4912
- printSummaries();
4913
- }
6491
+ printSummaries();
4914
6492
  };
4915
6493
 
4916
6494
  if (primaryStatuslineResult) {
@@ -4931,18 +6509,23 @@ if (process.env.SDD_TEST_MODE) {
4931
6509
  convertClaudeAgentToCodexAgent,
4932
6510
  generateCodexAgentToml,
4933
6511
  generateCodexConfigBlock,
4934
- stripGsdFromCodexConfig,
6512
+ stripSddFromCodexConfig,
4935
6513
  mergeCodexConfig,
4936
6514
  installCodexConfig,
4937
6515
  install,
6516
+ uninstall,
4938
6517
  convertClaudeCommandToCodexSkill,
4939
6518
  convertClaudeToOpencodeFrontmatter,
6519
+ convertClaudeToKiloFrontmatter,
6520
+ configureOpencodePermissions,
4940
6521
  neutralizeAgentReferences,
4941
6522
  SDD_CODEX_MARKER,
4942
6523
  CODEX_AGENT_SANDBOX,
4943
6524
  getDirName,
4944
6525
  getGlobalDir,
4945
6526
  getConfigDirFromHome,
6527
+ resolveKiloConfigPath,
6528
+ configureKiloPermissions,
4946
6529
  claudeToCopilotTools,
4947
6530
  convertCopilotToolName,
4948
6531
  convertClaudeToCopilotContent,
@@ -4952,58 +6535,75 @@ if (process.env.SDD_TEST_MODE) {
4952
6535
  SDD_COPILOT_INSTRUCTIONS_MARKER,
4953
6536
  SDD_COPILOT_INSTRUCTIONS_CLOSE_MARKER,
4954
6537
  mergeCopilotInstructions,
4955
- stripGsdFromCopilotInstructions,
6538
+ stripSddFromCopilotInstructions,
4956
6539
  convertClaudeToAntigravityContent,
4957
6540
  convertClaudeCommandToAntigravitySkill,
4958
6541
  convertClaudeAgentToAntigravityAgent,
4959
6542
  copyCommandsAsAntigravitySkills,
6543
+ convertClaudeCommandToClaudeSkill,
6544
+ copyCommandsAsClaudeSkills,
4960
6545
  convertClaudeToWindsurfMarkdown,
4961
6546
  convertClaudeCommandToWindsurfSkill,
4962
6547
  convertClaudeAgentToWindsurfAgent,
4963
6548
  copyCommandsAsWindsurfSkills,
6549
+ convertClaudeToAugmentMarkdown,
6550
+ convertClaudeCommandToAugmentSkill,
6551
+ convertClaudeAgentToAugmentAgent,
6552
+ copyCommandsAsAugmentSkills,
6553
+ convertClaudeToTraeMarkdown,
6554
+ convertClaudeCommandToTraeSkill,
6555
+ convertClaudeAgentToTraeAgent,
6556
+ copyCommandsAsTraeSkills,
6557
+ convertClaudeToCodebuddyMarkdown,
6558
+ convertClaudeCommandToCodebuddySkill,
6559
+ convertClaudeAgentToCodebuddyAgent,
6560
+ copyCommandsAsCodebuddySkills,
6561
+ convertClaudeToCliineMarkdown,
6562
+ convertClaudeAgentToClineAgent,
4964
6563
  writeManifest,
4965
6564
  reportLocalPatches,
4966
6565
  validateHookFields,
4967
- installSdk,
4968
- promptSdk,
6566
+ preserveUserArtifacts,
6567
+ restoreUserArtifacts,
6568
+ finishInstall,
4969
6569
  };
4970
6570
  } else {
4971
6571
 
4972
- // Main logic
4973
- if (hasGlobal && hasLocal) {
4974
- console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
4975
- process.exit(1);
4976
- } else if (explicitConfigDir && hasLocal) {
4977
- console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
4978
- process.exit(1);
4979
- } else if (hasUninstall) {
4980
- if (!hasGlobal && !hasLocal) {
4981
- console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
6572
+ // Main logic
6573
+ if (hasGlobal && hasLocal) {
6574
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
4982
6575
  process.exit(1);
4983
- }
4984
- const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
4985
- for (const runtime of runtimes) {
4986
- uninstall(hasGlobal, runtime);
4987
- }
4988
- } else if (selectedRuntimes.length > 0) {
4989
- if (!hasGlobal && !hasLocal) {
4990
- promptLocation(selectedRuntimes);
4991
- } else {
4992
- installAllRuntimes(selectedRuntimes, hasGlobal, false);
4993
- }
4994
- } else if (hasGlobal || hasLocal) {
4995
- // Default to Claude if no runtime specified but location is
4996
- installAllRuntimes(['claude'], hasGlobal, false);
4997
- } else {
4998
- // Interactive
4999
- if (!process.stdin.isTTY) {
5000
- console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
5001
- installAllRuntimes(['claude'], true, false);
6576
+ } else if (explicitConfigDir && hasLocal) {
6577
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
6578
+ process.exit(1);
6579
+ } else if (hasUninstall) {
6580
+ if (!hasGlobal && !hasLocal) {
6581
+ console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
6582
+ process.exit(1);
6583
+ }
6584
+ const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
6585
+ for (const runtime of runtimes) {
6586
+ uninstall(hasGlobal, runtime);
6587
+ }
6588
+ } else if (selectedRuntimes.length > 0) {
6589
+ if (!hasGlobal && !hasLocal) {
6590
+ promptLocation(selectedRuntimes);
6591
+ } else {
6592
+ installAllRuntimes(selectedRuntimes, hasGlobal, false);
6593
+ }
6594
+ } else if (hasGlobal || hasLocal) {
6595
+ // Default to Claude if no runtime specified but location is
6596
+ installAllRuntimes(['claude'], hasGlobal, false);
5002
6597
  } else {
5003
- promptRuntime((runtimes) => {
5004
- promptLocation(runtimes);
5005
- });
6598
+ // Interactive
6599
+ if (!process.stdin.isTTY) {
6600
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
6601
+ installAllRuntimes(['claude'], true, false);
6602
+ } else {
6603
+ promptRuntime((runtimes) => {
6604
+ promptLocation(runtimes);
6605
+ });
6606
+ }
5006
6607
  }
5007
- }
5008
6608
 
5009
6609
  } // end of else block for SDD_TEST_MODE