@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 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: 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-1519%20passing-success)](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.
9
+ [![Tests](https://img.shields.io/badge/tests-1524%20passing-success)](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.23.1",
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, 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: "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
 
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.
@@ -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.1.0"
14352
+ version: "1.2.0"
14353
14353
  created: "2026-03-28"
14354
- last_updated: "2026-07-14"
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.23.1",
2
+ "version": "4.24.1",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.23.1",
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
- 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
 
@@ -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:
@@ -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.