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:
Cristian Uibar
2026-05-16 20:09:58 +03:00
committed by GitHub
parent 7f8b5701bf
commit 05316369ae
11 changed files with 631 additions and 26 deletions

View File

@@ -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');
});
});

View File

@@ -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', () => {

View File

@@ -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',
]);
/**