@givolt/i3-composer-agent 1.0.1 → 2.1.0

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 (238) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/{build.md → i3-build.md} +10 -10
  3. package/.claude/commands/{code-simplify.md → i3-code-simplify.md} +2 -2
  4. package/.claude/commands/{constraints.md → i3-constraints.md} +4 -4
  5. package/.claude/commands/{plan.md → i3-plan.md} +1 -1
  6. package/.claude/commands/{review.md → i3-review.md} +3 -3
  7. package/.claude/commands/{ship.md → i3-ship.md} +4 -4
  8. package/.claude/commands/{spec.md → i3-spec.md} +1 -1
  9. package/.claude/commands/{test.md → i3-test.md} +2 -2
  10. package/.claude/commands/{webperf.md → i3-webperf.md} +1 -1
  11. package/.claude-plugin/marketplace.json +1 -1
  12. package/.claude-plugin/plugin.json +1 -1
  13. package/.codex-plugin/plugin.json +1 -1
  14. package/.gemini/commands/{build.toml → i3-build.toml} +10 -10
  15. package/.gemini/commands/{code-simplify.toml → i3-code-simplify.toml} +2 -2
  16. package/.gemini/commands/{constraints.toml → i3-constraints.toml} +4 -4
  17. package/.gemini/commands/{planning.toml → i3-planning.toml} +1 -1
  18. package/{commands/review.toml → .gemini/commands/i3-review.toml} +3 -3
  19. package/.gemini/commands/{ship.toml → i3-ship.toml} +4 -4
  20. package/.gemini/commands/{spec.toml → i3-spec.toml} +1 -1
  21. package/{commands/test.toml → .gemini/commands/i3-test.toml} +2 -2
  22. package/.gemini/commands/{webperf.toml → i3-webperf.toml} +1 -1
  23. package/README.md +72 -69
  24. package/agents/code-reviewer.md +2 -2
  25. package/agents/security-auditor.md +1 -1
  26. package/agents/test-engineer.md +1 -1
  27. package/agents/web-performance-auditor.md +3 -3
  28. package/bin/agent-targets.json +11 -0
  29. package/bin/asset-fingerprint.cjs +38 -0
  30. package/bin/cleanup-backups.cjs +16 -0
  31. package/bin/i3-composer-agent.cjs +29 -14
  32. package/bin/install.cjs +81 -19
  33. package/bin/native-content.cjs +29 -0
  34. package/bin/wizard.cjs +2 -1
  35. package/commands/{build.toml → i3-build.toml} +10 -10
  36. package/commands/{code-simplify.toml → i3-code-simplify.toml} +2 -2
  37. package/commands/{constraints.toml → i3-constraints.toml} +4 -4
  38. package/commands/{planning.toml → i3-planning.toml} +1 -1
  39. package/{.gemini/commands/review.toml → commands/i3-review.toml} +3 -3
  40. package/commands/{ship.toml → i3-ship.toml} +4 -4
  41. package/commands/{spec.toml → i3-spec.toml} +1 -1
  42. package/{.gemini/commands/test.toml → commands/i3-test.toml} +2 -2
  43. package/commands/{webperf.toml → i3-webperf.toml} +1 -1
  44. package/docs/adoption-guide.md +35 -35
  45. package/docs/agents.md +12 -12
  46. package/docs/antigravity-setup.md +14 -14
  47. package/docs/codebuddy-setup.md +27 -18
  48. package/docs/codex-setup.md +6 -6
  49. package/docs/commandcode-setup.md +5 -5
  50. package/docs/copilot-cli-setup.md +5 -5
  51. package/docs/copilot-setup.md +22 -22
  52. package/docs/cursor-setup.md +22 -22
  53. package/docs/gemini-cli-setup.md +26 -26
  54. package/docs/getting-started.md +36 -38
  55. package/docs/opencode-setup.md +31 -31
  56. package/docs/windsurf-setup.md +7 -7
  57. package/docs/workbuddy-setup.md +92 -0
  58. package/hooks/SDD-CACHE.md +3 -3
  59. package/hooks/SIMPLIFY-IGNORE.md +2 -2
  60. package/hooks/session-start-test.sh +1 -1
  61. package/hooks/session-start.sh +3 -3
  62. package/package.json +36 -20
  63. package/plugin.json +1 -1
  64. package/references/accessibility-checklist.md +1 -1
  65. package/references/definition-of-done.md +7 -7
  66. package/references/observability-checklist.md +3 -3
  67. package/references/orchestration-patterns.md +17 -17
  68. package/references/performance-checklist.md +2 -2
  69. package/references/security-checklist.md +1 -1
  70. package/references/testing-patterns.md +1 -1
  71. package/skills/{api-and-interface-design → i3-api-and-interface-design}/SKILL.md +2 -2
  72. package/skills/{browser-testing-with-devtools → i3-browser-testing-with-devtools}/SKILL.md +1 -1
  73. package/skills/{ci-cd-and-automation → i3-ci-cd-and-automation}/SKILL.md +2 -2
  74. package/skills/{code-review-and-quality → i3-code-review-and-quality}/SKILL.md +5 -5
  75. package/skills/{code-simplification → i3-code-simplification}/SKILL.md +1 -1
  76. package/skills/{constraint-driven-development → i3-constraint-driven-development}/SKILL.md +15 -15
  77. package/skills/{context-engineering → i3-context-engineering}/SKILL.md +1 -1
  78. package/skills/{debugging-and-error-recovery → i3-debugging-and-error-recovery}/SKILL.md +2 -2
  79. package/skills/{deprecation-and-migration → i3-deprecation-and-migration}/SKILL.md +2 -2
  80. package/skills/{documentation-and-adrs → i3-documentation-and-adrs}/SKILL.md +1 -1
  81. package/skills/{doubt-driven-development → i3-doubt-driven-development}/SKILL.md +8 -8
  82. package/skills/{frontend-ui-engineering → i3-frontend-ui-engineering}/SKILL.md +1 -1
  83. package/skills/{git-workflow-and-versioning → i3-git-workflow-and-versioning}/SKILL.md +4 -4
  84. package/skills/{idea-refine → i3-idea-refine}/SKILL.md +2 -2
  85. package/skills/{idea-refine → i3-idea-refine}/scripts/idea-refine.sh +1 -1
  86. package/skills/{incremental-implementation → i3-incremental-implementation}/SKILL.md +3 -3
  87. package/skills/{interview-me → i3-interview-me}/SKILL.md +12 -12
  88. package/skills/{observability-and-instrumentation → i3-observability-and-instrumentation}/SKILL.md +5 -5
  89. package/skills/{performance-optimization → i3-performance-optimization}/SKILL.md +1 -1
  90. package/skills/{performance-optimization → i3-performance-optimization}/references/optimization-patterns.md +1 -1
  91. package/skills/{planning-and-task-breakdown → i3-planning-and-task-breakdown}/SKILL.md +2 -2
  92. package/skills/{security-and-hardening → i3-security-and-hardening}/SKILL.md +3 -3
  93. package/skills/{security-and-hardening → i3-security-and-hardening}/references/hardening-patterns.md +1 -1
  94. package/skills/{shipping-and-launch → i3-shipping-and-launch}/SKILL.md +2 -2
  95. package/skills/{source-driven-development → i3-source-driven-development}/SKILL.md +2 -2
  96. package/skills/{spec-driven-development → i3-spec-driven-development}/SKILL.md +7 -7
  97. package/skills/{test-driven-development → i3-test-driven-development}/SKILL.md +2 -2
  98. package/skills/{using-agent-skills → i3-using-agent-skills}/SKILL.md +67 -67
  99. package/CHANGELOG.md +0 -34
  100. package/NOTICE.md +0 -14
  101. package/docs/advanced-per-agent-configuration.md +0 -70
  102. package/docs/comparison.md +0 -131
  103. package/docs/developer-onboarding.md +0 -116
  104. package/docs/skill-anatomy.md +0 -203
  105. package/evals/README.md +0 -101
  106. package/evals/cases/api-and-interface-design.json +0 -45
  107. package/evals/cases/browser-testing-with-devtools.json +0 -44
  108. package/evals/cases/ci-cd-and-automation.json +0 -45
  109. package/evals/cases/code-review-and-quality.json +0 -49
  110. package/evals/cases/code-simplification.json +0 -45
  111. package/evals/cases/constraint-driven-development.json +0 -79
  112. package/evals/cases/context-engineering.json +0 -44
  113. package/evals/cases/debugging-and-error-recovery.json +0 -58
  114. package/evals/cases/deprecation-and-migration.json +0 -52
  115. package/evals/cases/documentation-and-adrs.json +0 -44
  116. package/evals/cases/doubt-driven-development.json +0 -43
  117. package/evals/cases/frontend-ui-engineering.json +0 -52
  118. package/evals/cases/git-workflow-and-versioning.json +0 -47
  119. package/evals/cases/idea-refine.json +0 -43
  120. package/evals/cases/incremental-implementation.json +0 -57
  121. package/evals/cases/interview-me.json +0 -42
  122. package/evals/cases/observability-and-instrumentation.json +0 -44
  123. package/evals/cases/performance-optimization.json +0 -53
  124. package/evals/cases/planning-and-task-breakdown.json +0 -45
  125. package/evals/cases/security-and-hardening.json +0 -53
  126. package/evals/cases/shipping-and-launch.json +0 -56
  127. package/evals/cases/source-driven-development.json +0 -44
  128. package/evals/cases/spec-driven-development.json +0 -72
  129. package/evals/cases/test-driven-development.json +0 -73
  130. package/evals/cases/using-agent-skills.json +0 -44
  131. package/evals/fixtures/api-and-interface-design/service-brief.md +0 -19
  132. package/evals/fixtures/browser-testing-with-devtools/README.md +0 -5
  133. package/evals/fixtures/browser-testing-with-devtools/index.html +0 -24
  134. package/evals/fixtures/browser-testing-with-devtools/server.js +0 -15
  135. package/evals/fixtures/ci-cd-and-automation/package.json +0 -8
  136. package/evals/fixtures/ci-cd-and-automation/src/slug.js +0 -3
  137. package/evals/fixtures/ci-cd-and-automation/test/slug.test.js +0 -9
  138. package/evals/fixtures/code-review-and-quality/user-search.diff +0 -16
  139. package/evals/fixtures/code-simplification/config-parser.js +0 -46
  140. package/evals/fixtures/code-simplification/config-parser.test.js +0 -15
  141. package/evals/fixtures/context-engineering/context-audit.md +0 -15
  142. package/evals/fixtures/debugging-and-error-recovery/pagination.js +0 -8
  143. package/evals/fixtures/debugging-and-error-recovery/pagination.test.js +0 -9
  144. package/evals/fixtures/debugging-and-error-recovery/time-pressure.md +0 -6
  145. package/evals/fixtures/deprecation-and-migration/api-inventory.md +0 -9
  146. package/evals/fixtures/documentation-and-adrs/decision-context.md +0 -16
  147. package/evals/fixtures/doubt-driven-development/migration-plan.md +0 -19
  148. package/evals/fixtures/frontend-ui-engineering/Button.tsx +0 -7
  149. package/evals/fixtures/frontend-ui-engineering/design-system.md +0 -11
  150. package/evals/fixtures/git-workflow-and-versioning/.eval/working-tree.patch +0 -21
  151. package/evals/fixtures/git-workflow-and-versioning/app.js +0 -7
  152. package/evals/fixtures/git-workflow-and-versioning/app.test.js +0 -9
  153. package/evals/fixtures/incremental-implementation/reports.js +0 -7
  154. package/evals/fixtures/incremental-implementation/reports.test.js +0 -12
  155. package/evals/fixtures/incremental-implementation/tasks/plan.md +0 -8
  156. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
  157. package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
  158. package/evals/fixtures/observability-and-instrumentation/operations.md +0 -11
  159. package/evals/fixtures/observability-and-instrumentation/payment-retry.js +0 -14
  160. package/evals/fixtures/performance-optimization/benchmark.js +0 -15
  161. package/evals/fixtures/performance-optimization/products.js +0 -14
  162. package/evals/fixtures/planning-and-task-breakdown/notifications-spec.md +0 -18
  163. package/evals/fixtures/security-and-hardening/webhook.js +0 -11
  164. package/evals/fixtures/security-and-hardening/webhook.test.js +0 -13
  165. package/evals/fixtures/shipping-and-launch/authority-pressure.md +0 -6
  166. package/evals/fixtures/shipping-and-launch/launch-status.md +0 -11
  167. package/evals/fixtures/source-driven-development/framework-task.md +0 -10
  168. package/evals/fixtures/spec-driven-development/billing-brief.md +0 -16
  169. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
  170. package/evals/fixtures/test-driven-development/BUG.md +0 -10
  171. package/evals/fixtures/test-driven-development/README.md +0 -28
  172. package/evals/fixtures/test-driven-development/package.json +0 -9
  173. package/evals/fixtures/test-driven-development/src/split.js +0 -8
  174. package/evals/fixtures/test-driven-development/test/split.test.js +0 -13
  175. package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
  176. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
  177. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
  178. package/evals/fixtures/using-agent-skills/incident.md +0 -6
  179. package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
  180. package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
  181. package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
  182. package/evals/plugin/code-review-fires/prompt.md +0 -31
  183. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
  184. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
  185. package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
  186. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
  187. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
  188. package/evals/skill-impact.md +0 -6
  189. package/rebrand/INSTALLATION-SPEC.md +0 -46
  190. package/rebrand/MAINTENANCE.md +0 -113
  191. package/rebrand/NOTICE.md +0 -14
  192. package/rebrand/PR-WORKFLOW.md +0 -34
  193. package/rebrand/UNIFICATION-SPEC.md +0 -33
  194. package/rebrand/VALIDATION.md +0 -32
  195. package/rebrand/VERSIONING.md +0 -33
  196. package/rebrand/apply.cjs +0 -194
  197. package/rebrand/brand.json +0 -13
  198. package/rebrand/catalog.cjs +0 -56
  199. package/rebrand/fork-patches.json +0 -12
  200. package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
  201. package/rebrand/overlay/CHANGELOG.md +0 -34
  202. package/rebrand/overlay/bin/agent-targets.json +0 -726
  203. package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
  204. package/rebrand/overlay/bin/install.cjs +0 -164
  205. package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
  206. package/rebrand/overlay/bin/wizard.cjs +0 -66
  207. package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
  208. package/rebrand/overlay/tests/bundle.test.cjs +0 -85
  209. package/rebrand/overlay/tests/installer.test.cjs +0 -193
  210. package/rebrand/package.template.json +0 -82
  211. package/rebrand/rebrand.sh +0 -4
  212. package/rebrand/record-upstream.cjs +0 -37
  213. package/rebrand/templates/README.md +0 -124
  214. package/rebrand/templates/codebuddy-setup.md +0 -88
  215. package/rebrand/templates/codex-setup.md +0 -45
  216. package/rebrand/tests/pipeline.test.cjs +0 -185
  217. package/rebrand/upstream.json +0 -171
  218. package/scripts/floor-guard-reference-test.js +0 -291
  219. package/scripts/lib/skill-lint-test.js +0 -650
  220. package/scripts/lib/skill-lint.js +0 -584
  221. package/scripts/run-evals-test.js +0 -623
  222. package/scripts/run-evals.js +0 -638
  223. package/scripts/validate-artifact-paths-test.js +0 -179
  224. package/scripts/validate-artifact-paths.js +0 -128
  225. package/scripts/validate-brand.cjs +0 -44
  226. package/scripts/validate-commands-test.js +0 -216
  227. package/scripts/validate-commands.js +0 -222
  228. package/scripts/validate-reference-links-test.js +0 -495
  229. package/scripts/validate-reference-links.js +0 -187
  230. package/scripts/validate-skills.js +0 -69
  231. package/scripts/validate-versions-test.js +0 -32
  232. package/scripts/validate-versions.js +0 -35
  233. package/tests/bundle.test.cjs +0 -85
  234. package/tests/installer.test.cjs +0 -193
  235. /package/skills/{constraint-driven-development → i3-constraint-driven-development}/references/floor-guard.md +0 -0
  236. /package/skills/{idea-refine → i3-idea-refine}/examples.md +0 -0
  237. /package/skills/{idea-refine → i3-idea-refine}/frameworks.md +0 -0
  238. /package/skills/{idea-refine → i3-idea-refine}/refinement-criteria.md +0 -0
@@ -1,584 +0,0 @@
1
- 'use strict';
2
- /**
3
- * skill-lint.js — the skill validation rules, as a shared library.
4
- *
5
- * This is the single source of truth for what makes a SKILL.md valid
6
- * (docs/skill-anatomy.md). The CLI in scripts/validate-skills.js is a thin
7
- * wrapper over it. Splitting the rules out of the CLI keeps them importable
8
- * and unit-testable without spawning a process or touching the filesystem.
9
- *
10
- * Checks (errors block CI):
11
- * - SKILL.md exists in every skill directory
12
- * - YAML frontmatter present with 'name' and 'description' fields
13
- * - frontmatter 'name' matches the directory name
14
- * - directory name is lowercase-hyphen-separated (skill-anatomy.md: Naming Conventions)
15
- * - description does not exceed 1024 characters
16
- * - description includes a 'when to use' trigger (skill-anatomy.md: Required)
17
- * - required sections are present
18
- * - a skill's subdirectories are non-empty (skill-anatomy.md: Supporting Files)
19
- * - supporting .md filenames are lowercase-hyphen-separated
20
- *
21
- * Checks (warnings, do not block CI):
22
- * - cross-skill references point to known skills
23
- * - SKILL.md stays under the 500-line context budget
24
- */
25
-
26
- const fs = require('fs');
27
- const path = require('path');
28
-
29
- // ─── Config ──────────────────────────────────────────────────────────────────
30
-
31
- const MAX_DESCRIPTION_LENGTH = 1024;
32
-
33
- // SKILL.md loads in full the moment an agent activates the skill, so its length
34
- // is paid for on every activation (docs/skill-anatomy.md -> Context Efficiency).
35
- // A warning, not an error: trimming a skill is an editorial call, and failing
36
- // CI on line count would block unrelated work on an over-budget skill.
37
- const MAX_SKILL_LINES = 500;
38
-
39
- // The only keys the Agent Skills specification allows at the top level of
40
- // SKILL.md frontmatter. Anything else — model hints, tool lists, turn limits,
41
- // host-specific switches — belongs under `metadata` or in a per-agent adapter
42
- // file (docs/advanced-per-agent-configuration.md). Hosts do not promise to
43
- // ignore unknown top-level keys, and a published skill carrying one can fail
44
- // packaging on a strict client, so this is an error rather than a warning.
45
- const SPEC_FRONTMATTER_KEYS = new Set([
46
- 'name',
47
- 'description',
48
- 'license',
49
- 'compatibility',
50
- 'metadata',
51
- 'allowed-tools',
52
- ]);
53
-
54
- // A skill directory name must be lowercase-hyphen-separated
55
- // (docs/skill-anatomy.md → Naming Conventions).
56
- const KEBAB_CASE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
57
-
58
- // A description must state WHEN to use the skill, not just what it does
59
- // (docs/skill-anatomy.md → Required). Accept the canonical "Use when …"
60
- // plus the equivalent "Use before/after/during …" phrasings in use today.
61
- // Reject negated forms ("Do not use when …", "Don't use when …") — those
62
- // describe exclusions, not trigger conditions.
63
- const DESCRIPTION_TRIGGER = /\buse (this )?when\b|\buse (before|after|during)\b/i;
64
- const DESCRIPTION_TRIGGER_NEGATE = /\b(do not|don't|never) use (this )?(when|before|after|during)\b/i;
65
- // Same pattern, global — used to strip *every* negated clause before checking
66
- // whether a positive trigger remains. Without /g only the first is removed, so
67
- // a description carrying two or more negated clauses and no positive one would
68
- // still match DESCRIPTION_TRIGGER on the leftovers and pass. Kept separate from
69
- // the non-global form above because a /g regex carries lastIndex across .test().
70
- const DESCRIPTION_TRIGGER_NEGATE_ALL = new RegExp(DESCRIPTION_TRIGGER_NEGATE.source, 'gi');
71
-
72
- // Sections every standard SKILL.md must contain.
73
- // Each entry is an array of acceptable heading strings — the first
74
- // match wins, so you can list canonical + legacy aliases.
75
- const REQUIRED_SECTIONS = [
76
- ['## Overview'],
77
- ['## When to Use'],
78
- ['## Common Rationalizations'],
79
- ['## Red Flags'],
80
- ['## Verification'],
81
- ];
82
-
83
- // Skills that are intentionally exempt from section checks.
84
- // Exemptions live HERE, not in skill frontmatter, so contributors
85
- // cannot bypass the validator by editing their own skill file.
86
- // Every entry must have a documented reason.
87
- const SECTION_EXEMPT_SKILLS = {
88
- 'using-agent-skills': 'Meta-skill — orchestrates other skills; When-to-Use and Verification are not applicable to a routing document.',
89
- 'idea-refine': 'Legacy structure predating skill-anatomy.md — uses How-It-Works/Usage/Anti-patterns instead of standard headings. Tracked for conformance in https://github.com/addyosmani/agent-skills/issues',
90
- };
91
-
92
- // Regex patterns that indicate an explicit cross-skill reference.
93
- // Only these patterns trigger the dead-reference warning — generic
94
- // backtick strings in code blocks are intentionally excluded.
95
- const SKILL_REF_PATTERNS = [
96
- /\buse the `([a-z][a-z0-9-]+[a-z0-9])` skill/g,
97
- /\bfollow the `([a-z][a-z0-9-]+[a-z0-9])` skill/g,
98
- /\binvoke the `([a-z][a-z0-9-]+[a-z0-9])` skill/g,
99
- /\bcontinue with `([a-z][a-z0-9-]+[a-z0-9])`/g,
100
- /\buse `([a-z][a-z0-9-]+[a-z0-9])` skill/g,
101
- /`([a-z][a-z0-9-]+[a-z0-9])` skill\b/g,
102
- /`([a-z][a-z0-9-]+[a-z0-9])` persona\b/g,
103
- /\bsee `([a-z][a-z0-9-]+[a-z0-9])`/g,
104
- /──→ ([a-z][a-z0-9-]+[a-z0-9])\b/g, // ASCII diagram arrows
105
- /→ `([a-z][a-z0-9-]+[a-z0-9])`/g,
106
- ];
107
-
108
- // ─── Helpers ─────────────────────────────────────────────────────────────────
109
-
110
- /**
111
- * Strip fenced code blocks from markdown content so that headings, references,
112
- * and trigger phrases inside examples or templates are not matched by lint rules.
113
- *
114
- * Scans line by line and follows the CommonMark fence rules rather than a single
115
- * whole-document regex, which only recognised a column-zero backtick fence closed
116
- * by a run of exactly the same length (#437). The forms that regex let through:
117
- *
118
- * - tilde fences (`~~~`)
119
- * - fences indented by one to three spaces (common inside list items)
120
- * - a closing fence longer than its opener
121
- *
122
- * A fence closes only on a run of the *same* marker at least as long as the
123
- * opener; a shorter run, or the other marker, is content. An unterminated fence
124
- * runs to end of file, so everything after it is treated as fenced and any
125
- * required section below it is reported missing rather than silently accepted.
126
- *
127
- * Stripped lines are replaced with empty lines so line numbers are preserved
128
- * for callers that report positions.
129
- */
130
- function stripFencedCodeBlocks(content) {
131
- const out = [];
132
- let open = null; // { marker: '`' | '~', length: number } while inside a fence
133
-
134
- for (const line of content.split(/\r?\n/)) {
135
- if (open) {
136
- const close = line.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
137
- if (close && close[1][0] === open.marker && close[1].length >= open.length) {
138
- open = null;
139
- }
140
- out.push('');
141
- continue;
142
- }
143
-
144
- const start = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
145
- // CommonMark: the info string of a backtick fence may not contain backticks,
146
- // so a line like ````js``` is inline code` is prose, not an opener.
147
- if (start && !(start[1][0] === '`' && start[2].includes('`'))) {
148
- open = { marker: start[1][0], length: start[1].length };
149
- out.push('');
150
- continue;
151
- }
152
-
153
- out.push(line);
154
- }
155
-
156
- return out.join('\n');
157
- }
158
-
159
- /**
160
- * List the top-level keys in the frontmatter block, in file order. Only
161
- * column-zero `key:` lines count: indented lines are children of the key
162
- * above them (e.g. fields under `metadata`), and `- item` lines are list
163
- * entries. Returns [] when there is no frontmatter block.
164
- */
165
- function topLevelFrontmatterKeys(content) {
166
- const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/);
167
- if (!match) return [];
168
- const keys = [];
169
- for (const line of match[1].split(/\r?\n/)) {
170
- const m = line.match(/^([A-Za-z0-9_-]+)\s*:/);
171
- if (m) keys.push(m[1]);
172
- }
173
- return keys;
174
- }
175
-
176
- /**
177
- * Parse YAML-style frontmatter from the top of a markdown file.
178
- * Returns a key→value object, or null if no frontmatter block found.
179
- * Values are stripped of surrounding quotes.
180
- */
181
- function parseFrontmatter(content) {
182
- const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/);
183
- if (!match) return null;
184
-
185
- const result = {};
186
- for (const line of match[1].split(/\r?\n/)) {
187
- const colonIdx = line.indexOf(':');
188
- if (colonIdx === -1) continue;
189
- const key = line.slice(0, colonIdx).trim();
190
- const value = line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '');
191
- if (key) result[key] = value;
192
- }
193
- return result;
194
- }
195
-
196
- /**
197
- * Report frontmatter that this file's own parser accepts but a real YAML
198
- * parser rejects.
199
- *
200
- * `parseFrontmatter` above splits each line on its first colon. That is enough
201
- * to read `name` and `description`, and it is deliberately forgiving — but it
202
- * means a SKILL.md whose frontmatter is not valid YAML passes every check here.
203
- * The hosts that consume these skills do not share that forgiveness: Cursor
204
- * parses the frontmatter as YAML when a skill is attached to a message, and a
205
- * parse failure there fails the whole request with a generic server error and
206
- * takes the rest of the chat's context with it (#494).
207
- *
208
- * So the repo could ship frontmatter that breaks a host while CI stayed green.
209
- * That was verified once by hand, against all 25 skills, in the #494 thread;
210
- * this makes it a check instead of a memory.
211
- *
212
- * Scope is deliberately narrow — the shapes a strict parser rejects and the
213
- * split-on-first-colon parser does not, each confirmed by measurement against
214
- * both PyYAML and ruby's psych rather than read off the spec:
215
- *
216
- * description: Use this: when X unquoted value with a colon-space: YAML
217
- * reads a nested mapping and errors
218
- * \tkey: value tab indentation: invalid YAML whitespace
219
- * description: "unterminated an unclosed quote
220
- * description: `skill` does X a value opening with a YAML indicator
221
- * (` @ % * ,) or a one-line | / > header
222
- *
223
- * It is not a YAML implementation. This repo has no package.json and therefore
224
- * no parser to depend on, so the check stays a small set of rules aimed at the
225
- * classes actually observed to break a host. No current frontmatter uses block
226
- * scalars or multi-line values, and the check assumes that stays true; add a
227
- * rule here rather than loosening one if it changes.
228
- */
229
- function frontmatterYamlErrors(content) {
230
- const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/);
231
- if (!match) return [];
232
-
233
- const errors = [];
234
- const lines = match[1].split(/\r?\n/);
235
- lines.forEach((line, i) => {
236
- const lineNo = i + 2; // the opening `---` is line 1
237
- if (!line.trim()) return;
238
-
239
- if (/^[ ]*\t/.test(line)) {
240
- errors.push(
241
- `Frontmatter line ${lineNo} indents with a tab — YAML forbids tabs in indentation, ` +
242
- `so a host that parses this frontmatter rejects the whole file`
243
- );
244
- return;
245
- }
246
-
247
- const colonIdx = line.indexOf(':');
248
- if (colonIdx === -1) return;
249
- const value = line.slice(colonIdx + 1).trim();
250
- if (!value) return;
251
-
252
- const quote = value[0] === '"' || value[0] === "'" ? value[0] : null;
253
- if (quote) {
254
- // An opening quote must close. `endsWith` is enough here because a
255
- // trailing comment after a quoted scalar does not appear in this repo.
256
- if (value.length < 2 || !value.endsWith(quote)) {
257
- errors.push(
258
- `Frontmatter line ${lineNo} opens a ${quote === '"' ? 'double' : 'single'} quote that never closes — ` +
259
- `a host parsing this frontmatter as YAML rejects the whole file`
260
- );
261
- }
262
- return;
263
- }
264
-
265
- // Unquoted scalar. A plain YAML scalar may not BEGIN with certain indicator
266
- // characters. The set below was measured against a real parser rather than
267
- // read off the spec, both with and without a following space, and only the
268
- // characters invalid in *both* forms with no legitimate single-line use are
269
- // rejected — a guard that flags valid frontmatter gets switched off.
270
- //
271
- // Deliberately not rejected, because each parses cleanly: `:` or `-` when NOT
272
- // followed by a space (`:platform`, `-hyphenated`), `#`, `&anchor`, and
273
- // `[`/`{`, which begin valid flow collections — `[a, b]` and `{a: b}` both
274
- // parse, so flagging the opening bracket would reject valid frontmatter.
275
- //
276
- // An earlier version of this comment claimed a leading `,`, `]` or `}` also
277
- // parsed cleanly. Measured against PyYAML and psych, all three are rejected;
278
- // `,` is flagged below. `]` and `}` are left out only because no frontmatter
279
- // has ever started with them — say so and they can be added.
280
- //
281
- // The backtick is the one that bites this repo: descriptions routinely name
282
- // other skills, and "`other-skill` designs things" is a natural way to start.
283
- if (/^[`@%*,]/.test(value)) {
284
- errors.push(
285
- `Frontmatter line ${lineNo} starts an unquoted value with '${value[0]}' — YAML reserves ` +
286
- `that character and cannot begin a plain scalar with it, so a host parsing this ` +
287
- `frontmatter rejects the whole file; wrap the value in quotes`
288
- );
289
- return;
290
- }
291
- // `|` and `>` open a block scalar only when nothing follows them on the line.
292
- // `|` with indented lines under it is valid and stays allowed; `|foo` is not
293
- // a block scalar, it is a plain scalar starting with an indicator, and both
294
- // parsers reject it.
295
- if (/^[|>]\S/.test(value)) {
296
- errors.push(
297
- `Frontmatter line ${lineNo} starts an unquoted value with '${value[0]}' followed by text — ` +
298
- `YAML reads that as a block scalar header, which may not carry content on its own line, ` +
299
- `so a host parsing this frontmatter rejects the whole file; wrap the value in quotes`
300
- );
301
- return;
302
- }
303
- if (/^[-?&](\s|$)/.test(value)) {
304
- errors.push(
305
- `Frontmatter line ${lineNo} starts an unquoted value with '${value[0]} ' — YAML reads ` +
306
- `that as an indicator rather than text and rejects the file; wrap the value in quotes`
307
- );
308
- return;
309
- }
310
-
311
- // A colon followed by a space (or ending the line) makes YAML read a nested
312
- // mapping where a plain string was meant.
313
- if (/:(\s|$)/.test(value)) {
314
- errors.push(
315
- `Frontmatter line ${lineNo} has an unquoted value containing ': ' — YAML reads that as a ` +
316
- `nested mapping and rejects the file; wrap the value in quotes`
317
- );
318
- }
319
- });
320
-
321
- return errors;
322
- }
323
-
324
- /**
325
- * Collect all explicit skill cross-references from content.
326
- * Only matches against the SKILL_REF_PATTERNS list to avoid
327
- * false-positives from inline code snippets.
328
- */
329
- function extractSkillReferences(content) {
330
- const refs = new Set();
331
- for (const pattern of SKILL_REF_PATTERNS) {
332
- // Reset lastIndex for global regexes
333
- pattern.lastIndex = 0;
334
- let m;
335
- while ((m = pattern.exec(content)) !== null) {
336
- refs.add(m[1]);
337
- }
338
- }
339
- return refs;
340
- }
341
-
342
- // ─── Linter ──────────────────────────────────────────────────────────────────
343
-
344
- /**
345
- * Lint already-read SKILL.md content. Pure: no filesystem access, so the rules
346
- * can be exercised against crafted fixtures in a unit test.
347
- * Returns { errors, warnings, exempt }.
348
- */
349
- function lintSkillContent(dirName, content, knownSkills) {
350
- const errors = [];
351
- const warnings = [];
352
- let exempt = false;
353
-
354
- // ── Frontmatter ──────────────────────────────────────────────────────────
355
- const fm = parseFrontmatter(content);
356
- if (!fm) {
357
- errors.push('Missing or malformed YAML frontmatter (expected --- block at top of file)');
358
- return { errors, warnings, exempt };
359
- }
360
-
361
- // The parser above is forgiving by design; the hosts that read this
362
- // frontmatter are not (#494).
363
- errors.push(...frontmatterYamlErrors(content));
364
-
365
- // Only the specification's keys may sit at the top level. Vendor and
366
- // runtime fields go under `metadata` or in a per-agent adapter file.
367
- for (const key of topLevelFrontmatterKeys(content)) {
368
- if (!SPEC_FRONTMATTER_KEYS.has(key)) {
369
- errors.push(
370
- `Frontmatter key '${key}' is not an Agent Skills spec field — top level allows only ` +
371
- `${[...SPEC_FRONTMATTER_KEYS].join(', ')}; put vendor or runtime fields under 'metadata' ` +
372
- `or in a per-agent adapter file (docs/advanced-per-agent-configuration.md)`
373
- );
374
- }
375
- }
376
-
377
- if (!fm.name) {
378
- errors.push("Frontmatter missing required field: 'name'");
379
- } else if (fm.name !== dirName) {
380
- errors.push(`Frontmatter name '${fm.name}' does not match directory name '${dirName}'`);
381
- }
382
-
383
- if (!KEBAB_CASE.test(dirName)) {
384
- errors.push(`Directory name '${dirName}' is not lowercase-hyphen-separated (skill-anatomy.md: Naming Conventions)`);
385
- }
386
-
387
- if (!fm.description) {
388
- errors.push("Frontmatter missing required field: 'description'");
389
- } else {
390
- if (fm.description.length > MAX_DESCRIPTION_LENGTH) {
391
- errors.push(
392
- `Description is ${fm.description.length} chars — exceeds the ${MAX_DESCRIPTION_LENGTH}-char limit` +
393
- ` (agents inject this into the system prompt)`
394
- );
395
- }
396
- const hasTrigger = DESCRIPTION_TRIGGER.test(fm.description);
397
- const onlyNegated = hasTrigger && DESCRIPTION_TRIGGER_NEGATE.test(fm.description)
398
- && !fm.description.replace(DESCRIPTION_TRIGGER_NEGATE_ALL, '').match(DESCRIPTION_TRIGGER);
399
- if (!hasTrigger || onlyNegated) {
400
- errors.push(
401
- `Description has no 'when to use' trigger — add a "Use when …" clause ` +
402
- `(skill-anatomy.md: Required — the description must say both what the skill does and when to use it)`
403
- );
404
- }
405
- }
406
-
407
- // ── Exemption guard ──────────────────────────────────────────────────────
408
- // Exemptions are validator-owned (SECTION_EXEMPT_SKILLS above).
409
- // If a skill's frontmatter tries to declare its own exemption, fail loud —
410
- // that's a sign someone is trying to bypass the validator.
411
- if (fm.type === 'meta' || fm.exempt === 'sections') {
412
- if (!Object.hasOwn(SECTION_EXEMPT_SKILLS, dirName)) {
413
- errors.push(
414
- `Frontmatter declares 'type: meta' or 'exempt: sections' but '${dirName}' is not in ` +
415
- `the validator's SECTION_EXEMPT_SKILLS allowlist. ` +
416
- `Add an entry to scripts/lib/skill-lint.js with a documented reason.`
417
- );
418
- }
419
- }
420
-
421
- // ── Required sections ────────────────────────────────────────────────────
422
- // `Object.hasOwn`, not `in`: `in` walks the prototype chain, so a skill
423
- // directory named `constructor` — which passes the kebab-case check — would
424
- // otherwise resolve to Object.prototype.constructor and be silently exempt
425
- // from every required-section check.
426
- exempt = Object.hasOwn(SECTION_EXEMPT_SKILLS, dirName);
427
-
428
- if (!exempt) {
429
- // Strip fenced code blocks so headings inside examples/templates don't
430
- // satisfy the check, and match headings at the start of a line so
431
- // `### Verification` inside a block doesn't satisfy `## Verification`.
432
- const proseContent = stripFencedCodeBlocks(content);
433
- for (const aliases of REQUIRED_SECTIONS) {
434
- const found = aliases.some(heading => {
435
- const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
436
- return new RegExp(`^${escaped}\\s*$`, 'm').test(proseContent);
437
- });
438
- if (!found) {
439
- errors.push(`Missing required section: ${aliases[0]}`);
440
- }
441
- }
442
- }
443
-
444
- // A named workflow that advertises numbered steps must document each step
445
- // before the next level-two section. Otherwise the summary promises a
446
- // process stage that the skill never teaches agents how to perform.
447
- const workflowSections = content.matchAll(
448
- /^## The [^\n]+ Workflow\s*\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/gm
449
- );
450
- for (const match of workflowSections) {
451
- const section = match[1];
452
- const declared = [...section.matchAll(/^\s*(\d+)\.\s+[A-Z][A-Z -]*\s+→/gm)];
453
- if (declared.length < 2) continue;
454
-
455
- const documented = new Set(
456
- [...section.matchAll(/^### Step\s+(\d+):/gm)].map(step => step[1])
457
- );
458
- for (const step of declared) {
459
- if (!documented.has(step[1])) {
460
- errors.push(`Workflow declares Step ${step[1]} but has no matching process section`);
461
- }
462
- }
463
- }
464
-
465
- // ── Context budget ───────────────────────────────────────────────────────
466
- // Counted as newlines so the number matches what `wc -l` reports, which is
467
- // how a contributor would check this by hand.
468
- const lineCount = (content.match(/\n/g) || []).length;
469
- if (lineCount > MAX_SKILL_LINES) {
470
- warnings.push(
471
- `SKILL.md is ${lineCount} lines, over the ${MAX_SKILL_LINES}-line context budget ` +
472
- `(skill-anatomy.md: Context Efficiency) — move detail into a supporting file`,
473
- );
474
- }
475
-
476
- // ── Cross-skill references ───────────────────────────────────────────────
477
- const refs = extractSkillReferences(content);
478
- for (const ref of refs) {
479
- if (!knownSkills.has(ref)) {
480
- warnings.push(`Dead cross-reference: \`${ref}\` is not a known skill`);
481
- }
482
- }
483
-
484
- return { errors, warnings, exempt };
485
- }
486
-
487
- /** True when dir contains no regular file at any depth. */
488
- function isEffectivelyEmpty(dir) {
489
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
490
- if (entry.isDirectory()) {
491
- if (!isEffectivelyEmpty(path.join(dir, entry.name))) return false;
492
- } else {
493
- return false;
494
- }
495
- }
496
- return true;
497
- }
498
-
499
- /**
500
- * Lint the files that travel with a skill, as opposed to the contents of
501
- * SKILL.md. These rules need the filesystem, so they live here rather than in
502
- * the pure content linter. Returns an array of error strings.
503
- *
504
- * Enforced (docs/skill-anatomy.md):
505
- * - Supporting Files: "do not create an empty `scripts/` directory just to
506
- * mirror other skills". Generalized to any subdirectory — an empty
507
- * `references/` is noise for the same reason.
508
- * - Naming Conventions: "Supporting files: lowercase-hyphen-separated.md".
509
- * Scoped to .md, which is what that line covers; `scripts/` helpers carry
510
- * their own conventions under Script Requirements.
511
- */
512
- function lintSkillLayout(skillDir) {
513
- const errors = [];
514
-
515
- const walk = (dir, relBase) => {
516
- for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
517
- const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
518
- const abs = path.join(dir, entry.name);
519
-
520
- if (entry.isDirectory()) {
521
- if (isEffectivelyEmpty(abs)) {
522
- errors.push(
523
- `Empty directory \`${rel}/\` — remove it rather than mirroring another skill's layout ` +
524
- `(skill-anatomy.md: Supporting Files)`,
525
- );
526
- } else {
527
- walk(abs, rel);
528
- }
529
- continue;
530
- }
531
-
532
- if (rel === 'SKILL.md' || !entry.name.endsWith('.md')) continue;
533
- if (!KEBAB_CASE.test(entry.name.slice(0, -'.md'.length))) {
534
- errors.push(
535
- `Supporting file \`${rel}\` is not lowercase-hyphen-separated ` +
536
- `(skill-anatomy.md: Naming Conventions)`,
537
- );
538
- }
539
- }
540
- };
541
-
542
- walk(skillDir, '');
543
- return errors;
544
- }
545
-
546
- /**
547
- * Lint a skill by directory name: reads its SKILL.md, then delegates to
548
- * lintSkillContent. This is the thin filesystem wrapper the CLI uses.
549
- * Returns { errors, warnings, exempt }.
550
- */
551
- function lintSkill(dirName, skillsDir, knownSkills) {
552
- const skillDir = path.join(skillsDir, dirName);
553
- const skillPath = path.join(skillDir, 'SKILL.md');
554
-
555
- if (!fs.existsSync(skillPath)) {
556
- return { errors: ['Missing SKILL.md'], warnings: [], exempt: false };
557
- }
558
-
559
- let content;
560
- try {
561
- content = fs.readFileSync(skillPath, 'utf8');
562
- } catch (err) {
563
- return { errors: [`Unreadable SKILL.md: ${err.message}`], warnings: [], exempt: false };
564
- }
565
-
566
- const result = lintSkillContent(dirName, content, knownSkills);
567
- result.errors.push(...lintSkillLayout(skillDir));
568
- return result;
569
- }
570
-
571
- // Export only functions. The policy collections (REQUIRED_SECTIONS,
572
- // SECTION_EXEMPT_SKILLS, SKILL_REF_PATTERNS, and the regexes) stay private so a
573
- // test or future consumer cannot mutate shared state and change lint results for
574
- // the rest of the process. Exercise the rules through these functions.
575
- module.exports = {
576
- stripFencedCodeBlocks,
577
- parseFrontmatter,
578
- topLevelFrontmatterKeys,
579
- frontmatterYamlErrors,
580
- extractSkillReferences,
581
- lintSkillContent,
582
- lintSkillLayout,
583
- lintSkill,
584
- };