fix(codex): convert /gsd- workflow commands to $gsd- during installation (#1994)

The convertSlashCommandsToCodexSkillMentions function only converted
colon-style skill invocations (/gsd:command) but not hyphen-style
command references (/gsd-command) used in workflow output templates
(Next Up blocks, phase completion messages, etc.). This caused Codex
users to see /gsd- prefixed commands instead of $gsd- in chat output.

- Add regex to convert /gsd-command → $gsd-command with negative
  lookbehind to exclude file paths (e.g. bin/gsd-tools.cjs)
- Strip /clear references in Codex output (no Codex equivalent)
- Add 5 regression tests covering command conversion, path
  preservation, and /clear removal

Co-authored-by: Lakshman <lakshman@lakshman-GG9LQ90J61.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lakshman Turlapati
2026-04-10 09:49:58 -05:00
committed by GitHub
parent f8526b5c01
commit d0ab1d8aaa
2 changed files with 95 additions and 1 deletions

View File

@@ -1545,16 +1545,29 @@ function convertClaudeAgentToCodebuddyAgent(content) {
}
function convertSlashCommandsToCodexSkillMentions(content) {
// Convert colon-style skill invocations to Codex $ prefix
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
return `$gsd-${String(commandName).toLowerCase()}`;
});
converted = converted.replace(/\/gsd-help\b/g, '$gsd-help');
// Convert hyphen-style command references (workflow output) to Codex $ prefix.
// Negative lookbehind excludes file paths like bin/gsd-tools.cjs where
// the slash is preceded by a word char, dot, or another slash.
converted = converted.replace(/(?<![a-zA-Z0-9./])\/gsd-([a-z0-9-]+)/gi, (_, commandName) => {
return `$gsd-${String(commandName).toLowerCase()}`;
});
return converted;
}
function convertClaudeToCodexMarkdown(content) {
let converted = convertSlashCommandsToCodexSkillMentions(content);
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Remove /clear references — Codex has no equivalent command
// Handle backtick-wrapped: `\/clear` then: → (removed)
converted = converted.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
// Handle bare: /clear then: → (removed)
converted = converted.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
// Handle standalone /clear on its own line
converted = converted.replace(/^\s*`?\/clear`?\s*$/gm, '');
// Path replacement: .claude → .codex (#1430)
converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
converted = converted.replace(/~\/\.claude\//g, '~/.codex/');

View File

@@ -17,6 +17,7 @@ const os = require('os');
const {
getCodexSkillAdapterHeader,
convertClaudeAgentToCodexAgent,
convertClaudeCommandToCodexSkill,
generateCodexAgentToml,
generateCodexConfigBlock,
stripGsdFromCodexConfig,
@@ -186,6 +187,86 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: resolve"`;
});
});
// ─── Codex command prefix conversion ────────────────────────────────────────────
describe('Codex hyphen-style command prefix conversion', () => {
test('converts /gsd-command in workflow output to $gsd-command', () => {
const input = `---
name: gsd-test
description: Test
tools: Read
---
/gsd-discuss-phase 1 — gather context
/gsd-plan-phase 2 — create plan
/gsd-execute-phase 3 — run it`;
const result = convertClaudeCommandToCodexSkill(input, 'gsd-test');
assert.ok(result.includes('$gsd-discuss-phase'), 'converts /gsd-discuss-phase');
assert.ok(result.includes('$gsd-plan-phase'), 'converts /gsd-plan-phase');
assert.ok(result.includes('$gsd-execute-phase'), 'converts /gsd-execute-phase');
assert.ok(!result.includes('/gsd-discuss-phase'), 'no /gsd-discuss-phase remains');
});
test('converts backtick-wrapped /gsd- commands', () => {
const input = `---
name: gsd-test
description: Test
tools: Read
---
Run \`/gsd-plan-phase 1\` to plan.`;
const result = convertClaudeCommandToCodexSkill(input, 'gsd-test');
assert.ok(result.includes('$gsd-plan-phase'), 'converts backtick-wrapped command');
});
test('does not convert /gsd- in file paths', () => {
const input = `---
name: gsd-test
description: Test
tools: Read
---
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init`;
const result = convertClaudeCommandToCodexSkill(input, 'gsd-test');
assert.ok(result.includes('gsd-tools.cjs'), 'gsd-tools.cjs preserved in path');
assert.ok(!result.includes('$gsd-tools'), 'no $gsd-tools in file path');
});
test('removes /clear then: for Codex', () => {
const input = `---
name: gsd-test
description: Test
tools: Read
---
\`/clear\` then:
\`$gsd-plan-phase 1\``;
const result = convertClaudeCommandToCodexSkill(input, 'gsd-test');
assert.ok(!result.includes('/clear'), 'no /clear remains');
assert.ok(result.includes('$gsd-plan-phase'), 'command preserved after /clear removal');
});
test('removes bare /clear then: for Codex', () => {
const input = `---
name: gsd-test
description: Test
tools: Read
---
/clear then:
/gsd-execute-phase 2`;
const result = convertClaudeCommandToCodexSkill(input, 'gsd-test');
assert.ok(!result.includes('/clear'), 'no /clear remains');
assert.ok(result.includes('$gsd-execute-phase'), 'command converted');
});
});
// ─── generateCodexAgentToml ─────────────────────────────────────────────────────
describe('generateCodexAgentToml', () => {