@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 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: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-109-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1508%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1513%20passing-success)](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.0",
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 app/skills/brand-voice/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.
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
 
@@ -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
- python ${CLAUDE_SKILL_DIR}/scripts/visualize.py .
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
- target = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve()
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')
@@ -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).
@@ -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
- bash scripts/rollback_info.py
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.0.0"
6
+ version: "1.1.0"
7
7
  created: "2026-07-26"
8
- last_updated: "2026-07-26"
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 every step here would have caught it."
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.0.0"
7022
+ version: "1.1.0"
7023
7023
  created: "2026-07-26"
7024
- last_updated: "2026-07-26"
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 every step here would have caught it."
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.22.0",
2
+ "version": "4.22.1",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.22.0",
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",
@@ -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")