@softspark/ai-toolkit 4.23.0 → 4.24.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 CHANGED
@@ -7,6 +7,68 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.24.0 — external packs stop dying on upgrade (2026-08-18)
11
+
12
+ ### Added
13
+
14
+ - **`~/.softspark/ai-toolkit/plugins/` is a second pack root.** Until now
15
+ `plugin.py` scanned exactly one directory — `app/plugins/`, inside the npm
16
+ package — so the only way to add a pack from its own repository was to drop it
17
+ in there, where the next `npm install -g @softspark/ai-toolkit` deleted it along
18
+ with the rest of `app/`. External packs now live in the user-owned data dir and
19
+ survive every upgrade. `plugin list`, `install`, `update`, `remove` and `status`
20
+ all see both roots; `AI_TOOLKIT_HOME` moves the user root with the data dir.
21
+
22
+ A **core pack wins a name collision**, so an external directory cannot shadow
23
+ shipped behaviour. Each pack dict carries `_root` (`core` or `user`) — `_source`
24
+ was already taken as the hook-ownership marker.
25
+
26
+ The entry may be a directory or a **symlink**, which is what gives an external
27
+ pack a versioned update path: point it at an npm-installed package and
28
+ `npm install -g <pack>@latest` rewrites the target in place, with nothing to
29
+ re-link. Documented in `kb/reference/plugin-pack-conventions.md`
30
+ (*Where Packs Live*), including the per-scope registry caveat when one package
31
+ in a scope is public and another private.
32
+
33
+ - **Three tests.** A pack in the user root is listed, installs with both its agent
34
+ and skill linked back into that root, and loses a name collision to a core pack.
35
+
36
+ ## v4.23.1 — a pack could ship an agent nobody installed (2026-08-18)
37
+
38
+ ### Fixed
39
+
40
+ - **`plugin install` never linked an agent the pack shipped itself.**
41
+ `_install_claude_agents` resolved `includes.agents` only against the toolkit's
42
+ own `app/agents/<name>.md`, so a self-contained pack got `WARN agent not found`
43
+ and the agent silently stayed uninstalled. The asymmetry was visible inside the
44
+ same file: skills already resolved through `_resolve_skill_source`, which falls
45
+ back to the pack directory, and `_remove_claude_pack_links` already unlinked
46
+ agent symlinks pointing into a pack — a removal path for links the installer
47
+ could never create.
48
+
49
+ Agents now go through `_resolve_agent_source`, mirroring skills: a core agent in
50
+ `app/agents` still wins, otherwise `<pack>/agents/<name>.md` is linked. Packs
51
+ that merely reference a core agent behave exactly as before.
52
+
53
+ - **`validate.py` reported self-contained packs as broken.** `validate_references`
54
+ checked `app/agents` and `app/skills` only, so a pack shipping its own assets
55
+ drew `References missing agent: X` and `References missing skill: Y` even though
56
+ the installer handled the skill correctly. It now also accepts
57
+ `<pack>/agents/<name>.md` and `<pack>/skills/<name>/SKILL.md`; a genuinely
58
+ missing reference still errors. `pack_dir` is an optional argument, so existing
59
+ callers are unaffected.
60
+
61
+ Found by the post-release smoke test of a downstream pack (`legal-pl-pack`),
62
+ not by a user report.
63
+
64
+ ### Added
65
+
66
+ - **Three tests covering the class.** `tests/test_plugin.bats` builds a throwaway
67
+ toolkit whose only pack ships its own agent and skill, then asserts the agent is
68
+ linked into the pack (not `app/agents`), that removal drops the link, and that
69
+ reference validation accepts pack-shipped assets while still failing on a
70
+ genuinely missing one.
71
+
10
72
  ## v4.23.0 — a compliance scanner that says nothing must say why (2026-08-06)
11
73
 
12
74
  ### Fixed
package/README.md CHANGED
@@ -6,50 +6,18 @@
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-1516%20passing-success)](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.
27
-
28
- ## What's New in v4.22.0
29
-
30
- v4.22.0 turns compatibility promises into tests, and makes two validator skills
31
- actually run the scanners they ship.
32
-
33
- - **The public surface is checked, not documented.** `app/surface.json` pins 275
34
- entries — skills, agents, frontmatter fields, CLI commands, hook scripts and
35
- events, KB categories, plugin packs. Removing any of them fails `npm test`.
36
- Adding is free: a surface nobody has installed has no users to break.
37
- `BACKWARD_COMPATIBILITY.md` says which surfaces are load-bearing and how to
38
- deprecate one properly.
39
- - **`a11y-validate` and `seo-validate` shipped scanners that nothing invoked.**
40
- 1,196 lines of working Python sat on disk while the model was told to grep the
41
- pattern tables by hand. Both now run their script for a deterministic baseline,
42
- then take an explicit manual pass — with a measured coverage table saying which
43
- criteria the script handles and which are yours.
44
- - **`/brainstorm`** — the first planning skill allowed to conclude "do not build
45
- this". Every other one produces an artifact; a pipeline that cannot say no is a
46
- yes-machine.
47
- - **Skill bodies have a budget.** Error above 20,000 bytes, warn above 18,000,
48
- headroom printed every run. Three validator skills were split into
49
- `reference/`, taking 47 KB off the hot path, with `scripts/check_split.py`
50
- proving nothing was lost.
51
- - **Four skills invoked their scripts through a path that never resolved.**
52
- `$(dirname "$0")` expands to the shell's directory, not the skill's.
9
+ [![Tests](https://img.shields.io/badge/tests-1522%20passing-success)](tests/)
10
+
11
+ ## What's New in v4.24.0
12
+
13
+ **v4.24.0** — a plugin pack maintained in its own repository had nowhere safe to
14
+ live: the only directory the toolkit scanned was `app/plugins/`, inside the npm
15
+ package, so every `npm install -g` upgrade deleted it. Packs now also load from
16
+ `~/.softspark/ai-toolkit/plugins/`, which the user owns and upgrades never touch.
17
+ The entry can be a symlink to an npm-installed package, so updating an external
18
+ pack is one `npm install -g` with nothing to re-link. Core packs still win a name
19
+ collision. Ships with v4.23.1's fix for pack-shipped agents, which the same
20
+ external-pack workflow depends on.
53
21
 
54
22
  See [CHANGELOG.md](CHANGELOG.md) for full history.
55
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.23.0",
6
+ "version": "4.24.0",
7
7
  "author": {
8
8
  "name": "SoftSpark",
9
9
  "url": "https://github.com/softspark"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: documentation-standards
3
- description: "KB conventions: YAML frontmatter, 8-category taxonomy (reference/howto/procedures/troubleshooting/best-practices/decisions/runbooks/planning). Triggers: kb/, SOP, runbook, howto, frontmatter, knowledge base."
3
+ description: "KB conventions: YAML frontmatter, 10-category taxonomy (reference/howto/procedures/troubleshooting/best-practices/decisions/runbooks/planning/business/templates). Triggers: kb/, SOP, runbook, howto, frontmatter, knowledge base."
4
4
  effort: medium
5
5
  user-invocable: false
6
6
  allowed-tools: Read
@@ -17,7 +17,7 @@ Every document in `kb/` MUST start with YAML frontmatter:
17
17
  ```yaml
18
18
  ---
19
19
  title: "Document Title" # REQUIRED — English, descriptive
20
- category: reference # REQUIRED — one of 5 valid categories
20
+ category: reference # REQUIRED — one of the 10 valid categories
21
21
  service: ai-toolkit # REQUIRED — service identifier
22
22
  tags: [tag1, tag2, tag3] # REQUIRED — minimum 1, recommended 3+
23
23
  last_updated: "YYYY-MM-DD" # REQUIRED — ISO format
@@ -53,11 +53,21 @@ and never authoritative on its own.
53
53
  | `decisions` | `kb/decisions/` | Architecture decision records and design rationale | `adr-004-kb-migration.md` |
54
54
  | `runbooks` | `kb/runbooks/` | Procedures run against a live system, usually under pressure | `deployment.md`, `incident-response.md` |
55
55
  | `planning` | `kb/planning/` | Roadmaps, PRDs, work not yet done | `q3-roadmap.md` |
56
+ | `business` | `kb/business/` | Domain model, requirements, use cases, user stories | `domain-model.md`, `user-stories.md` |
57
+ | `templates` | `kb/templates/` | Reusable document templates | `adr-template.md`, `sop-template.md` |
56
58
 
57
59
  **Rule:** A document filed under one of the directories above MUST declare that
58
60
  category. The rule is scoped to those directories deliberately: `kb/history/`
59
- and similar are lifecycle locations rather than types, and a finished plan filed
60
- under `history/completed/` is still a `planning` document.
61
+ and `kb/summaries/` are lifecycle and runtime locations rather than types, and a
62
+ finished plan filed under `history/completed/` is still a `planning` document.
63
+
64
+ **Templates carry placeholders on purpose.** A file under `templates/` exists to
65
+ be copied, so a literal `YYYY-MM-DD` date and `[placeholder]` body text are
66
+ correct there rather than defects. Every other convention still applies.
67
+
68
+ This taxonomy lives in three places: ai-toolkit's `scripts/validate.py`,
69
+ rag-mcp's `scripts/validate_kb_frontmatter.py`, and this document. They are one
70
+ list, and a change belongs in all three.
61
71
 
62
72
  ## Naming Conventions
63
73
 
package/app/surface.json CHANGED
@@ -230,12 +230,14 @@
230
230
  ],
231
231
  "kb_categories": [
232
232
  "best-practices",
233
+ "business",
233
234
  "decisions",
234
235
  "howto",
235
236
  "planning",
236
237
  "procedures",
237
238
  "reference",
238
239
  "runbooks",
240
+ "templates",
239
241
  "troubleshooting"
240
242
  ],
241
243
  "cli_commands": [
@@ -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.3.0"
6
+ version: "3.4.0"
7
7
  created: "2026-03-23"
8
- last_updated: "2026-07-14"
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. Update `kb/reference/skills-catalog.md` and `app/ARCHITECTURE.md`
213
- 3. Run `scripts/validate.py`
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.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,9 +3,9 @@ title: "Plugin Pack Conventions"
3
3
  category: reference
4
4
  service: ai-toolkit
5
5
  tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
6
- version: "1.1.0"
6
+ version: "1.2.0"
7
7
  created: "2026-03-28"
8
- last_updated: "2026-07-14"
8
+ last_updated: "2026-08-18"
9
9
  description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across Claude Code and Codex runtimes."
10
10
  ---
11
11
 
@@ -13,7 +13,46 @@ description: "Conventions for experimental ai-toolkit plugin packs, policy packs
13
13
 
14
14
  ## Purpose
15
15
 
16
- `ai-toolkit` includes experimental plugin packs under `app/plugins/` for Claude Code and optional global Codex layering. These internal pack manifests are distinct from the official uploadable Claude app plugin built by `ai-toolkit claude-app export`.
16
+ `ai-toolkit` includes experimental plugin packs under `app/plugins/` for Claude Code and optional global Codex layering, and discovers external packs under `~/.softspark/ai-toolkit/plugins/` (see *Where Packs Live*). These internal pack manifests are distinct from the official uploadable Claude app plugin built by `ai-toolkit claude-app export`.
17
+
18
+ ## Where Packs Live
19
+
20
+ | Root | Owner | Survives `npm install -g @softspark/ai-toolkit` | Use for |
21
+ |------|-------|------------------------------------------------|---------|
22
+ | `app/plugins/<pack>/` | this repository | no — the upgrade replaces `app/` wholesale | packs shipped with the toolkit (`memory-pack`, `enterprise-pack`) |
23
+ | `~/.softspark/ai-toolkit/plugins/<pack>/` | the user | **yes** | external / private packs maintained in their own repository |
24
+
25
+ Both roots are scanned by `plugin list`, `install`, `update`, `remove` and
26
+ `status`. A **core pack wins a name collision**, so an external directory cannot
27
+ shadow shipped behaviour. `AI_TOOLKIT_HOME` moves the user root along with the
28
+ rest of the toolkit data dir, which is what the test suite uses.
29
+
30
+ Before v4.24.0 there was only the first root, so the only way to add an external
31
+ pack was to drop it inside the npm package — where the next toolkit upgrade
32
+ deleted it. That is what the user root exists to end.
33
+
34
+ ### Installing an external pack
35
+
36
+ The entry may be a directory **or a symlink**, which is what gives an external
37
+ pack a versioned update path:
38
+
39
+ ```bash
40
+ # Distributed as a package (private registry example)
41
+ npm install -g @softspark/<pack> --registry=https://npm.pkg.github.com
42
+ mkdir -p ~/.softspark/ai-toolkit/plugins
43
+ ln -s "$(npm root -g)/@softspark/<pack>" ~/.softspark/ai-toolkit/plugins/<pack>
44
+ ai-toolkit plugin install --editor claude <pack>
45
+
46
+ # Update later: the symlink target is rewritten in place, nothing to re-link
47
+ npm install -g @softspark/<pack>@latest --registry=https://npm.pkg.github.com
48
+ ```
49
+
50
+ A plain `git clone` into the same directory works too; update it with `git pull`.
51
+
52
+ > **Scope note:** `npm` resolves a registry per **scope**, not per package. If one
53
+ > package in a scope lives on public npm and another on a private registry, the
54
+ > private one needs `--registry=` on every command — a scope-wide
55
+ > `npm config set @scope:registry` would misroute the public one.
17
56
 
18
57
  ## Pack Types
19
58
 
@@ -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.0.0"
6
+ version: "1.1.0"
7
7
  created: "2026-03-29"
8
- last_updated: "2026-04-01"
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. Run `ai-toolkit validate` to verify
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-07-10"
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
@@ -109,7 +109,7 @@
109
109
  - **design-engineering**: UI craftsmanship: animation rules, easing, micro-interactions, state polish. Triggers: animation, transition, ease-out, motion, micro-interaction, hover, loading state, UI polish.
110
110
  - **docker-devops**: Docker/K8s: Dockerfile, multi-stage, compose, manifests, Helm. Triggers: Docker, Dockerfile, container, Kubernetes, k8s, compose, Helm, pod.
111
111
  - **docs**: Generates/updates README, API docs, architecture notes. Triggers: docs, README, API docs, architecture note, documentation.
112
- - **documentation-standards**: KB conventions: YAML frontmatter, 8-category taxonomy (reference/howto/procedures/troubleshooting/best-practices/decisions/runbooks/planning). Triggers: kb/, SOP, runbook, howto, frontmatter, knowledge base.
112
+ - **documentation-standards**: KB conventions: YAML frontmatter, 10-category taxonomy (reference/howto/procedures/troubleshooting/best-practices/decisions/runbooks/planning/business/templates). Triggers: kb/, SOP, runbook, howto, frontmatter, knowledge base.
113
113
  - **ecommerce-patterns**: E-commerce: cart, checkout, payments (Stripe/Adyen), order state, inventory, promos, tax. Triggers: cart, checkout, SKU, payment, Stripe, Shopify, Medusa, Magento, coupon, refund.
114
114
  - **evaluate**: Evaluates RAG retrieval and LLM-as-judge metrics (faithfulness, relevancy, context precision). Triggers: measure RAG quality, knowledge gap, RAG eval, golden dataset.
115
115
  - **evolve**: Analyzes agent/skill failures, drafts prompt/permission fixes. Triggers: improve agent, refine skill, system prompt, optimize agent.
@@ -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.3.0"
6530
+ version: "3.4.0"
6531
6531
  created: "2026-03-23"
6532
- last_updated: "2026-07-14"
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. Update `kb/reference/skills-catalog.md` and `app/ARCHITECTURE.md`
6737
- 3. Run `scripts/validate.py`
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.6.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)
@@ -14324,9 +14349,9 @@ title: "Plugin Pack Conventions"
14324
14349
  category: reference
14325
14350
  service: ai-toolkit
14326
14351
  tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
14327
- version: "1.1.0"
14352
+ version: "1.2.0"
14328
14353
  created: "2026-03-28"
14329
- last_updated: "2026-07-14"
14354
+ last_updated: "2026-08-18"
14330
14355
  description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across Claude Code and Codex runtimes."
14331
14356
  ---
14332
14357
 
@@ -14334,7 +14359,46 @@ description: "Conventions for experimental ai-toolkit plugin packs, policy packs
14334
14359
 
14335
14360
  ## Purpose
14336
14361
 
14337
- `ai-toolkit` includes experimental plugin packs under `app/plugins/` for Claude Code and optional global Codex layering. These internal pack manifests are distinct from the official uploadable Claude app plugin built by `ai-toolkit claude-app export`.
14362
+ `ai-toolkit` includes experimental plugin packs under `app/plugins/` for Claude Code and optional global Codex layering, and discovers external packs under `~/.softspark/ai-toolkit/plugins/` (see *Where Packs Live*). These internal pack manifests are distinct from the official uploadable Claude app plugin built by `ai-toolkit claude-app export`.
14363
+
14364
+ ## Where Packs Live
14365
+
14366
+ | Root | Owner | Survives `npm install -g @softspark/ai-toolkit` | Use for |
14367
+ |------|-------|------------------------------------------------|---------|
14368
+ | `app/plugins/<pack>/` | this repository | no — the upgrade replaces `app/` wholesale | packs shipped with the toolkit (`memory-pack`, `enterprise-pack`) |
14369
+ | `~/.softspark/ai-toolkit/plugins/<pack>/` | the user | **yes** | external / private packs maintained in their own repository |
14370
+
14371
+ Both roots are scanned by `plugin list`, `install`, `update`, `remove` and
14372
+ `status`. A **core pack wins a name collision**, so an external directory cannot
14373
+ shadow shipped behaviour. `AI_TOOLKIT_HOME` moves the user root along with the
14374
+ rest of the toolkit data dir, which is what the test suite uses.
14375
+
14376
+ Before v4.24.0 there was only the first root, so the only way to add an external
14377
+ pack was to drop it inside the npm package — where the next toolkit upgrade
14378
+ deleted it. That is what the user root exists to end.
14379
+
14380
+ ### Installing an external pack
14381
+
14382
+ The entry may be a directory **or a symlink**, which is what gives an external
14383
+ pack a versioned update path:
14384
+
14385
+ ```bash
14386
+ # Distributed as a package (private registry example)
14387
+ npm install -g @softspark/<pack> --registry=https://npm.pkg.github.com
14388
+ mkdir -p ~/.softspark/ai-toolkit/plugins
14389
+ ln -s "$(npm root -g)/@softspark/<pack>" ~/.softspark/ai-toolkit/plugins/<pack>
14390
+ ai-toolkit plugin install --editor claude <pack>
14391
+
14392
+ # Update later: the symlink target is rewritten in place, nothing to re-link
14393
+ npm install -g @softspark/<pack>@latest --registry=https://npm.pkg.github.com
14394
+ ```
14395
+
14396
+ A plain `git clone` into the same directory works too; update it with `git pull`.
14397
+
14398
+ > **Scope note:** `npm` resolves a registry per **scope**, not per package. If one
14399
+ > package in a scope lives on public npm and another on a private registry, the
14400
+ > private one needs `--registry=` on every command — a scope-wide
14401
+ > `npm config set @scope:registry` would misroute the public one.
14338
14402
 
14339
14403
  ## Pack Types
14340
14404
 
@@ -14588,10 +14652,10 @@ title: "AI Toolkit - Skill Templates"
14588
14652
  category: reference
14589
14653
  service: ai-toolkit
14590
14654
  tags: [templates, scaffolding, create, skills]
14591
- version: "1.0.0"
14655
+ version: "1.1.0"
14592
14656
  created: "2026-03-29"
14593
- last_updated: "2026-04-01"
14594
- description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge."
14657
+ last_updated: "2026-08-06"
14658
+ description: "5 skill templates for scaffolding new skills: linter, reviewer, generator, workflow, knowledge. Includes the ${CLAUDE_SKILL_DIR} rule for shipping executable scripts."
14595
14659
  ---
14596
14660
 
14597
14661
  # Skill Templates
@@ -14632,7 +14696,40 @@ Templates are stored in `app/templates/skill/{type}/SKILL.md.template`.
14632
14696
 
14633
14697
  1. Edit the generated `app/skills/{name}/SKILL.md`
14634
14698
  2. Add `reference/` or `templates/` subdirectories if needed
14635
- 3. Run `ai-toolkit validate` to verify
14699
+ 3. If the skill ships an executable, invoke it **only** through
14700
+ `${CLAUDE_SKILL_DIR}` — see [Shipping a script](#shipping-a-script)
14701
+ 4. Run `ai-toolkit validate` to verify
14702
+
14703
+ ## Shipping a script
14704
+
14705
+ Put executables in `app/skills/{name}/scripts/` and document them as:
14706
+
14707
+ ```bash
14708
+ python3 ${CLAUDE_SKILL_DIR}/scripts/{name}.py [args]
14709
+ ```
14710
+
14711
+ That is the only path that resolves once a skill is installed and symlinked into
14712
+ `~/.claude/skills/`. Three other spellings look correct in the repo and fail for
14713
+ every user:
14714
+
14715
+ | Spelling | Resolves against | Result after install |
14716
+ |----------|------------------|----------------------|
14717
+ | `scripts/foo.py` | the user's working directory | not found |
14718
+ | `app/skills/{name}/scripts/foo.py` | a repo the user does not have | not found |
14719
+ | `$(dirname "$0")/scripts/foo.py` | the shell's directory | not found |
14720
+
14721
+ Nine skills shipped with one of these before `validate.py` began failing the
14722
+ build on it. The check also enforces that the interpreter matches the file — a
14723
+ `.py` run through `bash` is an error, and bare `python` a warning, because it is
14724
+ missing or Python 2 on many systems.
14725
+
14726
+ The frontmatter `scripts:` list stays relative. It declares which files the skill
14727
+ owns; it does not run them.
14728
+
14729
+ **Give the script `--help`.** A script that treats `--help` as a positional
14730
+ argument answers with a traceback, and a stdin filter with no input must return
14731
+ an error rather than blocking forever — the post-release SOP probes every shipped
14732
+ script exactly this way.
14636
14733
 
14637
14734
  ---
14638
14735
 
@@ -15457,7 +15554,7 @@ category: reference
15457
15554
  service: ai-toolkit
15458
15555
  tags: [features, differentiators, constitution, hooks, security, tdd, memory]
15459
15556
  created: "2026-04-13"
15460
- last_updated: "2026-07-10"
15557
+ last_updated: "2026-08-06"
15461
15558
  description: "Detailed description of ai-toolkit's unique features: constitution enforcement, hooks system, security scanning, effort budgeting, quality gates, and more."
15462
15559
  ---
15463
15560
 
@@ -15562,6 +15659,29 @@ The `Stop` hook runs after every response across 5 languages:
15562
15659
  | Dart | dart analyze | dart analyze |
15563
15660
  | Go | go vet | go vet |
15564
15661
 
15662
+ ## 5b. Repo-Integrity Gates (`validate.py` + `npm test`)
15663
+
15664
+ Four gates that fail the build rather than documenting a rule and hoping. Each was
15665
+ added after the thing it checks had already shipped broken.
15666
+
15667
+ | Gate | What it fails on | Why prose was not enough |
15668
+ |------|------------------|--------------------------|
15669
+ | **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 |
15670
+ | **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 |
15671
+ | **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 |
15672
+ | **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 |
15673
+
15674
+ The surface check is deliberately one-directional: **removals fail, additions pass.**
15675
+ A surface nobody has installed has no users to break, and letting additions through
15676
+ silently is what stops the gate from becoming a tax people learn to bypass. The
15677
+ manifest is never auto-regenerated — if `generate:all` rewrote it, deleting a skill
15678
+ would delete its entry in the same breath and the check would prove nothing.
15679
+
15680
+ `sync_badges.py` closes the matching hole in the other direction: README count
15681
+ badges are derived from the tree inside `generate:all`, before `validate.py --strict`
15682
+ reads them, so adding a test can no longer redden the build until someone edits a
15683
+ number by hand.
15684
+
15565
15685
  ## 6. Iron Law Enforcement
15566
15686
 
15567
15687
  Three skills enforce non-negotiable quality gates with anti-rationalization tables:
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.23.0",
2
+ "version": "4.24.0",
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.23.0",
3
+ "version": "4.24.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/plugin.py CHANGED
@@ -60,6 +60,12 @@ from plugin_schema import resolve_hook_event
60
60
 
61
61
 
62
62
  PLUGINS_DIR = app_dir / "plugins"
63
+ # External packs live outside the npm package so a toolkit upgrade cannot delete
64
+ # them: `npm install -g @softspark/ai-toolkit` replaces app/ wholesale, taking any
65
+ # third-party pack dropped in there with it. TOOLKIT_DATA_DIR is user-owned and
66
+ # survives. The entry may be a directory or a symlink (e.g. to an npm-installed
67
+ # pack), which is how an external pack gets a versioned update path.
68
+ USER_PLUGINS_DIR = TOOLKIT_DATA_DIR / "plugins"
63
69
  CLAUDE_DIR = Path.home() / ".claude"
64
70
  CODEX_ROOT = Path.home()
65
71
  CODEX_HOME = Path(os.environ.get("CODEX_HOME", CODEX_ROOT / ".codex")).expanduser()
@@ -178,22 +184,40 @@ def _set_installed(state: dict, editor: str, names: list[str]) -> None:
178
184
  # Plugin discovery
179
185
  # ---------------------------------------------------------------------------
180
186
 
187
+ def plugin_roots() -> list[Path]:
188
+ """Directories scanned for packs, in precedence order.
189
+
190
+ Core packs ship inside the npm package; user packs live under
191
+ TOOLKIT_DATA_DIR and outlive every toolkit upgrade. A core pack wins a name
192
+ collision so a stray external directory cannot shadow shipped behaviour.
193
+ """
194
+ return [PLUGINS_DIR, USER_PLUGINS_DIR]
195
+
196
+
181
197
  def list_available() -> list[dict]:
182
- """List all available plugin packs."""
198
+ """List all available plugin packs, core first, then user-installed."""
183
199
  packs: list[dict] = []
184
- if not PLUGINS_DIR.is_dir():
185
- return packs
186
- for d in sorted(PLUGINS_DIR.iterdir()):
187
- manifest = d / "plugin.json"
188
- if not manifest.is_file():
200
+ seen: set[str] = set()
201
+ for root in plugin_roots():
202
+ if not root.is_dir():
189
203
  continue
190
- try:
191
- with open(manifest, encoding="utf-8") as f:
192
- data = json.load(f)
204
+ for d in sorted(root.iterdir()):
205
+ manifest = d / "plugin.json"
206
+ if not manifest.is_file():
207
+ continue
208
+ try:
209
+ with open(manifest, encoding="utf-8") as f:
210
+ data = json.load(f)
211
+ except (json.JSONDecodeError, OSError):
212
+ continue
213
+ name = data.get("name")
214
+ if not name or name in seen:
215
+ continue
216
+ seen.add(name)
193
217
  data["_dir"] = str(d)
218
+ # "_source" is taken: hook entries use it as an ownership marker.
219
+ data["_root"] = "core" if root == PLUGINS_DIR else "user"
194
220
  packs.append(data)
195
- except (json.JSONDecodeError, OSError):
196
- continue
197
221
  return packs
198
222
 
199
223
 
@@ -215,6 +239,23 @@ def _resolve_skill_source(pack_dir: Path, skill: str) -> Path | None:
215
239
  return None
216
240
 
217
241
 
242
+ def _resolve_agent_source(pack_dir: Path, agent: str) -> Path | None:
243
+ """Locate the agent file a pack references.
244
+
245
+ Mirrors _resolve_skill_source: a core agent in app/agents wins, otherwise the
246
+ pack may ship its own under <pack>/agents/. Resolving only against app/agents
247
+ is what made every pack-shipped agent print "WARN agent not found" while
248
+ _remove_claude_pack_links already knew how to unlink one.
249
+ """
250
+ core = app_dir / "agents" / f"{agent}.md"
251
+ plugin = pack_dir / "agents" / f"{agent}.md"
252
+ if core.is_file():
253
+ return core
254
+ if plugin.is_file():
255
+ return plugin
256
+ return None
257
+
258
+
218
259
  def _resolve_rule_source(pack_dir: Path, rule_name: str) -> tuple[Path, bool] | None:
219
260
  candidates = [
220
261
  (pack_dir / f"{rule_name}.md", False),
@@ -577,13 +618,13 @@ def _install_claude_skills(pack: dict, pack_dir: Path, installed_items: list[str
577
618
  print(f" WARN skill not found: {skill}")
578
619
 
579
620
 
580
- def _install_claude_agents(pack: dict, installed_items: list[str]) -> None:
621
+ def _install_claude_agents(pack: dict, pack_dir: Path, installed_items: list[str]) -> None:
581
622
  for agent in pack.get("includes", {}).get("agents", []):
582
623
  agent_file = CLAUDE_DIR / "agents" / f"{agent}.md"
583
- source_file = app_dir / "agents" / f"{agent}.md"
624
+ source_file = _resolve_agent_source(pack_dir, agent)
584
625
  if agent_file.exists() or agent_file.is_symlink():
585
626
  print(f" OK agent: {agent}")
586
- elif source_file.is_file():
627
+ elif source_file is not None:
587
628
  agent_file.parent.mkdir(parents=True, exist_ok=True)
588
629
  agent_file.symlink_to(source_file)
589
630
  print(f" Linked agent: {agent}")
@@ -676,7 +717,7 @@ def install_pack_claude(name: str, pack: dict, pack_dir: Path) -> bool:
676
717
  rule_specs = _resolve_pack_rules(pack, pack_dir)
677
718
 
678
719
  _ensure_core_hook_scripts()
679
- _install_claude_agents(pack, installed_items)
720
+ _install_claude_agents(pack, pack_dir, installed_items)
680
721
  _install_claude_skills(pack, pack_dir, installed_items)
681
722
  _copy_plugin_hook_scripts(name, hook_specs, installed_items)
682
723
  _copy_plugin_scripts(name, pack_dir, installed_items)
@@ -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
- if not (agents_dir / f"{agent}.md").is_file():
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
- if not (skills_dir / skill / "SKILL.md").is_file():
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
@@ -97,15 +97,26 @@ HOOK_REQUIRED_FIELDS = {
97
97
  }
98
98
 
99
99
  # The taxonomy. `app/skills/documentation-standards/SKILL.md` documents the same
100
- # eight and is what authors read; this set is what rejects a typo. They are one
101
- # list in two places, so a change belongs in both.
100
+ # list and is what authors read; this set is what rejects a typo. rag-mcp holds
101
+ # a third copy in `scripts/validate_kb_frontmatter.py`, which runs against
102
+ # knowledge bases this validator never sees. They are one list in three places,
103
+ # so a change belongs in all of them.
102
104
  #
103
105
  # `decisions` and `runbooks` were missing until v4.21.0 while the kb-migration
104
106
  # SOP had been telling people to create those directories for months, so a
105
107
  # correctly-filed ADR failed validation.
108
+ #
109
+ # `business` and `templates` were missing for the same reason: rag-mcp's
110
+ # `add-new-repository-to-rag-mcp.md` mandates `kb/business/` and
111
+ # `kb/templates/`. A directory an SOP mandates must be a category the validator
112
+ # accepts, or the two documents contradict each other.
113
+ #
114
+ # `history/` and `summaries/` are deliberately absent. They are lifecycle and
115
+ # runtime locations, not types: a finished plan under `history/completed/` is
116
+ # still a `planning` document and declares that.
106
117
  VALID_KB_CATEGORIES = frozenset({
107
118
  "reference", "howto", "procedures", "troubleshooting", "best-practices",
108
- "decisions", "runbooks", "planning",
119
+ "decisions", "runbooks", "planning", "business", "templates",
109
120
  })
110
121
 
111
122
  # Skill body budget, in bytes after the frontmatter block.
@@ -764,6 +775,7 @@ def _validate_pack_refs(tk_dir: Path, pack_path: Path, d: dict,
764
775
  d,
765
776
  agents_dir=tk_dir / "app" / "agents",
766
777
  skills_dir=tk_dir / "app" / "skills",
778
+ pack_dir=pack_path,
767
779
  )
768
780
  for err in ref_errors:
769
781
  vr.error(f"app/plugins/{pack_name}/plugin.json {err}")