mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-10-26 15:05:04 +00:00
ci: guard artifact paths on every command surface, not only Claude Code
validate-artifact-paths.js exists to stop the #93 class of drift, where a producer moves SPEC.md or tasks/plan.md and a consumer keeps reading the old path. It guarded only the .claude/commands copies plus two skills and two docs. The same commands ship twice more — .gemini/commands/*.toml and the root commands/*.toml — each with its own prompt body that names the same artifacts, and validate-commands.js deliberately compares only descriptions, so a drift on either TOML surface merged green while the Claude Code copy stayed canonical. docs/copilot-setup.md was also unguarded although its prompt-file aliases spell the artifact paths out for users. Add the six TOML producers and consumers and the Copilot doc to GUARDED_FILES, grouped by surface. Rule, allowlist and regex are unchanged. Four new tests cover a Gemini producer drifting alone, a root TOML consumer drifting alone, all three surfaces agreeing, and the Copilot doc. All seven newly guarded files already use canonical paths: 14 files checked, 0 errors.
This commit is contained in:
2 files changed
+80
No files matched your search
@@ -114,3 +114,66 @@ test('skips guarded files that do not exist', () => {
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
||||
});
|
||||
|
||||
// ─── Every command surface is guarded, not only the Claude Code copy ────────
|
||||
//
|
||||
// The Gemini and root TOML command sets carry their own prompt bodies, which
|
||||
// validate-commands.js deliberately does not compare. A producer can therefore
|
||||
// drift on one surface while `.claude/commands/` stays correct, and before
|
||||
// these files were guarded that drift merged silently.
|
||||
|
||||
test('fails when a Gemini command drifts while the Claude Code copy stays canonical', () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
|
||||
writeFile(
|
||||
root,
|
||||
'.gemini/commands/spec.toml',
|
||||
'description = "Start spec-driven development"\n\nprompt = """\nSave the spec to `docs/features/[feature-name]/spec.md`.\n"""\n',
|
||||
);
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 1, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /\.gemini\/commands\/spec\.toml/);
|
||||
assert.match(result.stdout, /L4: docs\/features\/\[feature-name\]\/spec\.md/);
|
||||
assert.match(result.stdout, /1 error\(s\) — FAILED/);
|
||||
});
|
||||
|
||||
test('fails when a root TOML consumer drifts away from the producers', () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, 'commands/planning.toml', 'prompt = """\nSave the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n"""\n');
|
||||
writeFile(root, 'commands/build.toml', 'prompt = """\nRead the plan from `planning/plan.md` before starting.\n"""\n');
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 1, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /commands\/build\.toml/);
|
||||
assert.match(result.stdout, /planning\/plan\.md/);
|
||||
});
|
||||
|
||||
test('passes when all three command surfaces agree on the canonical paths', () => {
|
||||
const root = makeSandbox();
|
||||
for (const dir of ['.claude/commands', '.gemini/commands', 'commands']) {
|
||||
const ext = dir === '.claude/commands' ? 'md' : 'toml';
|
||||
const planStem = dir === '.claude/commands' ? 'plan' : 'planning';
|
||||
writeFile(root, `${dir}/spec.${ext}`, 'Save the spec as `SPEC.md`.\n');
|
||||
writeFile(root, `${dir}/${planStem}.${ext}`, 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
|
||||
writeFile(root, `${dir}/build.${ext}`, 'Require `SPEC.md` or `docs/SPEC.md`, and `tasks/plan.md`.\n');
|
||||
}
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /9 files checked — 0 error\(s\) — PASSED/);
|
||||
});
|
||||
|
||||
test('guards the Copilot setup doc, whose prompt-file aliases name the artifacts', () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, 'docs/copilot-setup.md', 'Read `tasks/plan.md`, then write the spec to `specs/[name]/spec.md`.\n');
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 1, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /docs\/copilot-setup\.md/);
|
||||
assert.match(result.stdout, /specs\/\[name\]\/spec\.md/);
|
||||
});
|
||||
@@ -41,14 +41,31 @@ const ARTIFACT_ALLOWLIST = new Set([
|
||||
|
||||
// The files that make up the spec -> plan -> build pipeline. Absent files are
|
||||
// skipped, not failed: this validator checks path consistency, not presence.
|
||||
//
|
||||
// The command bodies exist once per host surface (validate-commands.js checks
|
||||
// that the three sets stay in step on descriptions, but their prompt bodies
|
||||
// are allowed to differ), so a producer or consumer can drift on one surface
|
||||
// while the Claude Code copy stays correct. Every surface is guarded here.
|
||||
const GUARDED_FILES = [
|
||||
// Claude Code commands
|
||||
'.claude/commands/spec.md',
|
||||
'.claude/commands/plan.md',
|
||||
'.claude/commands/build.md',
|
||||
// Gemini CLI commands
|
||||
'.gemini/commands/spec.toml',
|
||||
'.gemini/commands/planning.toml',
|
||||
'.gemini/commands/build.toml',
|
||||
// Root command set (Antigravity, Codex, and other TOML-based hosts)
|
||||
'commands/spec.toml',
|
||||
'commands/planning.toml',
|
||||
'commands/build.toml',
|
||||
// Skills the commands invoke
|
||||
'skills/spec-driven-development/SKILL.md',
|
||||
'skills/planning-and-task-breakdown/SKILL.md',
|
||||
// Docs that tell users where the artifacts live
|
||||
'docs/getting-started.md',
|
||||
'docs/adoption-guide.md',
|
||||
'docs/copilot-setup.md', // its prompt-file aliases name the artifacts directly
|
||||
];
|
||||
|
||||
// Matches a path-like token ending in a spec/plan/todo artifact filename,
|
||||
|
||||
Reference in new issue
Block a user