@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.
Files changed (158) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +37 -33
  6. package/bin/agent-targets.json +11 -0
  7. package/bin/asset-fingerprint.cjs +38 -0
  8. package/bin/cleanup-backups.cjs +16 -0
  9. package/bin/i3-composer-agent.cjs +28 -12
  10. package/bin/install.cjs +74 -28
  11. package/bin/native-content.cjs +29 -0
  12. package/bin/wizard.cjs +3 -1
  13. package/docs/antigravity-setup.md +1 -1
  14. package/docs/codebuddy-setup.md +16 -3
  15. package/docs/cursor-setup.md +2 -2
  16. package/docs/getting-started.md +0 -2
  17. package/docs/opencode-setup.md +4 -4
  18. package/docs/workbuddy-setup.md +106 -0
  19. package/package.json +36 -20
  20. package/plugin.json +1 -1
  21. package/CHANGELOG.md +0 -51
  22. package/NOTICE.md +0 -15
  23. package/docs/advanced-per-agent-configuration.md +0 -70
  24. package/docs/comparison.md +0 -131
  25. package/docs/developer-onboarding.md +0 -116
  26. package/docs/skill-anatomy.md +0 -203
  27. package/evals/README.md +0 -101
  28. package/evals/cases/i3-api-and-interface-design.json +0 -45
  29. package/evals/cases/i3-browser-testing-with-devtools.json +0 -44
  30. package/evals/cases/i3-ci-cd-and-automation.json +0 -45
  31. package/evals/cases/i3-code-review-and-quality.json +0 -49
  32. package/evals/cases/i3-code-simplification.json +0 -45
  33. package/evals/cases/i3-constraint-driven-development.json +0 -79
  34. package/evals/cases/i3-context-engineering.json +0 -44
  35. package/evals/cases/i3-debugging-and-error-recovery.json +0 -58
  36. package/evals/cases/i3-deprecation-and-migration.json +0 -52
  37. package/evals/cases/i3-documentation-and-adrs.json +0 -44
  38. package/evals/cases/i3-doubt-driven-development.json +0 -43
  39. package/evals/cases/i3-frontend-ui-engineering.json +0 -52
  40. package/evals/cases/i3-git-workflow-and-versioning.json +0 -47
  41. package/evals/cases/i3-idea-refine.json +0 -43
  42. package/evals/cases/i3-incremental-implementation.json +0 -57
  43. package/evals/cases/i3-interview-me.json +0 -42
  44. package/evals/cases/i3-observability-and-instrumentation.json +0 -44
  45. package/evals/cases/i3-performance-optimization.json +0 -53
  46. package/evals/cases/i3-planning-and-task-breakdown.json +0 -45
  47. package/evals/cases/i3-security-and-hardening.json +0 -53
  48. package/evals/cases/i3-shipping-and-launch.json +0 -56
  49. package/evals/cases/i3-source-driven-development.json +0 -44
  50. package/evals/cases/i3-spec-driven-development.json +0 -72
  51. package/evals/cases/i3-test-driven-development.json +0 -73
  52. package/evals/cases/i3-using-agent-skills.json +0 -44
  53. package/evals/fixtures/i3-api-and-interface-design/service-brief.md +0 -19
  54. package/evals/fixtures/i3-browser-testing-with-devtools/README.md +0 -5
  55. package/evals/fixtures/i3-browser-testing-with-devtools/index.html +0 -24
  56. package/evals/fixtures/i3-browser-testing-with-devtools/server.js +0 -15
  57. package/evals/fixtures/i3-ci-cd-and-automation/package.json +0 -8
  58. package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +0 -3
  59. package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +0 -9
  60. package/evals/fixtures/i3-code-review-and-quality/user-search.diff +0 -16
  61. package/evals/fixtures/i3-code-simplification/config-parser.js +0 -46
  62. package/evals/fixtures/i3-code-simplification/config-parser.test.js +0 -15
  63. package/evals/fixtures/i3-context-engineering/context-audit.md +0 -15
  64. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +0 -8
  65. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +0 -9
  66. package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +0 -6
  67. package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +0 -9
  68. package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +0 -16
  69. package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +0 -19
  70. package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +0 -7
  71. package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +0 -11
  72. package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +0 -21
  73. package/evals/fixtures/i3-git-workflow-and-versioning/app.js +0 -7
  74. package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +0 -9
  75. package/evals/fixtures/i3-incremental-implementation/reports.js +0 -7
  76. package/evals/fixtures/i3-incremental-implementation/reports.test.js +0 -12
  77. package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +0 -8
  78. package/evals/fixtures/i3-observability-and-instrumentation/operations.md +0 -11
  79. package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +0 -14
  80. package/evals/fixtures/i3-performance-optimization/benchmark.js +0 -15
  81. package/evals/fixtures/i3-performance-optimization/products.js +0 -14
  82. package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +0 -18
  83. package/evals/fixtures/i3-security-and-hardening/webhook.js +0 -11
  84. package/evals/fixtures/i3-security-and-hardening/webhook.test.js +0 -13
  85. package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +0 -6
  86. package/evals/fixtures/i3-shipping-and-launch/launch-status.md +0 -11
  87. package/evals/fixtures/i3-source-driven-development/framework-task.md +0 -10
  88. package/evals/fixtures/i3-spec-driven-development/billing-brief.md +0 -16
  89. package/evals/fixtures/i3-test-driven-development/BUG.md +0 -10
  90. package/evals/fixtures/i3-test-driven-development/README.md +0 -28
  91. package/evals/fixtures/i3-test-driven-development/package.json +0 -9
  92. package/evals/fixtures/i3-test-driven-development/src/split.js +0 -8
  93. package/evals/fixtures/i3-test-driven-development/test/split.test.js +0 -13
  94. package/evals/fixtures/i3-using-agent-skills/incident.md +0 -6
  95. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
  96. package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
  97. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
  98. package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
  99. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
  100. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
  101. package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
  102. package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
  103. package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
  104. package/evals/plugin/code-review-fires/prompt.md +0 -31
  105. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
  106. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
  107. package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
  108. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
  109. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
  110. package/evals/skill-impact.md +0 -6
  111. package/rebrand/INSTALLATION-SPEC.md +0 -47
  112. package/rebrand/MAINTENANCE.md +0 -114
  113. package/rebrand/NOTICE.md +0 -15
  114. package/rebrand/PR-WORKFLOW.md +0 -34
  115. package/rebrand/PREFIX-SPEC.md +0 -16
  116. package/rebrand/UNIFICATION-SPEC.md +0 -33
  117. package/rebrand/VALIDATION.md +0 -32
  118. package/rebrand/VERSIONING.md +0 -35
  119. package/rebrand/apply.cjs +0 -217
  120. package/rebrand/brand.json +0 -13
  121. package/rebrand/catalog.cjs +0 -70
  122. package/rebrand/fork-patches.json +0 -27
  123. package/rebrand/identifiers.cjs +0 -41
  124. package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
  125. package/rebrand/overlay/CHANGELOG.md +0 -51
  126. package/rebrand/overlay/bin/agent-targets.json +0 -726
  127. package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
  128. package/rebrand/overlay/bin/install.cjs +0 -180
  129. package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
  130. package/rebrand/overlay/bin/wizard.cjs +0 -66
  131. package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
  132. package/rebrand/overlay/tests/bundle.test.cjs +0 -85
  133. package/rebrand/overlay/tests/installer.test.cjs +0 -241
  134. package/rebrand/package.template.json +0 -82
  135. package/rebrand/rebrand.sh +0 -4
  136. package/rebrand/record-upstream.cjs +0 -37
  137. package/rebrand/templates/README.md +0 -130
  138. package/rebrand/templates/codebuddy-setup.md +0 -89
  139. package/rebrand/templates/codex-setup.md +0 -45
  140. package/rebrand/tests/pipeline.test.cjs +0 -253
  141. package/rebrand/upstream.json +0 -171
  142. package/scripts/floor-guard-reference-test.js +0 -291
  143. package/scripts/lib/skill-lint-test.js +0 -650
  144. package/scripts/lib/skill-lint.js +0 -586
  145. package/scripts/run-evals-test.js +0 -623
  146. package/scripts/run-evals.js +0 -638
  147. package/scripts/validate-artifact-paths-test.js +0 -179
  148. package/scripts/validate-artifact-paths.js +0 -130
  149. package/scripts/validate-brand.cjs +0 -44
  150. package/scripts/validate-commands-test.js +0 -216
  151. package/scripts/validate-commands.js +0 -223
  152. package/scripts/validate-reference-links-test.js +0 -495
  153. package/scripts/validate-reference-links.js +0 -187
  154. package/scripts/validate-skills.js +0 -69
  155. package/scripts/validate-versions-test.js +0 -32
  156. package/scripts/validate-versions.js +0 -35
  157. package/tests/bundle.test.cjs +0 -85
  158. 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();