claude-skill-lint 0.3.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.
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/cli.js +10 -0
- package/dist/changed-files.d.ts +20 -0
- package/dist/changed-files.d.ts.map +1 -0
- package/dist/changed-files.js +49 -0
- package/dist/changed-files.js.map +1 -0
- package/dist/classify.d.ts +13 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +72 -0
- package/dist/classify.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +124 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/detect-format.d.ts +18 -0
- package/dist/detect-format.d.ts.map +1 -0
- package/dist/detect-format.js +137 -0
- package/dist/detect-format.js.map +1 -0
- package/dist/extract.d.ts +19 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +197 -0
- package/dist/extract.js.map +1 -0
- package/dist/graph.d.ts +21 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +94 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +45 -0
- package/dist/init.js.map +1 -0
- package/dist/lint.d.ts +16 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +152 -0
- package/dist/lint.js.map +1 -0
- package/dist/profiles.d.ts +37 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +161 -0
- package/dist/profiles.js.map +1 -0
- package/dist/reporter.d.ts +21 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +110 -0
- package/dist/reporter.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-frontmatter.d.ts +41 -0
- package/dist/validate-frontmatter.d.ts.map +1 -0
- package/dist/validate-frontmatter.js +409 -0
- package/dist/validate-frontmatter.js.map +1 -0
- package/dist/validate-graph.d.ts +46 -0
- package/dist/validate-graph.d.ts.map +1 -0
- package/dist/validate-graph.js +651 -0
- package/dist/validate-graph.js.map +1 -0
- package/dist/validate-manifest.d.ts +22 -0
- package/dist/validate-manifest.d.ts.map +1 -0
- package/dist/validate-manifest.js +365 -0
- package/dist/validate-manifest.js.map +1 -0
- package/package.json +64 -0
- package/schemas/agent.schema.json +58 -0
- package/schemas/command.schema.json +60 -0
- package/schemas/skill.schema.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robin Cannon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# claude-skill-lint
|
|
2
|
+
|
|
3
|
+
Structural validation and token-efficiency tooling for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills. Catches the bugs that Claude tolerates but your users pay for.
|
|
4
|
+
|
|
5
|
+
## The Problem No One Sees
|
|
6
|
+
|
|
7
|
+
Claude Code skills load into the context window on every invocation. When a skill references a context file that doesn't exist, Claude doesn't throw an error. It hallucinates the missing context and keeps going. When two skills resolve to the same install path, one silently overwrites the other. When a context file is loaded but nothing references it, you're burning tokens for nothing.
|
|
8
|
+
|
|
9
|
+
These aren't hypothetical failure modes. They're what we found running claude-skill-lint against a real 139-file production suite:
|
|
10
|
+
|
|
11
|
+
- **32 broken references.** 23 skills referenced a context file that's generated at setup time — in the shareable repo, before setup runs, those skills have no context at all. 9 more referenced a file that had been renamed months earlier. Every one of those skills was silently degraded.
|
|
12
|
+
- **11 orphaned files.** Context and agent files loaded into the window but referenced by zero skills. Dead weight on every invocation.
|
|
13
|
+
- **1 dependency cycle.** Two context files referencing each other, pulling both into the window when only one was needed.
|
|
14
|
+
|
|
15
|
+
Caught in under two seconds. No LLM calls. Deterministic.
|
|
16
|
+
|
|
17
|
+
"Tolerate" is not "work correctly."
|
|
18
|
+
|
|
19
|
+
## Two Layers
|
|
20
|
+
|
|
21
|
+
| | `claude-skill-lint` | `/te-review` |
|
|
22
|
+
|-|-------------|-------------|
|
|
23
|
+
| **What** | Structural validation | Token efficiency audit |
|
|
24
|
+
| **Speed** | <2s, every PR | ~60s, on demand |
|
|
25
|
+
| **Approach** | Deterministic CI — no LLM | LLM-powered deep analysis |
|
|
26
|
+
| **Catches** | Broken refs, orphans, cycles, collisions, parse errors, size violations, quality regressions | Redundant content, output format waste, instruction bloat, model routing, architecture-level optimization |
|
|
27
|
+
|
|
28
|
+
claude-skill-lint enforces the structural foundation. `/te-review` (included in [`skills/te-review.md`](skills/te-review.md)) goes deeper — four-pass analysis across architecture, efficiency, and instruction quality, producing a scored assessment (0-24) with a prioritized optimization plan and estimated token savings per fix.
|
|
29
|
+
|
|
30
|
+
We ran te-review against itself. It scored 20/24. The main finding: the skill didn't constrain its own output the way it tells others to. After adding "top 5 findings per category" and "each subagent returns top 10 findings only," estimated output token savings on large suite reviews dropped by ~50%. The tool practices what it preaches.
|
|
31
|
+
|
|
32
|
+
Install the deep audit skill:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cp node_modules/claude-skill-lint/skills/te-review.md ~/.claude/commands/te-review.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then in any Claude Code session:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/te-review suite # Full suite audit (scored 0-24)
|
|
42
|
+
/te-review audit my-skill # Single skill deep dive
|
|
43
|
+
/te-review compare old.md new.md # Before/after token impact
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### What te-review checks
|
|
47
|
+
|
|
48
|
+
Four sequential passes, each producing up to 5 findings per severity level:
|
|
49
|
+
|
|
50
|
+
1. **Structural Audit** — CLAUDE.md bloat, unused tool declarations, reference depth, subagent model routing, file size
|
|
51
|
+
2. **Redundancy Detection** — skill-context overlap, cross-skill duplication, CLAUDE.md-skill overlap. High-fanout context files are flagged as highest-ROI optimization targets.
|
|
52
|
+
3. **Output Efficiency** — missing format constraints, missing conciseness directives, unbounded output sections, prose where structured would suffice. Output tokens cost 5x input — this pass often finds the biggest savings.
|
|
53
|
+
4. **Instruction Quality** — motivational fluff, politeness tokens, filler phrases, default-behavior instructions, conflicting constraints, emphasis overuse
|
|
54
|
+
|
|
55
|
+
## What It Actually Found
|
|
56
|
+
|
|
57
|
+
### Against a 139-file shared skill suite
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
$ claude-skill-lint graph .
|
|
61
|
+
✖ 43 errors and 11 warnings in 54 files (139 files checked)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The headline: `inv-output-conventions.md` was renamed to `inv-output-patterns.md` at some point. Nine invention skills still referenced the old name. They silently got no output formatting guidance. A rename bug, invisible at authoring time, caught in seconds.
|
|
65
|
+
|
|
66
|
+
### Against Anthropic's official skills repo (plugin format)
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
$ claude-skill-lint graph ~/Development/anthropic-skills/
|
|
70
|
+
✖ 19 errors and 10 warnings in 26 files (63 files checked)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Name-collision findings in the `claude-api` skill — five language-specific `claude-api.md` files (PHP, Java, Ruby, Go, C#) all resolve to the same canonical name. Broken references to `shared/tool-use-concepts.md` and `shared/live-sources.md` — files that don't exist. Orphaned theme files in `theme-factory` (loaded dynamically, not via static references). Legitimate structural observations, not false positives.
|
|
74
|
+
|
|
75
|
+
### Against a multi-plugin production repo
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
$ claude-skill-lint lint ~/Development/work/ai-plugins/
|
|
79
|
+
✖ 3 warnings in 3 files (45 files checked)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Three unlisted plugins (valid `plugin.json` but not declared in the root `marketplace.json`). Graph validation: clean — zero errors across 45 files. The multi-plugin format's relative path resolution (`../../context/foo.md`) works correctly.
|
|
83
|
+
|
|
84
|
+
### Frontmatter lint: honest assessment
|
|
85
|
+
|
|
86
|
+
The lint pass found 34 type errors across the same 139-file suite — `argument-hint` values parsed as YAML arrays instead of strings, `tools` fields as comma-separated strings instead of arrays.
|
|
87
|
+
|
|
88
|
+
Claude Code tolerates all of them. Skills work fine. The linter is being opinionated about structure, enforcing that frontmatter conforms to a schema. That matters when you're sharing skills, publishing to a marketplace, or building tooling that expects consistent types. For personal skills that just work, graph validation is where the real value lives.
|
|
89
|
+
|
|
90
|
+
The 4 YAML parse errors, on the other hand, are genuinely broken. The frontmatter can't be read at all.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install -g claude-skill-lint
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or directly:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx claude-skill-lint lint .
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Requires Node.js 20+.
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
claude-skill-lint init # Auto-detect format, generate config
|
|
110
|
+
claude-skill-lint graph . # Cross-file references — the high-value bugs
|
|
111
|
+
claude-skill-lint lint . # Frontmatter structure
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## What It Checks
|
|
115
|
+
|
|
116
|
+
### Graph Validation
|
|
117
|
+
|
|
118
|
+
Builds a dependency graph from cross-file references. Finds:
|
|
119
|
+
|
|
120
|
+
- **Broken references** — skill says "read context/foo.md," file doesn't exist. Claude hallucinates the gap.
|
|
121
|
+
- **Orphaned files** — context or agent files that nothing references. Tokens loaded for nothing.
|
|
122
|
+
- **Name collisions** — two files resolve to the same canonical name. One overwrites the other on install.
|
|
123
|
+
- **Dependency cycles** — circular references between files. Context pollution.
|
|
124
|
+
|
|
125
|
+
Resolves both installed paths (`~/.claude/commands/context/foo.md`) and relative paths (`../../context/foo.md`, `./reference/guide.md`, `agents/scanner.md`), with automatic fallback between resolution strategies.
|
|
126
|
+
|
|
127
|
+
### Frontmatter Validation
|
|
128
|
+
|
|
129
|
+
Validates YAML frontmatter against file-type schemas:
|
|
130
|
+
|
|
131
|
+
| File Type | Required Fields | Optional Fields |
|
|
132
|
+
|-----------|----------------|-----------------|
|
|
133
|
+
| Command | `description` | `model`, `allowed-tools`, `argument-hint`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
|
|
134
|
+
| Agent | `name`, `description` | `model`, `tools`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
|
|
135
|
+
| Skill (plugin) | `name`, `description` | `invocable`, `argument-hint`, `user-invocable`, `allowed-tools`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
|
|
136
|
+
| Context | *(none)* | — |
|
|
137
|
+
|
|
138
|
+
#### Modern Frontmatter Fields
|
|
139
|
+
|
|
140
|
+
These fields are supported across all file types (command, agent, skill):
|
|
141
|
+
|
|
142
|
+
| Field | Type | Description |
|
|
143
|
+
|-------|------|-------------|
|
|
144
|
+
| `context` | `string` | Execution context for the skill (e.g. `fork` to run in a separate process) |
|
|
145
|
+
| `agent` | `string` | Agent mode or name to delegate execution to |
|
|
146
|
+
| `effort` | `string` | Reasoning effort level — controls how much thinking the model applies |
|
|
147
|
+
| `hooks` | `object` | Lifecycle hooks triggered before/after skill execution |
|
|
148
|
+
| `compatibility` | `string` | Compatibility requirements or version constraints |
|
|
149
|
+
| `metadata` | `object` | Arbitrary key-value metadata for tooling and marketplace use |
|
|
150
|
+
| `allowed-tools` | `array` or `string` | Tools the skill can use. Supports glob patterns like `mcp__*` and `Bash(*)` for broad matching, or specific tool names for fine-grained control |
|
|
151
|
+
|
|
152
|
+
At Level 1: model enum validation, known tool verification (including `Bash(python*)` pattern syntax), tool-to-body consistency, file size limits, `effort` value validation, skill name format.
|
|
153
|
+
|
|
154
|
+
### Manifest Validation (plugin format)
|
|
155
|
+
|
|
156
|
+
Validates `marketplace.json` and `plugin.json` structure, source path resolution, name consistency, and missing skill files.
|
|
157
|
+
|
|
158
|
+
## Progressive Quality Levels
|
|
159
|
+
|
|
160
|
+
Skills mature. The quality bar should mature with them.
|
|
161
|
+
|
|
162
|
+
| Level | What It Adds | When |
|
|
163
|
+
|-------|-------------|------|
|
|
164
|
+
| **0** | Valid YAML, required fields, non-empty body | New skills, prototyping |
|
|
165
|
+
| **1** | Model enum, known tools, tool-in-body check, file size limits | Established skills, shared suites |
|
|
166
|
+
|
|
167
|
+
Declare per file:
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
---
|
|
171
|
+
name: my-skill
|
|
172
|
+
description: Does something useful
|
|
173
|
+
quality_level: 1
|
|
174
|
+
---
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Or set directory defaults in `.skill-lint.yaml`:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
default_level: 0
|
|
181
|
+
levels:
|
|
182
|
+
commands/: 1
|
|
183
|
+
agents/: 1
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Effective level: `max(file declaration, directory default, --level flag)`. The highest value wins. You can raise the floor but never lower a file's declared level.
|
|
187
|
+
|
|
188
|
+
### Ratchet
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
claude-skill-lint lint . --ratchet --base origin/main
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Compares each file's `quality_level` against the base branch. If any level decreased, the build fails. Quality improvements become permanent. That's the point.
|
|
195
|
+
|
|
196
|
+
## Repository Formats
|
|
197
|
+
|
|
198
|
+
claude-skill-lint auto-detects your repository structure. Four formats are supported:
|
|
199
|
+
|
|
200
|
+
| Format | Structure | Detection Signal |
|
|
201
|
+
|--------|-----------|-----------------|
|
|
202
|
+
| **legacy-commands** | `commands/`, `agents/`, `context/` at repo root | No `.claude-plugin/` directory |
|
|
203
|
+
| **project-skills** | `.claude/skills/{name}/SKILL.md` | `.claude/skills/` with `SKILL.md` files |
|
|
204
|
+
| **plugin** | `skills/{name}/SKILL.md` with marketplace manifest | `.claude-plugin/marketplace.json` at root |
|
|
205
|
+
| **multi-plugin** | `plugins/{name}/skills/{skill}/SKILL.md` | Plugin subdirectories with `.claude-plugin/plugin.json` |
|
|
206
|
+
|
|
207
|
+
Detection priority: config override > multi-plugin > plugin > project-skills > legacy-commands. The first match wins.
|
|
208
|
+
|
|
209
|
+
### project-skills: The `.claude/skills/` Format
|
|
210
|
+
|
|
211
|
+
The `project-skills` format uses `.claude/skills/{name}/SKILL.md` — the same structure Claude Code uses for project-scoped skills. Each skill lives in its own directory under `.claude/skills/`.
|
|
212
|
+
|
|
213
|
+
claude-skill-lint discovers skills in nested `.claude/skills/` directories automatically. In monorepo setups where multiple packages each have their own `.claude/skills/` directory, point the linter at the repo root and it finds them all.
|
|
214
|
+
|
|
215
|
+
Hybrid repos work too. A repo with both `.claude/skills/` and legacy `commands/` directories — or a published plugin that also has project-level skills — gets everything linted in a single run. No configuration needed.
|
|
216
|
+
|
|
217
|
+
### Migration Note
|
|
218
|
+
|
|
219
|
+
For repos transitioning from legacy commands to modern skills, claude-skill-lint validates both locations in a single run. Set the format explicitly in `.skill-lint.yaml` if auto-detection picks the wrong one, or omit it and let detection handle the transition — legacy-commands is the fallback when no modern format signals are found.
|
|
220
|
+
|
|
221
|
+
## Custom Structures
|
|
222
|
+
|
|
223
|
+
Not every repo follows a standard layout. claude-skill-lint provides three configuration levers for non-standard structures:
|
|
224
|
+
|
|
225
|
+
### `skills_root`
|
|
226
|
+
|
|
227
|
+
If your skill files live in a subdirectory rather than the repo root:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
skills_root: "packages/my-plugin"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
All path resolution starts from this root. Useful for monorepos where skills are nested deep.
|
|
234
|
+
|
|
235
|
+
### `format` Override
|
|
236
|
+
|
|
237
|
+
Auto-detection works for standard layouts. When it doesn't — or when your repo is mid-migration between formats — set the format explicitly:
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
format: plugin # Force plugin format detection
|
|
241
|
+
# format: legacy-commands | plugin | multi-plugin | project-skills
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `ignore` Patterns
|
|
245
|
+
|
|
246
|
+
Exclude paths that look like skills but aren't:
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
ignore:
|
|
250
|
+
- "**/README.md"
|
|
251
|
+
- "**/CLAUDE.md"
|
|
252
|
+
- "node_modules/**"
|
|
253
|
+
- "docs/**/*.md"
|
|
254
|
+
- "archive/**"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Glob patterns, matched against file paths relative to `skills_root`. `node_modules/` is always excluded.
|
|
258
|
+
|
|
259
|
+
## Commands
|
|
260
|
+
|
|
261
|
+
### `claude-skill-lint graph [paths...]`
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
claude-skill-lint graph . # Full graph analysis
|
|
265
|
+
claude-skill-lint graph . --format json # JSON output
|
|
266
|
+
claude-skill-lint graph . --format github # GitHub annotations
|
|
267
|
+
claude-skill-lint graph . --strict # Orphan warnings become errors
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `claude-skill-lint lint [paths...]`
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
claude-skill-lint lint . # Lint everything
|
|
274
|
+
claude-skill-lint lint . --level 1 # Enforce Level 1
|
|
275
|
+
claude-skill-lint lint . --strict # Warnings become errors
|
|
276
|
+
claude-skill-lint lint . --ratchet # Prevent quality regression
|
|
277
|
+
claude-skill-lint lint . --changed-only --base origin/main # Only changed files
|
|
278
|
+
claude-skill-lint lint . --format json # JSON for tooling
|
|
279
|
+
claude-skill-lint lint . --format github # GitHub Actions annotations
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### `claude-skill-lint init`
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
claude-skill-lint init # Auto-detect format, generate config
|
|
286
|
+
claude-skill-lint init --force # Overwrite existing
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Exit codes: `0` clean, `1` errors found, `2` config error.
|
|
290
|
+
|
|
291
|
+
## Options
|
|
292
|
+
|
|
293
|
+
| Option | Default | Description |
|
|
294
|
+
|--------|---------|-------------|
|
|
295
|
+
| `--level` / `-l` | `0` | Minimum quality level (0-3) |
|
|
296
|
+
| `--changed-only` | `false` | Only check files changed since base ref |
|
|
297
|
+
| `--base` | `origin/main` | Git ref for `--changed-only` and `--ratchet` |
|
|
298
|
+
| `--format` / `-f` | `terminal` | Output: `terminal`, `json`, `github` |
|
|
299
|
+
| `--strict` | `false` | Treat warnings as errors |
|
|
300
|
+
| `--ratchet` | `false` | Fail if any quality_level decreased vs base |
|
|
301
|
+
|
|
302
|
+
## Configuration
|
|
303
|
+
|
|
304
|
+
`claude-skill-lint init` generates this. Or create `.skill-lint.yaml` manually:
|
|
305
|
+
|
|
306
|
+
```yaml
|
|
307
|
+
skills_root: "."
|
|
308
|
+
default_level: 0
|
|
309
|
+
|
|
310
|
+
levels:
|
|
311
|
+
commands/: 1
|
|
312
|
+
agents/: 1
|
|
313
|
+
|
|
314
|
+
# Auto-detected if omitted
|
|
315
|
+
# format: legacy-commands | plugin | multi-plugin | project-skills
|
|
316
|
+
|
|
317
|
+
models: [opus, sonnet, haiku]
|
|
318
|
+
|
|
319
|
+
tools:
|
|
320
|
+
mcp_pattern: "mcp__*"
|
|
321
|
+
custom: []
|
|
322
|
+
|
|
323
|
+
limits:
|
|
324
|
+
max_file_size: 15360
|
|
325
|
+
|
|
326
|
+
ignore:
|
|
327
|
+
- "**/README.md"
|
|
328
|
+
- "**/CLAUDE.md"
|
|
329
|
+
- "node_modules/**"
|
|
330
|
+
|
|
331
|
+
graph:
|
|
332
|
+
warn_orphans: true
|
|
333
|
+
detect_cycles: true
|
|
334
|
+
detect_duplicates: true
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## CI Integration
|
|
338
|
+
|
|
339
|
+
### GitHub Actions
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
name: Skill Lint
|
|
343
|
+
on:
|
|
344
|
+
pull_request:
|
|
345
|
+
paths: ['**/*.md', '.skill-lint.yaml']
|
|
346
|
+
|
|
347
|
+
jobs:
|
|
348
|
+
lint:
|
|
349
|
+
runs-on: ubuntu-latest
|
|
350
|
+
steps:
|
|
351
|
+
- uses: actions/checkout@v4
|
|
352
|
+
with:
|
|
353
|
+
fetch-depth: 0
|
|
354
|
+
- uses: actions/setup-node@v4
|
|
355
|
+
with:
|
|
356
|
+
node-version: '20'
|
|
357
|
+
- run: npm install -g claude-skill-lint
|
|
358
|
+
|
|
359
|
+
- name: Graph validation
|
|
360
|
+
run: claude-skill-lint graph . --format github
|
|
361
|
+
|
|
362
|
+
- name: Lint changed skills
|
|
363
|
+
run: claude-skill-lint lint . --format github --changed-only --base origin/${{ github.base_ref }}
|
|
364
|
+
|
|
365
|
+
- name: Quality ratchet
|
|
366
|
+
run: claude-skill-lint lint . --ratchet --base origin/${{ github.base_ref }} --format github
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`--format github` produces annotations that appear inline on PR diffs.
|
|
370
|
+
|
|
371
|
+
### Pre-commit Hook
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
#!/bin/sh
|
|
375
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM -- '*.md')
|
|
376
|
+
if [ -n "$STAGED" ]; then
|
|
377
|
+
npx claude-skill-lint lint $STAGED --level 1
|
|
378
|
+
fi
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Deprecation notice when invoked as the old binary name
|
|
5
|
+
const binName = basename(process.argv[1] || '');
|
|
6
|
+
if (binName === 'skill-lint') {
|
|
7
|
+
process.stderr.write('Note: skill-lint is deprecated. Use claude-skill-lint instead.\n');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
import '../dist/cli.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-aware file filtering for --changed-only mode.
|
|
3
|
+
* Returns absolute paths to .md files changed since a given base ref.
|
|
4
|
+
*/
|
|
5
|
+
/** Error thrown when git operations fail (non-git dir, bad ref, etc.). */
|
|
6
|
+
export declare class ChangedFilesError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get the list of changed .md files between `base` and HEAD.
|
|
11
|
+
*
|
|
12
|
+
* Runs `git diff --name-only --diff-filter=ACM {base}...HEAD -- '*.md'`
|
|
13
|
+
* and resolves repo-relative paths to absolute paths.
|
|
14
|
+
*
|
|
15
|
+
* @param base - Git ref to diff against (e.g. "origin/main", "main")
|
|
16
|
+
* @returns Array of absolute file paths to changed .md files
|
|
17
|
+
* @throws ChangedFilesError if git commands fail
|
|
18
|
+
*/
|
|
19
|
+
export declare function getChangedFiles(base: string): string[];
|
|
20
|
+
//# sourceMappingURL=changed-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changed-files.d.ts","sourceRoot":"","sources":["../src/changed-files.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiCtD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-aware file filtering for --changed-only mode.
|
|
3
|
+
* Returns absolute paths to .md files changed since a given base ref.
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
/** Error thrown when git operations fail (non-git dir, bad ref, etc.). */
|
|
8
|
+
export class ChangedFilesError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ChangedFilesError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the list of changed .md files between `base` and HEAD.
|
|
16
|
+
*
|
|
17
|
+
* Runs `git diff --name-only --diff-filter=ACM {base}...HEAD -- '*.md'`
|
|
18
|
+
* and resolves repo-relative paths to absolute paths.
|
|
19
|
+
*
|
|
20
|
+
* @param base - Git ref to diff against (e.g. "origin/main", "main")
|
|
21
|
+
* @returns Array of absolute file paths to changed .md files
|
|
22
|
+
* @throws ChangedFilesError if git commands fail
|
|
23
|
+
*/
|
|
24
|
+
export function getChangedFiles(base) {
|
|
25
|
+
let repoRoot;
|
|
26
|
+
try {
|
|
27
|
+
repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new ChangedFilesError('Not a git repository (or git is not installed)');
|
|
33
|
+
}
|
|
34
|
+
let diffOutput;
|
|
35
|
+
try {
|
|
36
|
+
diffOutput = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACM', `${base}...HEAD`, '--', '*.md'], { encoding: 'utf-8', cwd: repoRoot }).trim();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40
|
+
throw new ChangedFilesError(`Failed to get changed files (base: ${base}): ${msg}`);
|
|
41
|
+
}
|
|
42
|
+
if (diffOutput === '') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return diffOutput
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((relativePath) => resolve(repoRoot, relativePath));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=changed-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changed-files.js","sourceRoot":"","sources":["../src/changed-files.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,0EAA0E;AAC1E,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE;YAC/D,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CACzB,gDAAgD,CACjD,CAAC;IACJ,CAAC;IAED,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,UAAU,GAAG,YAAY,CACvB,KAAK,EACL,CAAC,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,GAAG,IAAI,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,EAC5E,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CACrC,CAAC,IAAI,EAAE,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,MAAM,IAAI,iBAAiB,CACzB,sCAAsC,IAAI,MAAM,GAAG,EAAE,CACtD,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,UAAU;SACd,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FileType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Classify a skill file by its path and frontmatter presence.
|
|
4
|
+
*
|
|
5
|
+
* Classification rules (in priority order):
|
|
6
|
+
* 1. Basename `SKILL.md` (case-sensitive) → skill
|
|
7
|
+
* 2. Basename `README.md` or `CLAUDE.md` (case-insensitive) → readme
|
|
8
|
+
* 3. Rightmost known directory segment → command | agent | context | skill
|
|
9
|
+
* 4. Agent path + hasFrontmatter false → legacy-agent
|
|
10
|
+
* 5. No match → unknown
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyFile(filePath: string, hasFrontmatter: boolean): FileType;
|
|
13
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAe3C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,OAAO,GACtB,QAAQ,CAyDV"}
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Known directory segments that map to file types. */
|
|
2
|
+
const SEGMENT_TYPE_MAP = new Map([
|
|
3
|
+
['commands', 'command'],
|
|
4
|
+
['agents', 'agent'],
|
|
5
|
+
['context', 'context'],
|
|
6
|
+
['skills', 'skill'],
|
|
7
|
+
['reference', 'context'],
|
|
8
|
+
['shared', 'context'],
|
|
9
|
+
['examples', 'context'],
|
|
10
|
+
['templates', 'context'],
|
|
11
|
+
['themes', 'context'],
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* Classify a skill file by its path and frontmatter presence.
|
|
15
|
+
*
|
|
16
|
+
* Classification rules (in priority order):
|
|
17
|
+
* 1. Basename `SKILL.md` (case-sensitive) → skill
|
|
18
|
+
* 2. Basename `README.md` or `CLAUDE.md` (case-insensitive) → readme
|
|
19
|
+
* 3. Rightmost known directory segment → command | agent | context | skill
|
|
20
|
+
* 4. Agent path + hasFrontmatter false → legacy-agent
|
|
21
|
+
* 5. No match → unknown
|
|
22
|
+
*/
|
|
23
|
+
export function classifyFile(filePath, hasFrontmatter) {
|
|
24
|
+
const basename = filePath.split('/').pop() ?? '';
|
|
25
|
+
// AC-3 (story-016): SKILL.md basename (case-sensitive) → skill
|
|
26
|
+
if (basename === 'SKILL.md') {
|
|
27
|
+
return 'skill';
|
|
28
|
+
}
|
|
29
|
+
// AC-9 (story-016): CLAUDE.md (case-insensitive) → readme
|
|
30
|
+
if (basename.toLowerCase() === 'claude.md') {
|
|
31
|
+
return 'readme';
|
|
32
|
+
}
|
|
33
|
+
// AC-4: README.md basename check (case-insensitive)
|
|
34
|
+
if (basename.toLowerCase() === 'readme.md') {
|
|
35
|
+
return 'readme';
|
|
36
|
+
}
|
|
37
|
+
// Split into segments and find the rightmost known directory segment (AC-7).
|
|
38
|
+
const segments = filePath.split('/');
|
|
39
|
+
let matchedType;
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
const type = SEGMENT_TYPE_MAP.get(segment);
|
|
42
|
+
if (type !== undefined) {
|
|
43
|
+
matchedType = type;
|
|
44
|
+
// Don't break — keep scanning so the rightmost wins.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (matchedType === undefined) {
|
|
48
|
+
return 'unknown'; // AC-9
|
|
49
|
+
}
|
|
50
|
+
// AC-5 / AC-6: legacy-agent reclassification
|
|
51
|
+
if (matchedType === 'agent') {
|
|
52
|
+
return hasFrontmatter ? 'agent' : 'legacy-agent';
|
|
53
|
+
}
|
|
54
|
+
// Non-SKILL.md markdown in skills/ dirs:
|
|
55
|
+
// - Directly in skills/name/ → readme
|
|
56
|
+
// - In skills/name/subdir/ where subdir is not in SEGMENT_TYPE_MAP → unknown
|
|
57
|
+
// (If the subdir were recognized, it would have overridden matchedType.)
|
|
58
|
+
if (matchedType === 'skill') {
|
|
59
|
+
const skillsIdx = segments.lastIndexOf('skills');
|
|
60
|
+
// afterSkills = [skillName, ...subdirs, basename]
|
|
61
|
+
const afterSkills = segments.slice(skillsIdx + 1);
|
|
62
|
+
if (afterSkills.length > 2) {
|
|
63
|
+
// File is nested in a subdirectory of the skill folder, and that
|
|
64
|
+
// subdirectory is unrecognized (otherwise matchedType would not
|
|
65
|
+
// still be 'skill'). Classify as unknown.
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
return 'readme';
|
|
69
|
+
}
|
|
70
|
+
return matchedType;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,MAAM,gBAAgB,GAAkC,IAAI,GAAG,CAAC;IAC9D,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,QAAQ,EAAE,OAAO,CAAC;IACnB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,QAAQ,EAAE,OAAO,CAAC;IACnB,CAAC,WAAW,EAAE,SAAS,CAAC;IACxB,CAAC,QAAQ,EAAE,SAAS,CAAC;IACrB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,WAAW,EAAE,SAAS,CAAC;IACxB,CAAC,QAAQ,EAAE,SAAS,CAAC;CACtB,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAgB,EAChB,cAAuB;IAEvB,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAEjD,+DAA+D;IAC/D,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,0DAA0D;IAC1D,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,oDAAoD;IACpD,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,WAAiC,CAAC;IAEtC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,WAAW,GAAG,IAAI,CAAC;YACnB,qDAAqD;QACvD,CAAC;IACH,CAAC;IAED,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC,CAAC,OAAO;IAC3B,CAAC;IAED,6CAA6C;IAC7C,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;QAC5B,OAAO,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;IACnD,CAAC;IAED,yCAAyC;IACzC,sCAAsC;IACtC,6EAA6E;IAC7E,2EAA2E;IAC3E,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACjD,kDAAkD;QAClD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,iEAAiE;YACjE,gEAAgE;YAChE,0CAA0C;YAC1C,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|