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:
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user