@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,495 @@
|
|
|
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-reference-links.js');
|
|
13
|
+
const sandboxes = [];
|
|
14
|
+
|
|
15
|
+
function makeSandbox() {
|
|
16
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-reference-links-test-'));
|
|
17
|
+
const scriptsDir = path.join(root, 'scripts');
|
|
18
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
19
|
+
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-reference-links.js'));
|
|
20
|
+
fs.mkdirSync(path.join(scriptsDir, 'lib'));
|
|
21
|
+
fs.copyFileSync(path.join(__dirname, 'lib', 'skill-lint.js'), path.join(scriptsDir, 'lib', 'skill-lint.js'));
|
|
22
|
+
sandboxes.push(root);
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeFile(root, relativePath, content) {
|
|
27
|
+
const file = path.join(root, relativePath);
|
|
28
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
+
fs.writeFileSync(file, content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function run(root) {
|
|
33
|
+
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-reference-links.js')], {
|
|
34
|
+
cwd: root,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
for (const root of sandboxes.splice(0)) {
|
|
41
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('passes when a skill reaches the shared checklist two levels up', () => {
|
|
46
|
+
const root = makeSandbox();
|
|
47
|
+
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
48
|
+
writeFile(
|
|
49
|
+
root,
|
|
50
|
+
'skills/i3-using-agent-skills/SKILL.md',
|
|
51
|
+
'See `../../references/definition-of-done.md`.\n'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = run(root);
|
|
55
|
+
|
|
56
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
57
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('fails when a skill links the shared checklist as if it were colocated', () => {
|
|
61
|
+
// The regression: references/ lives at the repo root, but the link is
|
|
62
|
+
// resolved from skills/<name>/, so it points two levels too deep.
|
|
63
|
+
const root = makeSandbox();
|
|
64
|
+
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
65
|
+
writeFile(
|
|
66
|
+
root,
|
|
67
|
+
'skills/i3-using-agent-skills/SKILL.md',
|
|
68
|
+
'See `references/definition-of-done.md`.\n'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const result = run(root);
|
|
72
|
+
|
|
73
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
74
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
75
|
+
assert.match(
|
|
76
|
+
result.stdout,
|
|
77
|
+
/L1: references\/definition-of-done\.md — resolves to skills\/i3-using-agent-skills\/references\/definition-of-done\.md/
|
|
78
|
+
);
|
|
79
|
+
assert.match(result.stdout, /use `\.\.\/\.\.\/references\/<file>\.md`/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('checks markdown link syntax, not just backtick mentions', () => {
|
|
83
|
+
const root = makeSandbox();
|
|
84
|
+
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
85
|
+
writeFile(root, 'skills/i3-using-agent-skills/SKILL.md', 'See [DoD](references/definition-of-done.md).\n');
|
|
86
|
+
|
|
87
|
+
const result = run(root);
|
|
88
|
+
|
|
89
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
90
|
+
assert.match(result.stdout, /L1: references\/definition-of-done\.md/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('passes when a skill colocates its own references directory', () => {
|
|
94
|
+
// CLAUDE.md allows self-contained skills to keep references under
|
|
95
|
+
// skills/<name>/references/. Those links are correct as written.
|
|
96
|
+
const root = makeSandbox();
|
|
97
|
+
writeFile(root, 'skills/dataviz/references/palette.md', '# Palette\n');
|
|
98
|
+
writeFile(root, 'skills/dataviz/SKILL.md', 'See `references/palette.md`.\n');
|
|
99
|
+
|
|
100
|
+
const result = run(root);
|
|
101
|
+
|
|
102
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
103
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('passes when a skill reference file reaches the shared checklist three levels up', () => {
|
|
107
|
+
// A file under skills/<name>/references/ sits one directory deeper than
|
|
108
|
+
// SKILL.md, so the shared checklists are three levels up from it, not two.
|
|
109
|
+
const root = makeSandbox();
|
|
110
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
111
|
+
writeFile(root, 'skills/hardening/SKILL.md', 'See [patterns](references/patterns.md).\n');
|
|
112
|
+
writeFile(
|
|
113
|
+
root,
|
|
114
|
+
'skills/hardening/references/patterns.md',
|
|
115
|
+
'Shared checklists live in `../../../references/security-checklist.md`.\n'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const result = run(root);
|
|
119
|
+
|
|
120
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
121
|
+
assert.match(result.stdout, /✓ {2}skills\/hardening\/references\/patterns\.md/);
|
|
122
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('fails when a skill reference file links the shared checklist with the SKILL.md prefix', () => {
|
|
126
|
+
// The same off-by-a-level mistake this validator exists to catch, made from
|
|
127
|
+
// inside the skill's references/ directory: the link is resolved from the
|
|
128
|
+
// file that contains it, so `../../` stops at skills/.
|
|
129
|
+
const root = makeSandbox();
|
|
130
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
131
|
+
writeFile(root, 'skills/hardening/SKILL.md', 'See [patterns](references/patterns.md).\n');
|
|
132
|
+
writeFile(
|
|
133
|
+
root,
|
|
134
|
+
'skills/hardening/references/patterns.md',
|
|
135
|
+
['# Patterns', '', 'See `../../references/security-checklist.md`.', ''].join('\n')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = run(root);
|
|
139
|
+
|
|
140
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
141
|
+
assert.match(result.stdout, /✓ {2}skills\/hardening\/SKILL\.md/);
|
|
142
|
+
assert.match(result.stdout, /✗ {2}skills\/hardening\/references\/patterns\.md/);
|
|
143
|
+
assert.match(
|
|
144
|
+
result.stdout,
|
|
145
|
+
/L3: \.\.\/\.\.\/references\/security-checklist\.md — resolves to skills\/references\/security-checklist\.md/
|
|
146
|
+
);
|
|
147
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
148
|
+
assert.match(result.stdout, /use `\.\.\/\.\.\/\.\.\/references\/<file>\.md`/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('keeps the same narrow rule inside a skill reference file', () => {
|
|
152
|
+
// Same exemptions as SKILL.md: fenced examples, sibling files named without
|
|
153
|
+
// a references/ prefix, and artifacts that do not exist yet.
|
|
154
|
+
const root = makeSandbox();
|
|
155
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
156
|
+
writeFile(root, 'skills/hardening/SKILL.md', 'See [patterns](references/patterns.md).\n');
|
|
157
|
+
writeFile(
|
|
158
|
+
root,
|
|
159
|
+
'skills/hardening/references/patterns.md',
|
|
160
|
+
[
|
|
161
|
+
'Record findings in `SECURITY.md` or `docs/threat-model.md`. See also `other-patterns.md`.',
|
|
162
|
+
'',
|
|
163
|
+
'```markdown',
|
|
164
|
+
'[wrong on purpose](../../references/security-checklist.md)',
|
|
165
|
+
'```',
|
|
166
|
+
'',
|
|
167
|
+
].join('\n')
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const result = run(root);
|
|
171
|
+
|
|
172
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
173
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('counts errors from SKILL.md and its reference files together', () => {
|
|
177
|
+
const root = makeSandbox();
|
|
178
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
179
|
+
writeFile(root, 'skills/hardening/SKILL.md', 'See `references/security-checklist.md`.\n');
|
|
180
|
+
writeFile(root, 'skills/hardening/references/a.md', 'See `../../references/security-checklist.md`.\n');
|
|
181
|
+
writeFile(root, 'skills/hardening/references/b.md', 'See `../../../references/security-checklist.md`.\n');
|
|
182
|
+
writeFile(root, 'skills/hardening/references/notes.txt', 'See ../../references/security-checklist.md\n');
|
|
183
|
+
|
|
184
|
+
const result = run(root);
|
|
185
|
+
|
|
186
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
187
|
+
assert.match(result.stdout, /✗ {2}skills\/hardening\/references\/a\.md/);
|
|
188
|
+
assert.match(result.stdout, /✓ {2}skills\/hardening\/references\/b\.md/);
|
|
189
|
+
assert.doesNotMatch(result.stdout, /notes\.txt/);
|
|
190
|
+
assert.match(result.stdout, /1 skills checked — 2 error\(s\) — FAILED/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('fails when a link points at a checklist that no longer exists', () => {
|
|
194
|
+
const root = makeSandbox();
|
|
195
|
+
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
196
|
+
writeFile(root, 'skills/i3-shipping-and-launch/SKILL.md', 'See `../../references/renamed.md`.\n');
|
|
197
|
+
|
|
198
|
+
const result = run(root);
|
|
199
|
+
|
|
200
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
201
|
+
assert.match(result.stdout, /L1: \.\.\/\.\.\/references\/renamed\.md/);
|
|
202
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('ignores paths that are not references/ links', () => {
|
|
206
|
+
// Skills legitimately name artifacts the user has yet to create. Widening
|
|
207
|
+
// this validator into a general markdown linter would fail the build on them.
|
|
208
|
+
const root = makeSandbox();
|
|
209
|
+
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
210
|
+
writeFile(
|
|
211
|
+
root,
|
|
212
|
+
'skills/i3-planning-and-task-breakdown/SKILL.md',
|
|
213
|
+
[
|
|
214
|
+
'Save the task list to `tasks/todo.md` and the plan to `tasks/plan.md`.',
|
|
215
|
+
'Record findings in `PERF.md` or `docs/ideas/[idea-name].md`.',
|
|
216
|
+
'Related: `skills/i3-incremental-implementation/SKILL.md`.',
|
|
217
|
+
'See `../../references/definition-of-done.md`.',
|
|
218
|
+
'',
|
|
219
|
+
].join('\n')
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = run(root);
|
|
223
|
+
|
|
224
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
225
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('reports every unresolvable link, not just the first per skill', () => {
|
|
229
|
+
const root = makeSandbox();
|
|
230
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
231
|
+
writeFile(root, 'references/performance-checklist.md', '# Performance\n');
|
|
232
|
+
writeFile(
|
|
233
|
+
root,
|
|
234
|
+
'skills/i3-code-review-and-quality/SKILL.md',
|
|
235
|
+
['See `references/security-checklist.md`.', 'And `references/performance-checklist.md`.', ''].join('\n')
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const result = run(root);
|
|
239
|
+
|
|
240
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
241
|
+
assert.match(result.stdout, /1 skills checked — 2 error\(s\) — FAILED/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('a link inside a fenced block is an example, not a link to resolve', () => {
|
|
245
|
+
const root = makeSandbox();
|
|
246
|
+
writeFile(root, 'references/definition-of-done.md', '# DoD\n');
|
|
247
|
+
writeFile(
|
|
248
|
+
root,
|
|
249
|
+
'skills/i3-using-agent-skills/SKILL.md',
|
|
250
|
+
[
|
|
251
|
+
'Shared checklists live two levels up:',
|
|
252
|
+
'',
|
|
253
|
+
'See `../../references/definition-of-done.md`.',
|
|
254
|
+
'',
|
|
255
|
+
'Do not write it this way:',
|
|
256
|
+
'',
|
|
257
|
+
'```markdown',
|
|
258
|
+
'[Definition of Done](references/definition-of-done.md)',
|
|
259
|
+
'```',
|
|
260
|
+
'',
|
|
261
|
+
].join('\n')
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const result = run(root);
|
|
265
|
+
|
|
266
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
267
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('fence exemption follows CommonMark rather than a bare ``` match', () => {
|
|
271
|
+
const root = makeSandbox();
|
|
272
|
+
writeFile(
|
|
273
|
+
root,
|
|
274
|
+
'skills/i3-using-agent-skills/SKILL.md',
|
|
275
|
+
[
|
|
276
|
+
'~~~markdown',
|
|
277
|
+
'[a](references/missing-a.md)',
|
|
278
|
+
'```', // wrong marker: must not close the ~~~ block
|
|
279
|
+
'[b](references/missing-b.md)',
|
|
280
|
+
'~~~',
|
|
281
|
+
'',
|
|
282
|
+
' ```markdown', // three-space indent is still a fence
|
|
283
|
+
'[c](references/missing-c.md)',
|
|
284
|
+
' `````', // a longer closer is legal
|
|
285
|
+
'',
|
|
286
|
+
].join('\n')
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const result = run(root);
|
|
290
|
+
|
|
291
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
292
|
+
assert.match(result.stdout, /0 error\(s\) — PASSED/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('a real broken link outside any fence is still reported', () => {
|
|
296
|
+
const root = makeSandbox();
|
|
297
|
+
writeFile(
|
|
298
|
+
root,
|
|
299
|
+
'skills/i3-using-agent-skills/SKILL.md',
|
|
300
|
+
[
|
|
301
|
+
'```markdown',
|
|
302
|
+
'[shown as an example](references/example-only.md)',
|
|
303
|
+
'```',
|
|
304
|
+
'',
|
|
305
|
+
'See `references/definition-of-done.md`.', // genuinely wrong, outside the fence
|
|
306
|
+
'',
|
|
307
|
+
].join('\n')
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const result = run(root);
|
|
311
|
+
|
|
312
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
313
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
314
|
+
assert.match(result.stdout, /L5: references\/definition-of-done\.md/);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
for (const [ending, newline] of [['LF', '\n'], ['CRLF', '\r\n']]) {
|
|
318
|
+
test(`inline backticks do not hide subsequent broken links (${ending})`, () => {
|
|
319
|
+
const root = makeSandbox();
|
|
320
|
+
writeFile(root, 'skills/example/SKILL.md', [
|
|
321
|
+
'```js``` is inline code, not a fence opener.',
|
|
322
|
+
'See [missing](references/missing.md).',
|
|
323
|
+
'',
|
|
324
|
+
].join(newline));
|
|
325
|
+
|
|
326
|
+
const result = run(root);
|
|
327
|
+
|
|
328
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
329
|
+
assert.match(result.stdout, /L2: references\/missing\.md/);
|
|
330
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
for (const marker of ['```', '~~~']) {
|
|
334
|
+
test(`trailing text does not close a ${marker} fence (${ending})`, () => {
|
|
335
|
+
const root = makeSandbox();
|
|
336
|
+
writeFile(root, 'skills/example/SKILL.md', [
|
|
337
|
+
marker + 'markdown',
|
|
338
|
+
marker + ' still part of the example',
|
|
339
|
+
'[example](references/example-only.md)',
|
|
340
|
+
marker + ' \t',
|
|
341
|
+
'[real link](references/missing.md)',
|
|
342
|
+
'',
|
|
343
|
+
].join(newline));
|
|
344
|
+
|
|
345
|
+
const result = run(root);
|
|
346
|
+
|
|
347
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
348
|
+
assert.match(result.stdout, /L5: references\/missing\.md/);
|
|
349
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
350
|
+
assert.doesNotMatch(result.stdout, /example-only\.md/);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test('tilde fence info strings may contain backticks', () => {
|
|
356
|
+
const root = makeSandbox();
|
|
357
|
+
writeFile(root, 'skills/example/SKILL.md', [
|
|
358
|
+
'~~~example `code`',
|
|
359
|
+
'[example](references/example-only.md)',
|
|
360
|
+
'~~~',
|
|
361
|
+
'',
|
|
362
|
+
].join('\n'));
|
|
363
|
+
|
|
364
|
+
const result = run(root);
|
|
365
|
+
|
|
366
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
367
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Anchors. A `#fragment` after a references/ link must match a heading in the
|
|
371
|
+
// target file, slugged the way GitHub does it. Without this, renaming a heading
|
|
372
|
+
// silently breaks every link to it while the file itself still exists.
|
|
373
|
+
|
|
374
|
+
function hardeningSkill(root, link) {
|
|
375
|
+
writeFile(root, 'skills/hardening/SKILL.md', `See [patterns](${link}).\n`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
test('passes when an anchor matches a heading in the target file', () => {
|
|
379
|
+
const root = makeSandbox();
|
|
380
|
+
hardeningSkill(root, 'references/patterns.md#cross-site-scripting-xss');
|
|
381
|
+
writeFile(root, 'skills/hardening/references/patterns.md', '# Patterns\n\n## Cross-Site Scripting (XSS)\n');
|
|
382
|
+
|
|
383
|
+
const result = run(root);
|
|
384
|
+
|
|
385
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
386
|
+
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('fails when an anchor matches no heading in the target file', () => {
|
|
390
|
+
// The file exists, so the existing check is satisfied; only the fragment is stale.
|
|
391
|
+
const root = makeSandbox();
|
|
392
|
+
hardeningSkill(root, 'references/patterns.md#injection');
|
|
393
|
+
writeFile(root, 'skills/hardening/references/patterns.md', '# Patterns\n\n## Injection attacks\n');
|
|
394
|
+
|
|
395
|
+
const result = run(root);
|
|
396
|
+
|
|
397
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
398
|
+
assert.match(
|
|
399
|
+
result.stdout,
|
|
400
|
+
/L1: references\/patterns\.md#injection — no heading in skills\/hardening\/references\/patterns\.md produces #injection/
|
|
401
|
+
);
|
|
402
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
403
|
+
assert.match(result.stdout, /GitHub's heading slugs/);
|
|
404
|
+
// The path was right, so the path hint would only mislead.
|
|
405
|
+
assert.doesNotMatch(result.stdout, /use `\.\.\/\.\.\/references/);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('a repeated heading gets a -1 suffix, as on GitHub', () => {
|
|
409
|
+
const root = makeSandbox();
|
|
410
|
+
writeFile(root, 'skills/hardening/SKILL.md', [
|
|
411
|
+
'See [first](references/patterns.md#example).',
|
|
412
|
+
'See [second](references/patterns.md#example-1).',
|
|
413
|
+
'See [third](references/patterns.md#example-2).',
|
|
414
|
+
'',
|
|
415
|
+
].join('\n'));
|
|
416
|
+
writeFile(root, 'skills/hardening/references/patterns.md', '## Example\n\n## Example\n');
|
|
417
|
+
|
|
418
|
+
const result = run(root);
|
|
419
|
+
|
|
420
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
421
|
+
assert.match(result.stdout, /L3: references\/patterns\.md#example-2 — no heading/);
|
|
422
|
+
assert.doesNotMatch(result.stdout, /L[12]:/);
|
|
423
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('slugs come from the rendered heading text, not its markdown', () => {
|
|
427
|
+
// Real headings in the pack carry emphasis, code spans and links.
|
|
428
|
+
const root = makeSandbox();
|
|
429
|
+
writeFile(root, 'skills/hardening/SKILL.md', [
|
|
430
|
+
'See [a](references/patterns.md#why-this-is-not-a-ship-job).',
|
|
431
|
+
'See [b](references/patterns.md#owasp-top-10).',
|
|
432
|
+
'See [c](references/patterns.md#snake_case-names-stay).',
|
|
433
|
+
'See [d](references/patterns.md#closing-hashes).',
|
|
434
|
+
'',
|
|
435
|
+
].join('\n'));
|
|
436
|
+
writeFile(root, 'skills/hardening/references/patterns.md', [
|
|
437
|
+
'### Why this is *not* a `/ship` job',
|
|
438
|
+
'## [OWASP](https://owasp.org/Top10/) Top 10',
|
|
439
|
+
'## snake_case names stay',
|
|
440
|
+
'## Closing hashes ##',
|
|
441
|
+
'',
|
|
442
|
+
].join('\n'));
|
|
443
|
+
|
|
444
|
+
const result = run(root);
|
|
445
|
+
|
|
446
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('a heading-like line inside a fenced block is not an anchor', () => {
|
|
450
|
+
// Bash comments in a fenced example look like H1s.
|
|
451
|
+
const root = makeSandbox();
|
|
452
|
+
hardeningSkill(root, 'references/patterns.md#chrome-devtools');
|
|
453
|
+
writeFile(root, 'skills/hardening/references/patterns.md', [
|
|
454
|
+
'# Patterns',
|
|
455
|
+
'',
|
|
456
|
+
'```bash',
|
|
457
|
+
'# Chrome DevTools',
|
|
458
|
+
'```',
|
|
459
|
+
'',
|
|
460
|
+
].join('\n'));
|
|
461
|
+
|
|
462
|
+
const result = run(root);
|
|
463
|
+
|
|
464
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
465
|
+
assert.match(result.stdout, /#chrome-devtools — no heading/);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('anchors resolve from a reference file to a shared checklist', () => {
|
|
469
|
+
const root = makeSandbox();
|
|
470
|
+
writeFile(root, 'references/security-checklist.md', '# Security\n\n## OWASP Top 10 Quick Reference\n');
|
|
471
|
+
hardeningSkill(root, 'references/patterns.md');
|
|
472
|
+
writeFile(root, 'skills/hardening/references/patterns.md', [
|
|
473
|
+
'See `../../../references/security-checklist.md#owasp-top-10-quick-reference`.',
|
|
474
|
+
'See `../../../references/security-checklist.md#destructive-path-operations`.',
|
|
475
|
+
'',
|
|
476
|
+
].join('\n'));
|
|
477
|
+
|
|
478
|
+
const result = run(root);
|
|
479
|
+
|
|
480
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
481
|
+
assert.match(result.stdout, /✗ {2}skills\/hardening\/references\/patterns\.md/);
|
|
482
|
+
assert.match(result.stdout, /L2: \.\.\/\.\.\/\.\.\/references\/security-checklist\.md#destructive-path-operations — no heading in references\/security-checklist\.md/);
|
|
483
|
+
assert.doesNotMatch(result.stdout, /L1:/);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('a missing file is reported once, not again for its anchor', () => {
|
|
487
|
+
const root = makeSandbox();
|
|
488
|
+
hardeningSkill(root, 'references/renamed.md#injection');
|
|
489
|
+
|
|
490
|
+
const result = run(root);
|
|
491
|
+
|
|
492
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
493
|
+
assert.match(result.stdout, /L1: references\/renamed\.md — resolves to skills\/hardening\/references\/renamed\.md, which does not exist/);
|
|
494
|
+
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
495
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const { readFileSync } = require("node:fs");
|
|
5
|
+
const test = require("node:test");
|
|
6
|
+
|
|
7
|
+
const manifestPaths = [
|
|
8
|
+
"package.json",
|
|
9
|
+
"plugin.json",
|
|
10
|
+
".codex-plugin/plugin.json",
|
|
11
|
+
".claude-plugin/plugin.json",
|
|
12
|
+
".claude-plugin/marketplace.json",
|
|
13
|
+
".agents/plugins/marketplace.json",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function readManifestVersion(manifestPath) {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
18
|
+
return manifest.version ?? manifest.plugins?.[0]?.version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("all plugin manifests use the root plugin.json version", () => {
|
|
22
|
+
const expectedVersion = readManifestVersion("plugin.json");
|
|
23
|
+
assert.ok(expectedVersion, "plugin.json must define a version");
|
|
24
|
+
|
|
25
|
+
for (const manifestPath of manifestPaths) {
|
|
26
|
+
assert.equal(
|
|
27
|
+
readManifestVersion(manifestPath),
|
|
28
|
+
expectedVersion,
|
|
29
|
+
`${manifestPath} must use version ${expectedVersion}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
const { loadInstallation } = require('./lib/installation.cjs');
|
|
6
|
-
const { manifest } = loadInstallation();
|
|
7
|
-
console.log(`安装版本 ${manifest.version},布局 ${manifest.layoutVersion};${manifest.assets.length} 个登记文件均存在。`);
|
|
8
|
-
} catch (error) { console.error(error.message); process.exitCode = 1; }
|