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