@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.3

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 (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-mempalace-curator.md +47 -0
  3. package/bin/install.js +25 -7
  4. package/commands/gsd/mempalace-capture.md +71 -0
  5. package/commands/gsd/mempalace-recall.md +102 -0
  6. package/commands/gsd/ns-context.md +4 -2
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +110 -92
  9. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  10. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  11. package/gsd-core/bin/lib/capability-state.cjs +31 -18
  12. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  13. package/gsd-core/bin/lib/clusters.cjs +2 -0
  14. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  15. package/gsd-core/bin/lib/commands.cjs +33 -10
  16. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  17. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  18. package/gsd-core/bin/lib/config.cjs +1 -2
  19. package/gsd-core/bin/lib/core.cjs +5 -2
  20. package/gsd-core/bin/lib/init.cjs +8 -5
  21. package/gsd-core/bin/lib/install-profiles.cjs +2 -1
  22. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  23. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  24. package/gsd-core/bin/lib/phase.cjs +71 -11
  25. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  26. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  27. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  28. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
  29. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
  30. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  31. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  32. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  33. package/gsd-core/bin/lib/state-document.cjs +46 -1
  34. package/gsd-core/bin/lib/state.cjs +221 -35
  35. package/gsd-core/bin/lib/verify.cjs +19 -4
  36. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  37. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  38. package/gsd-core/bin/shared/model-catalog.json +1 -0
  39. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  40. package/gsd-core/templates/config.json +1 -1
  41. package/gsd-core/workflows/audit-milestone.md +5 -3
  42. package/gsd-core/workflows/autonomous.md +10 -5
  43. package/gsd-core/workflows/code-review-fix.md +9 -7
  44. package/gsd-core/workflows/code-review.md +8 -6
  45. package/gsd-core/workflows/discuss-phase.md +14 -0
  46. package/gsd-core/workflows/execute-phase.md +82 -143
  47. package/gsd-core/workflows/help/modes/full.md +3 -1
  48. package/gsd-core/workflows/new-project.md +3 -3
  49. package/gsd-core/workflows/plan-phase.md +89 -221
  50. package/gsd-core/workflows/profile-user.md +3 -1
  51. package/gsd-core/workflows/progress.md +58 -1
  52. package/gsd-core/workflows/quick.md +9 -6
  53. package/gsd-core/workflows/review.md +19 -2
  54. package/gsd-core/workflows/secure-phase.md +4 -2
  55. package/gsd-core/workflows/settings-advanced.md +2 -0
  56. package/gsd-core/workflows/ship.md +57 -0
  57. package/gsd-core/workflows/validate-phase.md +4 -2
  58. package/gsd-core/workflows/verify-work.md +27 -11
  59. package/package.json +3 -2
  60. package/scripts/affected-tests-lib.cjs +16 -4
  61. package/scripts/gen-capability-registry.cjs +359 -48
  62. package/scripts/gen-inventory-manifest.cjs +1 -4
  63. package/scripts/gen-loop-host-contract.cjs +55 -0
  64. package/scripts/issue-version-gate.cjs +140 -0
  65. package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
  66. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  67. package/scripts/lint-test-file-count.allowlist.json +8 -0
  68. package/scripts/release-tarball-smoke.cjs +7 -1
  69. package/scripts/run-tests.cjs +74 -14
@@ -0,0 +1,1700 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment,
2
+ @typescript-eslint/no-require-imports,
3
+ @typescript-eslint/no-unsafe-assignment,
4
+ @typescript-eslint/no-unsafe-member-access,
5
+ @typescript-eslint/no-unsafe-return,
6
+ @typescript-eslint/no-unsafe-call,
7
+ @typescript-eslint/no-unsafe-argument */
8
+ // Mechanical extraction from bin/install.js; keep behavior parity before typing.
9
+ // @ts-nocheck
10
+ 'use strict';
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ /**
15
+ * Runtime Artifact Conversion Module.
16
+ *
17
+ * First slice: layout-reached command/skill artifact converters moved out of
18
+ * bin/install.js so Runtime Artifact Layout no longer reaches through the
19
+ * Installer Module for conversion behavior.
20
+ */
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const commandRoster = require("./command-roster.cjs");
23
+ const { readGsdCommandNames, transformContentToHyphen } = commandRoster;
24
+ const pkg = require('../../../package.json');
25
+ const colorNameToHex = {
26
+ cyan: '#00FFFF',
27
+ red: '#FF0000',
28
+ green: '#00FF00',
29
+ blue: '#0000FF',
30
+ yellow: '#FFFF00',
31
+ magenta: '#FF00FF',
32
+ orange: '#FFA500',
33
+ purple: '#800080',
34
+ pink: '#FFC0CB',
35
+ white: '#FFFFFF',
36
+ black: '#000000',
37
+ gray: '#808080',
38
+ grey: '#808080',
39
+ };
40
+ // Tool name mapping from Claude Code to OpenCode
41
+ // OpenCode uses lowercase tool names; special mappings for renamed tools
42
+ const claudeToOpencodeTools = {
43
+ AskUserQuestion: 'question',
44
+ SlashCommand: 'skill',
45
+ TodoWrite: 'todowrite',
46
+ WebFetch: 'webfetch',
47
+ WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
48
+ };
49
+ // Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
50
+ // Kimi custom agent YAML requires fully-qualified module paths.
51
+ const claudeToKimiTools = {
52
+ Read: 'kimi_cli.tools.file:ReadFile',
53
+ ReadFile: 'kimi_cli.tools.file:ReadFile',
54
+ Write: 'kimi_cli.tools.file:WriteFile',
55
+ WriteFile: 'kimi_cli.tools.file:WriteFile',
56
+ Edit: 'kimi_cli.tools.file:StrReplaceFile',
57
+ MultiEdit: 'kimi_cli.tools.file:StrReplaceFile',
58
+ StrReplaceFile: 'kimi_cli.tools.file:StrReplaceFile',
59
+ Bash: 'kimi_cli.tools.shell:Shell',
60
+ Shell: 'kimi_cli.tools.shell:Shell',
61
+ Grep: 'kimi_cli.tools.file:Grep',
62
+ Glob: 'kimi_cli.tools.file:Glob',
63
+ Agent: 'kimi_cli.tools.agent:Agent',
64
+ Task: 'kimi_cli.tools.agent:Agent',
65
+ AskUserQuestion: 'kimi_cli.tools.ask_user:AskUserQuestion',
66
+ TodoWrite: 'kimi_cli.tools.todo:SetTodoList',
67
+ SetTodoList: 'kimi_cli.tools.todo:SetTodoList',
68
+ WebSearch: 'kimi_cli.tools.web:SearchWeb',
69
+ SearchWeb: 'kimi_cli.tools.web:SearchWeb',
70
+ WebFetch: 'kimi_cli.tools.web:FetchURL',
71
+ FetchURL: 'kimi_cli.tools.web:FetchURL',
72
+ ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile',
73
+ TaskList: 'kimi_cli.tools.background:TaskList',
74
+ TaskOutput: 'kimi_cli.tools.background:TaskOutput',
75
+ TaskStop: 'kimi_cli.tools.background:TaskStop',
76
+ };
77
+ /**
78
+ * Convert a Claude Code tool name to OpenCode format
79
+ * - Applies special mappings (AskUserQuestion -> question, etc.)
80
+ * - Converts to lowercase (except MCP tools which keep their format)
81
+ */
82
+ function convertToolName(claudeTool) {
83
+ // Check for special mapping first
84
+ if (claudeToOpencodeTools[claudeTool]) {
85
+ return claudeToOpencodeTools[claudeTool];
86
+ }
87
+ // MCP tools (mcp__*) keep their format
88
+ if (claudeTool.startsWith('mcp__')) {
89
+ return claudeTool;
90
+ }
91
+ // Default: convert to lowercase
92
+ return claudeTool.toLowerCase();
93
+ }
94
+ function createKimiToolDiagnostic(reason, tool, source = null) {
95
+ const isMcp = reason === 'mcp_managed';
96
+ return {
97
+ level: 'warning',
98
+ code: isMcp ? 'kimi_mcp_tool_excluded' : 'kimi_unsupported_tool',
99
+ reason,
100
+ message: isMcp
101
+ ? `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.`
102
+ : `Tool '${tool}' is not supported by the Kimi tool mapper.`,
103
+ value: tool,
104
+ source,
105
+ };
106
+ }
107
+ /**
108
+ * Convert a Claude/GSD tool name to a Kimi CLI module path.
109
+ * @returns {string|null} Kimi module path, or null when excluded/unsupported.
110
+ */
111
+ function convertKimiToolName(claudeTool) {
112
+ const tool = String(claudeTool || '').trim();
113
+ if (!tool)
114
+ return null;
115
+ if (tool.startsWith('mcp__'))
116
+ return null;
117
+ return claudeToKimiTools[tool] || null;
118
+ }
119
+ function mapClaudeToolsToKimiTools(claudeTools, options = {}) {
120
+ const diagnostics = [];
121
+ const tools = [];
122
+ const seen = new Set();
123
+ const source = options && Object.prototype.hasOwnProperty.call(options, 'source')
124
+ ? options.source
125
+ : null;
126
+ for (const rawTool of Array.isArray(claudeTools) ? claudeTools : []) {
127
+ const tool = String(rawTool || '').trim();
128
+ if (!tool)
129
+ continue;
130
+ if (tool.startsWith('mcp__')) {
131
+ diagnostics.push(createKimiToolDiagnostic('mcp_managed', tool, source));
132
+ continue;
133
+ }
134
+ const kimiTool = convertKimiToolName(tool);
135
+ if (!kimiTool) {
136
+ diagnostics.push(createKimiToolDiagnostic('unsupported_tool', tool, source));
137
+ continue;
138
+ }
139
+ if (!seen.has(kimiTool)) {
140
+ seen.add(kimiTool);
141
+ tools.push(kimiTool);
142
+ }
143
+ }
144
+ return { tools, diagnostics };
145
+ }
146
+ const claudeToKiloAgentPermissions = {
147
+ Read: 'read',
148
+ Write: 'edit',
149
+ Edit: 'edit',
150
+ Bash: 'bash',
151
+ Grep: 'grep',
152
+ Glob: 'glob',
153
+ Task: 'task',
154
+ WebFetch: 'webfetch',
155
+ WebSearch: 'websearch',
156
+ TodoWrite: 'todowrite',
157
+ AskUserQuestion: 'question',
158
+ SlashCommand: 'skill',
159
+ };
160
+ const kiloAgentPermissionOrder = [
161
+ 'read',
162
+ 'edit',
163
+ 'bash',
164
+ 'grep',
165
+ 'glob',
166
+ 'task',
167
+ 'webfetch',
168
+ 'websearch',
169
+ 'skill',
170
+ 'question',
171
+ 'todowrite',
172
+ 'list',
173
+ 'codesearch',
174
+ 'lsp',
175
+ ];
176
+ function convertClaudeToKiloPermissionTool(claudeTool) {
177
+ return claudeToKiloAgentPermissions[claudeTool] || null;
178
+ }
179
+ function buildKiloAgentPermissionBlock(claudeTools) {
180
+ const allowedPermissions = new Set();
181
+ for (const tool of claudeTools) {
182
+ const mapped = convertClaudeToKiloPermissionTool(tool);
183
+ if (mapped) {
184
+ allowedPermissions.add(mapped);
185
+ }
186
+ }
187
+ const lines = ['permission:'];
188
+ for (const permission of kiloAgentPermissionOrder) {
189
+ lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`);
190
+ }
191
+ return lines;
192
+ }
193
+ function escapeRegExp(value) {
194
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
195
+ }
196
+ function replaceRelativePathReference(content, fromPath, toPath) {
197
+ const escapedPath = escapeRegExp(fromPath);
198
+ return content.replace(new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'), (_, prefix) => `${prefix}${toPath}`);
199
+ }
200
+ /**
201
+ * Apply Copilot-specific content conversion — CONV-06 (paths) + CONV-07 (command names).
202
+ * Path mappings depend on install mode:
203
+ * Global: ~/.claude/ → ~/.copilot/, ./.claude/ → ./.github/
204
+ * Local: ~/.claude/ → ./.github/, ./.claude/ → ./.github/
205
+ * Applied to ALL Copilot content (skills, agents, engine files).
206
+ * @param {string} content - Source content to convert
207
+ * @param {boolean} [isGlobal=false] - Whether this is a global install
208
+ */
209
+ function convertClaudeToCopilotContent(content, isGlobal = false) {
210
+ let c = content;
211
+ // CONV-06: Path replacement — most specific first to avoid substring matches.
212
+ // Handle both `~/.claude/foo` (trailing slash) and bare `~/.claude` forms in
213
+ // one pass via a capture group, matching the approach used by Antigravity,
214
+ // OpenCode, Kilo, and Codex converters (issue #2545).
215
+ if (isGlobal) {
216
+ c = c.replace(/\$HOME\/\.claude(\/|\b)/g, '$HOME/.copilot$1');
217
+ c = c.replace(/~\/\.claude(\/|\b)/g, '~/.copilot$1');
218
+ }
219
+ else {
220
+ c = c.replace(/\$HOME\/\.claude\//g, '.github/');
221
+ c = c.replace(/~\/\.claude\//g, '.github/');
222
+ c = c.replace(/\$HOME\/\.claude\b/g, '.github');
223
+ c = c.replace(/~\/\.claude\b/g, '.github');
224
+ }
225
+ c = c.replace(/\.\/\.claude\//g, './.github/');
226
+ c = c.replace(/\.claude\//g, '.github/');
227
+ // CONV-07: Command name conversion (all gsd: references → gsd-)
228
+ c = c.replace(/gsd:/g, 'gsd-');
229
+ // Runtime-neutral agent name replacement (#766)
230
+ c = neutralizeAgentReferences(c, 'copilot-instructions.md');
231
+ return c;
232
+ }
233
+ /**
234
+ * Convert a Claude command (.md) to a Copilot skill (SKILL.md).
235
+ * Transforms frontmatter only — body passes through with CONV-06/07 applied.
236
+ * Skills keep original tool names (no mapping) per CONTEXT.md decision.
237
+ */
238
+ // isGlobal is the 5th positional arg (3rd/4th are runtime/cmdNames passed by the skills wrapper). See runtime-artifact-layout skillsKind.
239
+ function convertClaudeCommandToCopilotSkill(content, skillName, _runtime = null, _cmdNames = null, isGlobal = false) {
240
+ const converted = convertClaudeToCopilotContent(content, isGlobal);
241
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
242
+ if (!frontmatter)
243
+ return converted;
244
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
245
+ const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
246
+ const agent = extractFrontmatterField(frontmatter, 'agent');
247
+ // CONV-02: Extract allowed-tools YAML multiline list → comma-separated string
248
+ const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
249
+ let toolsLine = '';
250
+ if (toolsMatch) {
251
+ const tools = toolsMatch[1].match(/^\s+-\s+(.+)/gm);
252
+ if (tools) {
253
+ toolsLine = tools.map(t => t.replace(/^\s+-\s+/, '').trim()).join(', ');
254
+ }
255
+ }
256
+ // Reconstruct frontmatter in Copilot format
257
+ // #2876: descriptions starting with a YAML flow indicator (`[BETA] …`,
258
+ // `{ … }`, `*ref`, `&anchor`, etc.) parse as flow sequences/mappings and
259
+ // crash gh-copilot's frontmatter loader. Always quote so any leading
260
+ // character is parser-safe.
261
+ let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
262
+ if (argumentHint)
263
+ fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
264
+ if (agent)
265
+ fm += `agent: ${agent}\n`;
266
+ if (toolsLine)
267
+ fm += `allowed-tools: ${toolsLine}\n`;
268
+ fm += '---';
269
+ return `${fm}\n${body}`;
270
+ }
271
+ /**
272
+ * Map a skill directory name (gsd-<cmd>) to the frontmatter `name:` used
273
+ * by Claude Code as the skill identity. Emits the hyphen form (gsd-<cmd>)
274
+ * so Claude Code autocomplete shows the canonical invocation form, not the
275
+ * deprecated colon form. See #2808.
276
+ *
277
+ * Historical note: this previously returned `gsd:<cmd>` (colon) because
278
+ * workflows called Skill(skill="gsd:<cmd>"). Those calls have been updated
279
+ * to use hyphen form (#2808) so the colon rewrite is no longer needed.
280
+ *
281
+ * Codex must NOT use this helper: its adapter invokes skills as `$gsd-<cmd>`
282
+ * (shell-var syntax) — hyphen form is already correct there.
283
+ */
284
+ function skillFrontmatterName(skillDirName) {
285
+ if (typeof skillDirName !== 'string')
286
+ return skillDirName;
287
+ // Return the hyphen form as-is (gsd-<cmd>) — canonical since #2808.
288
+ return skillDirName;
289
+ }
290
+ /**
291
+ * Qwen Code skills accept an optional numeric `priority` frontmatter field.
292
+ * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
293
+ * #778): HIGHER values sort EARLIER in the `/skills` TUI listing (omitted ≈ 0;
294
+ * negatives sort below unset). It affects ONLY the `/skills` list order —
295
+ * slash-command completion and the `/help` view stay alphabetical.
296
+ *
297
+ * We assign descending priorities to GSD's main-loop commands so the most-used
298
+ * workflow skills surface first; utility skills are deliberately left unset
299
+ * (default 0) and sort below.
300
+ *
301
+ * NOTE: the #778 issue body proposed the INVERSE numbering (plan-phase: 10,
302
+ * utilities: 90+). The verified spec shows that would BURY the core loop below
303
+ * utilities, so we implement the spec-correct direction (core = high) instead.
304
+ * Keyed by command stem (skill dir is `gsd-<stem>`).
305
+ */
306
+ const QWEN_SKILL_PRIORITY = Object.freeze({
307
+ 'new-project': 100,
308
+ 'discuss-phase': 95,
309
+ 'plan-phase': 90,
310
+ 'execute-phase': 85,
311
+ progress: 80,
312
+ 'verify-work': 75,
313
+ phase: 70,
314
+ review: 65,
315
+ ship: 60,
316
+ config: 55,
317
+ surface: 50,
318
+ 'resume-work': 45,
319
+ 'pause-work': 40,
320
+ help: 35,
321
+ update: 30,
322
+ });
323
+ /**
324
+ * Convert a Claude command (.md) to a Claude skill (SKILL.md).
325
+ * Claude Code is the native format, so minimal conversion needed —
326
+ * preserve allowed-tools as YAML multiline list, preserve argument-hint.
327
+ * Emits `name: gsd-<cmd>` (hyphen) so Skill(skill="gsd-<cmd>") calls and
328
+ * tab autocomplete use the canonical command namespace.
329
+ */
330
+ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, cmdNames = null) {
331
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
332
+ if (!frontmatter)
333
+ return content;
334
+ // #3583: rewrite any /gsd:<cmd> or gsd:<cmd> in the body to the canonical
335
+ // hyphen form (gsd-<cmd>) so installed SKILL.md bodies match the hyphen
336
+ // `name:` Claude Code (and Qwen/Hermes) register under (#2808). `cmdNames`
337
+ // is optional and pre-computed by the caller for performance; direct test
338
+ // calls fall back to reading the list.
339
+ const names = cmdNames || readGsdCommandNames();
340
+ const normalizedBody = transformContentToHyphen(body, names);
341
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
342
+ const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
343
+ const agent = extractFrontmatterField(frontmatter, 'agent');
344
+ // #769: preserve context: and effort: from source command files so they
345
+ // are emitted into the installed SKILL.md frontmatter unchanged.
346
+ const context = extractFrontmatterField(frontmatter, 'context');
347
+ const effort = extractFrontmatterField(frontmatter, 'effort');
348
+ // Preserve allowed-tools as YAML multiline list (Claude native format)
349
+ const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
350
+ let toolsBlock = '';
351
+ if (toolsMatch) {
352
+ toolsBlock = 'allowed-tools:\n' + toolsMatch[1];
353
+ // Ensure trailing newline
354
+ if (!toolsBlock.endsWith('\n'))
355
+ toolsBlock += '\n';
356
+ }
357
+ // Reconstruct frontmatter in Claude skill format
358
+ const frontmatterName = skillFrontmatterName(skillName);
359
+ let fm = `---\nname: ${frontmatterName}\ndescription: ${yamlQuote(description)}\n`;
360
+ // Hermes' SKILL.md spec lists `version` as a required frontmatter field.
361
+ // Track GSD's package version so Hermes' skill_view() reports a stable
362
+ // identifier per install.
363
+ if (runtime === 'hermes')
364
+ fm += `version: ${yamlQuote(pkg.version)}\n`;
365
+ // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
366
+ // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
367
+ // we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
368
+ if (runtime === 'qwen') {
369
+ const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
370
+ ? skillName.slice(4)
371
+ : skillName;
372
+ const priority = Object.prototype.hasOwnProperty.call(QWEN_SKILL_PRIORITY, stem)
373
+ ? QWEN_SKILL_PRIORITY[stem]
374
+ : undefined;
375
+ if (typeof priority === 'number')
376
+ fm += `priority: ${priority}\n`;
377
+ }
378
+ if (argumentHint)
379
+ fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
380
+ if (agent)
381
+ fm += `agent: ${agent}\n`;
382
+ // #769: emit context: and effort: when present so the runtime can honour
383
+ // them natively (context: fork = isolated subagent window; effort: =
384
+ // token-budget tier). Fields are Claude-specific; unknown frontmatter
385
+ // fields are silently ignored by other runtimes (backward-compatible).
386
+ if (context)
387
+ fm += `context: ${context}\n`;
388
+ if (effort)
389
+ fm += `effort: ${effort}\n`;
390
+ if (toolsBlock)
391
+ fm += toolsBlock;
392
+ fm += '---';
393
+ return `${fm}\n${normalizedBody}`;
394
+ }
395
+ function normalizeKimiSkillName(skillName) {
396
+ let text = String(skillName || '').trim().toLowerCase();
397
+ if (text.startsWith('/'))
398
+ text = text.slice(1);
399
+ if (text.startsWith('$'))
400
+ text = text.slice(1);
401
+ text = text.replace(/^gsd:/, 'gsd-');
402
+ if (!text.startsWith('gsd-'))
403
+ text = `gsd-${text}`;
404
+ text = text.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
405
+ return text || 'gsd-command';
406
+ }
407
+ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
408
+ if (!Array.isArray(cmdNames) || cmdNames.length === 0)
409
+ return content;
410
+ const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp);
411
+ const commandGroup = commands.join('|');
412
+ const colonPattern = new RegExp(`(?<![A-Za-z0-9_/:.-])/?gsd:(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
413
+ const hyphenPattern = new RegExp(`(?:/|\\$)gsd-(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
414
+ return content
415
+ .replace(colonPattern, (_, cmd) => `/skill:gsd-${cmd}`)
416
+ .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
417
+ }
418
+ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
419
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
420
+ const kimiSkillName = normalizeKimiSkillName(skillName);
421
+ const names = cmdNames || readGsdCommandNames();
422
+ const description = frontmatter
423
+ ? extractFrontmatterField(frontmatter, 'description') || `Run GSD workflow ${kimiSkillName}.`
424
+ : `Run GSD workflow ${kimiSkillName}.`;
425
+ const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations(frontmatter ? body : content, names);
426
+ return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`;
427
+ }
428
+ const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/;
429
+ function parseKimiAgentSource(source) {
430
+ if (typeof source === 'string') {
431
+ return {
432
+ path: null,
433
+ content: source,
434
+ };
435
+ }
436
+ if (!source || typeof source !== 'object' || typeof source.content !== 'string') {
437
+ return null;
438
+ }
439
+ return {
440
+ path: typeof source.path === 'string' ? source.path : null,
441
+ content: source.content,
442
+ };
443
+ }
444
+ function parseFrontmatterTools(frontmatter) {
445
+ if (!frontmatter)
446
+ return [];
447
+ const lines = frontmatter.split(/\r?\n/);
448
+ const tools = [];
449
+ let collecting = false;
450
+ for (const line of lines) {
451
+ const trimmed = line.trim();
452
+ if (!trimmed)
453
+ continue;
454
+ if (collecting) {
455
+ if (trimmed.startsWith('- ')) {
456
+ tools.push(trimmed.slice(2).trim());
457
+ continue;
458
+ }
459
+ collecting = false;
460
+ }
461
+ if (trimmed === 'tools:' || trimmed === 'allowed-tools:') {
462
+ collecting = true;
463
+ continue;
464
+ }
465
+ if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) {
466
+ const value = trimmed.slice(trimmed.indexOf(':') + 1).trim();
467
+ if (value) {
468
+ for (const tool of value.split(',')) {
469
+ const name = tool.trim();
470
+ if (name)
471
+ tools.push(name);
472
+ }
473
+ }
474
+ else {
475
+ collecting = true;
476
+ }
477
+ }
478
+ }
479
+ return tools;
480
+ }
481
+ function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) {
482
+ diagnostics.push({
483
+ level: 'warning',
484
+ code,
485
+ message,
486
+ value,
487
+ source,
488
+ });
489
+ }
490
+ function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) {
491
+ const result = mapClaudeToolsToKimiTools(toolNames, { source: sourceName });
492
+ diagnostics.push(...result.diagnostics);
493
+ return result.tools;
494
+ }
495
+ function neutralizeKimiAgentPrompt(content) {
496
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
497
+ let prompt = frontmatter ? body : content;
498
+ prompt = neutralizeAgentReferences(prompt, 'AGENTS.md');
499
+ prompt = prompt.replace(/~\/\.claude\/gsd-core\b/g, 'GSD core');
500
+ prompt = prompt.replace(/\$HOME\/\.claude\/gsd-core\b/g, 'GSD core');
501
+ return prompt.replace(/^\s*\r?\n/, '');
502
+ }
503
+ function pushKimiToolsYaml(lines, indent, tools) {
504
+ const prefix = ' '.repeat(indent);
505
+ if (!Array.isArray(tools) || tools.length === 0) {
506
+ lines.push(`${prefix}tools: []`);
507
+ return;
508
+ }
509
+ lines.push(`${prefix}tools:`);
510
+ for (const tool of tools) {
511
+ lines.push(`${prefix} - ${yamlQuote(tool)}`);
512
+ }
513
+ }
514
+ function buildKimiRootAgentYaml({ description, tools, subagents }) {
515
+ const lines = [
516
+ 'version: 1',
517
+ 'agent:',
518
+ ' name: gsd',
519
+ ` description: ${yamlQuote(toSingleLine(description || 'Run GSD workflows in Kimi CLI.'))}`,
520
+ ' extend: default',
521
+ ' system_prompt_path: ./gsd.md',
522
+ ];
523
+ pushKimiToolsYaml(lines, 2, tools);
524
+ if (subagents.length > 0) {
525
+ lines.push(' subagents:');
526
+ for (const subagent of subagents) {
527
+ lines.push(` ${subagent.name}:`);
528
+ lines.push(` path: ./subagents/${subagent.name}.yaml`);
529
+ lines.push(` description: ${yamlQuote(toSingleLine(subagent.description))}`);
530
+ }
531
+ }
532
+ return `${lines.join('\n')}\n`;
533
+ }
534
+ function buildKimiSubagentYaml({ name, description, tools }) {
535
+ const lines = [
536
+ 'version: 1',
537
+ 'agent:',
538
+ ` name: ${name}`,
539
+ ` description: ${yamlQuote(toSingleLine(description || `Run ${name}.`))}`,
540
+ ` system_prompt_path: ./${name}.md`,
541
+ ];
542
+ pushKimiToolsYaml(lines, 2, tools);
543
+ return `${lines.join('\n')}\n`;
544
+ }
545
+ function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], requestedSubagents = null, } = {}) {
546
+ const diagnostics = [];
547
+ const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' };
548
+ const { frontmatter: rootFrontmatter } = extractFrontmatterAndBody(rootSource.content);
549
+ const rootDescription = rootFrontmatter
550
+ ? extractFrontmatterField(rootFrontmatter, 'description') || 'Run GSD workflows in Kimi CLI.'
551
+ : 'Run GSD workflows in Kimi CLI.';
552
+ const subagentSources = Array.isArray(subagents) ? subagents : [];
553
+ if (!Array.isArray(subagents)) {
554
+ addKimiAgentDiagnostic(diagnostics, 'kimi_unsupported_subagents_input', 'Subagents input must be an array of Markdown strings or source objects.', typeof subagents, null);
555
+ }
556
+ const subagentMap = new Map();
557
+ for (const source of subagentSources) {
558
+ const parsed = parseKimiAgentSource(source);
559
+ if (!parsed) {
560
+ addKimiAgentDiagnostic(diagnostics, 'kimi_unsupported_subagent_input', 'Subagent source must be a Markdown string or an object with content.', typeof source, null);
561
+ continue;
562
+ }
563
+ const { frontmatter } = extractFrontmatterAndBody(parsed.content);
564
+ const fallbackName = parsed.path ? node_path_1.default.basename(parsed.path, node_path_1.default.extname(parsed.path)) : null;
565
+ const name = frontmatter
566
+ ? extractFrontmatterField(frontmatter, 'name') || fallbackName
567
+ : fallbackName;
568
+ if (!name || !KIMI_CANONICAL_GSD_AGENT_RE.test(name)) {
569
+ addKimiAgentDiagnostic(diagnostics, 'kimi_invalid_subagent_name', 'Subagent source does not use a canonical gsd-* Kimi agent name.', name || '(missing)', parsed.path);
570
+ continue;
571
+ }
572
+ const description = frontmatter
573
+ ? extractFrontmatterField(frontmatter, 'description') || `Run ${name}.`
574
+ : `Run ${name}.`;
575
+ const tools = mapKimiAgentContractTools(parseFrontmatterTools(frontmatter), diagnostics, name);
576
+ subagentMap.set(name, {
577
+ name,
578
+ description,
579
+ tools,
580
+ prompt: neutralizeKimiAgentPrompt(parsed.content),
581
+ });
582
+ }
583
+ const requested = Array.isArray(requestedSubagents) && requestedSubagents.length > 0
584
+ ? requestedSubagents
585
+ : [...subagentMap.keys()];
586
+ const selectedSubagents = [];
587
+ for (const requestedName of requested) {
588
+ if (subagentMap.has(requestedName)) {
589
+ selectedSubagents.push(subagentMap.get(requestedName));
590
+ continue;
591
+ }
592
+ addKimiAgentDiagnostic(diagnostics, 'kimi_unknown_subagent', 'Requested subagent was not generated and will not be emitted in Kimi YAML.', requestedName, null);
593
+ }
594
+ const rootTools = mapKimiAgentContractTools(parseFrontmatterTools(rootFrontmatter), diagnostics, 'gsd');
595
+ if (selectedSubagents.length > 0 && !rootTools.includes('kimi_cli.tools.agent:Agent')) {
596
+ rootTools.push('kimi_cli.tools.agent:Agent');
597
+ }
598
+ return {
599
+ root: {
600
+ name: 'gsd',
601
+ yamlPath: 'agents/gsd.yaml',
602
+ promptPath: 'agents/gsd.md',
603
+ yaml: buildKimiRootAgentYaml({
604
+ description: rootDescription,
605
+ tools: rootTools,
606
+ subagents: selectedSubagents,
607
+ }),
608
+ prompt: neutralizeKimiAgentPrompt(rootSource.content),
609
+ },
610
+ subagents: selectedSubagents.map((subagent) => ({
611
+ name: subagent.name,
612
+ yamlPath: `agents/subagents/${subagent.name}.yaml`,
613
+ promptPath: `agents/subagents/${subagent.name}.md`,
614
+ yaml: buildKimiSubagentYaml(subagent),
615
+ prompt: subagent.prompt,
616
+ })),
617
+ diagnostics,
618
+ };
619
+ }
620
+ /**
621
+ * Apply Antigravity-specific content conversion — path replacement + command name conversion.
622
+ * Path mappings depend on install mode:
623
+ * Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
624
+ * Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/
625
+ * Applied to ALL Antigravity content (skills, agents, engine files).
626
+ * @param {string} content - Source content to convert
627
+ * @param {boolean} [isGlobal=false] - Whether this is a global install
628
+ */
629
+ function convertClaudeToAntigravityContent(content, isGlobal = false) {
630
+ let c = content;
631
+ if (isGlobal) {
632
+ c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
633
+ c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
634
+ // Bare form (no trailing slash) — must come after slash form to avoid double-replace
635
+ c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity');
636
+ c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity');
637
+ }
638
+ else {
639
+ c = c.replace(/\$HOME\/\.claude\//g, '.agents/');
640
+ c = c.replace(/~\/\.claude\//g, '.agents/');
641
+ // Bare form (no trailing slash) — must come after slash form to avoid double-replace
642
+ c = c.replace(/\$HOME\/\.claude\b/g, '.agents');
643
+ c = c.replace(/~\/\.claude\b/g, '.agents');
644
+ }
645
+ c = c.replace(/\.\/\.claude\//g, './.agents/');
646
+ c = c.replace(/\.claude\//g, '.agents/');
647
+ // Command name conversion (all gsd: references → gsd-)
648
+ c = c.replace(/gsd:/g, 'gsd-');
649
+ // Runtime-neutral agent name replacement (#766)
650
+ c = neutralizeAgentReferences(c, 'GEMINI.md');
651
+ return c;
652
+ }
653
+ /**
654
+ * Convert a Claude command (.md) to an Antigravity skill (SKILL.md).
655
+ * Transforms frontmatter to minimal name + description only.
656
+ * Body passes through with path/command conversions applied.
657
+ */
658
+ // isGlobal is the 5th positional arg (3rd/4th are runtime/cmdNames passed by the skills wrapper). See runtime-artifact-layout skillsKind.
659
+ function convertClaudeCommandToAntigravitySkill(content, skillName, _runtime = null, _cmdNames = null, isGlobal = false) {
660
+ const converted = convertClaudeToAntigravityContent(content, isGlobal);
661
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
662
+ if (!frontmatter)
663
+ return converted;
664
+ const name = skillName || extractFrontmatterField(frontmatter, 'name') || 'unknown';
665
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
666
+ // #2876: quote description so YAML flow indicators in the source
667
+ // (e.g. `[BETA] …`) don't break downstream frontmatter parsers.
668
+ const fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---`;
669
+ return `${fm}\n${body}`;
670
+ }
671
+ function toSingleLine(value) {
672
+ return value.replace(/\s+/g, ' ').trim();
673
+ }
674
+ function yamlQuote(value) {
675
+ return JSON.stringify(value);
676
+ }
677
+ function yamlIdentifier(value) {
678
+ const text = String(value).trim();
679
+ if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) {
680
+ return text;
681
+ }
682
+ return yamlQuote(text);
683
+ }
684
+ function extractFrontmatterAndBody(content) {
685
+ if (!content.startsWith('---')) {
686
+ return { frontmatter: null, body: content };
687
+ }
688
+ const endIndex = content.indexOf('---', 3);
689
+ if (endIndex === -1) {
690
+ return { frontmatter: null, body: content };
691
+ }
692
+ return {
693
+ frontmatter: content.substring(3, endIndex).trim(),
694
+ body: content.substring(endIndex + 3),
695
+ };
696
+ }
697
+ function extractFrontmatterField(frontmatter, fieldName) {
698
+ const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, 'm');
699
+ const match = frontmatter.match(regex);
700
+ if (!match)
701
+ return null;
702
+ return match[1].trim().replace(/^['"]|['"]$/g, '');
703
+ }
704
+ function convertSlashCommandsToCursorSkillMentions(content) {
705
+ // Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
706
+ // This preserves rendered "next step" commands like "/gsd-execute-phase 17".
707
+ return content.replace(/gsd:/gi, 'gsd-');
708
+ }
709
+ function convertClaudeToCursorMarkdown(content) {
710
+ let converted = convertSlashCommandsToCursorSkillMentions(content);
711
+ // Replace tool name references in body text
712
+ converted = converted.replace(/\bBash\(/g, 'Shell(');
713
+ converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
714
+ converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
715
+ // Replace subagent_type from Claude to Cursor format
716
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
717
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
718
+ // Replace project-level Claude conventions with Cursor equivalents
719
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`');
720
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/');
721
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`');
722
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/');
723
+ converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/');
724
+ // Remove Claude Code-specific bug workarounds before brand replacement
725
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
726
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
727
+ // Replace "Claude Code" brand references with "Cursor"
728
+ converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
729
+ return converted;
730
+ }
731
+ function getCursorSkillAdapterHeader(skillName) {
732
+ return `<cursor_skill_adapter>
733
+ ## A. Skill Invocation
734
+ - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
735
+ - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
736
+ - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
737
+
738
+ ## B. User Prompting
739
+ When the workflow needs user input, prompt the user conversationally:
740
+ - Present options as a numbered list in your response text
741
+ - Ask the user to reply with their choice
742
+ - For multi-select, ask for comma-separated numbers
743
+
744
+ ## C. Tool Usage
745
+ Use these Cursor tools when executing GSD workflows:
746
+ - \`Shell\` for running commands (terminal operations)
747
+ - \`StrReplace\` for editing existing files
748
+ - \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
749
+
750
+ ## D. Subagent Spawning
751
+ When the workflow needs to spawn a subagent:
752
+ - Use \`Task(subagent_type="generalPurpose", ...)\`
753
+ - The \`model\` parameter maps to Cursor's model options (e.g., "fast")
754
+ </cursor_skill_adapter>`;
755
+ }
756
+ function convertClaudeCommandToCursorSkill(content, skillName) {
757
+ const converted = convertClaudeToCursorMarkdown(content);
758
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
759
+ let description = `Run GSD workflow ${skillName}.`;
760
+ if (frontmatter) {
761
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
762
+ if (maybeDescription) {
763
+ description = maybeDescription;
764
+ }
765
+ }
766
+ description = toSingleLine(description);
767
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
768
+ const adapter = getCursorSkillAdapterHeader(skillName);
769
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
770
+ }
771
+ /**
772
+ * Convert a Claude Code command to a Cursor 1.6 slash command (#785).
773
+ *
774
+ * Cursor slash commands live in `.cursor/commands/<name>.md` and are
775
+ * plain markdown — no YAML frontmatter, no adapter header. The filename
776
+ * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
777
+ *
778
+ * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
779
+ * converter (tool renames, brand substitution, slash-command normalisation),
780
+ * then strips the YAML frontmatter block so only the prose body remains.
781
+ *
782
+ * @param {string} content raw Claude Code command markdown (may have frontmatter)
783
+ * @param {string} _commandName the target command name (unused; present for
784
+ * API symmetry with other converters so the runtime-artifact-layout stage
785
+ * function can call it uniformly)
786
+ * @returns {string} plain markdown body, no frontmatter
787
+ */
788
+ function convertClaudeCommandToCursorCommand(content, _commandName) {
789
+ const converted = convertClaudeToCursorMarkdown(content);
790
+ const { body } = extractFrontmatterAndBody(converted);
791
+ return body.trimStart();
792
+ }
793
+ // --- Windsurf converters ---
794
+ // Windsurf uses a tool set similar to Cursor.
795
+ // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
796
+ function convertSlashCommandsToWindsurfSkillMentions(content) {
797
+ // Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
798
+ return content.replace(/gsd:/gi, 'gsd-');
799
+ }
800
+ function convertClaudeToWindsurfMarkdown(content) {
801
+ let converted = convertSlashCommandsToWindsurfSkillMentions(content);
802
+ // Replace tool name references in body text
803
+ converted = converted.replace(/\bBash\(/g, 'Shell(');
804
+ converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
805
+ converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
806
+ // Replace subagent_type from Claude to Windsurf format
807
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
808
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
809
+ // Replace project-level Claude conventions with Windsurf/Devin equivalents
810
+ // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
811
+ // Legacy .windsurf/ is still recognized on read but new installs use .devin/.
812
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`');
813
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules');
814
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`');
815
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules');
816
+ converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/');
817
+ converted = converted.replace(/\.\/\.claude\//g, './.devin/');
818
+ converted = converted.replace(/\.claude\//g, '.devin/');
819
+ // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
820
+ // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
821
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin');
822
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin');
823
+ // Environment variable name rewrite
824
+ converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
825
+ // Remove Claude Code-specific bug workarounds before brand replacement
826
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
827
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
828
+ // Replace "Claude Code" brand references with "Windsurf"
829
+ converted = converted.replace(/\bClaude Code\b/g, 'Windsurf');
830
+ return converted;
831
+ }
832
+ function getWindsurfSkillAdapterHeader(skillName) {
833
+ return `<windsurf_skill_adapter>
834
+ ## A. Skill Invocation
835
+ - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
836
+ - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
837
+ - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
838
+
839
+ ## B. User Prompting
840
+ When the workflow needs user input, prompt the user conversationally:
841
+ - Present options as a numbered list in your response text
842
+ - Ask the user to reply with their choice
843
+ - For multi-select, ask for comma-separated numbers
844
+
845
+ ## C. Tool Usage
846
+ Use these Windsurf tools when executing GSD workflows:
847
+ - \`Shell\` for running commands (terminal operations)
848
+ - \`StrReplace\` for editing existing files
849
+ - \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
850
+
851
+ ## D. Subagent Spawning
852
+ When the workflow needs to spawn a subagent:
853
+ - Use \`Task(subagent_type="generalPurpose", ...)\`
854
+ - The \`model\` parameter maps to Windsurf's model options (e.g., "fast")
855
+ </windsurf_skill_adapter>`;
856
+ }
857
+ function convertClaudeCommandToWindsurfSkill(content, skillName) {
858
+ const converted = convertClaudeToWindsurfMarkdown(content);
859
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
860
+ let description = `Run GSD workflow ${skillName}.`;
861
+ if (frontmatter) {
862
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
863
+ if (maybeDescription) {
864
+ description = maybeDescription;
865
+ }
866
+ }
867
+ description = toSingleLine(description);
868
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
869
+ const adapter = getWindsurfSkillAdapterHeader(skillName);
870
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
871
+ }
872
+ // --- Augment converters ---
873
+ // Augment uses a tool set similar to Cursor/Windsurf.
874
+ // Config lives in .augment/ (local) and ~/.augment/ (global).
875
+ function convertSlashCommandsToAugmentSkillMentions(content) {
876
+ return content.replace(/gsd:/gi, 'gsd-');
877
+ }
878
+ function convertClaudeToAugmentMarkdown(content) {
879
+ let converted = convertSlashCommandsToAugmentSkillMentions(content);
880
+ converted = converted.replace(/\bBash\(/g, 'launch-process(');
881
+ converted = converted.replace(/\bEdit\(/g, 'str-replace-editor(');
882
+ converted = converted.replace(/\bRead\(/g, 'view(');
883
+ converted = converted.replace(/\bWrite\(/g, 'save-file(');
884
+ converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks(');
885
+ converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
886
+ // Replace subagent_type from Claude to Augment format
887
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
888
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
889
+ // Replace project-level Claude conventions with Augment equivalents
890
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`');
891
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/');
892
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`');
893
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/');
894
+ converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/');
895
+ // Remove Claude Code-specific bug workarounds before brand replacement
896
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
897
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
898
+ // Replace "Claude Code" brand references with "Augment"
899
+ converted = converted.replace(/\bClaude Code\b/g, 'Augment');
900
+ return converted;
901
+ }
902
+ function getAugmentSkillAdapterHeader(skillName) {
903
+ return `<augment_skill_adapter>
904
+ ## A. Skill Invocation
905
+ - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
906
+ - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
907
+ - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
908
+
909
+ ## B. User Prompting
910
+ When the workflow needs user input, prompt the user conversationally:
911
+ - Present options as a numbered list in your response text
912
+ - Ask the user to reply with their choice
913
+ - For multi-select, ask for comma-separated numbers
914
+
915
+ ## C. Tool Usage
916
+ Use these Augment tools when executing GSD workflows:
917
+ - \`launch-process\` for running commands (terminal operations)
918
+ - \`str-replace-editor\` for editing existing files
919
+ - \`view\` for reading files and listing directories
920
+ - \`save-file\` for creating new files
921
+ - \`grep\` for searching code (or use MCP servers for advanced search)
922
+ - \`web-search\`, \`web-fetch\` for web queries
923
+ - \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management
924
+
925
+ ## D. Subagent Spawning
926
+ When the workflow needs to spawn a subagent:
927
+ - Use the built-in subagent spawning capability
928
+ - Define agent prompts in \`.augment/agents/\` directory
929
+ </augment_skill_adapter>`;
930
+ }
931
+ function convertClaudeCommandToAugmentSkill(content, skillName) {
932
+ const converted = convertClaudeToAugmentMarkdown(content);
933
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
934
+ let description = `Run GSD workflow ${skillName}.`;
935
+ if (frontmatter) {
936
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
937
+ if (maybeDescription) {
938
+ description = maybeDescription;
939
+ }
940
+ }
941
+ description = toSingleLine(description);
942
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
943
+ const adapter = getAugmentSkillAdapterHeader(skillName);
944
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
945
+ }
946
+ function convertSlashCommandsToTraeSkillMentions(content) {
947
+ return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
948
+ return `/gsd-${commandName}`;
949
+ });
950
+ }
951
+ function convertClaudeToTraeMarkdown(content) {
952
+ let converted = convertSlashCommandsToTraeSkillMentions(content);
953
+ converted = converted.replace(/\bBash\(/g, 'Shell(');
954
+ converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
955
+ // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
956
+ converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
957
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
958
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
959
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
960
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
961
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
962
+ converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
963
+ converted = converted.replace(/\.\/\.claude\//g, './.trae/');
964
+ converted = converted.replace(/\.claude\//g, '.trae/');
965
+ // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
966
+ // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
967
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae');
968
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae');
969
+ // Environment variable name rewrite
970
+ converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
971
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
972
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
973
+ converted = converted.replace(/\bClaude Code\b/g, 'Trae');
974
+ return converted;
975
+ }
976
+ function convertClaudeCommandToTraeSkill(content, skillName) {
977
+ const converted = convertClaudeToTraeMarkdown(content);
978
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
979
+ let description = `Run GSD workflow ${skillName}.`;
980
+ if (frontmatter) {
981
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
982
+ if (maybeDescription) {
983
+ description = maybeDescription;
984
+ }
985
+ }
986
+ description = toSingleLine(description);
987
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
988
+ // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
989
+ // frontmatter parser.
990
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
991
+ }
992
+ function convertSlashCommandsToCodebuddySkillMentions(content) {
993
+ return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
994
+ return `/gsd-${commandName}`;
995
+ });
996
+ }
997
+ function convertClaudeToCodebuddyMarkdown(content) {
998
+ let converted = convertSlashCommandsToCodebuddySkillMentions(content);
999
+ // CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
1000
+ // No tool name conversion needed
1001
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
1002
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`CODEBUDDY.md`');
1003
+ converted = converted.replace(/\.\/CLAUDE\.md/g, 'CODEBUDDY.md');
1004
+ converted = converted.replace(/`CLAUDE\.md`/g, '`CODEBUDDY.md`');
1005
+ converted = converted.replace(/\bCLAUDE\.md\b/g, 'CODEBUDDY.md');
1006
+ converted = converted.replace(/\.claude\/skills\//g, '.codebuddy/skills/');
1007
+ converted = converted.replace(/\.\/\.claude\//g, './.codebuddy/');
1008
+ converted = converted.replace(/\.claude\//g, '.codebuddy/');
1009
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1010
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1011
+ converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
1012
+ return converted;
1013
+ }
1014
+ function convertClaudeCommandToCodebuddySkill(content, skillName) {
1015
+ const converted = convertClaudeToCodebuddyMarkdown(content);
1016
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1017
+ let description = `Run GSD workflow ${skillName}.`;
1018
+ if (frontmatter) {
1019
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1020
+ if (maybeDescription) {
1021
+ description = maybeDescription;
1022
+ }
1023
+ }
1024
+ description = toSingleLine(description);
1025
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1026
+ // #2876: quote so YAML flow indicators (`[BETA] …`) don't break
1027
+ // CodeBuddy's frontmatter parser.
1028
+ //
1029
+ // #789: mark user-invocable:false so the skill is NOT shown in CodeBuddy's
1030
+ // '/' menu (it defaults to true). The commands/ surface (#789) is the sole
1031
+ // '/' entry point; skills remain model-invocable background knowledge,
1032
+ // avoiding a duplicated /gsd-* entry per workflow.
1033
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n${body}`;
1034
+ }
1035
+ /**
1036
+ * Convert a Claude Code slash-command (.md) to a CodeBuddy slash-command (.md).
1037
+ *
1038
+ * CodeBuddy reads user-level slash commands from ~/.codebuddy/commands/<name>.md
1039
+ * (https://www.codebuddy.ai/docs/cli/slash-commands). The filename determines the
1040
+ * command name (gsd-help.md → /gsd-help), so the Claude-specific `name: gsd:<x>`
1041
+ * frontmatter field is dropped. CodeBuddy command frontmatter supports
1042
+ * `description` and `argument-hint`; both are preserved when present. The body is
1043
+ * brand/path-converted via convertClaudeToCodebuddyMarkdown.
1044
+ *
1045
+ * @param {string} content raw Claude command markdown
1046
+ * @param {string} commandName installed command name (e.g. 'gsd-help')
1047
+ * @returns {string}
1048
+ */
1049
+ function convertClaudeCommandToCodebuddyCommand(content, commandName) {
1050
+ const converted = convertClaudeToCodebuddyMarkdown(content);
1051
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1052
+ let description = `Run GSD workflow ${commandName}.`;
1053
+ let argumentHint = '';
1054
+ if (frontmatter) {
1055
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1056
+ if (maybeDescription)
1057
+ description = maybeDescription;
1058
+ const maybeArgHint = extractFrontmatterField(frontmatter, 'argument-hint');
1059
+ if (maybeArgHint)
1060
+ argumentHint = maybeArgHint;
1061
+ }
1062
+ description = toSingleLine(description);
1063
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1064
+ // #2876: quote values so YAML flow indicators (`[BETA] …`, `[name]`) don't
1065
+ // break CodeBuddy's frontmatter parser.
1066
+ const lines = ['---', `description: ${yamlQuote(shortDescription)}`];
1067
+ if (argumentHint)
1068
+ lines.push(`argument-hint: ${yamlQuote(toSingleLine(argumentHint))}`);
1069
+ lines.push('---', body.trimStart());
1070
+ return lines.join('\n');
1071
+ }
1072
+ // ── Cline converters ────────────────────────────────────────────────────────
1073
+ function convertClaudeToCliineMarkdown(content) {
1074
+ let converted = content;
1075
+ // Cline uses the same tool names as Claude Code — no tool name conversion needed
1076
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
1077
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
1078
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`');
1079
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules');
1080
+ // Slash forms first (most specific — superset of bare forms)
1081
+ converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/');
1082
+ converted = converted.replace(/\.\/\.claude\//g, './.cline/');
1083
+ converted = converted.replace(/\.claude\//g, '.cline/');
1084
+ // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite
1085
+ converted = converted.replace(/~\/\.claude\b/g, '~/.cline');
1086
+ converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.cline');
1087
+ // Environment variable name rewrite
1088
+ converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
1089
+ converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1090
+ converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1091
+ converted = converted.replace(/\bClaude Code\b/g, 'Cline');
1092
+ return converted;
1093
+ }
1094
+ /**
1095
+ * Convert a Claude command (.md) to a Cline skill (SKILL.md).
1096
+ * Emits ONLY name + description frontmatter per the Cline skills spec
1097
+ * (https://docs.cline.bot/customization/skills) — no allowed-tools,
1098
+ * argument-hint, agent, or other Claude-specific fields.
1099
+ * Body is hyphen-normalised then converted via convertClaudeToCliineMarkdown
1100
+ * (.claude/→.cline/, "Claude Code"→"Cline", etc.).
1101
+ * Cline uses Claude-Code-compatible tool names, so no adapter header is needed.
1102
+ * Targets ~/.cline/skills/<name>/SKILL.md for Cline >= v3.48.0.
1103
+ */
1104
+ function convertClaudeCommandToClineSkill(content, skillName, _runtime = null, cmdNames = null) {
1105
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
1106
+ if (!frontmatter)
1107
+ return content;
1108
+ // Hyphen-normalise /gsd:<cmd> → gsd-<cmd> references in the body, then
1109
+ // apply Cline-specific markdown rewrites (.claude/→.cline/, etc.).
1110
+ const names = cmdNames || readGsdCommandNames();
1111
+ const normalizedBody = transformContentToHyphen(body, names);
1112
+ const clineBody = convertClaudeToCliineMarkdown(normalizedBody);
1113
+ // Extract description; fall back to a generic string if absent.
1114
+ let description = extractFrontmatterField(frontmatter, 'description');
1115
+ if (!description)
1116
+ description = `Run GSD workflow ${skillName}.`;
1117
+ description = toSingleLine(description);
1118
+ // Cline documented max is 1024 code points (not UTF-16 code units).
1119
+ // Use Array.from to iterate by code point so that multibyte characters
1120
+ // (e.g. emoji, astral-plane chars) are never split, which would produce
1121
+ // lone surrogates and corrupt the YAML output.
1122
+ const cp = Array.from(description);
1123
+ const shortDescription = cp.length > 1024
1124
+ ? cp.slice(0, 1021).join('') + '...'
1125
+ : description;
1126
+ const fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---`;
1127
+ return `${fm}\n${clineBody}`;
1128
+ }
1129
+ // ── End Cline converters ─────────────────────────────────────────────────────
1130
+ function convertSlashCommandsToCodexSkillMentions(content) {
1131
+ // Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below).
1132
+ let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
1133
+ return `$gsd-${String(commandName).toLowerCase()}`;
1134
+ });
1135
+ // Convert hyphen-style command references (workflow output) to Codex $ prefix.
1136
+ // A real /gsd-<cmd> MENTION is defined positively by two boundaries, so any
1137
+ // in-path occurrence is excluded by construction (no denylist of preceding
1138
+ // chars to maintain — see #712, supersedes the #637/#704 lookbehind treadmill):
1139
+ // 1. Left boundary: opens at start-of-string, whitespace, or an inline-prose
1140
+ // delimiter (backtick/quote/paren/bracket) — e.g. `/gsd-execute-phase`.
1141
+ // 2. Right boundary: the command token is NOT followed by a path separator
1142
+ // `/` (a path continues: `/gsd-core/bin/...`; a command does not). The
1143
+ // `(?![a-z0-9/-])` also blocks regex backtracking to a shorter command.
1144
+ // This converts backtick-wrapped MENTIONS (`/gsd-foo`) while leaving backtick-
1145
+ // wrapped PATHS (`/gsd-core/workflows/update.md`) untouched (#712).
1146
+ converted = converted.replace(/(?<=^|[\s`"'([])\/gsd-([a-z0-9-]+)(?![a-z0-9/-])/gi, (_, commandName) => {
1147
+ return `$gsd-${String(commandName).toLowerCase()}`;
1148
+ });
1149
+ return converted;
1150
+ }
1151
+ const CODEX_GSD_TOOLS_INVOCATION = 'node "$HOME/.codex/gsd-core/bin/gsd-tools.cjs"';
1152
+ function rewriteBareGsdToolsCommandsForCodex(content) {
1153
+ return content
1154
+ .replace(/(^[ \t]*)gsd-tools(?=\s)/gm, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
1155
+ .replace(/(\$\(\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
1156
+ .replace(/(`\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
1157
+ .replace(/((?:&&|\|\||[;|])\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`);
1158
+ }
1159
+ function convertClaudeToCodexMarkdown(content) {
1160
+ let converted = convertSlashCommandsToCodexSkillMentions(content);
1161
+ converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
1162
+ // Remove /clear references — Codex has no equivalent command
1163
+ // Handle backtick-wrapped: `\/clear` then: → (removed)
1164
+ converted = converted.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
1165
+ // Handle bare: /clear then: → (removed)
1166
+ converted = converted.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
1167
+ // Handle standalone /clear on its own line
1168
+ converted = converted.replace(/^\s*`?\/clear`?\s*$/gm, '');
1169
+ // Path replacement: .claude → .codex (#1430)
1170
+ converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
1171
+ converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
1172
+ converted = converted.replace(/\.\/\.claude\//g, './.codex/');
1173
+ // Bare ~/.claude without trailing slash (e.g. configDir = ~/.claude)
1174
+ converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
1175
+ converted = converted.replace(/~\/\.claude\b/g, '~/.codex');
1176
+ // Bare/project-relative .claude/... references (#2639). Covers strings like
1177
+ // "check `.claude/skills/`" where there is no ~/, $HOME/, or ./ anchor.
1178
+ // Negative lookbehind prevents double-replacing already-anchored forms and
1179
+ // avoids matching inside URLs or other slash-prefixed paths.
1180
+ converted = converted.replace(/(?<![A-Za-z0-9_\-./~$])\.claude\//g, '.codex/');
1181
+ // `.claudeignore` → `.codexignore` (#2639). Codex honors its own ignore
1182
+ // file; leaving the Claude-specific name is misleading in agent prompts.
1183
+ converted = converted.replace(/\.claudeignore\b/g, '.codexignore');
1184
+ // Codex installs the tools shim under ~/.codex but does not guarantee a
1185
+ // bare `gsd-tools` binary on PATH. Keep resolver probes such as
1186
+ // `command -v gsd-tools` intact; rewrite only command invocations.
1187
+ converted = rewriteBareGsdToolsCommandsForCodex(converted);
1188
+ // Runtime-neutral agent name replacement (#766)
1189
+ converted = neutralizeAgentReferences(converted, 'AGENTS.md');
1190
+ return converted;
1191
+ }
1192
+ function getCodexSkillAdapterHeader(skillName) {
1193
+ const invocation = `$${skillName}`;
1194
+ return `<codex_skill_adapter>
1195
+ ## A. Skill Invocation
1196
+ - This skill is invoked by mentioning \`${invocation}\`.
1197
+ - Treat all user text after \`${invocation}\` as \`{{GSD_ARGS}}\`.
1198
+ - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
1199
+
1200
+ ## B. AskUserQuestion → request_user_input Mapping
1201
+ GSD workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
1202
+
1203
+ Parameter mapping:
1204
+ - \`header\` → \`header\`
1205
+ - \`question\` → \`question\`
1206
+ - Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
1207
+ - Generate \`id\` from header: lowercase, replace spaces with underscores
1208
+
1209
+ Batched calls:
1210
+ - \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
1211
+
1212
+ Multi-select workaround:
1213
+ - Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
1214
+
1215
+ Execute mode fallback:
1216
+ - When \`request_user_input\` is rejected or unavailable, activate TEXT_MODE: append \`--text\` to \`{{GSD_ARGS}}\` so the workflow's built-in text-mode branching takes over. Present every \`AskUserQuestion\` call as a plain-text numbered list, then stop and wait for the user's reply. Do NOT pick a default and continue (#3018 / #3808).
1217
+ - You may only proceed without a user answer when one of these is true:
1218
+ (a) the invocation included an explicit non-interactive flag (\`--auto\` or \`--all\`),
1219
+ (b) the user has explicitly approved a specific default for this question, or
1220
+ (c) the workflow's documented contract says defaults are safe (e.g. autonomous lifecycle paths).
1221
+ - Do NOT write workflow artifacts (CONTEXT.md, DISCUSSION-LOG.md, PLAN.md, checkpoint files) until the user has answered the plain-text questions or one of (a)-(c) above applies. Surfacing the questions and waiting is the correct response — silently defaulting and writing artifacts is the #3018 failure mode.
1222
+
1223
+ ## C. Task() → spawn_agent Mapping
1224
+ GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
1225
+
1226
+ **Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas:
1227
+ - **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available.
1228
+ - **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session.
1229
+
1230
+ Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active.
1231
+
1232
+ Typed mapping (agent_type-capable schema only):
1233
+ - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1234
+ - \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1235
+ - \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
1236
+ GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
1237
+ at install time so \`model_overrides\` from \`.planning/config.json\` and
1238
+ \`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
1239
+ - Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\`
1240
+ to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
1241
+ inherited, or unsupported values; do not invent one-off effort literals in
1242
+ workflow prose.
1243
+ - \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
1244
+ - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping.
1245
+ Codex \`spawn_agent\` does not create or bind a git worktree automatically.
1246
+ Workflows that require this isolation must fail closed or use an explicit
1247
+ manual worktree protocol before spawning (#3360).
1248
+
1249
+ Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
1250
+ When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
1251
+ (\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
1252
+ (openai/codex#15250). **This workaround is NOT equivalent to typed gsd-planner/gsd-executor
1253
+ execution** — GSD agents carry project-aware prompts, audit logging, and workflow context
1254
+ that a generic subagent lacks. Use the following fallback:
1255
+ 1. Resolve your active Codex config root — the directory that contains your \`config.toml\`.
1256
+ This directory is determined in priority order: \`$CODEX_HOME\` (if set), the path given
1257
+ by \`--config-dir\` (if passed on invocation), a local \`.codex\` directory in the current
1258
+ project (if \`--local\` was used), or the default global config directory. Read
1259
+ \`agents/<agent-name>.toml\` relative to that config root to extract the agent's system
1260
+ instructions.
1261
+ 2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call.
1262
+ 3. Label results and logs clearly as "generic-agent workaround" so the orchestrator and user
1263
+ know full typed-agent guarantees are not in effect.
1264
+ 4. Where typed dispatch is mandatory for correctness (e.g. worktree isolation), fail closed
1265
+ and report the schema limitation rather than silently degrading.
1266
+
1267
+ Spawn restriction:
1268
+ - Codex restricts \`spawn_agent\` to cases where the user has explicitly
1269
+ requested sub-agents. When automatic spawning is not permitted, do the
1270
+ work inline in the current agent rather than attempting to force a spawn.
1271
+ - In some Codex sessions, multi-agent tooling can be deferred. If \`spawn_agent\`
1272
+ is not currently visible, discover tools first via \`tool_search\` before
1273
+ defaulting to inline execution.
1274
+
1275
+ Parallel fan-out:
1276
+ - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
1277
+
1278
+ Result parsing:
1279
+ - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
1280
+ - \`close_agent(id)\` after collecting results from each agent
1281
+ </codex_skill_adapter>`;
1282
+ }
1283
+ function convertClaudeCommandToCodexSkill(content, skillName) {
1284
+ const converted = convertClaudeToCodexMarkdown(content);
1285
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1286
+ let description = `Run GSD workflow ${skillName}.`;
1287
+ if (frontmatter) {
1288
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1289
+ if (maybeDescription) {
1290
+ description = maybeDescription;
1291
+ }
1292
+ }
1293
+ description = toSingleLine(description);
1294
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1295
+ const adapter = getCodexSkillAdapterHeader(skillName);
1296
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
1297
+ }
1298
+ function neutralizeAgentReferences(content, instructionFile) {
1299
+ let c = content;
1300
+ // Replace standalone "Claude" (the agent) but preserve product/model names.
1301
+ // Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based
1302
+ c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent');
1303
+ // Replace CLAUDE.md with runtime-appropriate instruction file
1304
+ if (instructionFile) {
1305
+ c = c.replace(/CLAUDE\.md/g, instructionFile);
1306
+ }
1307
+ // Remove instructions that conflict with AGENTS.md-based runtimes
1308
+ c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, '');
1309
+ return c;
1310
+ }
1311
+ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
1312
+ // Replace tool name references in content (applies to all files)
1313
+ let convertedContent = content;
1314
+ convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
1315
+ convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
1316
+ convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
1317
+ // Replace /gsd-command colon variant with /gsd-command for opencode (flat command structure)
1318
+ convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
1319
+ // Replace ~/.claude and $HOME/.claude with OpenCode's config location
1320
+ convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
1321
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
1322
+ // Replace general-purpose subagent type with OpenCode's equivalent "general"
1323
+ convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
1324
+ // Runtime-neutral agent name replacement (#766)
1325
+ convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
1326
+ // Check if content has frontmatter
1327
+ if (!convertedContent.startsWith('---')) {
1328
+ return convertedContent;
1329
+ }
1330
+ // Find the end of frontmatter
1331
+ const endIndex = convertedContent.indexOf('---', 3);
1332
+ if (endIndex === -1) {
1333
+ return convertedContent;
1334
+ }
1335
+ const frontmatter = convertedContent.substring(3, endIndex).trim();
1336
+ const body = convertedContent.substring(endIndex + 3);
1337
+ // Parse frontmatter line by line (simple YAML parsing)
1338
+ const lines = frontmatter.split('\n');
1339
+ const newLines = [];
1340
+ let inAllowedTools = false;
1341
+ let inSkippedArray = false;
1342
+ const allowedTools = [];
1343
+ for (const line of lines) {
1344
+ const trimmed = line.trim();
1345
+ // For agents: skip commented-out lines (e.g. hooks blocks)
1346
+ if (isAgent && trimmed.startsWith('#')) {
1347
+ continue;
1348
+ }
1349
+ // Detect start of allowed-tools array
1350
+ if (trimmed.startsWith('allowed-tools:')) {
1351
+ inAllowedTools = true;
1352
+ continue;
1353
+ }
1354
+ // Detect inline tools: field (comma-separated string)
1355
+ if (trimmed.startsWith('tools:')) {
1356
+ if (isAgent) {
1357
+ // Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
1358
+ inSkippedArray = true;
1359
+ continue;
1360
+ }
1361
+ const toolsValue = trimmed.substring(6).trim();
1362
+ if (toolsValue) {
1363
+ // Parse comma-separated tools
1364
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
1365
+ allowedTools.push(...tools);
1366
+ }
1367
+ continue;
1368
+ }
1369
+ // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
1370
+ if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
1371
+ inSkippedArray = true;
1372
+ continue;
1373
+ }
1374
+ // Skip continuation lines of a stripped array/object field
1375
+ if (inSkippedArray) {
1376
+ if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
1377
+ continue;
1378
+ }
1379
+ inSkippedArray = false;
1380
+ }
1381
+ // For commands: remove name: field (opencode uses filename for command name)
1382
+ // For agents: keep name: (required by OpenCode agents)
1383
+ if (!isAgent && trimmed.startsWith('name:')) {
1384
+ continue;
1385
+ }
1386
+ // Strip model: field — OpenCode doesn't support Claude Code model aliases
1387
+ // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
1388
+ // its configured default model. See #1156.
1389
+ if (trimmed.startsWith('model:')) {
1390
+ continue;
1391
+ }
1392
+ // Convert color names to hex for opencode (commands only; agents strip color above)
1393
+ if (trimmed.startsWith('color:')) {
1394
+ const colorValue = trimmed.substring(6).trim().toLowerCase();
1395
+ const hexColor = colorNameToHex[colorValue];
1396
+ if (hexColor) {
1397
+ newLines.push(`color: "${hexColor}"`);
1398
+ }
1399
+ else if (colorValue.startsWith('#')) {
1400
+ // Validate hex color format (#RGB or #RRGGBB)
1401
+ if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
1402
+ // Already hex and valid, keep as is
1403
+ newLines.push(line);
1404
+ }
1405
+ // Skip invalid hex colors
1406
+ }
1407
+ // Skip unknown color names
1408
+ continue;
1409
+ }
1410
+ // Collect allowed-tools items
1411
+ if (inAllowedTools) {
1412
+ if (trimmed.startsWith('- ')) {
1413
+ allowedTools.push(trimmed.substring(2).trim());
1414
+ continue;
1415
+ }
1416
+ else if (trimmed && !trimmed.startsWith('-')) {
1417
+ // End of array, new field started
1418
+ inAllowedTools = false;
1419
+ }
1420
+ }
1421
+ // Keep other fields
1422
+ if (!inAllowedTools) {
1423
+ newLines.push(line);
1424
+ }
1425
+ }
1426
+ // For agents: add required OpenCode agent fields
1427
+ // Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
1428
+ // keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
1429
+ // use its default model for subagents. See #1156.
1430
+ if (isAgent) {
1431
+ newLines.push('mode: subagent');
1432
+ // Embed model override from ~/.gsd/defaults.json so model_overrides is
1433
+ // respected on OpenCode (which uses static agent frontmatter, not inline
1434
+ // Task() model parameters). See #2256.
1435
+ if (modelOverride) {
1436
+ newLines.push(['model:', modelOverride].join(' '));
1437
+ }
1438
+ }
1439
+ // For commands: add tools object if we had allowed-tools or tools
1440
+ if (!isAgent && allowedTools.length > 0) {
1441
+ newLines.push('tools:');
1442
+ for (const tool of allowedTools) {
1443
+ newLines.push(` ${convertToolName(tool)}: true`);
1444
+ }
1445
+ }
1446
+ // Rebuild frontmatter (body already has tool names converted)
1447
+ const newFrontmatter = newLines.join('\n').trim();
1448
+ return `---\n${newFrontmatter}\n---${body}`;
1449
+ }
1450
+ // Kilo CLI — same conversion logic as OpenCode, different config paths.
1451
+ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
1452
+ // Replace tool name references in content (applies to all files)
1453
+ let convertedContent = content;
1454
+ convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
1455
+ convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
1456
+ convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
1457
+ // Replace /gsd-command colon variant with /gsd-command for Kilo (flat command structure)
1458
+ convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
1459
+ // Replace ~/.claude and $HOME/.claude with Kilo's config location
1460
+ convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo');
1461
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo');
1462
+ convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/');
1463
+ // Normalize both Claude skill directory variants to Kilo's canonical skills dir.
1464
+ convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/');
1465
+ convertedContent = replaceRelativePathReference(convertedContent, '.agents/skills/', '.kilo/skills/');
1466
+ convertedContent = replaceRelativePathReference(convertedContent, '.claude/agents/', '.kilo/agents/');
1467
+ // Replace general-purpose subagent type with Kilo's equivalent "general"
1468
+ convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
1469
+ // Runtime-neutral agent name replacement (#766)
1470
+ convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
1471
+ // Check if content has frontmatter
1472
+ if (!convertedContent.startsWith('---')) {
1473
+ return convertedContent;
1474
+ }
1475
+ // Find the end of frontmatter
1476
+ const endIndex = convertedContent.indexOf('---', 3);
1477
+ if (endIndex === -1) {
1478
+ return convertedContent;
1479
+ }
1480
+ const frontmatter = convertedContent.substring(3, endIndex).trim();
1481
+ const body = convertedContent.substring(endIndex + 3);
1482
+ // Parse frontmatter line by line (simple YAML parsing)
1483
+ const lines = frontmatter.split('\n');
1484
+ const newLines = [];
1485
+ let inAllowedTools = false;
1486
+ let inAgentTools = false;
1487
+ let inSkippedArray = false;
1488
+ const allowedTools = [];
1489
+ const agentTools = [];
1490
+ for (const line of lines) {
1491
+ const trimmed = line.trim();
1492
+ // For agents: skip commented-out lines (e.g. hooks blocks)
1493
+ if (isAgent && trimmed.startsWith('#')) {
1494
+ continue;
1495
+ }
1496
+ // Detect start of allowed-tools array
1497
+ if (trimmed.startsWith('allowed-tools:')) {
1498
+ inAllowedTools = true;
1499
+ continue;
1500
+ }
1501
+ if (isAgent && inAgentTools) {
1502
+ if (trimmed.startsWith('- ')) {
1503
+ agentTools.push(trimmed.substring(2).trim());
1504
+ continue;
1505
+ }
1506
+ if (trimmed && !trimmed.startsWith('-')) {
1507
+ inAgentTools = false;
1508
+ }
1509
+ }
1510
+ // Detect inline tools: field (comma-separated string)
1511
+ if (trimmed.startsWith('tools:')) {
1512
+ if (isAgent) {
1513
+ const toolsValue = trimmed.substring(6).trim();
1514
+ if (toolsValue) {
1515
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
1516
+ agentTools.push(...tools);
1517
+ }
1518
+ else {
1519
+ inAgentTools = true;
1520
+ }
1521
+ continue;
1522
+ }
1523
+ const toolsValue = trimmed.substring(6).trim();
1524
+ if (toolsValue) {
1525
+ // Parse comma-separated tools
1526
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
1527
+ allowedTools.push(...tools);
1528
+ }
1529
+ continue;
1530
+ }
1531
+ // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
1532
+ if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
1533
+ inSkippedArray = true;
1534
+ continue;
1535
+ }
1536
+ // Skip continuation lines of a stripped array/object field
1537
+ if (inSkippedArray) {
1538
+ if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
1539
+ continue;
1540
+ }
1541
+ inSkippedArray = false;
1542
+ }
1543
+ // For commands: remove name: field (Kilo uses filename for command name)
1544
+ // For agents: keep name: (required by Kilo agents)
1545
+ if (!isAgent && trimmed.startsWith('name:')) {
1546
+ continue;
1547
+ }
1548
+ // Strip model: field — Kilo doesn't support Claude Code model aliases
1549
+ // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use
1550
+ // its configured default model.
1551
+ if (trimmed.startsWith('model:')) {
1552
+ continue;
1553
+ }
1554
+ // Convert color names to hex for Kilo (commands only; agents strip color above)
1555
+ if (trimmed.startsWith('color:')) {
1556
+ const colorValue = trimmed.substring(6).trim().toLowerCase();
1557
+ const hexColor = colorNameToHex[colorValue];
1558
+ if (hexColor) {
1559
+ newLines.push(`color: "${hexColor}"`);
1560
+ }
1561
+ else if (colorValue.startsWith('#')) {
1562
+ // Validate hex color format (#RGB or #RRGGBB)
1563
+ if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
1564
+ // Already hex and valid, keep as is
1565
+ newLines.push(line);
1566
+ }
1567
+ // Skip invalid hex colors
1568
+ }
1569
+ // Skip unknown color names
1570
+ continue;
1571
+ }
1572
+ // Collect allowed-tools items
1573
+ if (inAllowedTools) {
1574
+ if (trimmed.startsWith('- ')) {
1575
+ const tool = trimmed.substring(2).trim();
1576
+ if (isAgent) {
1577
+ agentTools.push(tool);
1578
+ }
1579
+ else {
1580
+ allowedTools.push(tool);
1581
+ }
1582
+ continue;
1583
+ }
1584
+ else if (trimmed && !trimmed.startsWith('-')) {
1585
+ // End of array, new field started
1586
+ inAllowedTools = false;
1587
+ }
1588
+ }
1589
+ // Keep other fields
1590
+ if (!inAllowedTools) {
1591
+ newLines.push(line);
1592
+ }
1593
+ }
1594
+ // For agents: add required Kilo agent fields
1595
+ if (isAgent) {
1596
+ newLines.push('mode: subagent');
1597
+ newLines.push(...buildKiloAgentPermissionBlock(agentTools));
1598
+ }
1599
+ // For commands: add tools object if we had allowed-tools or tools
1600
+ if (!isAgent && allowedTools.length > 0) {
1601
+ newLines.push('tools:');
1602
+ for (const tool of allowedTools) {
1603
+ newLines.push(` ${convertToolName(tool)}: true`);
1604
+ }
1605
+ }
1606
+ // Rebuild frontmatter (body already has tool names converted)
1607
+ const newFrontmatter = newLines.join('\n').trim();
1608
+ return `---\n${newFrontmatter}\n---${body}`;
1609
+ }
1610
+ /**
1611
+ * Shared SKILL.md writer for the OpenCode-family runtimes (OpenCode + Kilo),
1612
+ * which share a config schema (Kilo derives from OpenCode). OpenCode discovers
1613
+ * skills as `skills/<name>/SKILL.md` and Kilo follows the same layout
1614
+ * (https://opencode.ai/docs/skills, https://kilo.ai/docs/customize/skills).
1615
+ *
1616
+ * The skill body reuses the runtime's command-frontmatter converter for tool,
1617
+ * path, and `/gsd:`→`/gsd-` body rewrites, then rebuilds a minimal skill
1618
+ * frontmatter: only `name` (lowercase-hyphen, must match the containing
1619
+ * directory) and `description` (1–1024 chars) are emitted, per the OpenCode
1620
+ * skill spec. The command's `tools:`/`permission:` block is intentionally
1621
+ * dropped — OpenCode skills are loaded on-demand via the native skill tool and
1622
+ * inherit the calling agent's permissions.
1623
+ *
1624
+ * @param {string} content - Claude command markdown (with YAML frontmatter)
1625
+ * @param {string} skillName - Skill directory name (e.g. gsd-help)
1626
+ * @param {(content: string) => string} frontmatterConverter - runtime command converter
1627
+ * @returns {string} SKILL.md content
1628
+ */
1629
+ function convertClaudeCommandToOpencodeFamilySkill(content, skillName, frontmatterConverter) {
1630
+ const converted = frontmatterConverter(content);
1631
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
1632
+ let description = `Run GSD workflow ${skillName}.`;
1633
+ if (frontmatter) {
1634
+ const maybeDescription = extractFrontmatterField(frontmatter, 'description');
1635
+ if (maybeDescription) {
1636
+ description = maybeDescription;
1637
+ }
1638
+ }
1639
+ description = toSingleLine(description);
1640
+ // OpenCode skill descriptions must be 1–1024 characters.
1641
+ if (description.length > 1024) {
1642
+ description = `${description.slice(0, 1021)}...`;
1643
+ }
1644
+ // `name` must be lowercase alphanumeric with single-hyphen separators and
1645
+ // match the containing directory name (the staged dir is `${skillName}/`).
1646
+ const name = yamlIdentifier(skillName);
1647
+ return `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trimStart()}`;
1648
+ }
1649
+ /**
1650
+ * Convert a Claude command (.md) to an OpenCode skill (SKILL.md).
1651
+ * Thin wrapper over the shared OpenCode-family writer.
1652
+ */
1653
+ function convertClaudeCommandToOpencodeSkill(content, skillName) {
1654
+ return convertClaudeCommandToOpencodeFamilySkill(content, skillName, (c) => convertClaudeToOpencodeFrontmatter(c));
1655
+ }
1656
+ /**
1657
+ * Convert a Claude command (.md) to a Kilo skill (SKILL.md).
1658
+ * Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema).
1659
+ */
1660
+ function convertClaudeCommandToKiloSkill(content, skillName) {
1661
+ return convertClaudeCommandToOpencodeFamilySkill(content, skillName, (c) => convertClaudeToKiloFrontmatter(c));
1662
+ }
1663
+ module.exports = {
1664
+ yamlIdentifier,
1665
+ yamlQuote,
1666
+ toSingleLine,
1667
+ extractFrontmatterAndBody,
1668
+ extractFrontmatterField,
1669
+ skillFrontmatterName,
1670
+ convertClaudeToCopilotContent,
1671
+ convertClaudeCommandToCopilotSkill,
1672
+ convertClaudeToAntigravityContent,
1673
+ convertClaudeCommandToAntigravitySkill,
1674
+ convertClaudeCommandToClaudeSkill,
1675
+ convertClaudeCommandToKimiSkill,
1676
+ buildKimiAgentArtifacts,
1677
+ convertClaudeToCursorMarkdown,
1678
+ convertClaudeCommandToCursorSkill,
1679
+ convertClaudeCommandToCursorCommand,
1680
+ convertClaudeToWindsurfMarkdown,
1681
+ convertClaudeCommandToWindsurfSkill,
1682
+ convertClaudeToAugmentMarkdown,
1683
+ convertClaudeCommandToAugmentSkill,
1684
+ convertClaudeToTraeMarkdown,
1685
+ convertClaudeCommandToTraeSkill,
1686
+ convertClaudeToCodebuddyMarkdown,
1687
+ convertClaudeCommandToCodebuddySkill,
1688
+ convertClaudeCommandToCodebuddyCommand,
1689
+ convertClaudeToCliineMarkdown,
1690
+ convertClaudeCommandToClineSkill,
1691
+ convertSlashCommandsToCodexSkillMentions,
1692
+ getCodexSkillAdapterHeader,
1693
+ convertClaudeToCodexMarkdown,
1694
+ convertClaudeCommandToCodexSkill,
1695
+ neutralizeAgentReferences,
1696
+ convertClaudeCommandToOpencodeSkill,
1697
+ convertClaudeCommandToKiloSkill,
1698
+ readGsdCommandNames,
1699
+ transformContentToHyphen,
1700
+ };