@softspark/ai-toolkit 4.23.1 → 4.24.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 +53 -0
- package/README.md +11 -12
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/documentation-standards/SKILL.md +14 -4
- package/app/surface.json +2 -0
- package/kb/reference/plugin-pack-conventions.md +42 -3
- package/llms-full.txt +43 -4
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/plugin.py +56 -11
- package/scripts/validate.py +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,59 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.24.1 — a dead link reported itself as installed (2026-08-19)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`plugin install` treated a dangling symlink as an installed asset.** The guard
|
|
15
|
+
was `if path.exists() or path.is_symlink()`, and a link whose target is gone
|
|
16
|
+
satisfies the second half — so the installer printed `OK agent` / `OK skill`,
|
|
17
|
+
changed nothing, and left the user with a dead link and a green log. This is the
|
|
18
|
+
exact state a toolkit upgrade produces: `npm install -g @softspark/ai-toolkit`
|
|
19
|
+
replaces `app/` wholesale, so every `~/.claude` link into the old package points
|
|
20
|
+
at nothing, and re-running install could not repair it.
|
|
21
|
+
|
|
22
|
+
A dangling link is now dropped before the guard runs and relinked to the
|
|
23
|
+
resolved source, printing `Dangling agent link removed: <name>`. **Live links and
|
|
24
|
+
real files are untouched** — those are user content, and the merge-friendly
|
|
25
|
+
install model keeps them.
|
|
26
|
+
|
|
27
|
+
Surfaced while migrating a pack from `app/plugins/` to the v4.24.0 user root:
|
|
28
|
+
the upgrade broke both links and `plugin install` insisted everything was fine.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Two tests.** One builds the post-upgrade state (links pointing into a path
|
|
33
|
+
that no longer exists) and asserts install removes and relinks both; the other
|
|
34
|
+
pins the negative — a real file on the agent name and a live skill link survive
|
|
35
|
+
a reinstall untouched.
|
|
36
|
+
|
|
37
|
+
## v4.24.0 — external packs stop dying on upgrade (2026-08-18)
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`~/.softspark/ai-toolkit/plugins/` is a second pack root.** Until now
|
|
42
|
+
`plugin.py` scanned exactly one directory — `app/plugins/`, inside the npm
|
|
43
|
+
package — so the only way to add a pack from its own repository was to drop it
|
|
44
|
+
in there, where the next `npm install -g @softspark/ai-toolkit` deleted it along
|
|
45
|
+
with the rest of `app/`. External packs now live in the user-owned data dir and
|
|
46
|
+
survive every upgrade. `plugin list`, `install`, `update`, `remove` and `status`
|
|
47
|
+
all see both roots; `AI_TOOLKIT_HOME` moves the user root with the data dir.
|
|
48
|
+
|
|
49
|
+
A **core pack wins a name collision**, so an external directory cannot shadow
|
|
50
|
+
shipped behaviour. Each pack dict carries `_root` (`core` or `user`) — `_source`
|
|
51
|
+
was already taken as the hook-ownership marker.
|
|
52
|
+
|
|
53
|
+
The entry may be a directory or a **symlink**, which is what gives an external
|
|
54
|
+
pack a versioned update path: point it at an npm-installed package and
|
|
55
|
+
`npm install -g <pack>@latest` rewrites the target in place, with nothing to
|
|
56
|
+
re-link. Documented in `kb/reference/plugin-pack-conventions.md`
|
|
57
|
+
(*Where Packs Live*), including the per-scope registry caveat when one package
|
|
58
|
+
in a scope is public and another private.
|
|
59
|
+
|
|
60
|
+
- **Three tests.** A pack in the user root is listed, installs with both its agent
|
|
61
|
+
and skill linked back into that root, and loses a name collision to a core pack.
|
|
62
|
+
|
|
10
63
|
## v4.23.1 — a pack could ship an agent nobody installed (2026-08-18)
|
|
11
64
|
|
|
12
65
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.24.1
|
|
12
|
+
|
|
13
|
+
**v4.24.1** — `plugin install` accepted a dead symlink as proof of installation.
|
|
14
|
+
The guard asked `exists() or is_symlink()`, and a link with no target still
|
|
15
|
+
answers yes to the second half, so the installer logged `OK` and repaired
|
|
16
|
+
nothing — precisely the state a toolkit upgrade leaves, since replacing `app/`
|
|
17
|
+
breaks every link into it. Dangling links are now dropped and relinked; real
|
|
18
|
+
files and live links stay untouched. Pairs with v4.24.0's user-level pack root,
|
|
19
|
+
which is what stops the breakage happening in the first place.
|
|
21
20
|
|
|
22
21
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
23
22
|
|
|
@@ -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.24.1",
|
|
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,
|
|
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
|
|
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
|
|
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: "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.
|
|
6
|
+
version: "1.2.0"
|
|
7
7
|
created: "2026-03-28"
|
|
8
|
-
last_updated: "2026-
|
|
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
|
|
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,
|
|
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.
|
|
@@ -14349,9 +14349,9 @@ title: "Plugin Pack Conventions"
|
|
|
14349
14349
|
category: reference
|
|
14350
14350
|
service: ai-toolkit
|
|
14351
14351
|
tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
|
|
14352
|
-
version: "1.
|
|
14352
|
+
version: "1.2.0"
|
|
14353
14353
|
created: "2026-03-28"
|
|
14354
|
-
last_updated: "2026-
|
|
14354
|
+
last_updated: "2026-08-18"
|
|
14355
14355
|
description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across Claude Code and Codex runtimes."
|
|
14356
14356
|
---
|
|
14357
14357
|
|
|
@@ -14359,7 +14359,46 @@ description: "Conventions for experimental ai-toolkit plugin packs, policy packs
|
|
|
14359
14359
|
|
|
14360
14360
|
## Purpose
|
|
14361
14361
|
|
|
14362
|
-
`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.
|
|
14363
14402
|
|
|
14364
14403
|
## Pack Types
|
|
14365
14404
|
|
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.24.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
|
@@ -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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
|
@@ -579,10 +603,30 @@ def _ensure_claude_settings() -> Path:
|
|
|
579
603
|
return settings_path
|
|
580
604
|
|
|
581
605
|
|
|
606
|
+
def _clear_dangling_link(path: Path, label: str) -> bool:
|
|
607
|
+
"""Drop a symlink whose target no longer exists, so install can relink it.
|
|
608
|
+
|
|
609
|
+
A toolkit upgrade replaces app/ wholesale, which leaves every ~/.claude link
|
|
610
|
+
into the old package pointing at nothing. The install guard treated such a
|
|
611
|
+
link as installed (`exists() or is_symlink()`), reported OK and repaired
|
|
612
|
+
nothing — the user was left with a dead link and a green log. Only dangling
|
|
613
|
+
links are removed: a live link or a real file is user content and stays.
|
|
614
|
+
"""
|
|
615
|
+
if path.is_symlink() and not path.exists():
|
|
616
|
+
try:
|
|
617
|
+
path.unlink()
|
|
618
|
+
except OSError:
|
|
619
|
+
return False
|
|
620
|
+
print(f" Dangling {label} link removed: {path.name}")
|
|
621
|
+
return True
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
|
|
582
625
|
def _install_claude_skills(pack: dict, pack_dir: Path, installed_items: list[str]) -> None:
|
|
583
626
|
for skill in pack.get("includes", {}).get("skills", []):
|
|
584
627
|
skill_dir = CLAUDE_DIR / "skills" / skill
|
|
585
628
|
source_dir = _resolve_skill_source(pack_dir, skill)
|
|
629
|
+
_clear_dangling_link(skill_dir, "skill")
|
|
586
630
|
if skill_dir.exists() or skill_dir.is_symlink():
|
|
587
631
|
print(f" OK skill: {skill}")
|
|
588
632
|
elif source_dir:
|
|
@@ -598,6 +642,7 @@ def _install_claude_agents(pack: dict, pack_dir: Path, installed_items: list[str
|
|
|
598
642
|
for agent in pack.get("includes", {}).get("agents", []):
|
|
599
643
|
agent_file = CLAUDE_DIR / "agents" / f"{agent}.md"
|
|
600
644
|
source_file = _resolve_agent_source(pack_dir, agent)
|
|
645
|
+
_clear_dangling_link(agent_file, "agent")
|
|
601
646
|
if agent_file.exists() or agent_file.is_symlink():
|
|
602
647
|
print(f" OK agent: {agent}")
|
|
603
648
|
elif source_file is not None:
|
package/scripts/validate.py
CHANGED
|
@@ -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
|
-
#
|
|
101
|
-
#
|
|
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.
|