@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/bin/install.js +25 -7
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +110 -92
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +31 -18
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +1 -2
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/init.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -1
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +71 -11
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +221 -35
- package/gsd-core/bin/lib/verify.cjs +19 -4
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +82 -143
- package/gsd-core/workflows/help/modes/full.md +3 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/plan-phase.md +89 -221
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +9 -6
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/ship.md +57 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-work.md +27 -11
- package/package.json +3 -2
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/gen-capability-registry.cjs +359 -48
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +8 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- 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
|
+
};
|