@softspark/ai-toolkit 4.22.1 → 4.23.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 +65 -0
- package/README.md +12 -35
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/hipaa-validate/SKILL.md +3 -0
- package/app/skills/hipaa-validate/scripts/hipaa_scan.py +47 -7
- package/kb/procedures/maintenance-sop.md +26 -4
- package/kb/reference/architecture-overview.md +5 -2
- package/kb/reference/skill-templates.md +37 -4
- package/kb/reference/unique-features.md +24 -1
- package/llms-full.txt +92 -11
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/plugin.py +21 -4
- package/scripts/plugin_schema.py +14 -2
- package/scripts/validate.py +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,71 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.23.1 — a pack could ship an agent nobody installed (2026-08-18)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`plugin install` never linked an agent the pack shipped itself.**
|
|
15
|
+
`_install_claude_agents` resolved `includes.agents` only against the toolkit's
|
|
16
|
+
own `app/agents/<name>.md`, so a self-contained pack got `WARN agent not found`
|
|
17
|
+
and the agent silently stayed uninstalled. The asymmetry was visible inside the
|
|
18
|
+
same file: skills already resolved through `_resolve_skill_source`, which falls
|
|
19
|
+
back to the pack directory, and `_remove_claude_pack_links` already unlinked
|
|
20
|
+
agent symlinks pointing into a pack — a removal path for links the installer
|
|
21
|
+
could never create.
|
|
22
|
+
|
|
23
|
+
Agents now go through `_resolve_agent_source`, mirroring skills: a core agent in
|
|
24
|
+
`app/agents` still wins, otherwise `<pack>/agents/<name>.md` is linked. Packs
|
|
25
|
+
that merely reference a core agent behave exactly as before.
|
|
26
|
+
|
|
27
|
+
- **`validate.py` reported self-contained packs as broken.** `validate_references`
|
|
28
|
+
checked `app/agents` and `app/skills` only, so a pack shipping its own assets
|
|
29
|
+
drew `References missing agent: X` and `References missing skill: Y` even though
|
|
30
|
+
the installer handled the skill correctly. It now also accepts
|
|
31
|
+
`<pack>/agents/<name>.md` and `<pack>/skills/<name>/SKILL.md`; a genuinely
|
|
32
|
+
missing reference still errors. `pack_dir` is an optional argument, so existing
|
|
33
|
+
callers are unaffected.
|
|
34
|
+
|
|
35
|
+
Found by the post-release smoke test of a downstream pack (`legal-pl-pack`),
|
|
36
|
+
not by a user report.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Three tests covering the class.** `tests/test_plugin.bats` builds a throwaway
|
|
41
|
+
toolkit whose only pack ships its own agent and skill, then asserts the agent is
|
|
42
|
+
linked into the pack (not `app/agents`), that removal drops the link, and that
|
|
43
|
+
reference validation accepts pack-shipped assets while still failing on a
|
|
44
|
+
genuinely missing one.
|
|
45
|
+
|
|
46
|
+
## v4.23.0 — a compliance scanner that says nothing must say why (2026-08-06)
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **`hipaa-validate` scanned a manifest-less project silently clean.** Category
|
|
51
|
+
1, 3, 4 and 7 patterns are language-tagged and fire only for a detected
|
|
52
|
+
language, and detection read manifest files only. A directory of Python with
|
|
53
|
+
PHI in its loggers therefore reported `HIGH: 0` while every Python rule sat
|
|
54
|
+
unused — the worst possible output for a compliance tool, because zero reads
|
|
55
|
+
as compliant. Found by the post-release SOP on v4.22.1.
|
|
56
|
+
|
|
57
|
+
Detection now falls back to the extensions of the files being scanned when no
|
|
58
|
+
manifest is present. **Manifests still win**, so a project that already
|
|
59
|
+
declared itself keeps exactly the behaviour it had.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **The scan summary reports how the language was decided.** A new
|
|
64
|
+
`language_detection` field says `manifest`, `file extensions (no manifest
|
|
65
|
+
found)`, or `none`. When it is `none` the text report writes to stderr that
|
|
66
|
+
language-tagged patterns did not run and that a zero-finding result means
|
|
67
|
+
unscanned rather than compliant.
|
|
68
|
+
|
|
69
|
+
**Upgrade note:** a project scanned without a manifest may now report HIGH
|
|
70
|
+
findings where v4.22.x reported none. Those findings were always there. A CI
|
|
71
|
+
job gating on the exit code can start failing on real PHI exposure.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
10
75
|
## v4.22.1 — nine skills, not four, could not find their own scripts (2026-08-06)
|
|
11
76
|
|
|
12
77
|
v4.22.0 claimed to fix four skills whose documented script path never resolved.
|
package/README.md
CHANGED
|
@@ -6,41 +6,18 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[` expands to the shell's directory, not the skill's.
|
|
9
|
+
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.23.1
|
|
12
|
+
|
|
13
|
+
**v4.23.1** — a plugin pack that shipped its own agent never got it installed.
|
|
14
|
+
`plugin install` resolved `includes.agents` against the toolkit's own
|
|
15
|
+
`app/agents/`, printed `WARN agent not found`, and moved on — while the removal
|
|
16
|
+
path already knew how to unlink a pack-owned agent, and skills had resolved from
|
|
17
|
+
the pack directory all along. `validate.py` had the same blind spot for both
|
|
18
|
+
agents and skills, so a self-contained pack was reported broken twice over.
|
|
19
|
+
Agents now resolve from the pack first-class, core assets still win, and
|
|
20
|
+
validation accepts either. Found by a downstream pack's post-release smoke test.
|
|
44
21
|
|
|
45
22
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
46
23
|
|
|
@@ -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.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -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")
|
|
@@ -3,9 +3,9 @@ title: "SOP: AI Toolkit Maintenance"
|
|
|
3
3
|
category: procedures
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [sop, maintenance, agents, skills, install]
|
|
6
|
-
version: "3.
|
|
6
|
+
version: "3.4.0"
|
|
7
7
|
created: "2026-03-23"
|
|
8
|
-
last_updated: "2026-
|
|
8
|
+
last_updated: "2026-08-06"
|
|
9
9
|
description: "Standard operating procedures for installing, maintaining, and evolving the ai-toolkit."
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -209,8 +209,30 @@ that runtime should receive the change.
|
|
|
209
209
|
user-invocable: false # knowledge skill
|
|
210
210
|
---
|
|
211
211
|
```
|
|
212
|
-
2.
|
|
213
|
-
|
|
212
|
+
2. If the skill ships an executable, put it in `app/skills/<skill-name>/scripts/`
|
|
213
|
+
and invoke it **only** as:
|
|
214
|
+
```bash
|
|
215
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/<name>.py [args]
|
|
216
|
+
```
|
|
217
|
+
`${CLAUDE_SKILL_DIR}` is the sole path that resolves once the skill is
|
|
218
|
+
installed. `scripts/<name>.py` resolves against the user's working directory,
|
|
219
|
+
`app/skills/<name>/scripts/…` against a repo they do not have, and
|
|
220
|
+
`$(dirname "$0")` against the shell's directory — all three look right in the
|
|
221
|
+
source and fail for every user. Nine skills shipped with one of them before
|
|
222
|
+
`validate.py` started failing the build on it.
|
|
223
|
+
|
|
224
|
+
Match the interpreter to the file (`python3` for `.py`, never `bash`, never
|
|
225
|
+
bare `python`). The frontmatter `scripts:` list stays relative — it declares
|
|
226
|
+
ownership rather than running anything.
|
|
227
|
+
|
|
228
|
+
Add `--help` handling. A script that treats `--help` as a positional argument
|
|
229
|
+
greets the user with a traceback, and one that reads stdin must answer an
|
|
230
|
+
empty stdin with an error rather than blocking forever.
|
|
231
|
+
3. Update `kb/reference/skills-catalog.md` and `app/ARCHITECTURE.md`
|
|
232
|
+
4. Run `scripts/validate.py` — it checks the invocation, the body budget, and
|
|
233
|
+
`reference/` link resolution
|
|
234
|
+
5. Run `python3 scripts/surface_manifest.py` before the next release to adopt the
|
|
235
|
+
new skill into the protected surface
|
|
214
236
|
|
|
215
237
|
## Adding a New Hook
|
|
216
238
|
|
|
@@ -3,7 +3,7 @@ title: "AI Toolkit - Architecture Overview"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [architecture, overview, design, structure]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.7.0"
|
|
7
7
|
created: "2026-03-23"
|
|
8
8
|
last_updated: "2026-08-06"
|
|
9
9
|
description: "Architecture of ai-toolkit: directory layout, Claude app export, global install model, editor-aware MCP install, Codex translation layer, skill tiers, and integration with projects."
|
|
@@ -50,7 +50,10 @@ ai-toolkit/
|
|
|
50
50
|
inject_section_cli.py # Marker-based content injection (canonical implementation)
|
|
51
51
|
_common.py # Shared helper for generators (frontmatter, agents/skills emission)
|
|
52
52
|
merge-hooks.py # JSON merge for hooks into settings.json (inject/strip modes)
|
|
53
|
-
validate.py # Toolkit integrity check
|
|
53
|
+
validate.py # Toolkit integrity check (+ skill body budget, script-invocation gate)
|
|
54
|
+
surface_manifest.py # Public-surface snapshot vs app/surface.json; removals fail the build
|
|
55
|
+
check_split.py # Split gate: proves a SKILL.md -> reference/ refactor lost nothing
|
|
56
|
+
sync_badges.py # Derives README count badges from the tree (runs inside generate:all)
|
|
54
57
|
evaluate_skills.py # Skill quality report
|
|
55
58
|
generate_agents_md.py # Regenerates AGENTS.md
|
|
56
59
|
generate_cursor_rules.py # Generates .cursorrules (sources _common.py)
|
|
@@ -3,10 +3,10 @@ title: "AI Toolkit - Skill Templates"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [templates, scaffolding, create, skills]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.1.0"
|
|
7
7
|
created: "2026-03-29"
|
|
8
|
-
last_updated: "2026-
|
|
9
|
-
description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge."
|
|
8
|
+
last_updated: "2026-08-06"
|
|
9
|
+
description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge. Includes the ${CLAUDE_SKILL_DIR} rule for shipping executable scripts."
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Skill Templates
|
|
@@ -47,4 +47,37 @@ Templates are stored in `app/templates/skill/{type}/SKILL.md.template`.
|
|
|
47
47
|
|
|
48
48
|
1. Edit the generated `app/skills/{name}/SKILL.md`
|
|
49
49
|
2. Add `reference/` or `templates/` subdirectories if needed
|
|
50
|
-
3.
|
|
50
|
+
3. If the skill ships an executable, invoke it **only** through
|
|
51
|
+
`${CLAUDE_SKILL_DIR}` — see [Shipping a script](#shipping-a-script)
|
|
52
|
+
4. Run `ai-toolkit validate` to verify
|
|
53
|
+
|
|
54
|
+
## Shipping a script
|
|
55
|
+
|
|
56
|
+
Put executables in `app/skills/{name}/scripts/` and document them as:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/{name}.py [args]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That is the only path that resolves once a skill is installed and symlinked into
|
|
63
|
+
`~/.claude/skills/`. Three other spellings look correct in the repo and fail for
|
|
64
|
+
every user:
|
|
65
|
+
|
|
66
|
+
| Spelling | Resolves against | Result after install |
|
|
67
|
+
|----------|------------------|----------------------|
|
|
68
|
+
| `scripts/foo.py` | the user's working directory | not found |
|
|
69
|
+
| `app/skills/{name}/scripts/foo.py` | a repo the user does not have | not found |
|
|
70
|
+
| `$(dirname "$0")/scripts/foo.py` | the shell's directory | not found |
|
|
71
|
+
|
|
72
|
+
Nine skills shipped with one of these before `validate.py` began failing the
|
|
73
|
+
build on it. The check also enforces that the interpreter matches the file — a
|
|
74
|
+
`.py` run through `bash` is an error, and bare `python` a warning, because it is
|
|
75
|
+
missing or Python 2 on many systems.
|
|
76
|
+
|
|
77
|
+
The frontmatter `scripts:` list stays relative. It declares which files the skill
|
|
78
|
+
owns; it does not run them.
|
|
79
|
+
|
|
80
|
+
**Give the script `--help`.** A script that treats `--help` as a positional
|
|
81
|
+
argument answers with a traceback, and a stdin filter with no input must return
|
|
82
|
+
an error rather than blocking forever — the post-release SOP probes every shipped
|
|
83
|
+
script exactly this way.
|
|
@@ -4,7 +4,7 @@ category: reference
|
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [features, differentiators, constitution, hooks, security, tdd, memory]
|
|
6
6
|
created: "2026-04-13"
|
|
7
|
-
last_updated: "2026-
|
|
7
|
+
last_updated: "2026-08-06"
|
|
8
8
|
description: "Detailed description of ai-toolkit's unique features: constitution enforcement, hooks system, security scanning, effort budgeting, quality gates, and more."
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -109,6 +109,29 @@ The `Stop` hook runs after every response across 5 languages:
|
|
|
109
109
|
| Dart | dart analyze | dart analyze |
|
|
110
110
|
| Go | go vet | go vet |
|
|
111
111
|
|
|
112
|
+
## 5b. Repo-Integrity Gates (`validate.py` + `npm test`)
|
|
113
|
+
|
|
114
|
+
Four gates that fail the build rather than documenting a rule and hoping. Each was
|
|
115
|
+
added after the thing it checks had already shipped broken.
|
|
116
|
+
|
|
117
|
+
| Gate | What it fails on | Why prose was not enough |
|
|
118
|
+
|------|------------------|--------------------------|
|
|
119
|
+
| **Public surface** (`surface_manifest.py` + `app/surface.json`) | a skill, agent, CLI command, frontmatter field, hook, KB category or pack disappearing | `BACKWARD_COMPATIBILITY.md` listed the surfaces; renaming one still left every check green |
|
|
120
|
+
| **Script invocation** (`validate.py`) | a skill running its own script by any path other than `${CLAUDE_SKILL_DIR}`, or through the wrong interpreter | nine skills shipped invocations that resolved only in the repo, never on an installed machine |
|
|
121
|
+
| **Skill body budget** (`validate.py`) | a `SKILL.md` body over 20,000 bytes; warns over 18,000 | the body loads on every trigger match, including accidental ones; three skills sat at 24–35 KB |
|
|
122
|
+
| **Split integrity** (`check_split.py`) | a body → `reference/` refactor that lost a fenced code line, a required section, the description, or a working link | run by hand during a split; caught a heading corrupted inside a fenced example on first use |
|
|
123
|
+
|
|
124
|
+
The surface check is deliberately one-directional: **removals fail, additions pass.**
|
|
125
|
+
A surface nobody has installed has no users to break, and letting additions through
|
|
126
|
+
silently is what stops the gate from becoming a tax people learn to bypass. The
|
|
127
|
+
manifest is never auto-regenerated — if `generate:all` rewrote it, deleting a skill
|
|
128
|
+
would delete its entry in the same breath and the check would prove nothing.
|
|
129
|
+
|
|
130
|
+
`sync_badges.py` closes the matching hole in the other direction: README count
|
|
131
|
+
badges are derived from the tree inside `generate:all`, before `validate.py --strict`
|
|
132
|
+
reads them, so adding a test can no longer redden the build until someone edits a
|
|
133
|
+
number by hand.
|
|
134
|
+
|
|
112
135
|
## 6. Iron Law Enforcement
|
|
113
136
|
|
|
114
137
|
Three skills enforce non-negotiable quality gates with anti-rationalization tables:
|
package/llms-full.txt
CHANGED
|
@@ -6527,9 +6527,9 @@ title: "SOP: AI Toolkit Maintenance"
|
|
|
6527
6527
|
category: procedures
|
|
6528
6528
|
service: ai-toolkit
|
|
6529
6529
|
tags: [sop, maintenance, agents, skills, install]
|
|
6530
|
-
version: "3.
|
|
6530
|
+
version: "3.4.0"
|
|
6531
6531
|
created: "2026-03-23"
|
|
6532
|
-
last_updated: "2026-
|
|
6532
|
+
last_updated: "2026-08-06"
|
|
6533
6533
|
description: "Standard operating procedures for installing, maintaining, and evolving the ai-toolkit."
|
|
6534
6534
|
---
|
|
6535
6535
|
|
|
@@ -6733,8 +6733,30 @@ that runtime should receive the change.
|
|
|
6733
6733
|
user-invocable: false # knowledge skill
|
|
6734
6734
|
---
|
|
6735
6735
|
```
|
|
6736
|
-
2.
|
|
6737
|
-
|
|
6736
|
+
2. If the skill ships an executable, put it in `app/skills/<skill-name>/scripts/`
|
|
6737
|
+
and invoke it **only** as:
|
|
6738
|
+
```bash
|
|
6739
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/<name>.py [args]
|
|
6740
|
+
```
|
|
6741
|
+
`${CLAUDE_SKILL_DIR}` is the sole path that resolves once the skill is
|
|
6742
|
+
installed. `scripts/<name>.py` resolves against the user's working directory,
|
|
6743
|
+
`app/skills/<name>/scripts/…` against a repo they do not have, and
|
|
6744
|
+
`$(dirname "$0")` against the shell's directory — all three look right in the
|
|
6745
|
+
source and fail for every user. Nine skills shipped with one of them before
|
|
6746
|
+
`validate.py` started failing the build on it.
|
|
6747
|
+
|
|
6748
|
+
Match the interpreter to the file (`python3` for `.py`, never `bash`, never
|
|
6749
|
+
bare `python`). The frontmatter `scripts:` list stays relative — it declares
|
|
6750
|
+
ownership rather than running anything.
|
|
6751
|
+
|
|
6752
|
+
Add `--help` handling. A script that treats `--help` as a positional argument
|
|
6753
|
+
greets the user with a traceback, and one that reads stdin must answer an
|
|
6754
|
+
empty stdin with an error rather than blocking forever.
|
|
6755
|
+
3. Update `kb/reference/skills-catalog.md` and `app/ARCHITECTURE.md`
|
|
6756
|
+
4. Run `scripts/validate.py` — it checks the invocation, the body budget, and
|
|
6757
|
+
`reference/` link resolution
|
|
6758
|
+
5. Run `python3 scripts/surface_manifest.py` before the next release to adopt the
|
|
6759
|
+
new skill into the protected surface
|
|
6738
6760
|
|
|
6739
6761
|
## Adding a New Hook
|
|
6740
6762
|
|
|
@@ -8783,7 +8805,7 @@ title: "AI Toolkit - Architecture Overview"
|
|
|
8783
8805
|
category: reference
|
|
8784
8806
|
service: ai-toolkit
|
|
8785
8807
|
tags: [architecture, overview, design, structure]
|
|
8786
|
-
version: "1.
|
|
8808
|
+
version: "1.7.0"
|
|
8787
8809
|
created: "2026-03-23"
|
|
8788
8810
|
last_updated: "2026-08-06"
|
|
8789
8811
|
description: "Architecture of ai-toolkit: directory layout, Claude app export, global install model, editor-aware MCP install, Codex translation layer, skill tiers, and integration with projects."
|
|
@@ -8830,7 +8852,10 @@ ai-toolkit/
|
|
|
8830
8852
|
inject_section_cli.py # Marker-based content injection (canonical implementation)
|
|
8831
8853
|
_common.py # Shared helper for generators (frontmatter, agents/skills emission)
|
|
8832
8854
|
merge-hooks.py # JSON merge for hooks into settings.json (inject/strip modes)
|
|
8833
|
-
validate.py # Toolkit integrity check
|
|
8855
|
+
validate.py # Toolkit integrity check (+ skill body budget, script-invocation gate)
|
|
8856
|
+
surface_manifest.py # Public-surface snapshot vs app/surface.json; removals fail the build
|
|
8857
|
+
check_split.py # Split gate: proves a SKILL.md -> reference/ refactor lost nothing
|
|
8858
|
+
sync_badges.py # Derives README count badges from the tree (runs inside generate:all)
|
|
8834
8859
|
evaluate_skills.py # Skill quality report
|
|
8835
8860
|
generate_agents_md.py # Regenerates AGENTS.md
|
|
8836
8861
|
generate_cursor_rules.py # Generates .cursorrules (sources _common.py)
|
|
@@ -14588,10 +14613,10 @@ title: "AI Toolkit - Skill Templates"
|
|
|
14588
14613
|
category: reference
|
|
14589
14614
|
service: ai-toolkit
|
|
14590
14615
|
tags: [templates, scaffolding, create, skills]
|
|
14591
|
-
version: "1.
|
|
14616
|
+
version: "1.1.0"
|
|
14592
14617
|
created: "2026-03-29"
|
|
14593
|
-
last_updated: "2026-
|
|
14594
|
-
description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge."
|
|
14618
|
+
last_updated: "2026-08-06"
|
|
14619
|
+
description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge. Includes the ${CLAUDE_SKILL_DIR} rule for shipping executable scripts."
|
|
14595
14620
|
---
|
|
14596
14621
|
|
|
14597
14622
|
# Skill Templates
|
|
@@ -14632,7 +14657,40 @@ Templates are stored in `app/templates/skill/{type}/SKILL.md.template`.
|
|
|
14632
14657
|
|
|
14633
14658
|
1. Edit the generated `app/skills/{name}/SKILL.md`
|
|
14634
14659
|
2. Add `reference/` or `templates/` subdirectories if needed
|
|
14635
|
-
3.
|
|
14660
|
+
3. If the skill ships an executable, invoke it **only** through
|
|
14661
|
+
`${CLAUDE_SKILL_DIR}` — see [Shipping a script](#shipping-a-script)
|
|
14662
|
+
4. Run `ai-toolkit validate` to verify
|
|
14663
|
+
|
|
14664
|
+
## Shipping a script
|
|
14665
|
+
|
|
14666
|
+
Put executables in `app/skills/{name}/scripts/` and document them as:
|
|
14667
|
+
|
|
14668
|
+
```bash
|
|
14669
|
+
python3 ${CLAUDE_SKILL_DIR}/scripts/{name}.py [args]
|
|
14670
|
+
```
|
|
14671
|
+
|
|
14672
|
+
That is the only path that resolves once a skill is installed and symlinked into
|
|
14673
|
+
`~/.claude/skills/`. Three other spellings look correct in the repo and fail for
|
|
14674
|
+
every user:
|
|
14675
|
+
|
|
14676
|
+
| Spelling | Resolves against | Result after install |
|
|
14677
|
+
|----------|------------------|----------------------|
|
|
14678
|
+
| `scripts/foo.py` | the user's working directory | not found |
|
|
14679
|
+
| `app/skills/{name}/scripts/foo.py` | a repo the user does not have | not found |
|
|
14680
|
+
| `$(dirname "$0")/scripts/foo.py` | the shell's directory | not found |
|
|
14681
|
+
|
|
14682
|
+
Nine skills shipped with one of these before `validate.py` began failing the
|
|
14683
|
+
build on it. The check also enforces that the interpreter matches the file — a
|
|
14684
|
+
`.py` run through `bash` is an error, and bare `python` a warning, because it is
|
|
14685
|
+
missing or Python 2 on many systems.
|
|
14686
|
+
|
|
14687
|
+
The frontmatter `scripts:` list stays relative. It declares which files the skill
|
|
14688
|
+
owns; it does not run them.
|
|
14689
|
+
|
|
14690
|
+
**Give the script `--help`.** A script that treats `--help` as a positional
|
|
14691
|
+
argument answers with a traceback, and a stdin filter with no input must return
|
|
14692
|
+
an error rather than blocking forever — the post-release SOP probes every shipped
|
|
14693
|
+
script exactly this way.
|
|
14636
14694
|
|
|
14637
14695
|
---
|
|
14638
14696
|
|
|
@@ -15457,7 +15515,7 @@ category: reference
|
|
|
15457
15515
|
service: ai-toolkit
|
|
15458
15516
|
tags: [features, differentiators, constitution, hooks, security, tdd, memory]
|
|
15459
15517
|
created: "2026-04-13"
|
|
15460
|
-
last_updated: "2026-
|
|
15518
|
+
last_updated: "2026-08-06"
|
|
15461
15519
|
description: "Detailed description of ai-toolkit's unique features: constitution enforcement, hooks system, security scanning, effort budgeting, quality gates, and more."
|
|
15462
15520
|
---
|
|
15463
15521
|
|
|
@@ -15562,6 +15620,29 @@ The `Stop` hook runs after every response across 5 languages:
|
|
|
15562
15620
|
| Dart | dart analyze | dart analyze |
|
|
15563
15621
|
| Go | go vet | go vet |
|
|
15564
15622
|
|
|
15623
|
+
## 5b. Repo-Integrity Gates (`validate.py` + `npm test`)
|
|
15624
|
+
|
|
15625
|
+
Four gates that fail the build rather than documenting a rule and hoping. Each was
|
|
15626
|
+
added after the thing it checks had already shipped broken.
|
|
15627
|
+
|
|
15628
|
+
| Gate | What it fails on | Why prose was not enough |
|
|
15629
|
+
|------|------------------|--------------------------|
|
|
15630
|
+
| **Public surface** (`surface_manifest.py` + `app/surface.json`) | a skill, agent, CLI command, frontmatter field, hook, KB category or pack disappearing | `BACKWARD_COMPATIBILITY.md` listed the surfaces; renaming one still left every check green |
|
|
15631
|
+
| **Script invocation** (`validate.py`) | a skill running its own script by any path other than `${CLAUDE_SKILL_DIR}`, or through the wrong interpreter | nine skills shipped invocations that resolved only in the repo, never on an installed machine |
|
|
15632
|
+
| **Skill body budget** (`validate.py`) | a `SKILL.md` body over 20,000 bytes; warns over 18,000 | the body loads on every trigger match, including accidental ones; three skills sat at 24–35 KB |
|
|
15633
|
+
| **Split integrity** (`check_split.py`) | a body → `reference/` refactor that lost a fenced code line, a required section, the description, or a working link | run by hand during a split; caught a heading corrupted inside a fenced example on first use |
|
|
15634
|
+
|
|
15635
|
+
The surface check is deliberately one-directional: **removals fail, additions pass.**
|
|
15636
|
+
A surface nobody has installed has no users to break, and letting additions through
|
|
15637
|
+
silently is what stops the gate from becoming a tax people learn to bypass. The
|
|
15638
|
+
manifest is never auto-regenerated — if `generate:all` rewrote it, deleting a skill
|
|
15639
|
+
would delete its entry in the same breath and the check would prove nothing.
|
|
15640
|
+
|
|
15641
|
+
`sync_badges.py` closes the matching hole in the other direction: README count
|
|
15642
|
+
badges are derived from the tree inside `generate:all`, before `validate.py --strict`
|
|
15643
|
+
reads them, so adding a test can no longer redden the build until someone edits a
|
|
15644
|
+
number by hand.
|
|
15645
|
+
|
|
15565
15646
|
## 6. Iron Law Enforcement
|
|
15566
15647
|
|
|
15567
15648
|
Three skills enforce non-negotiable quality gates with anti-rationalization tables:
|
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.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/plugin.py
CHANGED
|
@@ -215,6 +215,23 @@ def _resolve_skill_source(pack_dir: Path, skill: str) -> Path | None:
|
|
|
215
215
|
return None
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def _resolve_agent_source(pack_dir: Path, agent: str) -> Path | None:
|
|
219
|
+
"""Locate the agent file a pack references.
|
|
220
|
+
|
|
221
|
+
Mirrors _resolve_skill_source: a core agent in app/agents wins, otherwise the
|
|
222
|
+
pack may ship its own under <pack>/agents/. Resolving only against app/agents
|
|
223
|
+
is what made every pack-shipped agent print "WARN agent not found" while
|
|
224
|
+
_remove_claude_pack_links already knew how to unlink one.
|
|
225
|
+
"""
|
|
226
|
+
core = app_dir / "agents" / f"{agent}.md"
|
|
227
|
+
plugin = pack_dir / "agents" / f"{agent}.md"
|
|
228
|
+
if core.is_file():
|
|
229
|
+
return core
|
|
230
|
+
if plugin.is_file():
|
|
231
|
+
return plugin
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
218
235
|
def _resolve_rule_source(pack_dir: Path, rule_name: str) -> tuple[Path, bool] | None:
|
|
219
236
|
candidates = [
|
|
220
237
|
(pack_dir / f"{rule_name}.md", False),
|
|
@@ -577,13 +594,13 @@ def _install_claude_skills(pack: dict, pack_dir: Path, installed_items: list[str
|
|
|
577
594
|
print(f" WARN skill not found: {skill}")
|
|
578
595
|
|
|
579
596
|
|
|
580
|
-
def _install_claude_agents(pack: dict, installed_items: list[str]) -> None:
|
|
597
|
+
def _install_claude_agents(pack: dict, pack_dir: Path, installed_items: list[str]) -> None:
|
|
581
598
|
for agent in pack.get("includes", {}).get("agents", []):
|
|
582
599
|
agent_file = CLAUDE_DIR / "agents" / f"{agent}.md"
|
|
583
|
-
source_file =
|
|
600
|
+
source_file = _resolve_agent_source(pack_dir, agent)
|
|
584
601
|
if agent_file.exists() or agent_file.is_symlink():
|
|
585
602
|
print(f" OK agent: {agent}")
|
|
586
|
-
elif source_file
|
|
603
|
+
elif source_file is not None:
|
|
587
604
|
agent_file.parent.mkdir(parents=True, exist_ok=True)
|
|
588
605
|
agent_file.symlink_to(source_file)
|
|
589
606
|
print(f" Linked agent: {agent}")
|
|
@@ -676,7 +693,7 @@ def install_pack_claude(name: str, pack: dict, pack_dir: Path) -> bool:
|
|
|
676
693
|
rule_specs = _resolve_pack_rules(pack, pack_dir)
|
|
677
694
|
|
|
678
695
|
_ensure_core_hook_scripts()
|
|
679
|
-
_install_claude_agents(pack, installed_items)
|
|
696
|
+
_install_claude_agents(pack, pack_dir, installed_items)
|
|
680
697
|
_install_claude_skills(pack, pack_dir, installed_items)
|
|
681
698
|
_copy_plugin_hook_scripts(name, hook_specs, installed_items)
|
|
682
699
|
_copy_plugin_scripts(name, pack_dir, installed_items)
|
package/scripts/plugin_schema.py
CHANGED
|
@@ -139,20 +139,32 @@ def validate_references(
|
|
|
139
139
|
data: dict,
|
|
140
140
|
agents_dir: Path,
|
|
141
141
|
skills_dir: Path,
|
|
142
|
+
pack_dir: Path | None = None,
|
|
142
143
|
) -> list[str]:
|
|
143
144
|
"""Validate that referenced agents and skills exist.
|
|
144
145
|
|
|
146
|
+
A pack may either reference a core asset (app/agents, app/skills) or ship its
|
|
147
|
+
own under <pack>/agents and <pack>/skills. Checking only the core directories
|
|
148
|
+
reported every self-contained pack as broken even though the installer links
|
|
149
|
+
its skills fine. pack_dir stays optional so existing callers keep working.
|
|
150
|
+
|
|
145
151
|
Returns a list of error messages.
|
|
146
152
|
"""
|
|
147
153
|
errors: list[str] = []
|
|
148
154
|
includes = data.get("includes", {})
|
|
149
155
|
|
|
150
156
|
for agent in includes.get("agents", []):
|
|
151
|
-
|
|
157
|
+
candidates = [agents_dir / f"{agent}.md"]
|
|
158
|
+
if pack_dir is not None:
|
|
159
|
+
candidates.append(pack_dir / "agents" / f"{agent}.md")
|
|
160
|
+
if not any(c.is_file() for c in candidates):
|
|
152
161
|
errors.append(f"References missing agent: {agent}")
|
|
153
162
|
|
|
154
163
|
for skill in includes.get("skills", []):
|
|
155
|
-
|
|
164
|
+
candidates = [skills_dir / skill / "SKILL.md"]
|
|
165
|
+
if pack_dir is not None:
|
|
166
|
+
candidates.append(pack_dir / "skills" / skill / "SKILL.md")
|
|
167
|
+
if not any(c.is_file() for c in candidates):
|
|
156
168
|
errors.append(f"References missing skill: {skill}")
|
|
157
169
|
|
|
158
170
|
return errors
|
package/scripts/validate.py
CHANGED
|
@@ -764,6 +764,7 @@ def _validate_pack_refs(tk_dir: Path, pack_path: Path, d: dict,
|
|
|
764
764
|
d,
|
|
765
765
|
agents_dir=tk_dir / "app" / "agents",
|
|
766
766
|
skills_dir=tk_dir / "app" / "skills",
|
|
767
|
+
pack_dir=pack_path,
|
|
767
768
|
)
|
|
768
769
|
for err in ref_errors:
|
|
769
770
|
vr.error(f"app/plugins/{pack_name}/plugin.json {err}")
|