@givolt/i3-composer-agent 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +28 -21
- package/bin/i3-composer-agent.cjs +5 -4
- package/bin/install.cjs +22 -10
- package/bin/native-command-validation.cjs +8 -13
- package/bin/native-layout.cjs +62 -28
- package/bin/native-migration.cjs +13 -7
- package/bin/native-runtime.cjs +10 -5
- package/docs/advanced-per-agent-configuration.md +78 -0
- package/docs/codebuddy-setup.md +3 -3
- package/docs/codex-setup.md +2 -2
- package/docs/workbuddy-setup.md +7 -7
- package/evals/README.md +26 -16
- package/package.json +37 -18
- package/plugin.json +1 -1
- package/scripts/floor-guard-reference-test.js +291 -0
- package/scripts/lib/skill-lint-test.js +650 -0
- package/scripts/run-evals-test.js +623 -0
- package/scripts/run-regressions.js +19 -0
- package/scripts/validate-artifact-paths-test.js +179 -0
- package/scripts/validate-commands-test.js +216 -0
- package/scripts/validate-reference-links-test.js +495 -0
- package/scripts/validate-versions-test.js +32 -0
- package/bin/native-version-validation.cjs +0 -8
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const { afterEach, test } = require('node:test');
|
|
11
|
+
|
|
12
|
+
const VALIDATOR = path.join(__dirname, 'validate-artifact-paths.js');
|
|
13
|
+
const sandboxes = [];
|
|
14
|
+
|
|
15
|
+
function makeSandbox() {
|
|
16
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-artifact-paths-test-'));
|
|
17
|
+
const scriptsDir = path.join(root, 'scripts');
|
|
18
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
19
|
+
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-artifact-paths.js'));
|
|
20
|
+
sandboxes.push(root);
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeFile(root, relativePath, content) {
|
|
25
|
+
const file = path.join(root, relativePath);
|
|
26
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
fs.writeFileSync(file, content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(root) {
|
|
31
|
+
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-artifact-paths.js')], {
|
|
32
|
+
cwd: root,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
for (const root of sandboxes.splice(0)) {
|
|
39
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('passes when producers and consumers use the canonical artifact paths', () => {
|
|
44
|
+
const root = makeSandbox();
|
|
45
|
+
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
|
|
46
|
+
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `tasks/plan.md` and task list to `tasks/todo.md`.\n');
|
|
47
|
+
writeFile(root, '.claude/commands/build.md', 'Look for a spec at `SPEC.md`, `docs/SPEC.md`, or under `spec/`. Require `tasks/plan.md`.\n');
|
|
48
|
+
writeFile(root, 'skills/i3-spec-driven-development/SKILL.md', 'Save the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n');
|
|
49
|
+
writeFile(root, 'skills/i3-planning-and-task-breakdown/SKILL.md', 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
|
|
50
|
+
|
|
51
|
+
const result = run(root);
|
|
52
|
+
|
|
53
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
54
|
+
assert.match(result.stdout, /5 files checked — 0 error\(s\) — PASSED/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('fails when a producer drifts to an unapproved artifact path (the #93 regression)', () => {
|
|
58
|
+
const root = makeSandbox();
|
|
59
|
+
// Only the producers are changed to the per-feature layout; consumers stay on the old paths.
|
|
60
|
+
writeFile(root, '.claude/commands/spec.md', 'Save the spec to `docs/features/[feature-name]/spec.md`.\n');
|
|
61
|
+
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `docs/features/[feature-name]/plan.md`.\n');
|
|
62
|
+
writeFile(root, '.claude/commands/build.md', 'Require a spec at `SPEC.md` and a plan at `tasks/plan.md`.\n');
|
|
63
|
+
|
|
64
|
+
const result = run(root);
|
|
65
|
+
|
|
66
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
67
|
+
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/spec\.md/);
|
|
68
|
+
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/plan\.md/);
|
|
69
|
+
assert.match(result.stdout, /error\(s\) — FAILED/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('reports the offending file and line number', () => {
|
|
73
|
+
const root = makeSandbox();
|
|
74
|
+
writeFile(root, '.claude/commands/plan.md', 'Intro line.\nSave the plan to `plan.md` in the root.\n');
|
|
75
|
+
|
|
76
|
+
const result = run(root);
|
|
77
|
+
|
|
78
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
79
|
+
assert.match(result.stdout, /\.claude\/commands\/plan\.md/);
|
|
80
|
+
assert.match(result.stdout, /L2:/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('accepts the docs/SPEC.md alternate spec location', () => {
|
|
84
|
+
const root = makeSandbox();
|
|
85
|
+
writeFile(root, '.claude/commands/build.md', 'Look for the spec at `docs/SPEC.md`.\n');
|
|
86
|
+
|
|
87
|
+
const result = run(root);
|
|
88
|
+
|
|
89
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
90
|
+
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('ignores non-artifact markdown references (no false positives)', () => {
|
|
94
|
+
const root = makeSandbox();
|
|
95
|
+
writeFile(
|
|
96
|
+
root,
|
|
97
|
+
'skills/i3-spec-driven-development/SKILL.md',
|
|
98
|
+
'See `SKILL.md` and `references/testing-patterns.md`. Save the plan to `tasks/plan.md`.\n',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = run(root);
|
|
102
|
+
|
|
103
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
104
|
+
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('skips guarded files that do not exist', () => {
|
|
108
|
+
const root = makeSandbox();
|
|
109
|
+
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md`.\n');
|
|
110
|
+
// No other guarded files present.
|
|
111
|
+
|
|
112
|
+
const result = run(root);
|
|
113
|
+
|
|
114
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
115
|
+
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── Every command surface is guarded, not only the Claude Code copy ────────
|
|
119
|
+
//
|
|
120
|
+
// The Gemini and root TOML command sets carry their own prompt bodies, which
|
|
121
|
+
// validate-commands.js deliberately does not compare. A producer can therefore
|
|
122
|
+
// drift on one surface while `.claude/commands/` stays correct, and before
|
|
123
|
+
// these files were guarded that drift merged silently.
|
|
124
|
+
|
|
125
|
+
test('fails when a Gemini command drifts while the Claude Code copy stays canonical', () => {
|
|
126
|
+
const root = makeSandbox();
|
|
127
|
+
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
|
|
128
|
+
writeFile(
|
|
129
|
+
root,
|
|
130
|
+
'.gemini/commands/spec.toml',
|
|
131
|
+
'description = "Start spec-driven development"\n\nprompt = """\nSave the spec to `docs/features/[feature-name]/spec.md`.\n"""\n',
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = run(root);
|
|
135
|
+
|
|
136
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
137
|
+
assert.match(result.stdout, /\.gemini\/commands\/spec\.toml/);
|
|
138
|
+
assert.match(result.stdout, /L4: docs\/features\/\[feature-name\]\/spec\.md/);
|
|
139
|
+
assert.match(result.stdout, /1 error\(s\) — FAILED/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('fails when a root TOML consumer drifts away from the producers', () => {
|
|
143
|
+
const root = makeSandbox();
|
|
144
|
+
writeFile(root, 'commands/planning.toml', 'prompt = """\nSave the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n"""\n');
|
|
145
|
+
writeFile(root, 'commands/build.toml', 'prompt = """\nRead the plan from `planning/plan.md` before starting.\n"""\n');
|
|
146
|
+
|
|
147
|
+
const result = run(root);
|
|
148
|
+
|
|
149
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
150
|
+
assert.match(result.stdout, /commands\/build\.toml/);
|
|
151
|
+
assert.match(result.stdout, /planning\/plan\.md/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('passes when all three command surfaces agree on the canonical paths', () => {
|
|
155
|
+
const root = makeSandbox();
|
|
156
|
+
for (const dir of ['.claude/commands', '.gemini/commands', 'commands']) {
|
|
157
|
+
const ext = dir === '.claude/commands' ? 'md' : 'toml';
|
|
158
|
+
const planStem = dir === '.claude/commands' ? 'plan' : 'planning';
|
|
159
|
+
writeFile(root, `${dir}/spec.${ext}`, 'Save the spec as `SPEC.md`.\n');
|
|
160
|
+
writeFile(root, `${dir}/${planStem}.${ext}`, 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
|
|
161
|
+
writeFile(root, `${dir}/build.${ext}`, 'Require `SPEC.md` or `docs/SPEC.md`, and `tasks/plan.md`.\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = run(root);
|
|
165
|
+
|
|
166
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
167
|
+
assert.match(result.stdout, /9 files checked — 0 error\(s\) — PASSED/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('guards the Copilot setup doc, whose prompt-file aliases name the artifacts', () => {
|
|
171
|
+
const root = makeSandbox();
|
|
172
|
+
writeFile(root, 'docs/copilot-setup.md', 'Read `tasks/plan.md`, then write the spec to `specs/[name]/spec.md`.\n');
|
|
173
|
+
|
|
174
|
+
const result = run(root);
|
|
175
|
+
|
|
176
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
177
|
+
assert.match(result.stdout, /docs\/copilot-setup\.md/);
|
|
178
|
+
assert.match(result.stdout, /specs\/\[name\]\/spec\.md/);
|
|
179
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const { afterEach, test } = require('node:test');
|
|
11
|
+
|
|
12
|
+
const VALIDATOR = path.join(__dirname, 'validate-commands.js');
|
|
13
|
+
// The validator shares the frontmatter-validity rules with validate-skills, so
|
|
14
|
+
// the sandbox needs the lib alongside it, not just the script.
|
|
15
|
+
const SKILL_LINT = path.join(__dirname, 'lib', 'skill-lint.js');
|
|
16
|
+
const sandboxes = [];
|
|
17
|
+
|
|
18
|
+
function makeSandbox() {
|
|
19
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-commands-test-'));
|
|
20
|
+
const scriptsDir = path.join(root, 'scripts');
|
|
21
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
22
|
+
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-commands.js'));
|
|
23
|
+
fs.mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true });
|
|
24
|
+
fs.copyFileSync(SKILL_LINT, path.join(scriptsDir, 'lib', 'skill-lint.js'));
|
|
25
|
+
sandboxes.push(root);
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeFile(root, relativePath, content) {
|
|
30
|
+
const file = path.join(root, relativePath);
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file, content);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeClaudeCommand(root, stem, descriptionLine) {
|
|
36
|
+
writeFile(
|
|
37
|
+
root,
|
|
38
|
+
path.join('.claude', 'commands', `${stem}.md`),
|
|
39
|
+
`---\n${descriptionLine}\n---\n\n# Command\n`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeTomlCommand(root, directory, stem, descriptionLine) {
|
|
44
|
+
writeFile(root, path.join(directory, `${stem}.toml`), `${descriptionLine}\nprompt = "Run command"\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeMatchingCommands(root, stem, description) {
|
|
48
|
+
writeClaudeCommand(root, stem, `description: ${description}`);
|
|
49
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), stem, `description = "${description}"`);
|
|
50
|
+
writeTomlCommand(root, 'commands', stem, `description = "${description}"`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function run(root) {
|
|
54
|
+
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-commands.js')], {
|
|
55
|
+
cwd: root,
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
for (const root of sandboxes.splice(0)) {
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('passes matching command twins and maps plan to planning', () => {
|
|
67
|
+
const root = makeSandbox();
|
|
68
|
+
const description = 'Break work into ordered tasks';
|
|
69
|
+
writeClaudeCommand(root, 'plan', `description: ${description}`);
|
|
70
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'planning', `description = '${description}'`);
|
|
71
|
+
writeTomlCommand(root, 'commands', 'planning', `description = '${description}'`);
|
|
72
|
+
|
|
73
|
+
const result = run(root);
|
|
74
|
+
|
|
75
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
76
|
+
assert.match(result.stdout, /✓ plan \(planning in toml dirs\)/);
|
|
77
|
+
assert.match(result.stdout, /1 commands checked — 0 error\(s\) — PASSED/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('fails when a Claude command is missing a TOML twin', () => {
|
|
81
|
+
const root = makeSandbox();
|
|
82
|
+
const description = 'Review a change';
|
|
83
|
+
writeClaudeCommand(root, 'review', `description: ${description}`);
|
|
84
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', `description = "${description}"`);
|
|
85
|
+
|
|
86
|
+
const result = run(root);
|
|
87
|
+
|
|
88
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
89
|
+
assert.match(result.stdout, /review — missing in: commands/);
|
|
90
|
+
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('fails when a TOML command has no Claude twin', () => {
|
|
94
|
+
const root = makeSandbox();
|
|
95
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'deploy', 'description = "Deploy a change"');
|
|
96
|
+
writeTomlCommand(root, 'commands', 'deploy', 'description = "Deploy a change"');
|
|
97
|
+
|
|
98
|
+
const result = run(root);
|
|
99
|
+
|
|
100
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
101
|
+
assert.match(result.stdout, /deploy — present in toml dirs but missing in \.claude\/commands/);
|
|
102
|
+
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('reports all descriptions when command twins drift', () => {
|
|
106
|
+
const root = makeSandbox();
|
|
107
|
+
writeClaudeCommand(root, 'review', 'description: Review a change');
|
|
108
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', 'description = "Inspect a change"');
|
|
109
|
+
writeTomlCommand(root, 'commands', 'review', 'description = "Audit a change"');
|
|
110
|
+
|
|
111
|
+
const result = run(root);
|
|
112
|
+
|
|
113
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
114
|
+
assert.match(result.stdout, /\.claude:\s+Review a change/);
|
|
115
|
+
assert.match(result.stdout, /\.gemini:\s+Inspect a change/);
|
|
116
|
+
assert.match(result.stdout, /commands\/:\s+Audit a change/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('fails with an actionable error for a malformed description', () => {
|
|
120
|
+
const root = makeSandbox();
|
|
121
|
+
writeMatchingCommands(root, 'review', 'Review a change');
|
|
122
|
+
writeFile(
|
|
123
|
+
root,
|
|
124
|
+
path.join('.gemini', 'commands', 'review.toml'),
|
|
125
|
+
'prompt = "Missing description"\n',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result = run(root);
|
|
129
|
+
|
|
130
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
131
|
+
assert.match(result.stdout, /\.gemini\/commands\/review — missing or malformed description/);
|
|
132
|
+
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('parses escaped quotes in double-quoted TOML descriptions', () => {
|
|
136
|
+
const root = makeSandbox();
|
|
137
|
+
writeClaudeCommand(root, 'review', 'description: Review "important" changes');
|
|
138
|
+
writeTomlCommand(
|
|
139
|
+
root,
|
|
140
|
+
path.join('.gemini', 'commands'),
|
|
141
|
+
'review',
|
|
142
|
+
'description = "Review \\"important\\" changes"',
|
|
143
|
+
);
|
|
144
|
+
writeTomlCommand(
|
|
145
|
+
root,
|
|
146
|
+
'commands',
|
|
147
|
+
'review',
|
|
148
|
+
'description = "Review \\"important\\" changes"',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const result = run(root);
|
|
152
|
+
|
|
153
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
154
|
+
assert.match(result.stdout, /1 commands checked — 0 error\(s\) — PASSED/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Claude parses a command's frontmatter as YAML when the command is loaded, and
|
|
158
|
+
// `descriptionFromMd` splits each line on its first colon — so a command whose
|
|
159
|
+
// frontmatter is not valid YAML passed every check here. Same class as the
|
|
160
|
+
// SKILL.md gap, on the other set of files the #494 thread checked by hand.
|
|
161
|
+
|
|
162
|
+
test('a command whose frontmatter is valid YAML passes', () => {
|
|
163
|
+
const root = makeSandbox();
|
|
164
|
+
writeMatchingCommands(root, 'build', 'Build the thing');
|
|
165
|
+
const result = run(root);
|
|
166
|
+
assert.equal(result.status, 0, result.stdout);
|
|
167
|
+
assert.match(result.stdout, /Checking Claude command frontmatter/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('an unquoted colon in a command description is rejected', () => {
|
|
171
|
+
const root = makeSandbox();
|
|
172
|
+
// Valid to the splitter, rejected by a real YAML parser: it reads a nested
|
|
173
|
+
// mapping. The TOML siblings keep the same text so only the .md is at fault.
|
|
174
|
+
writeClaudeCommand(root, 'build', 'description: Build the thing: quickly');
|
|
175
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing: quickly"');
|
|
176
|
+
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing: quickly"');
|
|
177
|
+
|
|
178
|
+
const result = run(root);
|
|
179
|
+
assert.equal(result.status, 1, result.stdout);
|
|
180
|
+
assert.match(result.stdout, /unquoted value containing/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('quoting the same description makes it pass', () => {
|
|
184
|
+
const root = makeSandbox();
|
|
185
|
+
writeClaudeCommand(root, 'build', 'description: "Build the thing: quickly"');
|
|
186
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing: quickly"');
|
|
187
|
+
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing: quickly"');
|
|
188
|
+
|
|
189
|
+
const result = run(root);
|
|
190
|
+
assert.equal(result.status, 0, result.stdout);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('a tab-indented command frontmatter is rejected', () => {
|
|
194
|
+
const root = makeSandbox();
|
|
195
|
+
writeFile(
|
|
196
|
+
root,
|
|
197
|
+
path.join('.claude', 'commands', 'build.md'),
|
|
198
|
+
'---\ndescription: Build the thing\nmeta:\n\tlevel: core\n---\n\n# Command\n',
|
|
199
|
+
);
|
|
200
|
+
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing"');
|
|
201
|
+
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing"');
|
|
202
|
+
|
|
203
|
+
const result = run(root);
|
|
204
|
+
assert.equal(result.status, 1, result.stdout);
|
|
205
|
+
assert.match(result.stdout, /indents with a tab/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('the TOML directories are not put through the YAML rules', () => {
|
|
209
|
+
const root = makeSandbox();
|
|
210
|
+
// A TOML description legitimately carries a colon. Running the YAML rules over
|
|
211
|
+
// these files would fail every command that has one.
|
|
212
|
+
writeMatchingCommands(root, 'build', 'Build the thing quickly');
|
|
213
|
+
const result = run(root);
|
|
214
|
+
assert.equal(result.status, 0, result.stdout);
|
|
215
|
+
assert.doesNotMatch(result.stdout, /\.gemini.*unquoted value/);
|
|
216
|
+
});
|