@givolt/i3-composer-agent 2.0.0 → 2.1.1
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 +37 -33
- package/bin/agent-targets.json +11 -0
- package/bin/asset-fingerprint.cjs +38 -0
- package/bin/cleanup-backups.cjs +16 -0
- package/bin/i3-composer-agent.cjs +28 -12
- package/bin/install.cjs +74 -28
- package/bin/native-content.cjs +29 -0
- package/bin/wizard.cjs +3 -1
- package/docs/antigravity-setup.md +1 -1
- package/docs/codebuddy-setup.md +16 -3
- package/docs/cursor-setup.md +2 -2
- package/docs/getting-started.md +0 -2
- package/docs/opencode-setup.md +4 -4
- package/docs/workbuddy-setup.md +106 -0
- package/package.json +36 -20
- package/plugin.json +1 -1
- package/CHANGELOG.md +0 -51
- package/NOTICE.md +0 -15
- package/docs/advanced-per-agent-configuration.md +0 -70
- package/docs/comparison.md +0 -131
- package/docs/developer-onboarding.md +0 -116
- package/docs/skill-anatomy.md +0 -203
- package/evals/README.md +0 -101
- package/evals/cases/i3-api-and-interface-design.json +0 -45
- package/evals/cases/i3-browser-testing-with-devtools.json +0 -44
- package/evals/cases/i3-ci-cd-and-automation.json +0 -45
- package/evals/cases/i3-code-review-and-quality.json +0 -49
- package/evals/cases/i3-code-simplification.json +0 -45
- package/evals/cases/i3-constraint-driven-development.json +0 -79
- package/evals/cases/i3-context-engineering.json +0 -44
- package/evals/cases/i3-debugging-and-error-recovery.json +0 -58
- package/evals/cases/i3-deprecation-and-migration.json +0 -52
- package/evals/cases/i3-documentation-and-adrs.json +0 -44
- package/evals/cases/i3-doubt-driven-development.json +0 -43
- package/evals/cases/i3-frontend-ui-engineering.json +0 -52
- package/evals/cases/i3-git-workflow-and-versioning.json +0 -47
- package/evals/cases/i3-idea-refine.json +0 -43
- package/evals/cases/i3-incremental-implementation.json +0 -57
- package/evals/cases/i3-interview-me.json +0 -42
- package/evals/cases/i3-observability-and-instrumentation.json +0 -44
- package/evals/cases/i3-performance-optimization.json +0 -53
- package/evals/cases/i3-planning-and-task-breakdown.json +0 -45
- package/evals/cases/i3-security-and-hardening.json +0 -53
- package/evals/cases/i3-shipping-and-launch.json +0 -56
- package/evals/cases/i3-source-driven-development.json +0 -44
- package/evals/cases/i3-spec-driven-development.json +0 -72
- package/evals/cases/i3-test-driven-development.json +0 -73
- package/evals/cases/i3-using-agent-skills.json +0 -44
- package/evals/fixtures/i3-api-and-interface-design/service-brief.md +0 -19
- package/evals/fixtures/i3-browser-testing-with-devtools/README.md +0 -5
- package/evals/fixtures/i3-browser-testing-with-devtools/index.html +0 -24
- package/evals/fixtures/i3-browser-testing-with-devtools/server.js +0 -15
- package/evals/fixtures/i3-ci-cd-and-automation/package.json +0 -8
- package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +0 -3
- package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +0 -9
- package/evals/fixtures/i3-code-review-and-quality/user-search.diff +0 -16
- package/evals/fixtures/i3-code-simplification/config-parser.js +0 -46
- package/evals/fixtures/i3-code-simplification/config-parser.test.js +0 -15
- package/evals/fixtures/i3-context-engineering/context-audit.md +0 -15
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +0 -8
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +0 -9
- package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +0 -6
- package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +0 -9
- package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +0 -16
- package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +0 -19
- package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +0 -7
- package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +0 -11
- package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +0 -21
- package/evals/fixtures/i3-git-workflow-and-versioning/app.js +0 -7
- package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +0 -9
- package/evals/fixtures/i3-incremental-implementation/reports.js +0 -7
- package/evals/fixtures/i3-incremental-implementation/reports.test.js +0 -12
- package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +0 -8
- package/evals/fixtures/i3-observability-and-instrumentation/operations.md +0 -11
- package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +0 -14
- package/evals/fixtures/i3-performance-optimization/benchmark.js +0 -15
- package/evals/fixtures/i3-performance-optimization/products.js +0 -14
- package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +0 -18
- package/evals/fixtures/i3-security-and-hardening/webhook.js +0 -11
- package/evals/fixtures/i3-security-and-hardening/webhook.test.js +0 -13
- package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +0 -6
- package/evals/fixtures/i3-shipping-and-launch/launch-status.md +0 -11
- package/evals/fixtures/i3-source-driven-development/framework-task.md +0 -10
- package/evals/fixtures/i3-spec-driven-development/billing-brief.md +0 -16
- package/evals/fixtures/i3-test-driven-development/BUG.md +0 -10
- package/evals/fixtures/i3-test-driven-development/README.md +0 -28
- package/evals/fixtures/i3-test-driven-development/package.json +0 -9
- package/evals/fixtures/i3-test-driven-development/src/split.js +0 -8
- package/evals/fixtures/i3-test-driven-development/test/split.test.js +0 -13
- package/evals/fixtures/i3-using-agent-skills/incident.md +0 -6
- package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
- package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
- package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
- package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
- package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
- package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
- package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
- package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
- package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
- package/evals/plugin/code-review-fires/prompt.md +0 -31
- package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
- package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
- package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
- package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
- package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
- package/evals/skill-impact.md +0 -6
- package/rebrand/INSTALLATION-SPEC.md +0 -47
- package/rebrand/MAINTENANCE.md +0 -114
- package/rebrand/NOTICE.md +0 -15
- package/rebrand/PR-WORKFLOW.md +0 -34
- package/rebrand/PREFIX-SPEC.md +0 -16
- package/rebrand/UNIFICATION-SPEC.md +0 -33
- package/rebrand/VALIDATION.md +0 -32
- package/rebrand/VERSIONING.md +0 -35
- package/rebrand/apply.cjs +0 -217
- package/rebrand/brand.json +0 -13
- package/rebrand/catalog.cjs +0 -70
- package/rebrand/fork-patches.json +0 -27
- package/rebrand/identifiers.cjs +0 -41
- package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
- package/rebrand/overlay/CHANGELOG.md +0 -51
- package/rebrand/overlay/bin/agent-targets.json +0 -726
- package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
- package/rebrand/overlay/bin/install.cjs +0 -180
- package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
- package/rebrand/overlay/bin/wizard.cjs +0 -66
- package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
- package/rebrand/overlay/tests/bundle.test.cjs +0 -85
- package/rebrand/overlay/tests/installer.test.cjs +0 -241
- package/rebrand/package.template.json +0 -82
- package/rebrand/rebrand.sh +0 -4
- package/rebrand/record-upstream.cjs +0 -37
- package/rebrand/templates/README.md +0 -130
- package/rebrand/templates/codebuddy-setup.md +0 -89
- package/rebrand/templates/codex-setup.md +0 -45
- package/rebrand/tests/pipeline.test.cjs +0 -253
- package/rebrand/upstream.json +0 -171
- package/scripts/floor-guard-reference-test.js +0 -291
- package/scripts/lib/skill-lint-test.js +0 -650
- package/scripts/lib/skill-lint.js +0 -586
- package/scripts/run-evals-test.js +0 -623
- package/scripts/run-evals.js +0 -638
- package/scripts/validate-artifact-paths-test.js +0 -179
- package/scripts/validate-artifact-paths.js +0 -130
- package/scripts/validate-brand.cjs +0 -44
- package/scripts/validate-commands-test.js +0 -216
- package/scripts/validate-commands.js +0 -223
- package/scripts/validate-reference-links-test.js +0 -495
- package/scripts/validate-reference-links.js +0 -187
- package/scripts/validate-skills.js +0 -69
- package/scripts/validate-versions-test.js +0 -32
- package/scripts/validate-versions.js +0 -35
- package/tests/bundle.test.cjs +0 -85
- package/tests/installer.test.cjs +0 -241
|
@@ -1,495 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* validate-reference-links.js
|
|
4
|
-
*
|
|
5
|
-
* Guards links from skills to the shared `references/` checklists.
|
|
6
|
-
*
|
|
7
|
-
* Those checklists live in the repo-root `references/` directory, but every
|
|
8
|
-
* SKILL.md used to link them as `references/<file>.md` — a path relative to
|
|
9
|
-
* the skill's own directory, which is two levels below the root. All 18 links
|
|
10
|
-
* across 11 skills resolved to files that do not exist, in the repo and in
|
|
11
|
-
* every plugin-install layout (~/.claude/plugins/cache/..., ~/.codex/...).
|
|
12
|
-
* Agents that followed the guidance — for example i3-using-agent-skills pointing
|
|
13
|
-
* at the Definition of Done — hit a file-not-found and stalled.
|
|
14
|
-
*
|
|
15
|
-
* Nothing else in CI catches this: validate-artifact-paths.js is scoped to
|
|
16
|
-
* spec/plan/todo artifacts and is explicitly not a general markdown linter.
|
|
17
|
-
*
|
|
18
|
-
* The rule enforced here: every `references/*.md` link in a SKILL.md must
|
|
19
|
-
* resolve to an existing file relative to that skill's own directory. This
|
|
20
|
-
* accepts both conventions in CLAUDE.md — shared checklists reached via
|
|
21
|
-
* `../../references/`, and a skill's own colocated `references/` directory.
|
|
22
|
-
*
|
|
23
|
-
* The markdown files inside a skill's own `references/` directory get the
|
|
24
|
-
* same check, with each link resolved from the file that contains it. They
|
|
25
|
-
* sit one directory deeper than SKILL.md, so from there the shared checklists
|
|
26
|
-
* are `../../../references/`: the same off-by-a-level mistake, one level down.
|
|
27
|
-
*
|
|
28
|
-
* A `#fragment` after such a link must match a heading in the target file,
|
|
29
|
-
* slugged the way GitHub renders anchors. The file existing is not enough:
|
|
30
|
-
* renaming a heading breaks every link to it while the path still resolves.
|
|
31
|
-
*
|
|
32
|
-
* Scope is deliberately narrow: only `references/*.md` links, only SKILL.md
|
|
33
|
-
* and `skills/<name>/references/*.md` files. It is not a general markdown
|
|
34
|
-
* path linter — skills legitimately mention paths that do not exist yet
|
|
35
|
-
* (`tasks/todo.md`, `PERF.md`, `docs/ideas/[idea-name].md`), and those must
|
|
36
|
-
* not fail the build.
|
|
37
|
-
*
|
|
38
|
-
* Fenced code blocks are exempt for the same reason. Text inside a fence is
|
|
39
|
-
* an example, not a link an agent will follow — and this rule in particular
|
|
40
|
-
* has to be documentable: the failure message below tells authors to write
|
|
41
|
-
* `../../references/<file>.md` rather than `references/<file>.md`, which no
|
|
42
|
-
* skill could show as an example without failing the very check explaining it.
|
|
43
|
-
*
|
|
44
|
-
* Exit codes: 0 = all clear, 1 = one or more unresolvable links.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
'use strict';
|
|
48
|
-
|
|
49
|
-
const fs = require('fs');
|
|
50
|
-
const path = require('path');
|
|
51
|
-
const { stripFencedCodeBlocks } = require('./lib/skill-lint');
|
|
52
|
-
|
|
53
|
-
const ROOT = path.resolve(__dirname, '..');
|
|
54
|
-
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
55
|
-
|
|
56
|
-
// Matches a link to a references/ markdown file, with any number of leading
|
|
57
|
-
// `../` segments: `references/x.md`, `../../references/x.md`. Anchored on a
|
|
58
|
-
// non-path character so `myreferences/x.md` does not match. An optional
|
|
59
|
-
// `#fragment` is captured separately.
|
|
60
|
-
const REFERENCE_LINK_RE = /(?<![A-Za-z0-9._/-])((?:\.\.\/)*references\/[A-Za-z0-9._-]+\.md)(?:#([\p{L}\p{N}_-]+))?/gu;
|
|
61
|
-
|
|
62
|
-
// GitHub's heading anchor: the rendered text, lowercased, with everything but
|
|
63
|
-
// letters, numbers, `_`, `-` and spaces dropped, then spaces turned to hyphens.
|
|
64
|
-
function slugify(heading) {
|
|
65
|
-
const text = heading
|
|
66
|
-
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') // links and images keep their text
|
|
67
|
-
.replace(/<[^>]+>/g, ''); // inline HTML renders no text
|
|
68
|
-
return text.toLowerCase().replace(/[^\p{L}\p{M}\p{N}_\- ]/gu, '').replace(/ /g, '-');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The anchors a file's ATX headings produce, outside fenced blocks. A repeated
|
|
72
|
-
// slug gets `-1`, `-2`, ... as on GitHub.
|
|
73
|
-
function headingAnchors(file) {
|
|
74
|
-
const anchors = new Set();
|
|
75
|
-
const seen = new Map();
|
|
76
|
-
for (const line of stripFencedCodeBlocks(fs.readFileSync(file, 'utf8')).split('\n')) {
|
|
77
|
-
const match = line.match(/^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/);
|
|
78
|
-
if (!match) continue;
|
|
79
|
-
const slug = slugify(match[1]);
|
|
80
|
-
const count = seen.get(slug) || 0;
|
|
81
|
-
anchors.add(count === 0 ? slug : `${slug}-${count}`);
|
|
82
|
-
seen.set(slug, count + 1);
|
|
83
|
-
}
|
|
84
|
-
return anchors;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// A link is resolved from the directory of the file that contains it.
|
|
88
|
-
function findViolations(file) {
|
|
89
|
-
const violations = [];
|
|
90
|
-
const baseDir = path.dirname(file);
|
|
91
|
-
// Share the linter's fence rules; blanked lines preserve diagnostic positions.
|
|
92
|
-
const lines = stripFencedCodeBlocks(fs.readFileSync(file, 'utf8')).split('\n');
|
|
93
|
-
|
|
94
|
-
lines.forEach((line, i) => {
|
|
95
|
-
for (const match of line.matchAll(REFERENCE_LINK_RE)) {
|
|
96
|
-
const [, link, anchor] = match;
|
|
97
|
-
const target = path.resolve(baseDir, link);
|
|
98
|
-
if (!fs.existsSync(target)) {
|
|
99
|
-
violations.push({ line: i + 1, link, target });
|
|
100
|
-
} else if (anchor && !headingAnchors(target).has(anchor)) {
|
|
101
|
-
violations.push({ line: i + 1, link, target, anchor });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return violations;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// The markdown files directly inside a skill's own references/ directory.
|
|
110
|
-
function skillReferenceFiles(skillDir) {
|
|
111
|
-
const dir = path.join(skillDir, 'references');
|
|
112
|
-
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
113
|
-
return fs.readdirSync(dir)
|
|
114
|
-
.filter((name) => name.endsWith('.md'))
|
|
115
|
-
.sort()
|
|
116
|
-
.map((name) => path.join(dir, name))
|
|
117
|
-
.filter((file) => fs.statSync(file).isFile());
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function toPosix(file) {
|
|
121
|
-
return path.relative(ROOT, file).split(path.sep).join('/');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function main() {
|
|
125
|
-
console.log('Checking references/ links in skills...\n');
|
|
126
|
-
|
|
127
|
-
if (!fs.existsSync(SKILLS_DIR)) {
|
|
128
|
-
console.log('No skills/ directory — nothing to check.');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let checked = 0;
|
|
133
|
-
let errors = 0;
|
|
134
|
-
let referenceFileErrors = 0;
|
|
135
|
-
let anchorErrors = 0;
|
|
136
|
-
|
|
137
|
-
const skillNames = fs.readdirSync(SKILLS_DIR).sort();
|
|
138
|
-
for (const name of skillNames) {
|
|
139
|
-
const skillDir = path.join(SKILLS_DIR, name);
|
|
140
|
-
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
141
|
-
if (!fs.statSync(skillDir).isDirectory() || !fs.existsSync(skillFile)) continue;
|
|
142
|
-
|
|
143
|
-
checked++;
|
|
144
|
-
for (const file of [skillFile, ...skillReferenceFiles(skillDir)]) {
|
|
145
|
-
const violations = findViolations(file);
|
|
146
|
-
|
|
147
|
-
if (violations.length === 0) {
|
|
148
|
-
console.log(` ✓ ${toPosix(file)}`);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
console.log(` ✗ ${toPosix(file)}`);
|
|
153
|
-
for (const { line, link, target, anchor } of violations) {
|
|
154
|
-
errors++;
|
|
155
|
-
if (anchor) {
|
|
156
|
-
console.log(` L${line}: ${link}#${anchor} — no heading in ${toPosix(target)} produces #${anchor}`);
|
|
157
|
-
anchorErrors++;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
console.log(` L${line}: ${link} — resolves to ${toPosix(target)}, which does not exist`);
|
|
161
|
-
if (file !== skillFile) referenceFileErrors++;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const status = errors > 0 ? 'FAILED' : 'PASSED';
|
|
167
|
-
console.log(`\n${checked} skills checked — ${errors} error(s) — ${status}`);
|
|
168
|
-
|
|
169
|
-
if (errors > 0) {
|
|
170
|
-
if (errors > anchorErrors) {
|
|
171
|
-
console.log('\nLinks to references/ are resolved from the directory of the file that contains them.');
|
|
172
|
-
console.log('Shared checklists live in the repo-root references/, two levels up from a SKILL.md:');
|
|
173
|
-
console.log('use `../../references/<file>.md`, not `references/<file>.md`.');
|
|
174
|
-
if (referenceFileErrors > 0) {
|
|
175
|
-
console.log('From a file inside skills/<name>/references/ they are three levels up:');
|
|
176
|
-
console.log('use `../../../references/<file>.md`.');
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (anchorErrors > 0) {
|
|
180
|
-
console.log("\nAnchors are matched against GitHub's heading slugs: the heading text lowercased,");
|
|
181
|
-
console.log('punctuation dropped, spaces turned to hyphens, and -1, -2, ... for repeated headings.');
|
|
182
|
-
}
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
main();
|