* fix(3683): normalize /gsd:<cmd> → /gsd-<cmd> in command, workflow, and reference bodies Extends #3677's agent-body normalizer to all body text staged through copyWithPathReplacement (commands, workflows, references). The initial isCommand guard was structurally redundant — normalizeAgentBodyForRuntime already self-gates on shouldNormalizeHyphenNamespaceInAgentBody(runtime), so dropping it covers all hyphen-name runtimes (Claude / Qwen / Hermes) without affecting colon-canonical runtimes (Gemini). Addresses the user-visible symptom in #3683: workflows like get-shit-done/workflows/discuss-phase.md (7 colon refs) leaked /gsd:<cmd> markers to the model context, which the model echoed at the end of /gsd-discuss-phase runs. Source-prose drift caught by the new cross-reference invariant test: - commands/gsd/plan-phase.md: removed a slash-form mention of the deleted /gsd-research-phase command (#3042) - commands/gsd/profile-user.md: replaced a slash-form artifact reference with a backticked bare name (the referenced item is a skill config, not a user-callable slash command) Tests: - tests/bug-3683-command-colon-namespace-leak.test.cjs — runtime-form regression for commands/gsd/*.md staging - tests/bug-3683-command-cross-reference-invariant.test.cjs — locks cross-reference coherence: every /gsd-X / /gsd:X reference in a command body must resolve to commands/gsd/X.md (so a future rename forces every cross-reference to update) - tests/bug-3683-workflow-colon-namespace-leak.test.cjs — runtime-form regression for workflows + references; negative test for gemini asserting the colon form is preserved Fixes #3683 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changeset): remove undefined cycle reference --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.3 KiB
JavaScript
68 lines
2.3 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// commands/gsd/*.md bodies are the deployed contract — cross-references between
|
|
// them must stay coherent. This test inspects .md source to enforce the invariant.
|
|
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const COMMANDS_DIR = path.resolve(__dirname, '..', 'commands', 'gsd');
|
|
|
|
function readKnownTargets() {
|
|
const commandNames = fs.readdirSync(COMMANDS_DIR)
|
|
.filter(f => f.endsWith('.md'))
|
|
.map(f => f.slice(0, -3));
|
|
return { commandNames, knownTargets: new Set(commandNames) };
|
|
}
|
|
|
|
function stripFrontmatter(src) {
|
|
return src.replace(/^---\r?\n[\s\S]*?\n---\r?\n/, '');
|
|
}
|
|
|
|
// Word-boundary lookbehind matching fix-slash-commands.cjs buildColonPattern / buildPattern
|
|
// Excludes path-y characters (~, ., /) so `~/gsd-workspaces`, `./gsd-foo`, `path/gsd-bar` don't match.
|
|
const REF_PATTERN = /(?<![a-zA-Z0-9_~./-])\/gsd[:-]([a-zA-Z0-9_-]+)/g;
|
|
|
|
describe('bug-3683 command cross-reference invariant', () => {
|
|
test('all /gsd:<X> and /gsd-<X> body refs resolve to known command base-names', () => {
|
|
const { commandNames, knownTargets: knownSet } = readKnownTargets();
|
|
const mdFiles = commandNames.sort().map(n => path.join(COMMANDS_DIR, `${n}.md`));
|
|
|
|
const failures = [];
|
|
|
|
for (const filePath of mdFiles) {
|
|
const src = fs.readFileSync(filePath, 'utf-8');
|
|
const body = stripFrontmatter(src);
|
|
const lines = body.split('\n');
|
|
const relFile = path.relative(path.resolve(__dirname, '..'), filePath);
|
|
|
|
lines.forEach((line, idx) => {
|
|
REF_PATTERN.lastIndex = 0;
|
|
let m;
|
|
while ((m = REF_PATTERN.exec(line)) !== null) {
|
|
const ref = m[1];
|
|
if (!knownSet.has(ref)) {
|
|
const sep = m[0].includes(':') ? ':' : '-';
|
|
failures.push({
|
|
file: relFile,
|
|
line: idx + 1,
|
|
ref: `/gsd${sep}${ref}`,
|
|
excerpt: line.trim(),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
const msg = failures
|
|
.map(f => ` ${f.file}:${f.line} — dangling ref "${f.ref}" — ${f.excerpt}`)
|
|
.join('\n');
|
|
assert.fail(`Dangling command cross-references found:\n${msg}`);
|
|
}
|
|
});
|
|
});
|