fix(claude): restore namespaced /gsd:<command> references (#3452)

* fix(claude): restore namespaced /gsd:<command> references

* test(claude): align slash-command expectations to /gsd: form

* test(claude): align generated command references to /gsd:

* test(claude): finish /gsd: namespace expectation updates
This commit is contained in:
Tom Boucher
2026-05-12 21:53:24 -04:00
committed by GitHub
parent 740d718941
commit a60e05c714
187 changed files with 1089 additions and 1142 deletions

View File

@@ -3,21 +3,16 @@
// allow-test-rule: structural-regression-guard
/**
* Slash-command namespace invariant (#2543, updated by #2697).
* Slash-command namespace invariant (#3443).
*
* History:
* #2543 switched user-facing references from /gsd-<cmd> (dash) to /gsd:<cmd> (colon)
* because Claude Code's skill frontmatter used `name: gsd:<cmd>`.
* #2697 reversed this: Claude Code slash commands are invoked by skill *directory*
* name (gsd-<cmd>), not frontmatter name. The colon form (/gsd:<cmd>) does not work
* as a user-typed slash command. Other environment installers (OpenCode, Copilot,
* Antigravity) already transform gsd: → gsd- at install time, so changing the source
* to use gsd- makes all environments consistent.
* #3443 re-establishes `/gsd:<cmd>` as canonical in Claude-facing source text.
* The source repo is authored for Claude command registration under
* `.claude/commands/gsd/` (namespaced slash commands), while non-Claude runtimes
* perform install-time conversion (for example `/gsd:<cmd>` -> `/gsd-<cmd>`).
*
* Invariant enforced here:
* No `/gsd:<cmd>` pattern in user-facing source text.
* `Skill(skill="gsd:<cmd>")` calls are checked by the skill frontmatter
* parity tests and should use `Skill(skill="gsd-<cmd>")`.
* No `/gsd-<cmd>` pattern in Claude-facing source text.
*
* Exceptions:
* - CHANGELOG.md: historical entries document commands under their original names.
@@ -37,10 +32,9 @@ const SEARCH_DIRS = [
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,
path.join(ROOT, 'agents'),
path.join(ROOT, 'sdk', 'src'),
path.join(ROOT, 'hooks'),
];
const TOP_LEVEL_FILES = [
@@ -53,50 +47,6 @@ const TOP_LEVEL_FILES = [
// the fixer also rewrites `.ts`/`.tsx`), so it is not shared.
const { SKIP_DIRS } = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
// Discover user-facing markdown surfaces dynamically so a freshly added
// doc (a new RELEASE-*.md, a new top-level guide) is automatically scanned
// for namespace drift. A hand-curated list silently weakens drift detection
// over time — every time a doc is added, someone has to remember to extend
// the list, and the failure mode is invisible: the test passes but doesn't
// actually inspect the new file. We scan every .md under docs/ plus
// README.md at the repo root.
function discoverDocSearchFiles(root) {
const out = [];
const readme = path.join(root, 'README.md');
if (fs.existsSync(readme)) out.push(readme);
// Walk docs/ recursively. Localized translation trees (docs/ja-JP/,
// docs/zh-CN/, docs/ko-KR/, docs/pt-BR/) and nested doc collections
// (docs/skills/, docs/superpowers/) all carry user-facing markdown that
// can drift; a top-level-only scan would silently exclude them. Iterative
// stack walk avoids recursion limits on deep trees.
const stack = [path.join(root, 'docs')];
while (stack.length > 0) {
const dir = stack.pop();
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
stack.push(full);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
out.push(full);
}
}
}
return out.sort();
}
const DOC_SEARCH_FILES = discoverDocSearchFiles(ROOT);
// Limited to .md (and pre-existing .cjs/.js) by the no-source-grep standard:
// markdown text IS the deployed product, but .ts/.tsx source must be guarded
// via runtime behavior. The fixer (scripts/fix-slash-commands.cjs) covers
// .ts/.tsx auto-rewrites at build time; idempotency (a no-op second run) is
// the runtime guard for those extensions.
const EXTENSIONS = new Set(['.md', '.cjs', '.js']);
function collectFiles(dir, results = []) {
@@ -118,24 +68,20 @@ const cmdNames = fs.readdirSync(COMMANDS_DIR)
.map(f => f.replace(/\.md$/, ''))
.sort((a, b) => b.length - a.length);
// Matches /gsd:<cmd> — the retired user-facing format.
// Does NOT match Skill(skill="gsd:<cmd>") because those have no leading slash.
const retiredPattern = new RegExp(`/gsd:(${cmdNames.join('|')})(?=[^a-zA-Z0-9_-]|$)`);
const retiredPattern = new RegExp(`/gsd-(${cmdNames.join('|')})(?=[^a-zA-Z0-9_-]|$)`);
const allFiles = SEARCH_DIRS.flatMap(d => collectFiles(d));
const topLevelFiles = TOP_LEVEL_FILES.filter((file) => fs.existsSync(file));
const allUserFacingFiles = allFiles
.concat(topLevelFiles)
.concat(DOC_SEARCH_FILES.filter((file) => fs.existsSync(file)));
const allUserFacingFiles = allFiles.concat(topLevelFiles);
describe('slash-command namespace invariant (#2697)', () => {
describe('slash-command namespace invariant (#3443)', () => {
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> retired syntax in user-facing source files', () => {
test('no /gsd-<cmd> retired syntax in Claude-facing source files', () => {
const violations = [];
for (const file of allUserFacingFiles) {
const src = fs.readFileSync(file, 'utf-8');
@@ -149,7 +95,7 @@ describe('slash-command namespace invariant (#2697)', () => {
assert.strictEqual(
violations.length,
0,
`Found ${violations.length} retired /gsd:<cmd> reference(s) — use /gsd-<cmd> instead:\n${violations.slice(0, 10).join('\n')}`,
`Found ${violations.length} retired /gsd-<cmd> reference(s) — use /gsd:<cmd> instead:\n${violations.slice(0, 10).join('\n')}`,
);
});
@@ -169,53 +115,45 @@ describe('slash-command namespace invariant (#2697)', () => {
// the production CLI rewrites.
const liveCmdNames = cmdNames;
test('rewrites /gsd:<cmd> to /gsd-<cmd>', () => {
const out = transformContent('See /gsd:plan-phase for details.', liveCmdNames);
assert.ok(out.includes('/gsd-plan-phase'), `expected /gsd-plan-phase, got: ${out}`);
assert.ok(!out.includes('/gsd:plan-phase'), `colon form must not survive, got: ${out}`);
test('rewrites /gsd-<cmd> to /gsd:<cmd>', () => {
const out = transformContent('See /gsd-plan-phase for details.', liveCmdNames);
assert.ok(out.includes('/gsd:plan-phase'), `expected /gsd:plan-phase, got: ${out}`);
assert.ok(!out.includes('/gsd-plan-phase'), `dash form must not survive, got: ${out}`);
});
test('rewrites multiple occurrences in one pass', () => {
const out = transformContent('Run /gsd:plan-phase then /gsd:execute-phase.', liveCmdNames);
assert.ok(out.includes('/gsd-plan-phase'));
assert.ok(out.includes('/gsd-execute-phase'));
assert.ok(!out.match(/\/gsd:[a-z]/), `no colon form may remain, got: ${out}`);
const out = transformContent('Run /gsd-plan-phase then /gsd-execute-phase.', liveCmdNames);
assert.ok(out.includes('/gsd:plan-phase'));
assert.ok(out.includes('/gsd:execute-phase'));
assert.ok(!out.match(/\/gsd-[a-z]/), `no dash form may remain, got: ${out}`);
});
test('does not rewrite canonical hyphen form (idempotent)', () => {
const input = '/gsd-plan-phase is the canonical name.';
test('does not rewrite canonical colon form (idempotent)', () => {
const input = '/gsd:plan-phase is the canonical name.';
assert.strictEqual(transformContent(input, liveCmdNames), input,
'transformer must be a no-op when input is already canonical');
});
test('does not rewrite gsd-sdk or gsd-tools (not slash commands)', () => {
// Edge case: even though sdk/tools aren't in cmdNames, defensively check
// that strings like "/gsd:sdk" pass through untouched.
const input = 'Run /gsd:sdk query and /gsd:tools init.';
const input = 'Run /gsd-sdk query and /gsd-tools init.';
assert.strictEqual(transformContent(input, liveCmdNames), input,
'transformer must leave non-command identifiers alone');
});
test('respects word boundary — does not rewrite /gsd:plan-phase-extra', () => {
// The trailing -extra means this is NOT the plan-phase command.
// The negative lookahead `[^a-zA-Z0-9_-]|$` should prevent the match.
const out = transformContent('/gsd:plan-phase-extra', liveCmdNames);
assert.strictEqual(out, '/gsd:plan-phase-extra',
test('respects word boundary — does not rewrite /gsd-plan-phase-extra', () => {
const out = transformContent('/gsd-plan-phase-extra', liveCmdNames);
assert.strictEqual(out, '/gsd-plan-phase-extra',
'word-boundary lookahead must prevent partial matches');
});
});
test('gsd-sdk and gsd-tools identifiers are not rewritten', () => {
for (const file of allUserFacingFiles) {
const src = fs.readFileSync(file, 'utf-8');
assert.ok(
!src.includes('/gsd:sdk'),
`${path.relative(ROOT, file)} must not contain /gsd:sdk (gsd-sdk is not a slash command)`,
);
assert.ok(
!src.includes('/gsd:tools'),
`${path.relative(ROOT, file)} must not contain /gsd:tools (gsd-tools is not a slash command)`,
);
}
test('transformer leaves non-command identifiers untouched', () => {
const { transformContent } = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
const sample = 'Use /gsd-sdk query and node bin/gsd-tools.cjs';
assert.strictEqual(
transformContent(sample, cmdNames),
sample,
'gsd-sdk and gsd-tools are not slash commands and must remain untouched'
);
});
});

View File

@@ -1,17 +1,17 @@
/**
* Regression test for bug #2948
*
* `/gsd-spike --wrap-up` was silently no-oping because:
* `/gsd:spike --wrap-up` was silently no-oping because:
* 1. `commands/gsd/spike.md` listed `--wrap-up` as a flag but had no dispatch block.
* 2. `workflows/spike.md` still referenced the deleted `/gsd-spike-wrap-up` entry-point
* instead of the correct `/gsd-spike --wrap-up` form.
* instead of the correct `/gsd:spike --wrap-up` form.
*
* Fix:
* - `commands/gsd/spike.md` now has a dispatch block that routes `--wrap-up` to
* spike-wrap-up.md, and spike-wrap-up.md is listed in execution_context so the
* runtime can find it.
* - `workflows/spike.md` companion references updated from `/gsd-spike-wrap-up` to
* `/gsd-spike --wrap-up`.
* `/gsd:spike --wrap-up`.
*/
// allow-test-rule: source-text-is-the-product
@@ -86,7 +86,7 @@ function parseExecutionContextRefs(section) {
.map(l => l.slice(1).trim());
}
describe('bug-2948: /gsd-spike --wrap-up dispatch wiring', () => {
describe('bug-2948: /gsd:spike --wrap-up dispatch wiring', () => {
describe('commands/gsd/spike.md — frontmatter and section contract', () => {
test('spike.md command file exists and has valid frontmatter', () => {
assert.ok(fs.existsSync(SPIKE_CMD_PATH), 'commands/gsd/spike.md should exist');
@@ -147,15 +147,15 @@ describe('bug-2948: /gsd-spike --wrap-up dispatch wiring', () => {
const fm = parseFrontmatter(fs.readFileSync(SPIKE_WORKFLOW_PATH, 'utf-8'));
assert.ok(
!fm._body.includes('/gsd-spike-wrap-up'),
'workflows/spike.md must not reference the deleted /gsd-spike-wrap-up command; use /gsd-spike --wrap-up instead'
'workflows/spike.md must not reference the deleted /gsd-spike-wrap-up command; use /gsd:spike --wrap-up instead'
);
});
test('references /gsd-spike --wrap-up as the canonical wrap-up invocation', () => {
test('references /gsd:spike --wrap-up as the canonical wrap-up invocation', () => {
const fm = parseFrontmatter(fs.readFileSync(SPIKE_WORKFLOW_PATH, 'utf-8'));
assert.ok(
fm._body.includes('/gsd-spike --wrap-up'),
'workflows/spike.md must reference /gsd-spike --wrap-up as the canonical wrap-up command'
fm._body.includes('/gsd:spike --wrap-up'),
'workflows/spike.md must reference /gsd:spike --wrap-up as the canonical wrap-up command'
);
});
});

View File

@@ -4,7 +4,7 @@
// reclassify some entries as source-text-is-the-product during migration.
/**
* GSD Tests — /gsd-sketch --wrap-up silently no-ops (#2949)
* GSD Tests — /gsd:sketch --wrap-up silently no-ops (#2949)
*
* The --wrap-up flag was documented in commands/gsd/sketch.md but never dispatched.
* The sketch-wrap-up.md micro-skill entry point was deleted in #2790 and the dispatch
@@ -56,11 +56,11 @@ describe('bug-2949: sketch --wrap-up dispatch wiring', () => {
);
});
test('workflows/sketch.md DOES contain new /gsd-sketch --wrap-up form', () => {
test('workflows/sketch.md DOES contain new /gsd:sketch --wrap-up form', () => {
const content = fs.readFileSync(SKETCH_WORKFLOW, 'utf8');
assert.ok(
content.includes('/gsd-sketch --wrap-up'),
'workflows/sketch.md should reference /gsd-sketch --wrap-up (the new form)'
content.includes('/gsd:sketch --wrap-up'),
'workflows/sketch.md should reference /gsd:sketch --wrap-up (the new form)'
);
});
});

View File

@@ -7,12 +7,12 @@
* gsd-spike-wrap-up, gsd-sketch-wrap-up, gsd-code-review-fix).
*
* Fix: Update every occurrence to the new consolidated forms:
* /gsd-phase (no flag | --insert | --remove)
* /gsd-capture
* /gsd-config (--profile | --integrations | --advanced)
* /gsd-spike --wrap-up
* /gsd-sketch --wrap-up
* /gsd-code-review --fix
* /gsd:phase (no flag | --insert | --remove)
* /gsd:capture
* /gsd:config (--profile | --integrations | --advanced)
* /gsd:spike --wrap-up
* /gsd:sketch --wrap-up
* /gsd:code-review --fix
*/
'use strict';
@@ -45,42 +45,42 @@ const DELETED_COMMANDS = [
// Per-file assertions: [file, deletedCmd, newForm]
const FILE_ASSERTIONS = [
// help.md
['help.md', '/gsd-add-phase', '/gsd-phase "Add admin dashboard"'],
['help.md', '/gsd-insert-phase', '/gsd-phase --insert 7 "Fix critical auth bug"'],
['help.md', '/gsd-remove-phase', '/gsd-phase --remove 17'],
['help.md', '/gsd-spike-wrap-up', '/gsd-spike --wrap-up'],
['help.md', '/gsd-sketch-wrap-up', '/gsd-sketch --wrap-up'],
['help.md', '/gsd-add-todo', '/gsd-capture'],
['help.md', '/gsd-set-profile', '/gsd-config --profile budget'],
['help.md', '/gsd-add-phase', '/gsd:phase "Add admin dashboard"'],
['help.md', '/gsd-insert-phase', '/gsd:phase --insert 7 "Fix critical auth bug"'],
['help.md', '/gsd-remove-phase', '/gsd:phase --remove 17'],
['help.md', '/gsd-spike-wrap-up', '/gsd:spike --wrap-up'],
['help.md', '/gsd-sketch-wrap-up', '/gsd:sketch --wrap-up'],
['help.md', '/gsd-add-todo', '/gsd:capture'],
['help.md', '/gsd-set-profile', '/gsd:config --profile budget'],
// do.md
['do.md', '/gsd-spike-wrap-up', '/gsd-spike --wrap-up'],
['do.md', '/gsd-sketch-wrap-up', '/gsd-sketch --wrap-up'],
['do.md', '/gsd-add-phase', '/gsd-phase'],
['do.md', '/gsd-add-todo', '/gsd-capture'],
['do.md', '/gsd-spike-wrap-up', '/gsd:spike --wrap-up'],
['do.md', '/gsd-sketch-wrap-up', '/gsd:sketch --wrap-up'],
['do.md', '/gsd-add-phase', '/gsd:phase'],
['do.md', '/gsd-add-todo', '/gsd:capture'],
// settings.md
['settings.md', '/gsd-code-review-fix', '/gsd-code-review --fix'],
['settings.md', '/gsd-settings-integrations', '/gsd-config --integrations'],
['settings.md', '/gsd-set-profile', '/gsd-config --profile'],
['settings.md', '/gsd-settings-advanced', '/gsd-config --advanced'],
['settings.md', '/gsd-code-review-fix', '/gsd:code-review --fix'],
['settings.md', '/gsd-settings-integrations', '/gsd:config --integrations'],
['settings.md', '/gsd-set-profile', '/gsd:config --profile'],
['settings.md', '/gsd-settings-advanced', '/gsd:config --advanced'],
// discuss-phase.md
['discuss-phase.md', '/gsd-spike-wrap-up', '/gsd-spike --wrap-up'],
['discuss-phase.md', '/gsd-sketch-wrap-up', '/gsd-sketch --wrap-up'],
['discuss-phase.md', '/gsd-spike-wrap-up', '/gsd:spike --wrap-up'],
['discuss-phase.md', '/gsd-sketch-wrap-up', '/gsd:sketch --wrap-up'],
// new-project.md
['new-project.md', '/gsd-spike-wrap-up', '/gsd-spike --wrap-up'],
['new-project.md', '/gsd-sketch-wrap-up', '/gsd-sketch --wrap-up'],
['new-project.md', '/gsd-spike-wrap-up', '/gsd:spike --wrap-up'],
['new-project.md', '/gsd-sketch-wrap-up', '/gsd:sketch --wrap-up'],
// plan-phase.md
['plan-phase.md', '/gsd-insert-phase', '/gsd-phase --insert'],
['plan-phase.md', '/gsd-insert-phase', '/gsd:phase --insert'],
// spike.md
['spike.md', '/gsd-spike-wrap-up', '/gsd-spike --wrap-up'],
['spike.md', '/gsd-spike-wrap-up', '/gsd:spike --wrap-up'],
// sketch.md
['sketch.md', '/gsd-sketch-wrap-up', '/gsd-sketch --wrap-up'],
['sketch.md', '/gsd-sketch-wrap-up', '/gsd:sketch --wrap-up'],
];
describe('bug #2950: stale deleted-command references removed from workflow files', () => {

View File

@@ -11,11 +11,11 @@ process.env.GSD_TEST_MODE = '1';
* Bug #2954: keep `help.md` and the live `commands/gsd/*` slash surface
* in lockstep. Two regression tests:
*
* 1. help.md must not advertise any /gsd-<name> that has no shipped
* 1. help.md must not advertise any /gsd[-:]<name> that has no shipped
* slash command. (Caught the original #2954 regression: #2824 deleted
* 31 stubs without updating help.md.)
*
* 2. Every shipped /gsd-<name> command must appear in help.md. (Caught
* 2. Every shipped /gsd[-:]<name> command must appear in help.md. (Caught
* the inverse: a command lands without docs, so users never discover it.)
*
* The shipped slash name is parsed from frontmatter `name:` (which can be
@@ -24,7 +24,7 @@ process.env.GSD_TEST_MODE = '1';
* different slash name (`gsd-context`) than their filename suggests.
*
* Also covers `do.md`, the dispatcher invoked at runtime by
* `/gsd-progress --do`: any `/gsd-<name>` token in its routing table must
* `/gsd:progress --do`: any `/gsd[-:]<name>` token in its routing table must
* resolve to a live command, otherwise the dispatcher emits "Unknown command".
*/
@@ -74,7 +74,7 @@ function listShippedSlashBaseNames() {
function extractSlashReferences(contents) {
const names = new Set();
const tokenRe = /\/gsd-([a-z][a-z0-9-]*)/g;
const tokenRe = /\/gsd[:-]([a-z][a-z0-9-]*)/g;
let match;
while ((match = tokenRe.exec(contents)) !== null) {
names.add(match[1]);
@@ -109,7 +109,7 @@ function listShippedFlagsByCommand() {
}
describe('Bug #2954: help.md ↔ commands/gsd/ bidirectional parity', () => {
test('every /gsd-<name> referenced in help.md is a shipped command', () => {
test('every /gsd[-:]<name> referenced in help.md is a shipped command', () => {
const helpContents = fs.readFileSync(HELP_MD, 'utf8');
const referenced = extractSlashReferences(helpContents);
const shipped = listShippedSlashBaseNames();
@@ -117,11 +117,11 @@ describe('Bug #2954: help.md ↔ commands/gsd/ bidirectional parity', () => {
assert.deepEqual(
dangling,
[],
`help.md advertises /gsd-<name> commands that are not shipped: ${dangling.join(', ')}`,
`help.md advertises /gsd[-:]<name> commands that are not shipped: ${dangling.join(', ')}`,
);
});
test('every shipped /gsd-<name> command is documented in help.md', () => {
test('every shipped /gsd[-:]<name> command is documented in help.md', () => {
const helpContents = fs.readFileSync(HELP_MD, 'utf8');
const referenced = extractSlashReferences(helpContents);
const shipped = listShippedSlashBaseNames();
@@ -129,11 +129,11 @@ describe('Bug #2954: help.md ↔ commands/gsd/ bidirectional parity', () => {
assert.deepEqual(
undocumented,
[],
`commands shipped under commands/gsd/ with no /gsd-<name> reference in help.md: ${undocumented.join(', ')}`,
`commands shipped under commands/gsd/ with no /gsd[-:]<name> reference in help.md: ${undocumented.join(', ')}`,
);
});
test('every /gsd-<name> in do.md (live dispatcher) is a shipped command', () => {
test('every /gsd[-:]<name> in do.md (live dispatcher) is a shipped command', () => {
const doContents = fs.readFileSync(DO_MD, 'utf8');
const referenced = extractSlashReferences(doContents);
const shipped = listShippedSlashBaseNames();
@@ -141,7 +141,7 @@ describe('Bug #2954: help.md ↔ commands/gsd/ bidirectional parity', () => {
assert.deepEqual(
dangling,
[],
`do.md routing table references /gsd-<name> that is not shipped: ${dangling.join(', ')}`,
`do.md routing table references /gsd[-:]<name> that is not shipped: ${dangling.join(', ')}`,
);
});
@@ -154,10 +154,15 @@ describe('Bug #2954: help.md ↔ commands/gsd/ bidirectional parity', () => {
// Accept `/gsd-<command> --<flag>` (precise) OR a bare `--<flag>` token
// anywhere in help.md (good enough for shared flags like `--force` that
// appear under multiple commands' descriptions).
const preciseToken = `/gsd-${command} --${flag}`;
const preciseDash = `/gsd-${command} --${flag}`;
const preciseColon = `/gsd:${command} --${flag}`;
const flagToken = `--${flag}`;
if (!helpContents.includes(preciseToken) && !helpContents.includes(flagToken)) {
gaps.push(`/gsd-${command} --${flag}`);
if (
!helpContents.includes(preciseDash) &&
!helpContents.includes(preciseColon) &&
!helpContents.includes(flagToken)
) {
gaps.push(`/gsd:${command} --${flag}`);
}
}
}

View File

@@ -40,9 +40,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

@@ -65,8 +65,8 @@ describe('buildBannerOutput', () => {
'banner should name latest version'
);
assert.ok(
out.systemMessage.includes('/gsd-update'),
'banner should reference /gsd-update command'
out.systemMessage.includes('/gsd:update'),
'banner should reference /gsd:update command'
);
});
@@ -211,7 +211,7 @@ describe('gsd-update-banner.js end-to-end', () => {
const parsed = JSON.parse(r.stdout);
assert.equal(typeof parsed.systemMessage, 'string');
assert.ok(parsed.systemMessage.includes('1.40.0'));
assert.ok(parsed.systemMessage.includes('/gsd-update'));
assert.ok(parsed.systemMessage.includes('/gsd:update'));
} finally {
fs.rmSync(home, { recursive: true, force: true });
}

View File

@@ -209,8 +209,8 @@ describe('/gsd-settings advertises /gsd-settings-advanced', () => {
test('settings workflow mentions canonical /gsd-config --advanced', () => {
const text = fs.readFileSync(SETTINGS_WORKFLOW_PATH, 'utf-8');
assert.ok(
text.includes('gsd-config --advanced'),
'get-shit-done/workflows/settings.md must mention /gsd-config --advanced'
text.includes('/gsd:config --advanced'),
'get-shit-done/workflows/settings.md must mention /gsd:config --advanced'
);
assert.ok(
!text.includes('gsd-settings-advanced') && !text.includes('gsd:settings-advanced'),

View File

@@ -234,7 +234,7 @@ describe('milestone complete command', () => {
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n\n## Current Position\n\nPhase: 03 — EXECUTING\nPlan: 03-02\nStatus: Executing\nLast activity: 2025-01-01 — Running phase\n\n## Operator Next Steps\n\n- Re-run /gsd-complete-milestone v1.0\n`
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n\n## Current Position\n\nPhase: 03 — EXECUTING\nPlan: 03-02\nStatus: Executing\nLast activity: 2025-01-01 — Running phase\n\n## Operator Next Steps\n\n- Re-run /gsd:complete-milestone v1.0\n`
);
const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir);
@@ -243,8 +243,8 @@ describe('milestone complete command', () => {
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('Phase: Milestone v1.0 complete'));
assert.ok(state.includes('Status: Awaiting next milestone'));
assert.ok(!state.includes('Re-run /gsd-complete-milestone'));
assert.ok(state.includes('/gsd-new-milestone'));
assert.ok(!state.includes('Re-run /gsd:complete-milestone'));
assert.ok(state.includes('/gsd:new-milestone'));
});
test('appends canonical narrative sections when STATE.md headings are missing (#3088)', () => {
@@ -261,7 +261,7 @@ describe('milestone complete command', () => {
assert.ok(state.includes('## Current Position'));
assert.ok(state.includes('Phase: Milestone v1.0 complete'));
assert.ok(state.includes('## Operator Next Steps'));
assert.ok(state.includes('/gsd-new-milestone'));
assert.ok(state.includes('/gsd:new-milestone'));
});
test('handles missing ROADMAP.md gracefully', () => {
@@ -864,4 +864,3 @@ describe('new-milestone workflow verification gate', () => {
// ─────────────────────────────────────────────────────────────────────────────
// validate consistency command
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -272,8 +272,8 @@ describe('#2529 /gsd-settings mentions new command', () => {
test('settings workflow mentions canonical /gsd-config --integrations', () => {
const src = fs.readFileSync(SETTINGS_WORKFLOW_PATH, 'utf-8');
assert.ok(
src.includes('gsd-config --integrations'),
'settings.md must mention /gsd-config --integrations'
src.includes('/gsd:config --integrations'),
'settings.md must mention /gsd:config --integrations'
);
assert.ok(
!src.includes('/gsd-settings-integrations'),

View File

@@ -58,7 +58,7 @@ describe('gsd-tools validate context — JSON vs human rendering', () => {
assert.strictEqual(r.success, true);
assert.match(r.output, /70%/);
assert.match(r.output, /critical/);
assert.match(r.output, /\/gsd-thread/);
assert.match(r.output, /\/gsd:thread/);
});
test('human mode omits the recommendation line for healthy state', () => {
@@ -66,25 +66,25 @@ describe('gsd-tools validate context — JSON vs human rendering', () => {
assert.strictEqual(r.success, true);
assert.match(r.output, /20%/);
assert.match(r.output, /healthy/);
assert.doesNotMatch(r.output, /\/gsd-thread/, 'healthy output must not nag the user');
assert.doesNotMatch(r.output, /\/gsd:thread/, 'healthy output must not nag the user');
});
});
describe('gsd-tools validate context — recommendation copy per state', () => {
// The CLI owns the recommendation strings (the classifier does not).
// These tests pin the wording so a regression to the prose is caught.
test('warning state recommends /gsd-thread', () => {
test('warning state recommends /gsd:thread', () => {
const r = runGsdTools(['validate', 'context', '--tokens-used', '130000', '--context-window', '200000', '--json']);
const obj = JSON.parse(r.output);
assert.strictEqual(obj.state, 'warning');
assert.match(obj.recommendation, /\/gsd-thread/);
assert.match(obj.recommendation, /\/gsd:thread/);
});
test('critical state names the fracture-point reasoning risk', () => {
const r = runGsdTools(['validate', 'context', '--tokens-used', '160000', '--context-window', '200000', '--json']);
const obj = JSON.parse(r.output);
assert.strictEqual(obj.state, 'critical');
assert.match(obj.recommendation, /\/gsd-thread/);
assert.match(obj.recommendation, /\/gsd:thread/);
assert.match(obj.recommendation, /reasoning|degrade|fracture/i);
});
});