fix(#2543): replace legacy /gsd-<cmd> syntax with /gsd:<cmd> across all source files (#2595)

Commands are now installed as commands/gsd/<name>.md and invoked as
/gsd:<name> in Claude Code. The old hyphen form /gsd-<name> was still
hardcoded in hundreds of places across workflows, references, templates,
lib modules, and command files — causing "Unknown command" errors
whenever GSD suggested a command to the user.

Replace all /gsd-<cmd> occurrences where <cmd> is a known command name
(derived at runtime from commands/gsd/*.md) using a targeted Node.js
script. Agent names, tool names (gsd-sdk, gsd-tools), directory names,
and path fragments are not touched.

Adds regression test tests/bug-2543-gsd-slash-namespace.test.cjs that
enforces zero legacy occurrences going forward. Removes inverted
tests/stale-colon-refs.test.cjs (bug #1748) which enforced the now-obsolete
hyphen form; the new bug-2543 test supersedes it. Updates 5 assertion
tests that hardcoded the old hyphen form to accept the new colon form.

Closes #2543

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-22 12:04:25 -04:00
committed by GitHub
parent 533973700c
commit 73c1af5168
150 changed files with 962 additions and 939 deletions

View File

@@ -0,0 +1,88 @@
'use strict';
/**
* Bug #2543: GSD emits legacy '/gsd-<cmd>' syntax in 102 places.
*
* Installed commands are under commands/gsd/<name>.md and invoked as
* /gsd:<name>. All internal references must use the colon form.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const ROOT = path.join(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const SEARCH_DIRS = [
path.join(ROOT, 'get-shit-done', 'bin', 'lib'),
path.join(ROOT, 'get-shit-done', 'workflows'),
path.join(ROOT, 'get-shit-done', 'references'),
path.join(ROOT, 'get-shit-done', 'templates'),
path.join(ROOT, 'get-shit-done', 'contexts'),
COMMANDS_DIR,
];
const EXTENSIONS = new Set(['.md', '.cjs', '.js']);
function collectFiles(dir, results = []) {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) collectFiles(full, results);
else if (EXTENSIONS.has(path.extname(e.name))) results.push(full);
}
return results;
}
const cmdNames = fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => f.replace(/\.md$/, ''))
.sort((a, b) => b.length - a.length);
const legacyPattern = new RegExp(`/gsd-(${cmdNames.join('|')})(?=[^a-zA-Z0-9_-]|$)`);
const allFiles = SEARCH_DIRS.flatMap(d => collectFiles(d));
describe('slash-command namespace fix (#2543)', () => {
test('commands/gsd/ directory contains known command files', () => {
assert.ok(cmdNames.length > 0, 'commands/gsd/ must contain .md files');
assert.ok(cmdNames.includes('plan-phase'), 'plan-phase must be a known command');
assert.ok(cmdNames.includes('execute-phase'), 'execute-phase must be a known command');
});
test('no /gsd-<cmd> legacy syntax remains in source files', () => {
const violations = [];
for (const file of allFiles) {
const src = fs.readFileSync(file, 'utf-8');
const lines = src.split('\n');
for (let i = 0; i < lines.length; i++) {
if (legacyPattern.test(lines[i])) {
violations.push(`${path.relative(ROOT, file)}:${i + 1}: ${lines[i].trim().slice(0, 80)}`);
}
}
}
assert.strictEqual(
violations.length,
0,
`Found ${violations.length} legacy /gsd-<cmd> reference(s):\n${violations.slice(0, 10).join('\n')}`,
);
});
test('gsd-sdk and gsd-tools identifiers are not rewritten', () => {
for (const file of allFiles) {
const src = fs.readFileSync(file, 'utf-8');
assert.ok(
!src.includes('/gsd:sdk'),
`${path.relative(ROOT, file)} must not contain /gsd:sdk (gsd-sdk was incorrectly renamed)`,
);
assert.ok(
!src.includes('/gsd:tools'),
`${path.relative(ROOT, file)} must not contain /gsd:tools (gsd-tools was incorrectly renamed)`,
);
}
});
});

View File

@@ -36,9 +36,9 @@ describe('generate-claude-md', () => {
const claudePath = path.join(tmpDir, 'CLAUDE.md');
const content = fs.readFileSync(claudePath, 'utf-8');
assert.ok(content.includes('## GSD Workflow Enforcement'));
assert.ok(content.includes('/gsd-quick'));
assert.ok(content.includes('/gsd-debug'));
assert.ok(content.includes('/gsd-execute-phase'));
assert.ok(content.includes('/gsd:quick'));
assert.ok(content.includes('/gsd:debug'));
assert.ok(content.includes('/gsd:execute-phase'));
assert.ok(content.includes('Do not make direct repo edits outside a GSD workflow'));
});

View File

@@ -101,7 +101,7 @@ describe('execute-phase docs: user-facing wave flag', () => {
'help.md should describe wave-specific execution'
);
assert.ok(
content.includes('Usage: `/gsd-execute-phase 5 --wave 2`'),
content.includes('Usage: `/gsd:execute-phase 5 --wave 2`') || content.includes('Usage: `/gsd-execute-phase 5 --wave 2`'),
'help.md should include wave-filter usage'
);
});

View File

@@ -114,7 +114,7 @@ describe('import workflow content', () => {
test('no-args usage display is present', () => {
assert.ok(
content.includes('Usage: /gsd-import'),
content.includes('Usage: /gsd:import') || content.includes('Usage: /gsd-import'),
'workflow should display usage when no arguments provided'
);
});

View File

@@ -1,117 +0,0 @@
/**
* Stale /gsd: colon reference detection test
*
* Guards against regression of bug #1748: after the command naming migration
* from colon to hyphen format, no stale colon references should remain in
* source, workflows, commands, docs, issue templates, or hooks.
*
* Test input strings that deliberately test colon-to-hyphen conversion are
* allowed (they are the INPUT to a converter function). Everything else is stale.
*
* Uses node:test and node:assert/strict (NOT Jest).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
/**
* Recursively collect files matching the given extensions, excluding
* CHANGELOG.md, node_modules/, .git/, dist/, local planning overlays, and
* root CLAUDE.md (gitignored local IDE overlay — see repo .gitignore).
*/
function collectFiles(dir, extensions, results = []) {
const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'dist', '.claude', '.worktrees', '.planning']);
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
if (EXCLUDED_DIRS.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
collectFiles(full, extensions, results);
} else if (entry.isFile()) {
if (entry.name === 'CLAUDE.md' && path.resolve(dir) === path.resolve(ROOT)) {
continue;
}
const ext = path.extname(entry.name);
if (extensions.has(ext) && entry.name !== 'CHANGELOG.md') {
results.push(full);
}
}
}
return results;
}
/**
* Determine whether a /gsd: match in a test file is a legitimate test input
* (i.e., the input string fed to a colon-to-hyphen converter).
*/
function isTestInput(filePath, line) {
const rel = path.relative(ROOT, filePath).replace(/\\/g, '/');
// SDK test files (.ts) that test sanitizer stripping of /gsd: patterns
if (rel === 'sdk/src/prompt-sanitizer.test.ts') return true;
if (rel === 'sdk/src/init-runner.test.ts') return true;
if (rel === 'sdk/src/phase-prompt.test.ts') return true;
// Conversion test files: input strings to convert* functions contain /gsd:
const conversionTestFiles = [
'tests/windsurf-conversion.test.cjs',
'tests/augment-conversion.test.cjs',
'tests/cursor-conversion.test.cjs',
'tests/antigravity-install.test.cjs',
'tests/copilot-install.test.cjs',
'tests/codex-config.test.cjs',
'tests/trae-install.test.cjs',
'tests/codebuddy-install.test.cjs',
];
if (conversionTestFiles.includes(rel)) {
const trimmed = line.trim();
// JSDoc block-comment lines with /gsd: in description are stale
if (/^\*/.test(trimmed)) return false;
// Everything else in conversion test files is a test input
return true;
}
return false;
}
describe('No stale /gsd: colon references (#1748)', () => {
test('all /gsd: references should be hyphenated except test inputs', () => {
const extensions = new Set(['.md', '.js', '.cjs', '.ts', '.yml', '.sh', '.svg']);
const files = collectFiles(ROOT, extensions);
const staleRefs = [];
const pattern = /\/gsd:[a-z]/g;
for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!pattern.test(line)) continue;
pattern.lastIndex = 0;
if (!isTestInput(filePath, line)) {
const rel = path.relative(ROOT, filePath).replace(/\\/g, '/');
staleRefs.push(` ${rel}:${i + 1}: ${line.trim()}`);
}
}
}
if (staleRefs.length > 0) {
assert.fail(
`Found ${staleRefs.length} stale /gsd: colon reference(s) that should use /gsd- hyphen format:\n${staleRefs.join('\n')}`
);
}
});
});

View File

@@ -87,7 +87,7 @@ describe('ultraplan-phase workflow runtime gate', () => {
test('error message references /gsd-plan-phase as local alternative', () => {
assert.ok(
content.includes('gsd-plan-phase'),
content.includes('gsd:plan-phase') || content.includes('gsd-plan-phase'),
'error message should direct users to /gsd-plan-phase as the local alternative'
);
});
@@ -104,7 +104,7 @@ describe('ultraplan-phase workflow initialization', () => {
test('handles missing .planning directory', () => {
assert.ok(
content.includes('gsd-new-project') || content.includes('/gsd-new-project'),
content.includes('gsd-new-project') || content.includes('/gsd-new-project') || content.includes('gsd:new-project'),
'workflow should direct user to /gsd-new-project when .planning is missing'
);
});
@@ -154,7 +154,7 @@ describe('ultraplan-phase workflow return path', () => {
});
test('directs user to run /gsd-import --from after ultraplan completes', () => {
assert.ok(content.includes('gsd-import'), 'workflow must direct user to run /gsd-import --from with the saved file path');
assert.ok(content.includes('gsd-import') || content.includes('gsd:import'), 'workflow must direct user to run /gsd:import --from with the saved file path');
});
test('mentions the --from flag for gsd-import', () => {

View File

@@ -76,8 +76,8 @@ describe('verify-work.md — auto-transition after UAT passes with 0 issues', ()
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
// The workflow should suggest /gsd-secure-phase when security is enabled but no file exists
assert.ok(
content.includes('gsd-secure-phase'),
'verify-work.md must suggest /gsd-secure-phase when security gate blocks transition'
content.includes('gsd-secure-phase') || content.includes('gsd:secure-phase'),
'verify-work.md must suggest /gsd:secure-phase when security gate blocks transition'
);
});
});