fix(validation): read top-level command descriptions

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
This commit is contained in:
Rudy Celekli committed 2026-10-10 19:28:09 -04:00
1 parent 1be8e34187
commit 51e4485f58
2 files changed
+36 -16

No files matched your search

+30
View File
@@ -214,3 +214,33 @@ test('the TOML directories are not put through the YAML rules', () => {
assert.equal(result.status, 0, result.stdout);
assert.doesNotMatch(result.stdout, /\.gemini.*unquoted value/);
});
test('nested metadata descriptions cannot hide command description drift', () => {
const root = makeSandbox();
writeClaudeCommand(root, 'review', 'metadata:\n description: Review a change\ndescription: Inspect a change');
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', 'description = "Review a change"');
writeTomlCommand(root, 'commands', 'review', 'description = "Review a change"');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /Inspect a change/);
});
test('matching top-level descriptions pass despite different nested metadata', () => {
const root = makeSandbox();
writeClaudeCommand(root, 'review', 'metadata:\n description: Internal metadata\ndescription: Review a change');
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', 'description = "Review a change"');
writeTomlCommand(root, 'commands', 'review', 'description = "Review a change"');
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
});
test('a nested description does not supply a missing command description', () => {
const root = makeSandbox();
writeClaudeCommand(root, 'review', 'metadata:\n description: Review a change');
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', 'description = "Review a change"');
writeTomlCommand(root, 'commands', 'review', 'description = "Review a change"');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /missing or malformed description/);
});
+6 -16
View File
@@ -25,12 +25,11 @@ const fs = require('fs');
const path = require('path');
// The same frontmatter-validity rules validate-skills applies to SKILL.md.
// `descriptionFromMd` below splits each line on its first colon, exactly as the
// skill reader used to, so a command whose frontmatter is not valid YAML passes
// every check here — and Claude Code parses that frontmatter when the command is
// loaded. The #494 thread verified all 25 SKILL.md files AND the command files by
// hand; this makes the second half a check too.
const { frontmatterYamlErrors } = require(path.join(__dirname, 'lib', 'skill-lint.js'));
// Description extraction shares the top-level frontmatter reader with skills.
// YAML validity is checked separately because Claude Code parses frontmatter
// when the command is loaded. The #494 thread verified all 25 SKILL.md files
// and the command files by hand; this makes the second half a check too.
const { frontmatterYamlErrors, parseFrontmatter } = require(path.join(__dirname, 'lib', 'skill-lint.js'));
// ─── Config ───────────────────────────────────────────────────────────────────
@@ -55,16 +54,7 @@ const NAME_MAP_REVERSE = Object.fromEntries(
function descriptionFromMd(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/);
if (!match) return null;
for (const line of match[1].split(/\r?\n/)) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
if (line.slice(0, colonIdx).trim() === 'description') {
return line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '');
}
}
return null;
return parseFrontmatter(content)?.description ?? null;
}
function descriptionFromToml(filePath) {