fix(3583): normalize retired colon-form commands in generated Claude/Qwen/Hermes SKILL.md bodies (#3629)
* Add first-class grok runtime support (maps to ~/.agents); wire installer, runtime-homes.cjs and sync-skills; update Grok Build engine in local ~/.agents to latest; record session progress in discussion doc * Normalize gsd colon references to hyphen in generated Claude SKILL.md bodies using the shared transformer. Fixes #3583. * Refine #3583 implementation after review: cache command names, improve tests, clean up comments * Harden gsd colon-to-hyphen transformer with bidirectional word boundaries and body-only regression guard * Track quick-wins batch status and local session notes for #3583/#3579 handoff * Port installer robustness (hoist copyLibDir + selective Codex hooks) from 3579 to make Codex tests pass on this branch. Fixes ReferenceError and prevents extra hook pollution in Codex installs. * Restore #3583 transformer wiring and Codex .sh GSD_VERSION branch lost in 50ff8f17 port Commit 50ff8f17 ('Port installer robustness from #3579') accidentally reverted: - the top-level require of transformContentToHyphen/readGsdCommandNames - the body normalization inside convertClaudeCommandToClaudeSkill - the Codex hook loop's .sh branch with {{GSD_VERSION}} substitution These were the actual #3583 fix and the Codex half of the #2136 invariant. Failing tests fixed: bug-2808-skill-hyphen-name, claude-skills-migration #3583 case, bug-2136 Codex .sh substitution. * Exempt 'sync-skills' slug from docs-parity check (skill dir name in path references) gsd-sync-skills is an installed Claude skill name and a workflow file but not a registered slash command. The docs-parity regex catches /gsd-sync-skills from filesystem path references like ~/.agents/skills/gsd-sync-skills/ in docs/discussions/grok-build-support-2026-05.md. Adding to INTERNAL_COMPONENT_SLUGS matches the existing exemption pattern for 'statusline', 'workspaces', 'graphify-update', etc. * Restrict hooks/lib/ install to hook-enabled runtimes and managed allowlist Codex/Copilot/Cursor/Windsurf/Trae/Cline already skip the hooks block but were still copying hooks/lib/ helpers, contradicting the downstream Codex comment. Gate the call on the same runtime check and pass GSD_HOOK_LIB_FILES so install scope matches the uninstall/manifest scope.
This commit is contained in:
@@ -87,6 +87,25 @@ describe('bug-2808: SKILL.md name: uses hyphen form', () => {
|
||||
name.startsWith('gsd-'),
|
||||
`${cmd}: SKILL.md name should start with gsd-, got "${name}"`
|
||||
);
|
||||
|
||||
// #3583 regression guard: the *body* must not leak retired colon-form
|
||||
// command references (e.g. /gsd:plan-phase or gsd:review). The converter
|
||||
// now uses transformContentToHyphen from the shared transformer.
|
||||
//
|
||||
// We explicitly scope to the body (after stripping the leading frontmatter
|
||||
// block) so that descriptions or other frontmatter fields containing example
|
||||
// gsd: references do not cause spurious failures.
|
||||
//
|
||||
// gsd:sdk and gsd:tools are intentionally excluded: they are not slash commands
|
||||
// (no commands/gsd/sdk.md or tools.md exist), so the transformer correctly leaves
|
||||
// them alone. They are benign and should not trigger this assertion.
|
||||
const bodyContent = skillContent.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
||||
const colonRefs = (bodyContent.match(/\bgsd:[a-z][a-z0-9-]*\b/g) || [])
|
||||
.filter(r => !/gsd:(sdk|tools)/.test(r));
|
||||
assert.strictEqual(
|
||||
colonRefs.length, 0,
|
||||
`${cmd}: generated SKILL.md body must not contain gsd: command references (found: ${colonRefs.join(', ')})`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,4 +197,52 @@ describe('bug-2808: SKILL.md name: uses hyphen form', () => {
|
||||
assert.ok(!name.includes('_'), `${skillDir}: autocomplete name must not contain underscore, got ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('transformContentToHyphen (from fix-slash-commands.cjs) rewrites colon to hyphen for known commands', () => {
|
||||
const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
|
||||
const { transformContentToHyphen, readCmdNames } = transformer;
|
||||
const liveCmdNames = readCmdNames();
|
||||
|
||||
const input = 'Run /gsd:plan-phase then gsd:execute-phase. Also see /gsd:review and gsd-sdk query.';
|
||||
const out = transformContentToHyphen(input, liveCmdNames);
|
||||
|
||||
assert.ok(out.includes('/gsd-plan-phase'), 'leading-/ colon form must become hyphen');
|
||||
assert.ok(out.includes('gsd-execute-phase'), 'bare colon form must become hyphen');
|
||||
assert.ok(out.includes('/gsd-review'), 'another command reference must be rewritten');
|
||||
assert.ok(out.includes('gsd-sdk'), 'non-command gsd-sdk must be left untouched');
|
||||
assert.ok(!out.match(/\bgsd:[a-z]/), 'no colon-form command reference may survive');
|
||||
});
|
||||
|
||||
test('respects word boundary — does not rewrite gsd:plan-phase-extra (partial match guard)', () => {
|
||||
const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
|
||||
const { transformContentToHyphen, readCmdNames } = transformer;
|
||||
const liveCmdNames = readCmdNames();
|
||||
|
||||
const out = transformContentToHyphen('gsd:plan-phase-extra and /gsd:execute-phase-extra', liveCmdNames);
|
||||
assert.strictEqual(out, 'gsd:plan-phase-extra and /gsd:execute-phase-extra',
|
||||
'word-boundary lookahead must prevent partial matches on the reverse transform');
|
||||
});
|
||||
|
||||
test('respects left word boundary — does not rewrite inside larger tokens (e.g. mygsd:cmd)', () => {
|
||||
const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
|
||||
const { transformContentToHyphen, readCmdNames } = transformer;
|
||||
const liveCmdNames = readCmdNames();
|
||||
|
||||
const input = 'See mygsd:plan-phase or prefix-gsd:execute in the docs.';
|
||||
const out = transformContentToHyphen(input, liveCmdNames);
|
||||
assert.strictEqual(out, input, 'negative lookbehind must prevent left-side in-word matches');
|
||||
});
|
||||
|
||||
test('leaves already-hyphen-form references untouched (idempotent on output)', () => {
|
||||
const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
|
||||
const { transformContentToHyphen, readCmdNames } = transformer;
|
||||
const liveCmdNames = readCmdNames();
|
||||
|
||||
const input = 'Run gsd-plan-phase and /gsd-execute-phase then gsd:review.'; // mixed, only colon should change
|
||||
const out = transformContentToHyphen(input, liveCmdNames);
|
||||
assert.ok(out.includes('gsd-plan-phase'), 'pre-existing hyphen stays');
|
||||
assert.ok(out.includes('/gsd-execute-phase'), 'pre-existing hyphen stays');
|
||||
assert.ok(out.includes('gsd-review'), 'colon form was normalized');
|
||||
assert.ok(!out.includes('gsd:review'), 'no colon form remains');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,8 +89,10 @@ describe('convertClaudeCommandToClaudeSkill', () => {
|
||||
assert.ok(result.includes('name: gsd-next'), 'frontmatter name uses hyphen form (#2808)');
|
||||
});
|
||||
|
||||
test('preserves body content unchanged', () => {
|
||||
const body = '\n<objective>\nDo the thing.\n</objective>\n\n<process>\nStep 1.\nStep 2.\n</process>\n';
|
||||
test('preserves body content while normalizing gsd: command references (#3583)', () => {
|
||||
// The body transformer now rewrites gsd: references (colon → hyphen) but must
|
||||
// leave all other custom prose, tags, and structure intact.
|
||||
const body = '\n<objective>\nSee /gsd:plan-phase and gsd:review for details.\n</objective>\n\n<process>\nStep 1.\nStep 2.\n</process>\n';
|
||||
const input = [
|
||||
'---',
|
||||
'name: gsd:test',
|
||||
@@ -100,10 +102,17 @@ describe('convertClaudeCommandToClaudeSkill', () => {
|
||||
].join('');
|
||||
|
||||
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-test');
|
||||
// Custom structure preserved
|
||||
assert.ok(result.includes('<objective>'), 'objective tag preserved');
|
||||
assert.ok(result.includes('Do the thing.'), 'body text preserved');
|
||||
assert.ok(result.includes('See /gsd-plan-phase'), 'rewritten command reference visible');
|
||||
assert.ok(result.includes('<process>'), 'process tag preserved');
|
||||
assert.ok(result.includes('Step 1.'), 'step text preserved');
|
||||
|
||||
// #3583: gsd: references in body are normalized to hyphen form
|
||||
assert.ok(result.includes('/gsd-plan-phase'), 'colon command ref rewritten to hyphen');
|
||||
assert.ok(result.includes('gsd-review'), 'bare colon ref rewritten to hyphen');
|
||||
assert.ok(!result.includes('gsd:plan-phase'), 'no colon form should survive in body');
|
||||
assert.ok(!result.includes('gsd:review'), 'no colon form should survive in body');
|
||||
});
|
||||
|
||||
test('preserves agent field', () => {
|
||||
|
||||
@@ -139,6 +139,14 @@ const INTERNAL_COMPONENT_SLUGS = new Set([
|
||||
// a belt-and-suspenders guard against the pattern returning in other locale docs.
|
||||
'alternative-1',
|
||||
'alternative-2',
|
||||
|
||||
// gsd-sync-skills — installed Claude skill directory name (also a workflow
|
||||
// under get-shit-done/workflows/sync-skills.md), but NOT a registered
|
||||
// slash command (no commands/gsd/sync-skills.md). Docs reference it as a
|
||||
// filesystem path component, e.g. "~/.agents/skills/gsd-sync-skills/" in
|
||||
// docs/discussions/grok-build-support-2026-05.md. The regex captures
|
||||
// "/gsd-sync-skills" from the path. Invoked via Skill(skill="gsd-sync-skills").
|
||||
'sync-skills',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user