@softspark/ai-toolkit 4.22.0 → 4.23.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/CHANGELOG.md +66 -0
- package/README.md +18 -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/hipaa-validate/SKILL.md +3 -0
- package/app/skills/hipaa-validate/scripts/hipaa_scan.py +47 -7
- 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,72 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.23.0 — a compliance scanner that says nothing must say why (2026-08-06)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`hipaa-validate` scanned a manifest-less project silently clean.** Category
|
|
15
|
+
1, 3, 4 and 7 patterns are language-tagged and fire only for a detected
|
|
16
|
+
language, and detection read manifest files only. A directory of Python with
|
|
17
|
+
PHI in its loggers therefore reported `HIGH: 0` while every Python rule sat
|
|
18
|
+
unused — the worst possible output for a compliance tool, because zero reads
|
|
19
|
+
as compliant. Found by the post-release SOP on v4.22.1.
|
|
20
|
+
|
|
21
|
+
Detection now falls back to the extensions of the files being scanned when no
|
|
22
|
+
manifest is present. **Manifests still win**, so a project that already
|
|
23
|
+
declared itself keeps exactly the behaviour it had.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **The scan summary reports how the language was decided.** A new
|
|
28
|
+
`language_detection` field says `manifest`, `file extensions (no manifest
|
|
29
|
+
found)`, or `none`. When it is `none` the text report writes to stderr that
|
|
30
|
+
language-tagged patterns did not run and that a zero-finding result means
|
|
31
|
+
unscanned rather than compliant.
|
|
32
|
+
|
|
33
|
+
**Upgrade note:** a project scanned without a manifest may now report HIGH
|
|
34
|
+
findings where v4.22.x reported none. Those findings were always there. A CI
|
|
35
|
+
job gating on the exit code can start failing on real PHI exposure.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## v4.22.1 — nine skills, not four, could not find their own scripts (2026-08-06)
|
|
40
|
+
|
|
41
|
+
v4.22.0 claimed to fix four skills whose documented script path never resolved.
|
|
42
|
+
The first real run of the post-release SOP found five more, plus a wrong
|
|
43
|
+
interpreter and a script that crashed on `--help`. The v4.22.0 fix had grepped
|
|
44
|
+
for one spelling of the bug and missed the others.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Five more skills ran their own script through a path that cannot resolve
|
|
49
|
+
after install**: `brand-voice` (repo-relative `app/skills/.../measure.py`),
|
|
50
|
+
`ci`, `deploy`, `migrate` and `rollback` (cwd-relative `scripts/*.py`). Four
|
|
51
|
+
further example lines in `debug`, `fix`, `pr` and `review` had the same defect
|
|
52
|
+
in a secondary snippet while the primary invocation was already correct.
|
|
53
|
+
- **`rollback` ran a Python file through `bash`.** `bash scripts/rollback_info.py`
|
|
54
|
+
was wrong twice over.
|
|
55
|
+
- **`explore` used `python` rather than `python3`**, which is missing or Python 2
|
|
56
|
+
on many systems.
|
|
57
|
+
- **`explore`'s `visualize.py` crashed with a traceback on `--help`**, treating
|
|
58
|
+
the flag as a directory to scan. It now prints usage, and reports a JSON error
|
|
59
|
+
with exit 1 for a path that does not exist.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **`validate.py` fails the build on the whole class.**
|
|
64
|
+
`_validate_skill_script_invocations` checks every documented command that runs
|
|
65
|
+
a skill-owned script: the path must go through `${CLAUDE_SKILL_DIR}`, and the
|
|
66
|
+
interpreter must match the file. `python` instead of `python3` is a warning.
|
|
67
|
+
Repo-level scripts such as `scripts/validate.py` are correctly ignored, and the
|
|
68
|
+
frontmatter `scripts:` list stays relative because it declares rather than runs.
|
|
69
|
+
- **Post-release SOP phases 4b and 4c** (`post-release-testing-sop.md` v1.1.0):
|
|
70
|
+
run every skill's documented invocation from the installed copy, and prove
|
|
71
|
+
scanner-wrapping skills actually scan a fixture with known defects. Phase 4b
|
|
72
|
+
found every defect in this release on its first run.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
10
76
|
## v4.22.0 — the public surface is a test, not a promise (2026-08-06)
|
|
11
77
|
|
|
12
78
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,24 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.23.0
|
|
12
|
+
|
|
13
|
+
**v4.23.0** — `hipaa-validate` scanned a project without a manifest silently
|
|
14
|
+
clean: its language-tagged patterns never ran, and the report said `HIGH: 0`.
|
|
15
|
+
Zero reads as compliant. Detection now falls back to file extensions, the summary
|
|
16
|
+
says how the language was decided, and an undecidable run warns that a zero means
|
|
17
|
+
unscanned. Manifest-declaring projects are unaffected. Found by the post-release
|
|
18
|
+
SOP, not by a user.
|
|
19
|
+
|
|
20
|
+
## What's New in v4.22.1
|
|
21
|
+
|
|
22
|
+
**v4.22.1** — nine skills, not four, could not find their own scripts. v4.22.0
|
|
23
|
+
grepped for one spelling of the bug and missed five more, a `.py` file run
|
|
24
|
+
through `bash`, and a script that crashed on `--help`. `validate.py` now fails
|
|
25
|
+
the build on the entire class, and the post-release SOP runs every skill's
|
|
26
|
+
documented command from the installed copy — which is how these were found.
|
|
10
27
|
|
|
11
28
|
## What's New in v4.22.0
|
|
12
29
|
|
|
@@ -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.
|
|
6
|
+
"version": "4.23.0",
|
|
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:
|
|
@@ -161,6 +161,9 @@ This distinction helps compliance officers prioritize immediate remediation (def
|
|
|
161
161
|
|
|
162
162
|
## Gotchas
|
|
163
163
|
|
|
164
|
+
- **Check `language_detection` in the summary before trusting a zero.** Category 1, 3, 4 and 7 patterns are language-tagged and only fire for a detected language. Manifests decide first (`pyproject.toml`, `package.json`, `go.mod`, …); without one the scanner falls back to file extensions. If it reports `languages: ["any"]` with `language_detection: "none"`, the language rules never ran and `HIGH: 0` means *unscanned*, not *compliant* — the scanner prints that warning to stderr, so a run whose stderr is discarded loses it.
|
|
165
|
+
- Scanning a monorepo package or a subdirectory can put you below the manifest. The extension fallback covers the common case, but a directory of `.sql`, `.yaml` or templates resolves to no language at all — scan from the level that holds the manifest.
|
|
166
|
+
|
|
164
167
|
- Test fixtures and seed data often contain **synthetic** PHI that looks real (SSN-shaped IDs, formatted phone numbers, sample email addresses). Flag them but lower severity — production code handling the same patterns is the actual risk.
|
|
165
168
|
- HIPAA §164.312(b) requires audit logging but does not specify a format. "Logs exist" is not evidence of compliance — the logs must capture WHO (authenticated user), WHAT (action), WHEN (timestamp), WHERE (resource), and they must be immutable (append-only or write-once storage).
|
|
166
169
|
- Encryption-at-rest varies silently by storage layer. RDS auto-encrypts new volumes since 2017, but older DB snapshots may not be; S3 bucket policies can override instance-level encryption. Treat "encryption enabled" as a claim to verify with the cloud provider, not a state to trust.
|
|
@@ -302,8 +302,24 @@ def context_gate(files: list[Path], keywords: list[str],
|
|
|
302
302
|
# Language detection (Step 1)
|
|
303
303
|
# ---------------------------------------------------------------------------
|
|
304
304
|
|
|
305
|
-
def detect_languages(root: Path) -> set[str]:
|
|
306
|
-
"""Detect project languages
|
|
305
|
+
def detect_languages(root: Path, files: list[Path] | None = None) -> tuple[set[str], str]:
|
|
306
|
+
"""Detect project languages. Returns (languages, how_they_were_detected).
|
|
307
|
+
|
|
308
|
+
Manifest files decide when present. When none is found, fall back to the
|
|
309
|
+
extensions of the files actually being scanned.
|
|
310
|
+
|
|
311
|
+
The fallback exists because language-tagged patterns fire only when the
|
|
312
|
+
project language includes the tag, so a manifest-less directory of Python
|
|
313
|
+
scanned clean while every Python rule sat unused — and the report said
|
|
314
|
+
`HIGH: 0`, which reads as compliant rather than unscanned. Silence is the
|
|
315
|
+
worst possible output for a compliance tool. A monorepo package, a scan
|
|
316
|
+
pointed at a subdirectory, or a service whose manifest lives one level up
|
|
317
|
+
all hit this.
|
|
318
|
+
|
|
319
|
+
Manifests still win: a project that declares itself keeps exactly the
|
|
320
|
+
behaviour it had, so upgrading changes nothing for anyone who was already
|
|
321
|
+
being scanned properly.
|
|
322
|
+
"""
|
|
307
323
|
langs = set()
|
|
308
324
|
for indicator, lang in LANGUAGE_INDICATORS.items():
|
|
309
325
|
if "*" in indicator:
|
|
@@ -314,7 +330,20 @@ def detect_languages(root: Path) -> set[str]:
|
|
|
314
330
|
else:
|
|
315
331
|
if (root / indicator).exists():
|
|
316
332
|
langs.add(lang)
|
|
317
|
-
|
|
333
|
+
if langs:
|
|
334
|
+
return langs, "manifest"
|
|
335
|
+
|
|
336
|
+
if files:
|
|
337
|
+
ext_to_lang: dict[str, set[str]] = {}
|
|
338
|
+
for lang, exts in LANG_EXTENSIONS.items():
|
|
339
|
+
for ext in exts:
|
|
340
|
+
ext_to_lang.setdefault(ext, set()).add(lang)
|
|
341
|
+
for f in files:
|
|
342
|
+
langs |= ext_to_lang.get(f.suffix, set())
|
|
343
|
+
if langs:
|
|
344
|
+
return langs, "file extensions (no manifest found)"
|
|
345
|
+
|
|
346
|
+
return {"any"}, "none — language-tagged patterns were skipped"
|
|
318
347
|
|
|
319
348
|
|
|
320
349
|
# ---------------------------------------------------------------------------
|
|
@@ -616,7 +645,8 @@ def format_text_report(findings: list[dict], mode: str,
|
|
|
616
645
|
|
|
617
646
|
def format_json_report(findings: list[dict], mode: str,
|
|
618
647
|
phi_count: int, scanned_count: int,
|
|
619
|
-
categories_run: list[int], languages: list[str]
|
|
648
|
+
categories_run: list[int], languages: list[str],
|
|
649
|
+
language_source: str = "manifest") -> str:
|
|
620
650
|
"""Format findings as structured JSON for CI integration."""
|
|
621
651
|
high_count = sum(1 for f in findings if f["severity"] == "HIGH")
|
|
622
652
|
warn_count = sum(1 for f in findings if f["severity"] == "WARN")
|
|
@@ -628,6 +658,7 @@ def format_json_report(findings: list[dict], mode: str,
|
|
|
628
658
|
"files_scanned": scanned_count,
|
|
629
659
|
"categories_run": sorted(categories_run),
|
|
630
660
|
"languages": sorted(languages),
|
|
661
|
+
"language_detection": language_source,
|
|
631
662
|
"high": high_count,
|
|
632
663
|
"warn": warn_count,
|
|
633
664
|
"total": len(findings),
|
|
@@ -723,8 +754,9 @@ def main() -> None:
|
|
|
723
754
|
file=sys.stderr)
|
|
724
755
|
print("Consider narrowing the scan path for targeted results.", file=sys.stderr)
|
|
725
756
|
|
|
726
|
-
# Step 1: Detect languages
|
|
727
|
-
|
|
757
|
+
# Step 1: Detect languages. Categories 1-2 scan everything, so the extension
|
|
758
|
+
# fallback looks at all collected files, not just the PHI-adjacent subset.
|
|
759
|
+
languages, lang_source = detect_languages(root, all_files)
|
|
728
760
|
|
|
729
761
|
# Step 2: Run categories
|
|
730
762
|
findings: list[dict] = []
|
|
@@ -789,10 +821,18 @@ def main() -> None:
|
|
|
789
821
|
if args.output == "json":
|
|
790
822
|
print(format_json_report(findings, args.mode, phi_count,
|
|
791
823
|
scanned_count, categories_run,
|
|
792
|
-
sorted(languages)))
|
|
824
|
+
sorted(languages), lang_source))
|
|
793
825
|
else:
|
|
794
826
|
print(format_text_report(findings, args.mode, phi_count,
|
|
795
827
|
scanned_count, categories_run))
|
|
828
|
+
if languages == {"any"}:
|
|
829
|
+
# Never let an unscanned run read as a clean one.
|
|
830
|
+
print("\nWARNING: no project language identified "
|
|
831
|
+
f"({lang_source}).", file=sys.stderr)
|
|
832
|
+
print("Language-tagged patterns (Python, JS/TS, Go, Java, Ruby, C#) "
|
|
833
|
+
"did NOT run.", file=sys.stderr)
|
|
834
|
+
print("A zero-finding result here means unscanned, not compliant. "
|
|
835
|
+
"Scan from the directory holding the manifest.", file=sys.stderr)
|
|
796
836
|
|
|
797
837
|
# Exit code: non-zero if HIGH findings
|
|
798
838
|
high_count = sum(1 for f in findings if f["severity"] == "HIGH")
|
|
@@ -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.
|
|
3
|
+
"version": "4.23.0",
|
|
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")
|