@softspark/ai-toolkit 4.22.0 → 4.22.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/CHANGELOG.md +37 -0
- package/README.md +9 -1
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/brand-voice/SKILL.md +1 -1
- package/app/skills/ci/SKILL.md +1 -1
- package/app/skills/debug/SKILL.md +1 -1
- package/app/skills/deploy/SKILL.md +1 -1
- package/app/skills/explore/SKILL.md +1 -1
- package/app/skills/explore/scripts/visualize.py +15 -1
- package/app/skills/fix/SKILL.md +2 -2
- package/app/skills/migrate/SKILL.md +1 -1
- package/app/skills/pr/SKILL.md +1 -1
- package/app/skills/review/SKILL.md +1 -1
- package/app/skills/rollback/SKILL.md +1 -1
- package/kb/procedures/post-release-testing-sop.md +92 -3
- package/llms-full.txt +92 -3
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/validate.py +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.22.1 — nine skills, not four, could not find their own scripts (2026-08-06)
|
|
11
|
+
|
|
12
|
+
v4.22.0 claimed to fix four skills whose documented script path never resolved.
|
|
13
|
+
The first real run of the post-release SOP found five more, plus a wrong
|
|
14
|
+
interpreter and a script that crashed on `--help`. The v4.22.0 fix had grepped
|
|
15
|
+
for one spelling of the bug and missed the others.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Five more skills ran their own script through a path that cannot resolve
|
|
20
|
+
after install**: `brand-voice` (repo-relative `app/skills/.../measure.py`),
|
|
21
|
+
`ci`, `deploy`, `migrate` and `rollback` (cwd-relative `scripts/*.py`). Four
|
|
22
|
+
further example lines in `debug`, `fix`, `pr` and `review` had the same defect
|
|
23
|
+
in a secondary snippet while the primary invocation was already correct.
|
|
24
|
+
- **`rollback` ran a Python file through `bash`.** `bash scripts/rollback_info.py`
|
|
25
|
+
was wrong twice over.
|
|
26
|
+
- **`explore` used `python` rather than `python3`**, which is missing or Python 2
|
|
27
|
+
on many systems.
|
|
28
|
+
- **`explore`'s `visualize.py` crashed with a traceback on `--help`**, treating
|
|
29
|
+
the flag as a directory to scan. It now prints usage, and reports a JSON error
|
|
30
|
+
with exit 1 for a path that does not exist.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`validate.py` fails the build on the whole class.**
|
|
35
|
+
`_validate_skill_script_invocations` checks every documented command that runs
|
|
36
|
+
a skill-owned script: the path must go through `${CLAUDE_SKILL_DIR}`, and the
|
|
37
|
+
interpreter must match the file. `python` instead of `python3` is a warning.
|
|
38
|
+
Repo-level scripts such as `scripts/validate.py` are correctly ignored, and the
|
|
39
|
+
frontmatter `scripts:` list stays relative because it declares rather than runs.
|
|
40
|
+
- **Post-release SOP phases 4b and 4c** (`post-release-testing-sop.md` v1.1.0):
|
|
41
|
+
run every skill's documented invocation from the installed copy, and prove
|
|
42
|
+
scanner-wrapping skills actually scan a fixture with known defects. Phase 4b
|
|
43
|
+
found every defect in this release on its first run.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
10
47
|
## v4.22.0 — the public surface is a test, not a promise (2026-08-06)
|
|
11
48
|
|
|
12
49
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.22.1
|
|
12
|
+
|
|
13
|
+
**v4.22.1** — nine skills, not four, could not find their own scripts. v4.22.0
|
|
14
|
+
grepped for one spelling of the bug and missed five more, a `.py` file run
|
|
15
|
+
through `bash`, and a script that crashed on `--help`. `validate.py` now fails
|
|
16
|
+
the build on the entire class, and the post-release SOP runs every skill's
|
|
17
|
+
documented command from the installed copy — which is how these were found.
|
|
10
18
|
|
|
11
19
|
## What's New in v4.22.0
|
|
12
20
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "ai-toolkit",
|
|
4
4
|
"displayName": "AI Toolkit",
|
|
5
5
|
"description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
|
|
6
|
-
"version": "4.22.
|
|
6
|
+
"version": "4.22.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -162,7 +162,7 @@ Modes change *prose*, not *data*. If you cut a fact to fit a length budget, you
|
|
|
162
162
|
|
|
163
163
|
### Measurement
|
|
164
164
|
|
|
165
|
-
Run `python3
|
|
165
|
+
Run `python3 ${CLAUDE_SKILL_DIR}/scripts/measure.py --fixtures tests/fixtures/output-modes/` to compare baseline vs mode tokens on the fixture set. Report shows per-fixture deltas and an aggregate ratio.
|
|
166
166
|
|
|
167
167
|
## When NOT to Load
|
|
168
168
|
|
package/app/skills/ci/SKILL.md
CHANGED
|
@@ -27,7 +27,7 @@ Generate, update, or troubleshoot CI/CD pipeline configuration based on project
|
|
|
27
27
|
|
|
28
28
|
Detect CI platform and analyze configuration:
|
|
29
29
|
```bash
|
|
30
|
-
python3 scripts/ci-detect.py [directory]
|
|
30
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/ci-detect.py [directory]
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Returns JSON with:
|
|
@@ -28,7 +28,7 @@ Pipe error output through the error parser for structured diagnosis:
|
|
|
28
28
|
your_command 2>&1 | python3 ${CLAUDE_SKILL_DIR}/scripts/error-parser.py
|
|
29
29
|
|
|
30
30
|
# Or from a log file
|
|
31
|
-
cat /var/log/app/error.log | python3 scripts/error-parser.py
|
|
31
|
+
cat /var/log/app/error.log | python3 ${CLAUDE_SKILL_DIR}/scripts/error-parser.py
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The script outputs JSON with:
|
|
@@ -50,7 +50,7 @@ Deploy application to target environment.
|
|
|
50
50
|
|
|
51
51
|
Run automated pre-deployment checks:
|
|
52
52
|
```bash
|
|
53
|
-
python3 scripts/pre_deploy_check.py [environment]
|
|
53
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/pre_deploy_check.py [environment]
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
Returns JSON with pass/fail for each check:
|
|
@@ -99,7 +99,7 @@ It does NOT write or modify any files.
|
|
|
99
99
|
For an interactive HTML tree visualization of the codebase:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
|
|
102
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/visualize.py .
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
This generates `codebase-map.html` with collapsible directories, file sizes, and type-colored indicators.
|
|
@@ -108,8 +108,22 @@ data.children.forEach(c=>render(c,document.getElementById('root')));
|
|
|
108
108
|
</script></body></html>'''
|
|
109
109
|
output.write_text(html)
|
|
110
110
|
|
|
111
|
+
USAGE = """usage: visualize.py [PATH]
|
|
112
|
+
|
|
113
|
+
Scan PATH (default: current directory) and write an interactive codebase map to
|
|
114
|
+
codebase-map.html, then open it in the default browser.
|
|
115
|
+
"""
|
|
116
|
+
|
|
111
117
|
if __name__ == '__main__':
|
|
112
|
-
|
|
118
|
+
if len(sys.argv) > 1 and sys.argv[1] in ('-h', '--help'):
|
|
119
|
+
print(USAGE)
|
|
120
|
+
raise SystemExit(0)
|
|
121
|
+
|
|
122
|
+
target = Path(sys.argv[1] if len(sys.argv) > 1 else '.')
|
|
123
|
+
if not target.is_dir():
|
|
124
|
+
print(f'{{"error": "Not a directory: {target}"}}')
|
|
125
|
+
raise SystemExit(1)
|
|
126
|
+
target = target.resolve()
|
|
113
127
|
stats = {"files": 0, "dirs": 0, "extensions": Counter(), "ext_sizes": Counter()}
|
|
114
128
|
data = scan(target, stats)
|
|
115
129
|
out = Path('codebase-map.html')
|
package/app/skills/fix/SKILL.md
CHANGED
|
@@ -27,8 +27,8 @@ Before entering the fix loop, classify errors to prioritize auto-fixable ones:
|
|
|
27
27
|
```bash
|
|
28
28
|
# Pipe lint or test output
|
|
29
29
|
ruff check . 2>&1 | python3 ${CLAUDE_SKILL_DIR}/scripts/error-classifier.py
|
|
30
|
-
mypy src/ 2>&1 | python3 scripts/error-classifier.py
|
|
31
|
-
npx eslint . 2>&1 | python3 scripts/error-classifier.py
|
|
30
|
+
mypy src/ 2>&1 | python3 ${CLAUDE_SKILL_DIR}/scripts/error-classifier.py
|
|
31
|
+
npx eslint . 2>&1 | python3 ${CLAUDE_SKILL_DIR}/scripts/error-classifier.py
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The script outputs JSON with:
|
|
@@ -31,7 +31,7 @@ Create, run, or manage database migrations with auto-detection of the migration
|
|
|
31
31
|
|
|
32
32
|
Detect migration tool and report status:
|
|
33
33
|
```bash
|
|
34
|
-
python3 scripts/migration-status.py [directory]
|
|
34
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/migration-status.py [directory]
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Returns JSON with: `tool`, `config_file`, `migrations_dir`, `total_migrations`, `latest`, `commands{}` (status/create/upgrade/downgrade).
|
package/app/skills/pr/SKILL.md
CHANGED
|
@@ -30,7 +30,7 @@ Generate a structured PR summary from the commit history before writing the PR d
|
|
|
30
30
|
```bash
|
|
31
31
|
python3 ${CLAUDE_SKILL_DIR}/scripts/pr-summary.py [base_branch]
|
|
32
32
|
# Default base branch: main
|
|
33
|
-
# Example: python3 scripts/pr-summary.py develop
|
|
33
|
+
# Example: python3 ${CLAUDE_SKILL_DIR}/scripts/pr-summary.py develop
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
The script outputs JSON with:
|
|
@@ -43,7 +43,7 @@ Before starting manual review, run the diff analyzer script to get a structured
|
|
|
43
43
|
```bash
|
|
44
44
|
python3 ${CLAUDE_SKILL_DIR}/scripts/diff-analyzer.py [base_branch]
|
|
45
45
|
# Default base branch: main
|
|
46
|
-
# Example: python3 scripts/diff-analyzer.py develop
|
|
46
|
+
# Example: python3 ${CLAUDE_SKILL_DIR}/scripts/diff-analyzer.py develop
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
The script outputs JSON with:
|
|
@@ -59,7 +59,7 @@ git reset --soft <commit-sha> # Keep changes staged
|
|
|
59
59
|
|
|
60
60
|
Run the rollback info script to assess current state before rolling back:
|
|
61
61
|
```bash
|
|
62
|
-
|
|
62
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/rollback_info.py
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Returns JSON with:
|
|
@@ -3,10 +3,10 @@ title: "SOP: Post-Release Testing"
|
|
|
3
3
|
category: procedures
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [sop, post-release, smoke-test, npm, sandbox, plugin-pack, provenance, isolation]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.1.0"
|
|
7
7
|
created: "2026-07-26"
|
|
8
|
-
last_updated: "2026-
|
|
9
|
-
description: "Smoke-test a published @softspark/ai-toolkit release from npm in an isolated HOME and npm prefix, without touching the maintainer's real install. Covers provenance, CLI, doctor, and the full plugin-pack lifecycle including the degraded-install path. Written for v4.18.0 and not run; v4.18.0 shipped a pack that broke every command it touched, and
|
|
8
|
+
last_updated: "2026-08-06"
|
|
9
|
+
description: "Smoke-test a published @softspark/ai-toolkit release from npm in an isolated HOME and npm prefix, without touching the maintainer's real install. Covers provenance, CLI, doctor, per-skill script resolution, scanner wiring, and the full plugin-pack lifecycle including the degraded-install path. Written for v4.18.0 and not run; v4.18.0 shipped a pack that broke every command it touched. First actually run on v4.22.0, which added Phases 4b and 4c after that release fixed four skills whose documented script path had never resolved and two that shipped a scanner nothing invoked."
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# SOP: Post-Release Testing
|
|
@@ -92,6 +92,95 @@ npm install -g --prefix "$SB/npm" "@softspark/ai-toolkit@${VERSION}"
|
|
|
92
92
|
`skills directory missing`.** That is the sandbox being empty, not a defect.
|
|
93
93
|
Install first, then judge doctor.
|
|
94
94
|
|
|
95
|
+
## Phase 4b: Skill scripts resolve and run from the installed copy
|
|
96
|
+
|
|
97
|
+
23 skills ship an executable script under `scripts/`. A skill body invokes it
|
|
98
|
+
through `${CLAUDE_SKILL_DIR}`, which only resolves once the skill is installed —
|
|
99
|
+
so a wrong path is invisible in the working tree and invisible to `validate.py`,
|
|
100
|
+
which checks that the file exists on disk, never that the documented command
|
|
101
|
+
finds it.
|
|
102
|
+
|
|
103
|
+
This is exactly how four skills shipped with `$(dirname "$0")`, which expands to
|
|
104
|
+
the shell's directory rather than the skill's. Every one of them had been broken
|
|
105
|
+
for as long as the line existed.
|
|
106
|
+
|
|
107
|
+
Iterate over every skill that ships a script. Do not hand-pick the ones the
|
|
108
|
+
release touched — the point of this phase is to catch the ones nobody remembered.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
for D in "$HOME"/.claude/skills/*/; do
|
|
112
|
+
s=$(basename "$D")
|
|
113
|
+
[ -d "$D/scripts" ] || continue
|
|
114
|
+
# Match ANY interpreter and ANY extension. Narrowing this to `python3` and
|
|
115
|
+
# `.py` is how the first version of this phase reported "no invocation" for a
|
|
116
|
+
# skill that used `python`, and missed one that ran a .py file through bash.
|
|
117
|
+
ref=$(grep -ohE '(python3?|bash|sh|node) +\$\{CLAUDE_SKILL_DIR\}/scripts/[A-Za-z0-9_.-]+' "$D/SKILL.md" | head -1)
|
|
118
|
+
[ -n "$ref" ] || { printf '%-22s NO ${CLAUDE_SKILL_DIR} INVOCATION\n' "$s"; continue; }
|
|
119
|
+
interp=${ref%% *}
|
|
120
|
+
rel=${ref##*\$\{CLAUDE_SKILL_DIR\}/}
|
|
121
|
+
printf '%-22s %-10s %-26s ' "$s" "$interp" "$rel"
|
|
122
|
+
[ -f "$D/$rel" ] || { echo 'PATH DOES NOT RESOLVE'; continue; }
|
|
123
|
+
out=$(CLAUDE_SKILL_DIR="$D" timeout 20 "$interp" "$D/$rel" --help </dev/null 2>&1 | head -1)
|
|
124
|
+
printf 'rc=%s %s\n' "$?" "$(echo "$out" | cut -c1-40)"
|
|
125
|
+
done
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Verify:**
|
|
129
|
+
- [ ] Every skill with a `scripts/` directory has a documented invocation
|
|
130
|
+
- [ ] Every documented path resolves to a file that exists
|
|
131
|
+
- [ ] Every script exits without a traceback and without hanging
|
|
132
|
+
- [ ] The interpreter matches the file: no `.py` through `bash`, no bare `python`
|
|
133
|
+
|
|
134
|
+
A skill reported as `NO ${CLAUDE_SKILL_DIR} INVOCATION` is not automatically a
|
|
135
|
+
defect — some ship assets rather than executables (`write-a-prd` ships `.html`,
|
|
136
|
+
`.js` and `.cjs`). Read the skill before filing it. What *is* always a defect is
|
|
137
|
+
an invocation that names a skill-owned script through any other path.
|
|
138
|
+
|
|
139
|
+
`validate.py` now fails the build on that class (`_validate_skill_script_invocations`),
|
|
140
|
+
so this phase is the second line rather than the first. Keep it: the validator
|
|
141
|
+
reasons about the source, this runs the real thing.
|
|
142
|
+
|
|
143
|
+
**`</dev/null` and `timeout` are not defensive padding.** Several of these
|
|
144
|
+
scripts are stdin filters (`error-parser.py`, `error-classifier.py`) documented
|
|
145
|
+
as `command 2>&1 | python3 …`. Probing one with `--help` and an open stdin
|
|
146
|
+
blocks forever, and the run looks like a slow test rather than a hung one. A
|
|
147
|
+
correct stdin filter answers an empty stdin with a JSON error and `rc=0`; a
|
|
148
|
+
traceback there is a real finding.
|
|
149
|
+
|
|
150
|
+
## Phase 4c: Skills that wrap a scanner actually scan
|
|
151
|
+
|
|
152
|
+
For any skill whose body tells the model to run a scanner, presence of the script
|
|
153
|
+
is not evidence the wiring works. Build a fixture with known defects and confirm
|
|
154
|
+
the scanner reports them.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
FX="$SB/fixture"; mkdir -p "$FX"
|
|
158
|
+
cat > "$FX/index.html" <<'EOF'
|
|
159
|
+
<!DOCTYPE html><html><head><title>t</title></head>
|
|
160
|
+
<body><h1>A</h1><h3>skipped h2</h3><img src="x.png"><input type="text"><div onclick="go()">click</div></body></html>
|
|
161
|
+
EOF
|
|
162
|
+
printf '{"name":"fx","dependencies":{"react":"18"}}\n' > "$FX/package.json"
|
|
163
|
+
|
|
164
|
+
for s in a11y-validate seo-validate hipaa-validate cve-scan; do
|
|
165
|
+
D="$HOME/.claude/skills/$s"
|
|
166
|
+
script=$(ls "$D"/scripts/*.py 2>/dev/null | head -1)
|
|
167
|
+
[ -n "$script" ] || continue
|
|
168
|
+
echo "=== $s ==="
|
|
169
|
+
CLAUDE_SKILL_DIR="$D" timeout 60 python3 "$script" "$FX" --output json </dev/null 2>&1 | head -3
|
|
170
|
+
done
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Verify:**
|
|
174
|
+
- [ ] The scanner returns findings, not an empty set — the fixture has real defects
|
|
175
|
+
- [ ] Findings span more than one category, proving the whole check set ran
|
|
176
|
+
- [ ] A scanner that exits non-zero on findings is doing its job, not failing
|
|
177
|
+
|
|
178
|
+
`a11y-validate` and `seo-validate` shipped working scanners that **no step in
|
|
179
|
+
either skill invoked** for their entire life before v4.22.0. The model was told to
|
|
180
|
+
grep the pattern tables by hand instead. Nothing in the test suite noticed,
|
|
181
|
+
because a script nobody calls still passes every check that asks whether it
|
|
182
|
+
exists.
|
|
183
|
+
|
|
95
184
|
## Phase 5: Plugin-pack lifecycle
|
|
96
185
|
|
|
97
186
|
Run this for any pack the release touched. For a pack that downloads a binary,
|
package/llms-full.txt
CHANGED
|
@@ -7019,10 +7019,10 @@ title: "SOP: Post-Release Testing"
|
|
|
7019
7019
|
category: procedures
|
|
7020
7020
|
service: ai-toolkit
|
|
7021
7021
|
tags: [sop, post-release, smoke-test, npm, sandbox, plugin-pack, provenance, isolation]
|
|
7022
|
-
version: "1.
|
|
7022
|
+
version: "1.1.0"
|
|
7023
7023
|
created: "2026-07-26"
|
|
7024
|
-
last_updated: "2026-
|
|
7025
|
-
description: "Smoke-test a published @softspark/ai-toolkit release from npm in an isolated HOME and npm prefix, without touching the maintainer's real install. Covers provenance, CLI, doctor, and the full plugin-pack lifecycle including the degraded-install path. Written for v4.18.0 and not run; v4.18.0 shipped a pack that broke every command it touched, and
|
|
7024
|
+
last_updated: "2026-08-06"
|
|
7025
|
+
description: "Smoke-test a published @softspark/ai-toolkit release from npm in an isolated HOME and npm prefix, without touching the maintainer's real install. Covers provenance, CLI, doctor, per-skill script resolution, scanner wiring, and the full plugin-pack lifecycle including the degraded-install path. Written for v4.18.0 and not run; v4.18.0 shipped a pack that broke every command it touched. First actually run on v4.22.0, which added Phases 4b and 4c after that release fixed four skills whose documented script path had never resolved and two that shipped a scanner nothing invoked."
|
|
7026
7026
|
---
|
|
7027
7027
|
|
|
7028
7028
|
# SOP: Post-Release Testing
|
|
@@ -7108,6 +7108,95 @@ npm install -g --prefix "$SB/npm" "@softspark/ai-toolkit@${VERSION}"
|
|
|
7108
7108
|
`skills directory missing`.** That is the sandbox being empty, not a defect.
|
|
7109
7109
|
Install first, then judge doctor.
|
|
7110
7110
|
|
|
7111
|
+
## Phase 4b: Skill scripts resolve and run from the installed copy
|
|
7112
|
+
|
|
7113
|
+
23 skills ship an executable script under `scripts/`. A skill body invokes it
|
|
7114
|
+
through `${CLAUDE_SKILL_DIR}`, which only resolves once the skill is installed —
|
|
7115
|
+
so a wrong path is invisible in the working tree and invisible to `validate.py`,
|
|
7116
|
+
which checks that the file exists on disk, never that the documented command
|
|
7117
|
+
finds it.
|
|
7118
|
+
|
|
7119
|
+
This is exactly how four skills shipped with `$(dirname "$0")`, which expands to
|
|
7120
|
+
the shell's directory rather than the skill's. Every one of them had been broken
|
|
7121
|
+
for as long as the line existed.
|
|
7122
|
+
|
|
7123
|
+
Iterate over every skill that ships a script. Do not hand-pick the ones the
|
|
7124
|
+
release touched — the point of this phase is to catch the ones nobody remembered.
|
|
7125
|
+
|
|
7126
|
+
```bash
|
|
7127
|
+
for D in "$HOME"/.claude/skills/*/; do
|
|
7128
|
+
s=$(basename "$D")
|
|
7129
|
+
[ -d "$D/scripts" ] || continue
|
|
7130
|
+
# Match ANY interpreter and ANY extension. Narrowing this to `python3` and
|
|
7131
|
+
# `.py` is how the first version of this phase reported "no invocation" for a
|
|
7132
|
+
# skill that used `python`, and missed one that ran a .py file through bash.
|
|
7133
|
+
ref=$(grep -ohE '(python3?|bash|sh|node) +\$\{CLAUDE_SKILL_DIR\}/scripts/[A-Za-z0-9_.-]+' "$D/SKILL.md" | head -1)
|
|
7134
|
+
[ -n "$ref" ] || { printf '%-22s NO ${CLAUDE_SKILL_DIR} INVOCATION\n' "$s"; continue; }
|
|
7135
|
+
interp=${ref%% *}
|
|
7136
|
+
rel=${ref##*\$\{CLAUDE_SKILL_DIR\}/}
|
|
7137
|
+
printf '%-22s %-10s %-26s ' "$s" "$interp" "$rel"
|
|
7138
|
+
[ -f "$D/$rel" ] || { echo 'PATH DOES NOT RESOLVE'; continue; }
|
|
7139
|
+
out=$(CLAUDE_SKILL_DIR="$D" timeout 20 "$interp" "$D/$rel" --help </dev/null 2>&1 | head -1)
|
|
7140
|
+
printf 'rc=%s %s\n' "$?" "$(echo "$out" | cut -c1-40)"
|
|
7141
|
+
done
|
|
7142
|
+
```
|
|
7143
|
+
|
|
7144
|
+
**Verify:**
|
|
7145
|
+
- [ ] Every skill with a `scripts/` directory has a documented invocation
|
|
7146
|
+
- [ ] Every documented path resolves to a file that exists
|
|
7147
|
+
- [ ] Every script exits without a traceback and without hanging
|
|
7148
|
+
- [ ] The interpreter matches the file: no `.py` through `bash`, no bare `python`
|
|
7149
|
+
|
|
7150
|
+
A skill reported as `NO ${CLAUDE_SKILL_DIR} INVOCATION` is not automatically a
|
|
7151
|
+
defect — some ship assets rather than executables (`write-a-prd` ships `.html`,
|
|
7152
|
+
`.js` and `.cjs`). Read the skill before filing it. What *is* always a defect is
|
|
7153
|
+
an invocation that names a skill-owned script through any other path.
|
|
7154
|
+
|
|
7155
|
+
`validate.py` now fails the build on that class (`_validate_skill_script_invocations`),
|
|
7156
|
+
so this phase is the second line rather than the first. Keep it: the validator
|
|
7157
|
+
reasons about the source, this runs the real thing.
|
|
7158
|
+
|
|
7159
|
+
**`</dev/null` and `timeout` are not defensive padding.** Several of these
|
|
7160
|
+
scripts are stdin filters (`error-parser.py`, `error-classifier.py`) documented
|
|
7161
|
+
as `command 2>&1 | python3 …`. Probing one with `--help` and an open stdin
|
|
7162
|
+
blocks forever, and the run looks like a slow test rather than a hung one. A
|
|
7163
|
+
correct stdin filter answers an empty stdin with a JSON error and `rc=0`; a
|
|
7164
|
+
traceback there is a real finding.
|
|
7165
|
+
|
|
7166
|
+
## Phase 4c: Skills that wrap a scanner actually scan
|
|
7167
|
+
|
|
7168
|
+
For any skill whose body tells the model to run a scanner, presence of the script
|
|
7169
|
+
is not evidence the wiring works. Build a fixture with known defects and confirm
|
|
7170
|
+
the scanner reports them.
|
|
7171
|
+
|
|
7172
|
+
```bash
|
|
7173
|
+
FX="$SB/fixture"; mkdir -p "$FX"
|
|
7174
|
+
cat > "$FX/index.html" <<'EOF'
|
|
7175
|
+
<!DOCTYPE html><html><head><title>t</title></head>
|
|
7176
|
+
<body><h1>A</h1><h3>skipped h2</h3><img src="x.png"><input type="text"><div onclick="go()">click</div></body></html>
|
|
7177
|
+
EOF
|
|
7178
|
+
printf '{"name":"fx","dependencies":{"react":"18"}}\n' > "$FX/package.json"
|
|
7179
|
+
|
|
7180
|
+
for s in a11y-validate seo-validate hipaa-validate cve-scan; do
|
|
7181
|
+
D="$HOME/.claude/skills/$s"
|
|
7182
|
+
script=$(ls "$D"/scripts/*.py 2>/dev/null | head -1)
|
|
7183
|
+
[ -n "$script" ] || continue
|
|
7184
|
+
echo "=== $s ==="
|
|
7185
|
+
CLAUDE_SKILL_DIR="$D" timeout 60 python3 "$script" "$FX" --output json </dev/null 2>&1 | head -3
|
|
7186
|
+
done
|
|
7187
|
+
```
|
|
7188
|
+
|
|
7189
|
+
**Verify:**
|
|
7190
|
+
- [ ] The scanner returns findings, not an empty set — the fixture has real defects
|
|
7191
|
+
- [ ] Findings span more than one category, proving the whole check set ran
|
|
7192
|
+
- [ ] A scanner that exits non-zero on findings is doing its job, not failing
|
|
7193
|
+
|
|
7194
|
+
`a11y-validate` and `seo-validate` shipped working scanners that **no step in
|
|
7195
|
+
either skill invoked** for their entire life before v4.22.0. The model was told to
|
|
7196
|
+
grep the pattern tables by hand instead. Nothing in the test suite noticed,
|
|
7197
|
+
because a script nobody calls still passes every check that asks whether it
|
|
7198
|
+
exists.
|
|
7199
|
+
|
|
7111
7200
|
## Phase 5: Plugin-pack lifecycle
|
|
7112
7201
|
|
|
7113
7202
|
Run this for any pack the release touched. For a pack that downloads a binary,
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.22.
|
|
3
|
+
"version": "4.22.1",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/scripts/validate.py
CHANGED
|
@@ -379,6 +379,71 @@ def _validate_skill_frontmatter(tk_dir: Path, skill_path: Path,
|
|
|
379
379
|
vr.warn(f"{name} - Description exceeds 1024 characters")
|
|
380
380
|
|
|
381
381
|
|
|
382
|
+
def _validate_skill_script_invocations(skill_path: Path, vr: ValidationResult) -> None:
|
|
383
|
+
"""Every documented run of a skill-owned script must resolve after install.
|
|
384
|
+
|
|
385
|
+
A skill's own scripts live beside it and are reachable only through
|
|
386
|
+
`${CLAUDE_SKILL_DIR}`. A relative path such as `python3 scripts/foo.py`
|
|
387
|
+
resolves against the user's working directory, and a repo-relative one such
|
|
388
|
+
as `app/skills/<name>/scripts/foo.py` against a tree the user does not have.
|
|
389
|
+
Both look correct in the source and fail for every installed user.
|
|
390
|
+
|
|
391
|
+
Nine skills shipped with one of those forms before this check existed.
|
|
392
|
+
`_validate_skill_references` never caught them because it asks whether the
|
|
393
|
+
file is on disk, not whether the documented command can find it.
|
|
394
|
+
|
|
395
|
+
Only executable invocations are checked. The frontmatter `scripts:` list
|
|
396
|
+
declares ownership and is correctly relative; prose that names a script in
|
|
397
|
+
backticks runs nothing.
|
|
398
|
+
"""
|
|
399
|
+
name = skill_path.name
|
|
400
|
+
scripts_dir = skill_path / "scripts"
|
|
401
|
+
if not scripts_dir.is_dir():
|
|
402
|
+
return
|
|
403
|
+
owned = {p.name for p in scripts_dir.iterdir() if p.is_file()}
|
|
404
|
+
if not owned:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
skill_file = skill_path / "SKILL.md"
|
|
408
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
409
|
+
|
|
410
|
+
# Skip the frontmatter block: `scripts:` entries there are declarations.
|
|
411
|
+
lines = text.splitlines()
|
|
412
|
+
start = 0
|
|
413
|
+
if lines and lines[0].strip() == "---":
|
|
414
|
+
for idx in range(1, len(lines)):
|
|
415
|
+
if lines[idx].strip() == "---":
|
|
416
|
+
start = idx + 1
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
interpreters = {"python3": {".py"}, "python": {".py"}, "bash": {".sh"}, "sh": {".sh"}}
|
|
420
|
+
invocation = re.compile(r"\b(python3|python|bash|sh)\s+(\S+)")
|
|
421
|
+
|
|
422
|
+
for offset, line in enumerate(lines[start:], start=start + 1):
|
|
423
|
+
for interp, target in invocation.findall(line):
|
|
424
|
+
base = target.rsplit("/", 1)[-1].strip("\"'`")
|
|
425
|
+
if base not in owned:
|
|
426
|
+
continue # a repo-level script such as scripts/validate.py
|
|
427
|
+
if "${CLAUDE_SKILL_DIR}" not in target:
|
|
428
|
+
vr.error(
|
|
429
|
+
f"skills/{name}/SKILL.md:{offset}: runs its own '{base}' via "
|
|
430
|
+
f"'{target}' - use ${{CLAUDE_SKILL_DIR}}/scripts/{base}, which "
|
|
431
|
+
f"is the only path that resolves after install"
|
|
432
|
+
)
|
|
433
|
+
suffix = base[base.rfind("."):] if "." in base else ""
|
|
434
|
+
allowed = interpreters.get(interp, set())
|
|
435
|
+
if suffix and allowed and suffix not in allowed:
|
|
436
|
+
vr.error(
|
|
437
|
+
f"skills/{name}/SKILL.md:{offset}: runs '{base}' with "
|
|
438
|
+
f"'{interp}' - wrong interpreter for a {suffix} file"
|
|
439
|
+
)
|
|
440
|
+
elif interp == "python":
|
|
441
|
+
vr.warn(
|
|
442
|
+
f"skills/{name}/SKILL.md:{offset}: uses 'python' for '{base}' "
|
|
443
|
+
f"- prefer 'python3', 'python' is absent or Python 2 on many systems"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
382
447
|
def _validate_skill_references(tk_dir: Path, skill_path: Path,
|
|
383
448
|
fm_lines: list[str], vr: ValidationResult) -> None:
|
|
384
449
|
"""Validate agent refs, depends-on, context/agent co-occurrence, and reference links."""
|
|
@@ -412,6 +477,8 @@ def _validate_skill_references(tk_dir: Path, skill_path: Path,
|
|
|
412
477
|
if _fm_has(fm_lines, "run-mode"):
|
|
413
478
|
vr.warn(f"{name} - Uses deprecated 'run-mode' field (rename to 'context:')")
|
|
414
479
|
|
|
480
|
+
_validate_skill_script_invocations(skill_path, vr)
|
|
481
|
+
|
|
415
482
|
ref_dir = skill_path / "reference"
|
|
416
483
|
if ref_dir.is_dir():
|
|
417
484
|
content = skill_file.read_text(encoding="utf-8")
|