skillcheck 0.1.0__tar.gz
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.
- skillcheck-0.1.0/.claude/settings.local.json +21 -0
- skillcheck-0.1.0/.github/banner.svg +26 -0
- skillcheck-0.1.0/.gitignore +10 -0
- skillcheck-0.1.0/LICENSE +21 -0
- skillcheck-0.1.0/PKG-INFO +142 -0
- skillcheck-0.1.0/README.md +128 -0
- skillcheck-0.1.0/SKILL.md +65 -0
- skillcheck-0.1.0/pyproject.toml +31 -0
- skillcheck-0.1.0/src/skillcheck/__init__.py +15 -0
- skillcheck-0.1.0/src/skillcheck/cli.py +228 -0
- skillcheck-0.1.0/src/skillcheck/config.py +21 -0
- skillcheck-0.1.0/src/skillcheck/core.py +50 -0
- skillcheck-0.1.0/src/skillcheck/parser.py +62 -0
- skillcheck-0.1.0/src/skillcheck/result.py +30 -0
- skillcheck-0.1.0/src/skillcheck/rules/__init__.py +49 -0
- skillcheck-0.1.0/src/skillcheck/rules/frontmatter.py +230 -0
- skillcheck-0.1.0/src/skillcheck/rules/patterns.py +11 -0
- skillcheck-0.1.0/src/skillcheck/rules/sizing.py +47 -0
- skillcheck-0.1.0/src/skillcheck/tokenizer.py +39 -0
- skillcheck-0.1.0/tests/conftest.py +10 -0
- skillcheck-0.1.0/tests/fixtures/bad_body_long.md +505 -0
- skillcheck-0.1.0/tests/fixtures/bad_desc_empty.md +6 -0
- skillcheck-0.1.0/tests/fixtures/bad_desc_person.md +6 -0
- skillcheck-0.1.0/tests/fixtures/bad_field_typo.md +7 -0
- skillcheck-0.1.0/tests/fixtures/bad_name_caps.md +6 -0
- skillcheck-0.1.0/tests/fixtures/bad_name_long.md +6 -0
- skillcheck-0.1.0/tests/fixtures/bad_name_reserved.md +6 -0
- skillcheck-0.1.0/tests/fixtures/bad_no_frontmatter.md +4 -0
- skillcheck-0.1.0/tests/fixtures/valid_basic.md +6 -0
- skillcheck-0.1.0/tests/fixtures/valid_full.md +23 -0
- skillcheck-0.1.0/tests/test_cli.py +223 -0
- skillcheck-0.1.0/tests/test_frontmatter.py +305 -0
- skillcheck-0.1.0/tests/test_parser.py +72 -0
- skillcheck-0.1.0/tests/test_sizing.py +96 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(pip install:*)",
|
|
5
|
+
"Bash(skillcheck --version)",
|
|
6
|
+
"Bash(python -m pytest tests/ -v)",
|
|
7
|
+
"Bash(skillcheck tests/fixtures/valid_basic.md)",
|
|
8
|
+
"Bash(skillcheck tests/fixtures/bad_name_caps.md)",
|
|
9
|
+
"Bash(echo \"exit: $?\")",
|
|
10
|
+
"Bash(skillcheck tests/fixtures/bad_name_caps.md --format json)",
|
|
11
|
+
"Bash(python3 -m json.tool --no-ensure-ascii)",
|
|
12
|
+
"Bash(for f:*)",
|
|
13
|
+
"Bash(do echo:*)",
|
|
14
|
+
"Bash(python3 -m pytest tests/ -v)",
|
|
15
|
+
"Bash(skillcheck /home/brad/projects/skillcheck/SKILL.md)",
|
|
16
|
+
"Bash(skillcheck /home/brad/projects/skillcheck/SKILL.md --format json)",
|
|
17
|
+
"Bash(python3 -m pytest tests/test_sizing.py -v)",
|
|
18
|
+
"Bash(python3 -m pytest tests/)"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 200" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="800" y2="200" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0%" stop-color="#0f0f23"/>
|
|
5
|
+
<stop offset="100%" stop-color="#1a1a3e"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
+
<stop offset="0%" stop-color="#d4a574"/>
|
|
9
|
+
<stop offset="100%" stop-color="#e8c9a0"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="800" height="200" rx="12" fill="url(#bg)"/>
|
|
13
|
+
<!-- check icon -->
|
|
14
|
+
<g transform="translate(280, 55)">
|
|
15
|
+
<circle cx="40" cy="40" r="36" stroke="url(#accent)" stroke-width="3" fill="none" opacity="0.9"/>
|
|
16
|
+
<polyline points="22,42 36,56 60,28" stroke="#4ade80" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
17
|
+
</g>
|
|
18
|
+
<!-- title -->
|
|
19
|
+
<text x="400" y="108" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace" font-size="42" font-weight="700" fill="url(#accent)">
|
|
20
|
+
<tspan dx="52">skillcheck</tspan>
|
|
21
|
+
</text>
|
|
22
|
+
<!-- subtitle -->
|
|
23
|
+
<text x="400" y="142" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="15" fill="#8b8ba7" letter-spacing="0.5">
|
|
24
|
+
Static analyzer for Claude Code SKILL.md files
|
|
25
|
+
</text>
|
|
26
|
+
</svg>
|
skillcheck-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 skillcheck contributors
|
|
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.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skillcheck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Validates Claude Code SKILL.md files against the Anthropic skill specification
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Provides-Extra: tiktoken
|
|
12
|
+
Requires-Dist: tiktoken>=0.7; extra == 'tiktoken'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/banner.svg">
|
|
19
|
+
<source media="(prefers-color-scheme: light)" srcset=".github/banner.svg">
|
|
20
|
+
<img alt="skillcheck" src=".github/banner.svg" width="600">
|
|
21
|
+
</picture>
|
|
22
|
+
|
|
23
|
+
<br/>
|
|
24
|
+
|
|
25
|
+
**Static analyzer for Claude Code `SKILL.md` files.**<br/>
|
|
26
|
+
Validates frontmatter structure and body sizing against the Anthropic skill specification.
|
|
27
|
+
|
|
28
|
+
<br/>
|
|
29
|
+
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](https://python.org)
|
|
32
|
+
[](https://pyyaml.org)
|
|
33
|
+
[](#testing)
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## What It Does
|
|
40
|
+
|
|
41
|
+
`skillcheck` catches problems in your `SKILL.md` files before they hit production:
|
|
42
|
+
|
|
43
|
+
- **Frontmatter validation** — required fields, character constraints, length limits, reserved words, first/second-person voice, XML tags, unknown fields
|
|
44
|
+
- **Body sizing** — line count and token estimate warnings to keep skills within context-window budgets
|
|
45
|
+
- **CI-friendly** — JSON output, deterministic exit codes, zero config
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install skillcheck
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install from source with dev dependencies:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e ".[dev]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Validate a single file
|
|
63
|
+
skillcheck path/to/SKILL.md
|
|
64
|
+
|
|
65
|
+
# Scan a directory recursively for all SKILL.md files
|
|
66
|
+
skillcheck skills/
|
|
67
|
+
|
|
68
|
+
# Machine-readable output for CI pipelines
|
|
69
|
+
skillcheck skills/ --format json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Example Output
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
✔ PASS skills/deploy.md
|
|
76
|
+
|
|
77
|
+
✗ FAIL skills/pdf-tool.md
|
|
78
|
+
line 2 ✗ error frontmatter.name.invalid-chars Name contains uppercase chars
|
|
79
|
+
line 3 ⚠ warning frontmatter.field.unknown Unknown field 'author'
|
|
80
|
+
|
|
81
|
+
Checked 2 files: 1 passed, 1 failed, 1 warning
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Options
|
|
85
|
+
|
|
86
|
+
| Flag | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `--format json` | Machine-readable JSON output |
|
|
89
|
+
| `--max-lines N` | Override line-count threshold (default: 500) |
|
|
90
|
+
| `--max-tokens N` | Override token-count threshold (default: 8000) |
|
|
91
|
+
| `--ignore PREFIX` | Suppress rules matching a prefix (repeatable) |
|
|
92
|
+
| `--no-color` | Disable colored output |
|
|
93
|
+
| `--version` | Show version |
|
|
94
|
+
|
|
95
|
+
### Examples
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Override sizing thresholds
|
|
99
|
+
skillcheck skills/ --max-lines 800 --max-tokens 6000
|
|
100
|
+
|
|
101
|
+
# Suppress specific rule categories
|
|
102
|
+
skillcheck SKILL.md --ignore frontmatter.description
|
|
103
|
+
|
|
104
|
+
# Pipe-friendly plain output
|
|
105
|
+
skillcheck SKILL.md --no-color
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Exit Codes
|
|
109
|
+
|
|
110
|
+
| Code | Meaning |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `0` | No errors (warnings are allowed) |
|
|
113
|
+
| `1` | One or more errors found |
|
|
114
|
+
| `2` | Input error (missing file, empty directory) |
|
|
115
|
+
|
|
116
|
+
## Rules
|
|
117
|
+
|
|
118
|
+
| Rule ID | Severity | What it checks |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `frontmatter.name.required` | error | `name` field must exist |
|
|
121
|
+
| `frontmatter.name.max-length` | error | Name ≤ 64 characters |
|
|
122
|
+
| `frontmatter.name.invalid-chars` | error | Lowercase, numbers, hyphens only |
|
|
123
|
+
| `frontmatter.name.reserved` | error | Not a reserved word (`claude`, `anthropic`, …) |
|
|
124
|
+
| `frontmatter.description.required` | error | `description` field must exist |
|
|
125
|
+
| `frontmatter.description.empty` | error | Description must not be blank |
|
|
126
|
+
| `frontmatter.description.max-length` | error | Description ≤ 1024 characters |
|
|
127
|
+
| `frontmatter.description.xml-tags` | error | No XML/HTML tags in description |
|
|
128
|
+
| `frontmatter.description.person-voice` | error | No first/second-person pronouns |
|
|
129
|
+
| `frontmatter.field.unknown` | warning | Flags fields not in the spec |
|
|
130
|
+
| `sizing.body.line-count` | warning | Body exceeds line threshold |
|
|
131
|
+
| `sizing.body.token-estimate` | warning | Body exceeds token threshold |
|
|
132
|
+
|
|
133
|
+
## Testing
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install -e ".[dev]"
|
|
137
|
+
python3 -m pytest tests/ -v
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/banner.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset=".github/banner.svg">
|
|
6
|
+
<img alt="skillcheck" src=".github/banner.svg" width="600">
|
|
7
|
+
</picture>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
**Static analyzer for Claude Code `SKILL.md` files.**<br/>
|
|
12
|
+
Validates frontmatter structure and body sizing against the Anthropic skill specification.
|
|
13
|
+
|
|
14
|
+
<br/>
|
|
15
|
+
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://python.org)
|
|
18
|
+
[](https://pyyaml.org)
|
|
19
|
+
[](#testing)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What It Does
|
|
26
|
+
|
|
27
|
+
`skillcheck` catches problems in your `SKILL.md` files before they hit production:
|
|
28
|
+
|
|
29
|
+
- **Frontmatter validation** — required fields, character constraints, length limits, reserved words, first/second-person voice, XML tags, unknown fields
|
|
30
|
+
- **Body sizing** — line count and token estimate warnings to keep skills within context-window budgets
|
|
31
|
+
- **CI-friendly** — JSON output, deterministic exit codes, zero config
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install skillcheck
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or install from source with dev dependencies:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Validate a single file
|
|
49
|
+
skillcheck path/to/SKILL.md
|
|
50
|
+
|
|
51
|
+
# Scan a directory recursively for all SKILL.md files
|
|
52
|
+
skillcheck skills/
|
|
53
|
+
|
|
54
|
+
# Machine-readable output for CI pipelines
|
|
55
|
+
skillcheck skills/ --format json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example Output
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
✔ PASS skills/deploy.md
|
|
62
|
+
|
|
63
|
+
✗ FAIL skills/pdf-tool.md
|
|
64
|
+
line 2 ✗ error frontmatter.name.invalid-chars Name contains uppercase chars
|
|
65
|
+
line 3 ⚠ warning frontmatter.field.unknown Unknown field 'author'
|
|
66
|
+
|
|
67
|
+
Checked 2 files: 1 passed, 1 failed, 1 warning
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `--format json` | Machine-readable JSON output |
|
|
75
|
+
| `--max-lines N` | Override line-count threshold (default: 500) |
|
|
76
|
+
| `--max-tokens N` | Override token-count threshold (default: 8000) |
|
|
77
|
+
| `--ignore PREFIX` | Suppress rules matching a prefix (repeatable) |
|
|
78
|
+
| `--no-color` | Disable colored output |
|
|
79
|
+
| `--version` | Show version |
|
|
80
|
+
|
|
81
|
+
### Examples
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Override sizing thresholds
|
|
85
|
+
skillcheck skills/ --max-lines 800 --max-tokens 6000
|
|
86
|
+
|
|
87
|
+
# Suppress specific rule categories
|
|
88
|
+
skillcheck SKILL.md --ignore frontmatter.description
|
|
89
|
+
|
|
90
|
+
# Pipe-friendly plain output
|
|
91
|
+
skillcheck SKILL.md --no-color
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Exit Codes
|
|
95
|
+
|
|
96
|
+
| Code | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `0` | No errors (warnings are allowed) |
|
|
99
|
+
| `1` | One or more errors found |
|
|
100
|
+
| `2` | Input error (missing file, empty directory) |
|
|
101
|
+
|
|
102
|
+
## Rules
|
|
103
|
+
|
|
104
|
+
| Rule ID | Severity | What it checks |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `frontmatter.name.required` | error | `name` field must exist |
|
|
107
|
+
| `frontmatter.name.max-length` | error | Name ≤ 64 characters |
|
|
108
|
+
| `frontmatter.name.invalid-chars` | error | Lowercase, numbers, hyphens only |
|
|
109
|
+
| `frontmatter.name.reserved` | error | Not a reserved word (`claude`, `anthropic`, …) |
|
|
110
|
+
| `frontmatter.description.required` | error | `description` field must exist |
|
|
111
|
+
| `frontmatter.description.empty` | error | Description must not be blank |
|
|
112
|
+
| `frontmatter.description.max-length` | error | Description ≤ 1024 characters |
|
|
113
|
+
| `frontmatter.description.xml-tags` | error | No XML/HTML tags in description |
|
|
114
|
+
| `frontmatter.description.person-voice` | error | No first/second-person pronouns |
|
|
115
|
+
| `frontmatter.field.unknown` | warning | Flags fields not in the spec |
|
|
116
|
+
| `sizing.body.line-count` | warning | Body exceeds line threshold |
|
|
117
|
+
| `sizing.body.token-estimate` | warning | Body exceeds token threshold |
|
|
118
|
+
|
|
119
|
+
## Testing
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install -e ".[dev]"
|
|
123
|
+
python3 -m pytest tests/ -v
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit-crafter
|
|
3
|
+
description: Generates conventional commit messages from staged diffs, enforcing semantic versioning conventions and team-defined scopes.
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
author: brad
|
|
6
|
+
tags:
|
|
7
|
+
- git
|
|
8
|
+
- commits
|
|
9
|
+
- devops
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
user-invocable: true
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# git-commit-crafter
|
|
16
|
+
|
|
17
|
+
Generates conventional commit messages from staged git diffs. Reads the diff, infers the change type and scope, and produces a formatted commit message ready to paste or pipe directly into `git commit`.
|
|
18
|
+
|
|
19
|
+
## Trigger
|
|
20
|
+
|
|
21
|
+
Invoke when the user has staged changes and needs a commit message:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/git-commit-crafter
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Behavior
|
|
28
|
+
|
|
29
|
+
1. Run `git diff --cached` to read the staged changes.
|
|
30
|
+
2. Identify the change type from the diff: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, or `perf`.
|
|
31
|
+
3. Infer the scope from the files touched (e.g., `auth`, `api`, `cli`, `db`).
|
|
32
|
+
4. Write a subject line under 72 characters in the form `type(scope): imperative summary`.
|
|
33
|
+
5. If the diff spans more than one logical concern, add a short body paragraph per concern.
|
|
34
|
+
6. If any file path contains `BREAKING` or the diff removes a public API surface, append `BREAKING CHANGE:` footer.
|
|
35
|
+
|
|
36
|
+
## Output format
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
feat(auth): add OAuth2 PKCE flow for CLI clients
|
|
40
|
+
|
|
41
|
+
Replaces the implicit grant with PKCE to comply with RFC 9700.
|
|
42
|
+
Adds a local redirect server on a random port for the callback.
|
|
43
|
+
|
|
44
|
+
Closes #418
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Constraints
|
|
48
|
+
|
|
49
|
+
- Never invent change details not present in the diff.
|
|
50
|
+
- Do not reference internal ticket numbers unless they appear in branch name or diff.
|
|
51
|
+
- Keep the subject line imperative mood: "add", "fix", "remove", not "added" or "fixes".
|
|
52
|
+
- If the diff is empty, output: `No staged changes found. Run git add first.`
|
|
53
|
+
|
|
54
|
+
## Scope inference rules
|
|
55
|
+
|
|
56
|
+
| File path pattern | Scope |
|
|
57
|
+
|------------------------|------------|
|
|
58
|
+
| `src/auth/**` | `auth` |
|
|
59
|
+
| `src/api/**` | `api` |
|
|
60
|
+
| `src/cli/**` | `cli` |
|
|
61
|
+
| `src/db/**` | `db` |
|
|
62
|
+
| `tests/**` | `test` |
|
|
63
|
+
| `docs/**` | `docs` |
|
|
64
|
+
| `*.toml`, `*.cfg` | `config` |
|
|
65
|
+
| Mixed or root-level | (omit scope) |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skillcheck"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Validates Claude Code SKILL.md files against the Anthropic skill specification"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
]
|
|
20
|
+
tiktoken = [
|
|
21
|
+
"tiktoken>=0.7",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
skillcheck = "skillcheck.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/skillcheck"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from skillcheck.core import validate
|
|
2
|
+
from skillcheck.parser import ParsedSkill, ParseError
|
|
3
|
+
from skillcheck.result import Diagnostic, Severity, ValidationResult
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"validate",
|
|
9
|
+
"ValidationResult",
|
|
10
|
+
"Diagnostic",
|
|
11
|
+
"Severity",
|
|
12
|
+
"ParsedSkill",
|
|
13
|
+
"ParseError",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skillcheck import __version__
|
|
9
|
+
from skillcheck.core import validate
|
|
10
|
+
from skillcheck.result import Severity, ValidationResult
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# ANSI helpers (zero dependencies)
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
_RESET = "\033[0m"
|
|
17
|
+
_BOLD = "\033[1m"
|
|
18
|
+
_DIM = "\033[2m"
|
|
19
|
+
_RED = "\033[31m"
|
|
20
|
+
_GREEN = "\033[32m"
|
|
21
|
+
_YELLOW = "\033[33m"
|
|
22
|
+
|
|
23
|
+
_SEV_SYMBOL = {Severity.ERROR: "✗", Severity.WARNING: "⚠", Severity.INFO: "·"}
|
|
24
|
+
_SEV_COLOR = {Severity.ERROR: _RED, Severity.WARNING: _YELLOW, Severity.INFO: _DIM}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _style(text: str, *codes: str, color: bool = True) -> str:
|
|
28
|
+
"""Wrap *text* in ANSI escape codes when *color* is enabled."""
|
|
29
|
+
if not color:
|
|
30
|
+
return text
|
|
31
|
+
return "".join(codes) + text + _RESET
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Path collection
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _collect_paths(target: Path) -> list[Path]:
|
|
40
|
+
"""Return a list of SKILL.md files to validate.
|
|
41
|
+
|
|
42
|
+
For a directory, recursively finds all files named exactly 'SKILL.md'.
|
|
43
|
+
For a file, returns it directly without name filtering.
|
|
44
|
+
"""
|
|
45
|
+
if target.is_dir():
|
|
46
|
+
return sorted(target.rglob("SKILL.md"))
|
|
47
|
+
return [target]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Formatters
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_text(results: list[ValidationResult], *, color: bool = False) -> str:
|
|
56
|
+
lines: list[str] = []
|
|
57
|
+
for result in results:
|
|
58
|
+
if result.valid:
|
|
59
|
+
tag = _style("✔ PASS", _BOLD, _GREEN, color=color)
|
|
60
|
+
else:
|
|
61
|
+
tag = _style("✗ FAIL", _BOLD, _RED, color=color)
|
|
62
|
+
lines.append(f"{tag} {result.path}")
|
|
63
|
+
|
|
64
|
+
for d in result.diagnostics:
|
|
65
|
+
sym = _SEV_SYMBOL.get(d.severity, "·")
|
|
66
|
+
sev_col = _SEV_COLOR.get(d.severity, "")
|
|
67
|
+
loc = f"line {d.line}" if d.line is not None else ""
|
|
68
|
+
sev_label = _style(f"{sym} {d.severity.value}", sev_col, color=color)
|
|
69
|
+
rule = _style(d.rule, _DIM, color=color)
|
|
70
|
+
lines.append(f" {loc:>8} {sev_label:<18s} {rule} {d.message}")
|
|
71
|
+
if d.context:
|
|
72
|
+
ctx = _style(d.context, _DIM, color=color)
|
|
73
|
+
lines.append(f"{'':>12} {ctx}")
|
|
74
|
+
|
|
75
|
+
# summary
|
|
76
|
+
total = len(results)
|
|
77
|
+
passed = sum(1 for r in results if r.valid)
|
|
78
|
+
failed = total - passed
|
|
79
|
+
warn_count = sum(
|
|
80
|
+
1 for r in results for d in r.diagnostics if d.severity == Severity.WARNING
|
|
81
|
+
)
|
|
82
|
+
noun = "file" if total == 1 else "files"
|
|
83
|
+
|
|
84
|
+
parts = [
|
|
85
|
+
_style(f"{passed} passed", _GREEN, color=color),
|
|
86
|
+
_style(f"{failed} failed", _RED, color=color) if failed else f"{failed} failed",
|
|
87
|
+
]
|
|
88
|
+
if warn_count:
|
|
89
|
+
w = f"{warn_count} warning{'s' if warn_count != 1 else ''}"
|
|
90
|
+
parts.append(_style(w, _YELLOW, color=color))
|
|
91
|
+
|
|
92
|
+
lines.append(f"\nChecked {total} {noun}: {', '.join(parts)}")
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _format_json(results: list[ValidationResult], version: str) -> str:
|
|
97
|
+
passed = sum(1 for r in results if r.valid)
|
|
98
|
+
payload = {
|
|
99
|
+
"version": version,
|
|
100
|
+
"files_checked": len(results),
|
|
101
|
+
"files_passed": passed,
|
|
102
|
+
"files_failed": len(results) - passed,
|
|
103
|
+
"results": [
|
|
104
|
+
{
|
|
105
|
+
"path": str(r.path),
|
|
106
|
+
"valid": r.valid,
|
|
107
|
+
"diagnostics": [
|
|
108
|
+
{
|
|
109
|
+
"rule": d.rule,
|
|
110
|
+
"severity": d.severity.value,
|
|
111
|
+
"message": d.message,
|
|
112
|
+
"line": d.line,
|
|
113
|
+
"context": d.context,
|
|
114
|
+
}
|
|
115
|
+
for d in r.diagnostics
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
for r in results
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
return json.dumps(payload, indent=2)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Argument parser
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
_EPILOG = """\
|
|
129
|
+
examples:
|
|
130
|
+
skillcheck SKILL.md validate a single file
|
|
131
|
+
skillcheck skills/ scan a directory recursively
|
|
132
|
+
skillcheck SKILL.md --format json machine-readable output for CI
|
|
133
|
+
skillcheck SKILL.md --max-lines 800 override sizing thresholds
|
|
134
|
+
skillcheck SKILL.md --ignore frontmatter suppress a rule category
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
139
|
+
parser = argparse.ArgumentParser(
|
|
140
|
+
prog="skillcheck",
|
|
141
|
+
description="Validate Claude Code SKILL.md files against the Anthropic skill specification.",
|
|
142
|
+
epilog=_EPILOG,
|
|
143
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"path",
|
|
147
|
+
type=Path,
|
|
148
|
+
help="Path to a SKILL.md file or a directory to scan recursively.",
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--format",
|
|
152
|
+
choices=["text", "json"],
|
|
153
|
+
default="text",
|
|
154
|
+
help="Output format (default: text).",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--max-lines",
|
|
158
|
+
type=int,
|
|
159
|
+
default=None,
|
|
160
|
+
metavar="N",
|
|
161
|
+
help="Override the line-count threshold (default: 500).",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--max-tokens",
|
|
165
|
+
type=int,
|
|
166
|
+
default=None,
|
|
167
|
+
metavar="N",
|
|
168
|
+
help="Override the token-count threshold (default: 8000).",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--ignore",
|
|
172
|
+
action="append",
|
|
173
|
+
dest="ignore_prefixes",
|
|
174
|
+
metavar="PREFIX",
|
|
175
|
+
default=[],
|
|
176
|
+
help="Suppress rules matching this prefix. Can be repeated.",
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--no-color",
|
|
180
|
+
action="store_true",
|
|
181
|
+
default=False,
|
|
182
|
+
help="Disable colored output.",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--version",
|
|
186
|
+
action="version",
|
|
187
|
+
version=f"%(prog)s {__version__}",
|
|
188
|
+
)
|
|
189
|
+
return parser
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Entry point
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main() -> None:
|
|
198
|
+
parser = _build_parser()
|
|
199
|
+
args = parser.parse_args()
|
|
200
|
+
|
|
201
|
+
target: Path = args.path
|
|
202
|
+
if not target.exists():
|
|
203
|
+
print(f"Error: path not found: {target}", file=sys.stderr)
|
|
204
|
+
sys.exit(2)
|
|
205
|
+
|
|
206
|
+
paths = _collect_paths(target)
|
|
207
|
+
if not paths:
|
|
208
|
+
print(f"No SKILL.md files found under: {target}", file=sys.stderr)
|
|
209
|
+
sys.exit(2)
|
|
210
|
+
|
|
211
|
+
results = [
|
|
212
|
+
validate(
|
|
213
|
+
p,
|
|
214
|
+
max_lines=args.max_lines,
|
|
215
|
+
max_tokens=args.max_tokens,
|
|
216
|
+
ignore_prefixes=args.ignore_prefixes or None,
|
|
217
|
+
)
|
|
218
|
+
for p in paths
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
if args.format == "json":
|
|
222
|
+
print(_format_json(results, __version__))
|
|
223
|
+
else:
|
|
224
|
+
use_color = not args.no_color and sys.stdout.isatty()
|
|
225
|
+
print(_format_text(results, color=use_color))
|
|
226
|
+
|
|
227
|
+
any_errors = any(not r.valid for r in results)
|
|
228
|
+
sys.exit(1 if any_errors else 0)
|