@softspark/ai-toolkit 3.0.1 → 3.0.2

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,29 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v3.0.2 — Validation, Windows, Telemetry, and generate:all parity (2026-04-24)
11
+
12
+ Patch release that closes known validation gaps, expands the hook-surface contracts, adds Windows dependency hints, exposes local product telemetry, and restores `npm run generate:all` parity with the CLI so registered custom rules reach every editor.
13
+
14
+ ### Added
15
+
16
+ - **Hook surface validation** — `scripts/validate.py` now accepts the current Claude Code hook event surface, including `PostToolUseFailure`, `PostToolBatch`, and `UserPromptExpansion`.
17
+ - **Hook handler contracts** — validation now checks supported handler types (`command`, `http`, `prompt`, `agent`, `mcp_tool`), required handler fields, and prompt/agent event compatibility.
18
+ - **Language rule validation** — `scripts/validate.py` now checks `app/rules/<language>/*.md` frontmatter, category names, filename/category alignment, directory/language alignment, and required category coverage.
19
+ - **Windows dependency hints** — `_common.detect_os()` recognizes Windows package managers (`winget`, Chocolatey, Scoop), and `check_deps.py` emits package install commands for Python, Git, Node, SQLite, and supported optional tooling.
20
+ - **Product telemetry summary** — `ai-toolkit stats --summary` reports local usage telemetry; `--summary --json` emits machine-readable totals, coverage, unused catalog count, recent activity, and top skills.
21
+ - **Dedicated gap tests** — added contracts for structured rules, Windows dependency support, council/brand-voice/introspect skill coverage, and `npm run generate:all` directory-rule generator coverage. Total test count: 945 -> 960.
22
+ - **Windows Support KB** — new `kb/reference/windows-support.md` documents WSL, Git Bash, package managers, and hook runtime constraints.
23
+
24
+ ### Changed
25
+
26
+ - **`package.json` `generate:all`** — now invokes all directory-based rule generators (`generate_cursor_mdc.py`, `generate_windsurf_rules.py`, `generate_roo_rules.py`, `generate_augment_rules.py`) so registered custom rules in `~/.softspark/ai-toolkit/rules/` propagate to every editor that supports per-rule module files. Restores parity with `ai-toolkit generate-all` (CLI).
27
+ - **`app/skills/hook-creator/SKILL.md`** — documents the expanded hook event and handler type surface.
28
+ - **`README.md` and `kb/reference/cli-reference.md`** — document `stats --summary` and Windows support.
29
+ - **`kb/reference/hooks-catalog.md`** — documents the validated hook surface and non-command handler support.
30
+
31
+ ---
32
+
10
33
  ## v3.0.1 — Release SOP Deep-Coverage Checks (2026-04-24)
11
34
 
12
35
  Doc-only patch. No code, generators, or runtime behavior changed.
package/README.md CHANGED
@@ -6,16 +6,19 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-99-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-945%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-960%20passing-success)](tests/)
10
10
 
11
11
  ---
12
12
 
13
- ## What's New in v3.0.1
13
+ ## What's New in v3.0.2
14
14
 
15
- **3.0.1 is a doc-only patch** that tightens the release SOPs. No code or generator changes. The v3.0.0 feature set below is still what you get when you upgrade from 2.x.
15
+ **3.0.2 is a patch release** that closes known validation gaps, expands the validated hook surface, adds Windows dependency hints, exposes local product telemetry, and restores `npm run generate:all` parity with the CLI.
16
16
 
17
- - Release Verification SOP gains **Phase 9: Deep-Coverage Checks** — six automated assertions for the v3.0.0 native surfaces (`--profile full` emission, `--codex-skills` orthogonality, breaking-change surfaces on `standard`, install idempotence, live JSON parse, registry/generator drift) plus refreshed thresholds and a HOME-scoped-write safety warning.
18
- - Release Preparation SOP now runs the registry-vs-generators drift check in Phase 5 so a misaligned `supported-tools-registry.md` can't escape into a release.
17
+ - `validate.py` now checks hook handler types, required handler fields, prompt/agent event compatibility, and structured language-rule frontmatter/category coverage.
18
+ - Hook docs and validation now cover `PostToolUseFailure`, `PostToolBatch`, `UserPromptExpansion`, plus `command`, `http`, `prompt`, `agent`, and `mcp_tool` handler types.
19
+ - `ai-toolkit stats --summary` reports local product telemetry: total invocations, unique skills used, catalog coverage, unused skills, recent activity, and top skills.
20
+ - Windows dependency detection now emits package hints for `winget`, Chocolatey, and Scoop; WSL remains the recommended runtime for Bash hooks.
21
+ - `npm run generate:all` now invokes every directory-based rule generator, so registered custom rules in `~/.softspark/ai-toolkit/rules/` propagate to every editor that supports per-rule module files.
19
22
 
20
23
  ### Carried from v3.0.0 (feature release)
21
24
 
@@ -66,6 +69,8 @@ npx @softspark/ai-toolkit install
66
69
 
67
70
  **That's it.** Claude Code picks up 99 skills, 44 agents, quality hooks, and the safety constitution automatically.
68
71
 
72
+ **Windows:** WSL is the recommended runtime. Native Windows works when Git Bash is available for hook scripts; dependency hints cover `winget`, Chocolatey, and Scoop. See [Windows Support](kb/reference/windows-support.md).
73
+
69
74
  ### Update
70
75
 
71
76
  ```bash
@@ -161,7 +166,7 @@ ai-toolkit/
161
166
  │ └── ARCHITECTURE.md # Full system design
162
167
  ├── kb/ # Reference docs, procedures, plans
163
168
  ├── scripts/ # Validation, install, evaluation scripts
164
- ├── tests/ # Bats test suite (945 tests)
169
+ ├── tests/ # Bats test suite (960 tests)
165
170
  └── CHANGELOG.md
166
171
  ```
167
172
 
@@ -187,6 +192,8 @@ ai-toolkit/
187
192
 
188
193
  **Persistent memory** — `memory-pack` plugin: SQLite + FTS5 search across past sessions.
189
194
 
195
+ **Local product telemetry** — `ai-toolkit stats --summary` reports total invocations, skill coverage, unused catalog skills, recent activity, and top skills from local usage data.
196
+
190
197
  **Persona presets** — 4 roles (backend-lead, frontend-lead, devops-eng, junior-dev) adjust style and priorities.
191
198
 
192
199
  **Config inheritance** — Enterprise `extends` system with constitution immutability and enforcement constraints. See [Enterprise Config Guide](kb/reference/enterprise-config-guide.md).
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-toolkit",
3
3
  "description": "Professional-grade Claude Code toolkit with persona presets, skill security auditor, expanded lifecycle hooks, experimental opt-in plugin packs, benchmark harvesting, and multi-tool support.",
4
- "version": "3.0.1",
4
+ "version": "3.0.2",
5
5
  "author": {
6
6
  "name": "SoftSpark",
7
7
  "url": "https://github.com/softspark"
@@ -30,6 +30,8 @@ Create a new Claude Code hook following ai-toolkit conventions.
30
30
  |-------|-----------|---------|-------------|
31
31
  | `PreToolUse` | Before a tool executes | tool name (e.g. `Bash`) or `if:` rule | Safety guards, validation, `"defer"` for headless |
32
32
  | `PostToolUse` | After a tool executes | tool name | Feedback loops, logging, format-on-save |
33
+ | `PostToolUseFailure` | After a tool fails | tool name | Failure telemetry, recovery hints |
34
+ | `PostToolBatch` | After a batch of tool calls completes | any | Batch summaries, aggregate validation |
33
35
 
34
36
  ### Turn lifecycle
35
37
 
@@ -37,6 +39,7 @@ Create a new Claude Code hook following ai-toolkit conventions.
37
39
  |-------|-----------|---------|-------------|
38
40
  | `Stop` | Claude finishes responding | any | Quality checks, session save |
39
41
  | `StopFailure` | Turn ends due to an API error (rate limit, auth) | any | Alerting, fallback behavior |
42
+ | `UserPromptExpansion` | Claude expands or rewrites a submitted prompt | any | Prompt policy and context shaping |
40
43
 
41
44
  ### Subagent lifecycle
42
45
 
@@ -88,12 +91,13 @@ Create a new Claude Code hook following ai-toolkit conventions.
88
91
 
89
92
  ## Hook Handler Types
90
93
 
91
- Claude Code supports four handler `type` values in `hooks.json`:
94
+ Claude Code supports five handler `type` values in `hooks.json`:
92
95
 
93
96
  | Type | Purpose | Required fields |
94
97
  |------|---------|-----------------|
95
98
  | `command` | Run a shell script / binary | `command` (path + args) |
96
- | `prompt` | Inject a prompt to the fast inline model and use its verdict | Handler-managed |
99
+ | `http` | Call a local or remote HTTP endpoint | `url` |
100
+ | `prompt` | Inject a prompt to the fast inline model and use its verdict | `prompt` |
97
101
  | `agent` | Spawn a full subagent to evaluate the event (must target `Stop` / `SubagentStop`) | `agent` (agent name) |
98
102
  | `mcp_tool` | Invoke an MCP tool directly (no subprocess) | `server`, `tool`, `arguments` |
99
103
 
@@ -137,7 +141,7 @@ Claude Code supports four handler `type` values in `hooks.json`:
137
141
  Required fields:
138
142
  - `_source`: always `"ai-toolkit"` (used by merge/strip logic)
139
143
  - `matcher`: tool name or regex for Pre/PostToolUse, empty string for global events
140
- - `hooks[].type`: `"command"`, `"prompt"`, `"agent"`, or `"mcp_tool"` (ai-toolkit uses `"command"`)
144
+ - `hooks[].type`: `"command"`, `"http"`, `"prompt"`, `"agent"`, or `"mcp_tool"` (ai-toolkit uses `"command"`)
141
145
  - `hooks[].command`: path to script using `$HOME/.softspark/ai-toolkit/hooks/` prefix (for `type: command`)
142
146
 
143
147
  Optional fields (read from Claude Code docs, not emitted by ai-toolkit by default):
@@ -61,7 +61,7 @@ Answer these three questions:
61
61
 
62
62
  ## Step 4: Select Recovery Action
63
63
 
64
- Apply the **smallest possible fix** — do not restart from scratch unless absolutely necessary:
64
+ Choose the **smallest recovery action** and apply the smallest possible fix — do not restart from scratch unless absolutely necessary:
65
65
 
66
66
  | Pattern | Recovery Action |
67
67
  |---------|----------------|
@@ -1,5 +1,5 @@
1
1
  {
2
- "last_run": "2026-04-23T13:13:00Z",
2
+ "last_run": "2026-04-24T08:04:02Z",
3
3
  "schema_version": 1,
4
4
  "tools": {
5
5
  "aider": {
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "augment": {
27
- "docs_hash": "91dbc84ba3b8652b",
27
+ "docs_hash": "5754553b0e436fd7",
28
28
  "headings": [
29
29
  "Agent",
30
30
  "Code Completions",
@@ -57,7 +57,7 @@
57
57
  }
58
58
  },
59
59
  "claude-code": {
60
- "docs_hash": "a117026a47c684b9",
60
+ "docs_hash": "0c111b1b1f28960d",
61
61
  "headings": [
62
62
  "Claude Code overview",
63
63
  "\u200bGet started",
@@ -102,10 +102,10 @@
102
102
  "slash command": true,
103
103
  "sub-agent": true
104
104
  },
105
- "version": "2.1.118 (Claude Code)"
105
+ "version": "2.1.119 (Claude Code)"
106
106
  },
107
107
  "cline": {
108
- "docs_hash": "5ed2cada4d9b3fa6",
108
+ "docs_hash": "17c24d27216f70cf",
109
109
  "headings": [
110
110
  "Cline CLI",
111
111
  "Cline Documentation",
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "codex-cli": {
138
- "docs_hash": "d66e6b65d5c69da4",
138
+ "docs_hash": "7a7f5cdf9411c5e2",
139
139
  "headings": [
140
140
  "About",
141
141
  "Contributing",
@@ -154,7 +154,7 @@
154
154
  "Packages 0",
155
155
  "Provide feedback",
156
156
  "Quickstart",
157
- "Releases 732",
157
+ "Releases 735",
158
158
  "Repository files navigation",
159
159
  "Resources",
160
160
  "Saved searches",
@@ -184,10 +184,10 @@
184
184
  "mcp_servers": false,
185
185
  "sandbox": true
186
186
  },
187
- "version": "codex-cli 0.122.0"
187
+ "version": "codex-cli 0.124.0"
188
188
  },
189
189
  "cursor": {
190
- "docs_hash": "594d7b663fb8b3e1",
190
+ "docs_hash": "a94b8e8f685b0831",
191
191
  "headings": [],
192
192
  "markers": {
193
193
  ".cursor/rules": false,
@@ -203,7 +203,7 @@
203
203
  }
204
204
  },
205
205
  "gemini-cli": {
206
- "docs_hash": "51d03cce48332772",
206
+ "docs_hash": "9415ea08e344013d",
207
207
  "headings": [
208
208
  "Breadcrumbs",
209
209
  "Directory actions",
@@ -242,7 +242,7 @@
242
242
  }
243
243
  },
244
244
  "github-copilot": {
245
- "docs_hash": "377b588e529c7deb",
245
+ "docs_hash": "8bcd2db0db925807",
246
246
  "headings": [
247
247
  "About Copilot auto model selection",
248
248
  "About Copilot integrations",
@@ -290,7 +290,7 @@
290
290
  }
291
291
  },
292
292
  "opencode": {
293
- "docs_hash": "81d548872ef2d912",
293
+ "docs_hash": "760fbf466036892f",
294
294
  "headings": [
295
295
  "Add features",
296
296
  "Ask questions",
@@ -351,7 +351,7 @@
351
351
  }
352
352
  },
353
353
  "windsurf": {
354
- "docs_hash": "99ecee90bc1efe60",
354
+ "docs_hash": "81d0aafbf97b18c8",
355
355
  "headings": [
356
356
  "Advanced",
357
357
  "App Deploys",
package/bin/ai-toolkit.js CHANGED
@@ -74,7 +74,7 @@ const COMMANDS = {
74
74
  benchmark: 'Benchmark toolkit (--my-config to compare your setup vs defaults vs ecosystem)',
75
75
  'benchmark-ecosystem': 'Generate ecosystem benchmark snapshot (GitHub metadata + offline fallback)',
76
76
  evaluate: 'Run skill evaluation suite',
77
- stats: 'Show skill usage statistics (--reset to clear, --json for raw output)',
77
+ stats: 'Show skill usage statistics (--summary for product telemetry, --reset to clear)',
78
78
  create: 'Scaffold new skill from template (e.g. create skill my-lint --template=linter)',
79
79
  mcp: 'Manage MCP templates and install native editor MCP configs',
80
80
  config: 'Manage config inheritance (validate, diff, init, create-base, check)',
@@ -280,6 +280,10 @@ function showHelp() {
280
280
  console.log(' Remove from .mcp.json or native editor configs');
281
281
  console.log('\nOptions for doctor:');
282
282
  console.log(' --fix Auto-repair detected issues');
283
+ console.log('\nOptions for stats:');
284
+ console.log(' --summary Show aggregated product telemetry (usage coverage, top skills, unused catalog)');
285
+ console.log(' --json Emit raw stats, or machine-readable telemetry when combined with --summary');
286
+ console.log(' --reset Clear local usage stats');
283
287
  console.log('\nOptions for eject:');
284
288
  console.log(' [target-dir] Target directory (default: current directory)');
285
289
  console.log('');
@@ -2,9 +2,9 @@
2
2
  title: "CLI Reference"
3
3
  category: reference
4
4
  service: ai-toolkit
5
- tags: [cli, commands, reference, install, update, plugin, mcp]
5
+ tags: [cli, commands, reference, install, update, plugin, mcp, telemetry]
6
6
  created: "2026-04-13"
7
- last_updated: "2026-04-15"
7
+ last_updated: "2026-04-24"
8
8
  description: "Complete CLI reference for all ai-toolkit commands, options, and flags."
9
9
  ---
10
10
 
@@ -111,7 +111,7 @@ Usage: ai-toolkit <command> [options]
111
111
 
112
112
  | Command | Description |
113
113
  |---------|-------------|
114
- | `stats` | Show skill usage statistics (`--reset` to clear, `--json` for raw output) |
114
+ | `stats` | Show skill usage statistics (`--summary` for product telemetry, `--reset` to clear, `--json` for raw output) |
115
115
  | `benchmark --my-config` | Compare your config vs defaults vs ecosystem |
116
116
  | `benchmark-ecosystem` | Generate ecosystem benchmark snapshot |
117
117
  | `create skill <name>` | Scaffold new skill from template (`--template=linter\|reviewer\|generator\|workflow\|knowledge`) |
@@ -119,6 +119,17 @@ Usage: ai-toolkit <command> [options]
119
119
  | `compile-slm` | Compile toolkit into minimal SLM system prompt (`--budget`, `--model-size`, `--dry-run`) |
120
120
  | `evaluate` | Run skill evaluation suite |
121
121
 
122
+ ### `stats`
123
+
124
+ ```bash
125
+ ai-toolkit stats # table of local skill usage
126
+ ai-toolkit stats --summary # product telemetry summary
127
+ ai-toolkit stats --summary --json # machine-readable telemetry
128
+ ai-toolkit stats --reset # clear local stats
129
+ ```
130
+
131
+ `--summary` reports total invocations, unique skills used, catalog coverage, unused catalog skills, active skills in the last 7 days, and top skills. Data stays local in `~/.softspark/ai-toolkit/stats.json`.
132
+
122
133
  ## Install / Update Options
123
134
 
124
135
  ```bash
@@ -13,7 +13,7 @@ tags:
13
13
  doc_type: plan
14
14
  status: completed
15
15
  created: "2026-04-07"
16
- last_updated: "2026-04-09"
16
+ last_updated: "2026-04-24"
17
17
  completion: "100%"
18
18
  description: "Implementation plan for features identified from competitive analysis of everything-claude-code and claude-mem. Focus on learning system, language rules, advanced hooks, MCP templates, and rag-mcp integration. COMPLETED: 8/9 features shipped (1 skipped). See kb/reference/ for permanent documentation."
19
19
  ---
@@ -142,8 +142,8 @@ app/rules/
142
142
  - [x] 13 languages × 5 rule files created (70 files: 13 dirs × 5 + 5 common)
143
143
  - [x] `ai-toolkit install --local` auto-detects language and injects rules (two-phase: marker files + extension scan)
144
144
  - [x] Manual override: `ai-toolkit install --local --lang typescript` (with aliases: go→golang, c++→cpp, cs→csharp)
145
- - [ ] validate.py checks rules format (not yet implemented)
146
- - [ ] Tests: >=13 (no test_rules file yet)
145
+ - [x] validate.py checks rules format
146
+ - [x] Tests: dedicated `tests/test_rules.bats`
147
147
 
148
148
  ---
149
149
 
@@ -497,7 +497,7 @@ context: fork
497
497
  - [x] `/council` invocable
498
498
  - [x] 4 perspectives generated
499
499
  - [x] Structured output with recommendation
500
- - [ ] Tests: dedicated council tests not yet written
500
+ - [x] Tests: dedicated council skill contract
501
501
 
502
502
  ---
503
503
 
@@ -516,7 +516,7 @@ context: fork
516
516
  **Success Criteria:**
517
517
  - [x] Skill auto-loads when writing docs/content
518
518
  - [x] Anti-trope list prevents generic LLM rhetoric
519
- - [ ] Tests: dedicated brand-voice tests not yet written
519
+ - [x] Tests: dedicated brand-voice skill contract
520
520
 
521
521
  ---
522
522
 
@@ -538,7 +538,7 @@ context: fork
538
538
  - [x] `/introspect` invocable when agent is stuck
539
539
  - [x] Classifies failure pattern
540
540
  - [x] Suggests recovery action
541
- - [ ] Tests: dedicated introspect tests not yet written
541
+ - [x] Tests: dedicated introspect skill contract
542
542
 
543
543
  ---
544
544
 
@@ -660,9 +660,9 @@ npx @softspark/ai-toolkit inject-hook ./rag-mcp-hooks.json # NEW
660
660
 
661
661
  All major features shipped. Outstanding items:
662
662
 
663
- 1. [ ] `validate.py` does not check rules format (1.1)
664
- 2. [ ] No dedicated `test_rules` test file (1.1)
665
- 3. [ ] No dedicated tests for council, brand-voice, introspect skills (3.1-3.3)
663
+ 1. [x] `validate.py` checks rules format (1.1)
664
+ 2. [x] Dedicated `test_rules` test file exists (1.1)
665
+ 3. [x] Dedicated tests exist for council, brand-voice, introspect skills (3.1-3.3)
666
666
  4. [x] `observe-session.sh` lives in rag-mcp (consumer), not ai-toolkit — by design
667
667
 
668
668
  ---
@@ -3,9 +3,9 @@ title: "Hooks Catalog"
3
3
  category: reference
4
4
  service: ai-toolkit
5
5
  tags: [hooks, quality, safety, enforcement, settings.json]
6
- version: "1.4.2"
6
+ version: "1.5.0"
7
7
  created: "2026-03-27"
8
- last_updated: "2026-04-09"
8
+ last_updated: "2026-04-24"
9
9
  description: "Complete reference of all ai-toolkit hooks: events, scripts, installation, and runtime behavior."
10
10
  ---
11
11
 
@@ -15,6 +15,12 @@ description: "Complete reference of all ai-toolkit hooks: events, scripts, insta
15
15
 
16
16
  ai-toolkit provides 21 global hook entries across 12 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
17
17
 
18
+ ## Supported Surface
19
+
20
+ `scripts/validate.py` validates both event names and handler shapes before release. The accepted lifecycle surface includes `PostToolUseFailure`, `PostToolBatch`, and `UserPromptExpansion` in addition to the installed ai-toolkit events below.
21
+
22
+ Supported handler types are `command`, `http`, `prompt`, `agent`, and `mcp_tool`. ai-toolkit ships command hooks by default; non-command handlers are validated so external consumers can safely inject richer hook definitions through `inject-hook`.
23
+
18
24
  ## Installation
19
25
 
20
26
  ```bash
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: "Windows Support"
3
+ category: reference
4
+ service: ai-toolkit
5
+ tags: [windows, wsl, install, dependencies, hooks]
6
+ created: "2026-04-24"
7
+ last_updated: "2026-04-24"
8
+ description: "Windows support model for ai-toolkit: WSL, Git Bash, dependency detection, and hook runtime constraints."
9
+ ---
10
+
11
+ # Windows Support
12
+
13
+ ai-toolkit supports Windows through two practical modes:
14
+
15
+ 1. **WSL recommended** — best compatibility for Bash hooks, POSIX paths, symlinks, and editor configs.
16
+ 2. **Native Windows with Git Bash** — supported for CLI usage when Bash is available on `PATH`.
17
+
18
+ ## Dependency Detection
19
+
20
+ `scripts/check_deps.py` now emits install hints for Windows package managers:
21
+
22
+ | Manager | Command Prefix |
23
+ |---------|----------------|
24
+ | winget | `winget install` |
25
+ | Chocolatey | `choco install -y` |
26
+ | Scoop | `scoop install` |
27
+
28
+ Required dependency package IDs:
29
+
30
+ | Dependency | winget | Chocolatey | Scoop |
31
+ |------------|--------|------------|-------|
32
+ | Python 3 | `Python.Python.3` | `python` | `python` |
33
+ | Git | `Git.Git` | `git` | `git` |
34
+ | Node.js | `OpenJS.NodeJS` | `nodejs` | `nodejs` |
35
+
36
+ ## Hook Runtime
37
+
38
+ ai-toolkit hooks are Bash scripts. On Windows, use WSL or Git Bash so Claude Code can execute `~/.softspark/ai-toolkit/hooks/*.sh`.
39
+
40
+ Cross-platform hooks should keep the Bash entrypoint small and delegate complex work to Python or Node when Windows behavior diverges.
41
+
42
+ ## Verification
43
+
44
+ ```bash
45
+ ai-toolkit doctor
46
+ python3 scripts/check_deps.py
47
+ python3 scripts/validate.py
48
+ ```
49
+
50
+ The Windows support contract is covered by `tests/test_windows_support.bats`.
package/llms-full.txt CHANGED
@@ -58,6 +58,7 @@
58
58
  - [Supported Tools Registry](kb/reference/supported-tools-registry.md)
59
59
  - [Config Sync](kb/reference/sync.md)
60
60
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
61
+ - [Windows Support](kb/reference/windows-support.md)
61
62
  - [Troubleshooting](kb/troubleshooting/README.md)
62
63
 
63
64
  ## Skills
@@ -6208,9 +6209,9 @@ The reference benchmark set is intentionally curated:
6208
6209
  title: "CLI Reference"
6209
6210
  category: reference
6210
6211
  service: ai-toolkit
6211
- tags: [cli, commands, reference, install, update, plugin, mcp]
6212
+ tags: [cli, commands, reference, install, update, plugin, mcp, telemetry]
6212
6213
  created: "2026-04-13"
6213
- last_updated: "2026-04-15"
6214
+ last_updated: "2026-04-24"
6214
6215
  description: "Complete CLI reference for all ai-toolkit commands, options, and flags."
6215
6216
  ---
6216
6217
 
@@ -6317,7 +6318,7 @@ Usage: ai-toolkit <command> [options]
6317
6318
 
6318
6319
  | Command | Description |
6319
6320
  |---------|-------------|
6320
- | `stats` | Show skill usage statistics (`--reset` to clear, `--json` for raw output) |
6321
+ | `stats` | Show skill usage statistics (`--summary` for product telemetry, `--reset` to clear, `--json` for raw output) |
6321
6322
  | `benchmark --my-config` | Compare your config vs defaults vs ecosystem |
6322
6323
  | `benchmark-ecosystem` | Generate ecosystem benchmark snapshot |
6323
6324
  | `create skill <name>` | Scaffold new skill from template (`--template=linter\|reviewer\|generator\|workflow\|knowledge`) |
@@ -6325,6 +6326,17 @@ Usage: ai-toolkit <command> [options]
6325
6326
  | `compile-slm` | Compile toolkit into minimal SLM system prompt (`--budget`, `--model-size`, `--dry-run`) |
6326
6327
  | `evaluate` | Run skill evaluation suite |
6327
6328
 
6329
+ ### `stats`
6330
+
6331
+ ```bash
6332
+ ai-toolkit stats # table of local skill usage
6333
+ ai-toolkit stats --summary # product telemetry summary
6334
+ ai-toolkit stats --summary --json # machine-readable telemetry
6335
+ ai-toolkit stats --reset # clear local stats
6336
+ ```
6337
+
6338
+ `--summary` reports total invocations, unique skills used, catalog coverage, unused catalog skills, active skills in the last 7 days, and top skills. Data stays local in `~/.softspark/ai-toolkit/stats.json`.
6339
+
6328
6340
  ## Install / Update Options
6329
6341
 
6330
6342
  ```bash
@@ -6538,7 +6550,7 @@ tags:
6538
6550
  doc_type: plan
6539
6551
  status: completed
6540
6552
  created: "2026-04-07"
6541
- last_updated: "2026-04-09"
6553
+ last_updated: "2026-04-24"
6542
6554
  completion: "100%"
6543
6555
  description: "Implementation plan for features identified from competitive analysis of everything-claude-code and claude-mem. Focus on learning system, language rules, advanced hooks, MCP templates, and rag-mcp integration. COMPLETED: 8/9 features shipped (1 skipped). See kb/reference/ for permanent documentation."
6544
6556
  ---
@@ -6667,8 +6679,8 @@ app/rules/
6667
6679
  - [x] 13 languages × 5 rule files created (70 files: 13 dirs × 5 + 5 common)
6668
6680
  - [x] `ai-toolkit install --local` auto-detects language and injects rules (two-phase: marker files + extension scan)
6669
6681
  - [x] Manual override: `ai-toolkit install --local --lang typescript` (with aliases: go→golang, c++→cpp, cs→csharp)
6670
- - [ ] validate.py checks rules format (not yet implemented)
6671
- - [ ] Tests: >=13 (no test_rules file yet)
6682
+ - [x] validate.py checks rules format
6683
+ - [x] Tests: dedicated `tests/test_rules.bats`
6672
6684
 
6673
6685
  ---
6674
6686
 
@@ -7022,7 +7034,7 @@ context: fork
7022
7034
  - [x] `/council` invocable
7023
7035
  - [x] 4 perspectives generated
7024
7036
  - [x] Structured output with recommendation
7025
- - [ ] Tests: dedicated council tests not yet written
7037
+ - [x] Tests: dedicated council skill contract
7026
7038
 
7027
7039
  ---
7028
7040
 
@@ -7041,7 +7053,7 @@ context: fork
7041
7053
  **Success Criteria:**
7042
7054
  - [x] Skill auto-loads when writing docs/content
7043
7055
  - [x] Anti-trope list prevents generic LLM rhetoric
7044
- - [ ] Tests: dedicated brand-voice tests not yet written
7056
+ - [x] Tests: dedicated brand-voice skill contract
7045
7057
 
7046
7058
  ---
7047
7059
 
@@ -7063,7 +7075,7 @@ context: fork
7063
7075
  - [x] `/introspect` invocable when agent is stuck
7064
7076
  - [x] Classifies failure pattern
7065
7077
  - [x] Suggests recovery action
7066
- - [ ] Tests: dedicated introspect tests not yet written
7078
+ - [x] Tests: dedicated introspect skill contract
7067
7079
 
7068
7080
  ---
7069
7081
 
@@ -7185,9 +7197,9 @@ npx @softspark/ai-toolkit inject-hook ./rag-mcp-hooks.json # NEW
7185
7197
 
7186
7198
  All major features shipped. Outstanding items:
7187
7199
 
7188
- 1. [ ] `validate.py` does not check rules format (1.1)
7189
- 2. [ ] No dedicated `test_rules` test file (1.1)
7190
- 3. [ ] No dedicated tests for council, brand-voice, introspect skills (3.1-3.3)
7200
+ 1. [x] `validate.py` checks rules format (1.1)
7201
+ 2. [x] Dedicated `test_rules` test file exists (1.1)
7202
+ 3. [x] Dedicated tests exist for council, brand-voice, introspect skills (3.1-3.3)
7191
7203
  4. [x] `observe-session.sh` lives in rag-mcp (consumer), not ai-toolkit — by design
7192
7204
 
7193
7205
  ---
@@ -8159,9 +8171,9 @@ title: "Hooks Catalog"
8159
8171
  category: reference
8160
8172
  service: ai-toolkit
8161
8173
  tags: [hooks, quality, safety, enforcement, settings.json]
8162
- version: "1.4.2"
8174
+ version: "1.5.0"
8163
8175
  created: "2026-03-27"
8164
- last_updated: "2026-04-09"
8176
+ last_updated: "2026-04-24"
8165
8177
  description: "Complete reference of all ai-toolkit hooks: events, scripts, installation, and runtime behavior."
8166
8178
  ---
8167
8179
 
@@ -8171,6 +8183,12 @@ description: "Complete reference of all ai-toolkit hooks: events, scripts, insta
8171
8183
 
8172
8184
  ai-toolkit provides 21 global hook entries across 12 lifecycle events that enforce quality, safety, and workflow rules across all Claude Code sessions. Hooks are merged into `~/.claude/settings.json` on install, with logic in standalone scripts at `~/.softspark/ai-toolkit/hooks/`.
8173
8185
 
8186
+ ## Supported Surface
8187
+
8188
+ `scripts/validate.py` validates both event names and handler shapes before release. The accepted lifecycle surface includes `PostToolUseFailure`, `PostToolBatch`, and `UserPromptExpansion` in addition to the installed ai-toolkit events below.
8189
+
8190
+ Supported handler types are `command`, `http`, `prompt`, `agent`, and `mcp_tool`. ai-toolkit ships command hooks by default; non-command handlers are validated so external consumers can safely inject richer hook definitions through `inject-hook`.
8191
+
8174
8192
  ## Installation
8175
8193
 
8176
8194
  ```bash
@@ -11326,6 +11344,61 @@ Agents follow a research-before-action protocol enforced via rules:
11326
11344
 
11327
11345
  ---
11328
11346
 
11347
+ ## kb/reference/windows-support.md
11348
+
11349
+ ---
11350
+ title: "Windows Support"
11351
+ category: reference
11352
+ service: ai-toolkit
11353
+ tags: [windows, wsl, install, dependencies, hooks]
11354
+ created: "2026-04-24"
11355
+ last_updated: "2026-04-24"
11356
+ description: "Windows support model for ai-toolkit: WSL, Git Bash, dependency detection, and hook runtime constraints."
11357
+ ---
11358
+
11359
+ # Windows Support
11360
+
11361
+ ai-toolkit supports Windows through two practical modes:
11362
+
11363
+ 1. **WSL recommended** — best compatibility for Bash hooks, POSIX paths, symlinks, and editor configs.
11364
+ 2. **Native Windows with Git Bash** — supported for CLI usage when Bash is available on `PATH`.
11365
+
11366
+ ## Dependency Detection
11367
+
11368
+ `scripts/check_deps.py` now emits install hints for Windows package managers:
11369
+
11370
+ | Manager | Command Prefix |
11371
+ |---------|----------------|
11372
+ | winget | `winget install` |
11373
+ | Chocolatey | `choco install -y` |
11374
+ | Scoop | `scoop install` |
11375
+
11376
+ Required dependency package IDs:
11377
+
11378
+ | Dependency | winget | Chocolatey | Scoop |
11379
+ |------------|--------|------------|-------|
11380
+ | Python 3 | `Python.Python.3` | `python` | `python` |
11381
+ | Git | `Git.Git` | `git` | `git` |
11382
+ | Node.js | `OpenJS.NodeJS` | `nodejs` | `nodejs` |
11383
+
11384
+ ## Hook Runtime
11385
+
11386
+ ai-toolkit hooks are Bash scripts. On Windows, use WSL or Git Bash so Claude Code can execute `~/.softspark/ai-toolkit/hooks/*.sh`.
11387
+
11388
+ Cross-platform hooks should keep the Bash entrypoint small and delegate complex work to Python or Node when Windows behavior diverges.
11389
+
11390
+ ## Verification
11391
+
11392
+ ```bash
11393
+ ai-toolkit doctor
11394
+ python3 scripts/check_deps.py
11395
+ python3 scripts/validate.py
11396
+ ```
11397
+
11398
+ The Windows support contract is covered by `tests/test_windows_support.bats`.
11399
+
11400
+ ---
11401
+
11329
11402
  ## kb/troubleshooting/README.md
11330
11403
 
11331
11404
  ---
package/llms.txt CHANGED
@@ -58,4 +58,5 @@
58
58
  - [Supported Tools Registry](kb/reference/supported-tools-registry.md)
59
59
  - [Config Sync](kb/reference/sync.md)
60
60
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
61
+ - [Windows Support](kb/reference/windows-support.md)
61
62
  - [Troubleshooting](kb/troubleshooting/README.md)
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.1",
2
+ "version": "3.0.2",
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": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "AI coding toolkit: 99 skills, 44 agents, 12-editor write-through (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo, Aider, Augment, Antigravity, Codex, opencode), machine-enforced safety constitution, SARIF audit, signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
@@ -62,7 +62,11 @@
62
62
  "generate:codex-rules": "python3 scripts/generate_codex_rules.py .",
63
63
  "generate:opencode-agents": "python3 scripts/generate_opencode_agents.py .",
64
64
  "generate:opencode-commands": "python3 scripts/generate_opencode_commands.py .",
65
- "generate:all": "npm run generate:agents && npm run generate:codex-rules && npm run generate:opencode-agents && npm run generate:opencode-commands && npm run generate:cursor && npm run generate:windsurf && npm run generate:copilot && npm run generate:gemini && npm run generate:cline && npm run generate:roo && npm run generate:aider && npm run generate:llms"
65
+ "generate:cursor-mdc": "python3 scripts/generate_cursor_mdc.py .",
66
+ "generate:windsurf-rules": "python3 scripts/generate_windsurf_rules.py .",
67
+ "generate:roo-rules": "python3 scripts/generate_roo_rules.py .",
68
+ "generate:augment-rules": "python3 scripts/generate_augment_rules.py .",
69
+ "generate:all": "npm run generate:agents && npm run generate:codex-rules && npm run generate:opencode-agents && npm run generate:opencode-commands && npm run generate:cursor && npm run generate:cursor-mdc && npm run generate:windsurf && npm run generate:windsurf-rules && npm run generate:copilot && npm run generate:gemini && npm run generate:cline && npm run generate:roo && npm run generate:roo-rules && npm run generate:aider && npm run generate:augment-rules && npm run generate:llms"
66
70
  },
67
71
  "files": [
68
72
  "bin/",
@@ -165,6 +165,27 @@ def detect_os() -> dict[str, str]:
165
165
  "install_cmd": install_cmd,
166
166
  }
167
167
 
168
+ if system == "windows":
169
+ managers = (
170
+ ("winget", "winget install"),
171
+ ("choco", "choco install -y"),
172
+ ("scoop", "scoop install"),
173
+ )
174
+ for manager, install_cmd in managers:
175
+ if shutil.which(manager):
176
+ return {
177
+ "os": "Windows",
178
+ "distro": platform.version(),
179
+ "pkg_manager": manager,
180
+ "install_cmd": install_cmd,
181
+ }
182
+ return {
183
+ "os": "Windows",
184
+ "distro": platform.version(),
185
+ "pkg_manager": "none",
186
+ "install_cmd": "",
187
+ }
188
+
168
189
  return {
169
190
  "os": system,
170
191
  "distro": "",
@@ -37,6 +37,9 @@ REQUIRED = [
37
37
  "pacman": "python3",
38
38
  "apk": "python3",
39
39
  "zypper": "python3",
40
+ "winget": "Python.Python.3",
41
+ "choco": "python",
42
+ "scoop": "python",
40
43
  },
41
44
  "reason": "All toolkit scripts run on Python 3 (stdlib only, no pip needed)",
42
45
  },
@@ -50,6 +53,9 @@ REQUIRED = [
50
53
  "pacman": "git",
51
54
  "apk": "git",
52
55
  "zypper": "git",
56
+ "winget": "Git.Git",
57
+ "choco": "git",
58
+ "scoop": "git",
53
59
  },
54
60
  "reason": "Version control — hooks, commits, PR workflows",
55
61
  },
@@ -64,6 +70,9 @@ REQUIRED = [
64
70
  "pacman": "nodejs",
65
71
  "apk": "nodejs",
66
72
  "zypper": "nodejs18",
73
+ "winget": "OpenJS.NodeJS",
74
+ "choco": "nodejs",
75
+ "scoop": "nodejs",
67
76
  },
68
77
  "reason": "CLI entry point (bin/ai-toolkit.js)",
69
78
  },
@@ -80,6 +89,9 @@ OPTIONAL = [
80
89
  "pacman": "sqlite",
81
90
  "apk": "sqlite",
82
91
  "zypper": "sqlite3",
92
+ "winget": "SQLite.SQLite",
93
+ "choco": "sqlite",
94
+ "scoop": "sqlite",
83
95
  },
84
96
  "reason": "Memory plugin pack (session persistence via SQLite + FTS5)",
85
97
  },
@@ -93,6 +105,8 @@ OPTIONAL = [
93
105
  "pacman": "bash-bats",
94
106
  "apk": "bats",
95
107
  "zypper": "bats",
108
+ "choco": "bats",
109
+ "scoop": "bats",
96
110
  },
97
111
  "reason": "Running toolkit test suite (npm test)",
98
112
  },
package/scripts/stats.py CHANGED
@@ -5,11 +5,13 @@ Reads ~/.softspark/ai-toolkit/stats.json (populated by track-usage.sh hook)
5
5
  and displays a sorted table of skill invocations.
6
6
 
7
7
  Options:
8
- --reset Clear all stats
9
- --json Output raw JSON
8
+ --reset Clear all stats
9
+ --json Output raw JSON
10
+ --summary Output product telemetry summary
10
11
  """
11
12
  from __future__ import annotations
12
13
 
14
+ from datetime import datetime, timedelta
13
15
  import json
14
16
  import sys
15
17
  from pathlib import Path
@@ -18,14 +20,120 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
20
  from paths import STATS_FILE as _STATS_FILE
19
21
 
20
22
  STATS_FILE = _STATS_FILE
23
+ TOOLKIT_DIR = Path(__file__).resolve().parent.parent
24
+
25
+
26
+ def _load_stats() -> dict:
27
+ """Load the stats file, returning an empty dict when absent."""
28
+ if not STATS_FILE.is_file():
29
+ return {}
30
+ with open(STATS_FILE, encoding="utf-8") as f:
31
+ return json.load(f)
32
+
33
+
34
+ def _aggregate_rows(data: dict) -> list[tuple[str, dict]]:
35
+ """Normalize supported stats formats into sorted (name, info) rows."""
36
+ if not data:
37
+ return []
38
+
39
+ # Handle both formats: {skill: {count, last_used}} and {loop_runs: [...]}
40
+ if "loop_runs" in data:
41
+ agg: dict[str, dict] = {}
42
+ for run in data.get("loop_runs", []):
43
+ cmd = run.get("command", "unknown")
44
+ iters = run.get("iterations", [])
45
+ if cmd not in agg:
46
+ agg[cmd] = {"count": 0, "last_used": "unknown"}
47
+ agg[cmd]["count"] += len(iters) if iters else 1
48
+ started = run.get("started_at", "")
49
+ if started > agg[cmd]["last_used"]:
50
+ agg[cmd]["last_used"] = started
51
+ return sorted(agg.items(), key=lambda x: x[1].get("count", 0), reverse=True)
52
+
53
+ skill_data = {k: v for k, v in data.items() if isinstance(v, dict)}
54
+ return sorted(skill_data.items(), key=lambda x: x[1].get("count", 0), reverse=True)
55
+
56
+
57
+ def _catalog_skill_names() -> set[str]:
58
+ """Return skill directory names from the installed toolkit catalog."""
59
+ skills_dir = TOOLKIT_DIR / "app" / "skills"
60
+ if not skills_dir.is_dir():
61
+ return set()
62
+ return {
63
+ p.name
64
+ for p in skills_dir.iterdir()
65
+ if p.is_dir() and not p.name.startswith("_") and (p / "SKILL.md").is_file()
66
+ }
67
+
68
+
69
+ def _parse_datetime(value: str) -> datetime | None:
70
+ """Parse stats timestamps emitted by toolkit hooks and loop runs."""
71
+ for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%SZ"):
72
+ try:
73
+ return datetime.strptime(value[:19] + ("Z" if fmt.endswith("Z") else ""), fmt)
74
+ except ValueError:
75
+ continue
76
+ return None
77
+
78
+
79
+ def _build_summary(rows: list[tuple[str, dict]]) -> dict:
80
+ """Build machine-readable product telemetry from aggregated rows."""
81
+ catalog = _catalog_skill_names()
82
+ used_names = {name for name, _ in rows}
83
+ used_catalog = used_names & catalog if catalog else used_names
84
+ total = sum(int(info.get("count", 0) or 0) for _, info in rows)
85
+
86
+ cutoff = datetime.now() - timedelta(days=7)
87
+ active_7d = 0
88
+ for _, info in rows:
89
+ last_used = _parse_datetime(str(info.get("last_used", "")))
90
+ if last_used and last_used >= cutoff:
91
+ active_7d += 1
92
+
93
+ catalog_total = len(catalog)
94
+ unused = max(catalog_total - len(used_catalog), 0) if catalog_total else 0
95
+
96
+ return {
97
+ "totalInvocations": total,
98
+ "uniqueSkillsUsed": len(used_names),
99
+ "catalogSkillsTotal": catalog_total,
100
+ "unusedCatalogSkills": unused,
101
+ "catalogCoveragePct": round((len(used_catalog) / catalog_total) * 100, 1) if catalog_total else 0,
102
+ "activeSkills7d": active_7d,
103
+ "topSkills": [
104
+ {
105
+ "name": name,
106
+ "count": int(info.get("count", 0) or 0),
107
+ "lastUsed": info.get("last_used", "unknown"),
108
+ }
109
+ for name, info in rows[:5]
110
+ ],
111
+ }
112
+
113
+
114
+ def _print_summary(summary: dict) -> None:
115
+ """Print a compact human-readable product telemetry summary."""
116
+ print("Product Telemetry")
117
+ print("=================")
118
+ print(f"Total invocations: {summary['totalInvocations']}")
119
+ print(f"Unique skills used: {summary['uniqueSkillsUsed']}")
120
+ print(f"Catalog skills total: {summary['catalogSkillsTotal']}")
121
+ print(f"Unused catalog skills: {summary['unusedCatalogSkills']}")
122
+ print(f"Catalog coverage: {summary['catalogCoveragePct']}%")
123
+ print(f"Active skills in last 7 days: {summary['activeSkills7d']}")
124
+ if summary["topSkills"]:
125
+ print()
126
+ print("Top skills:")
127
+ for item in summary["topSkills"]:
128
+ print(f"- {item['name']}: {item['count']} ({item['lastUsed']})")
21
129
 
22
130
 
23
131
  def main() -> None:
24
132
  """Display, export, or reset usage statistics."""
25
- flag = sys.argv[1] if len(sys.argv) > 1 else ""
133
+ args = set(sys.argv[1:])
26
134
 
27
135
  # --reset
28
- if flag == "--reset":
136
+ if "--reset" in args:
29
137
  if STATS_FILE.is_file():
30
138
  STATS_FILE.unlink()
31
139
  print("Stats reset.")
@@ -33,12 +141,20 @@ def main() -> None:
33
141
  print("No stats file found.")
34
142
  return
35
143
 
36
- # --json
37
- if flag == "--json":
38
- if STATS_FILE.is_file():
39
- print(STATS_FILE.read_text(encoding="utf-8"), end="")
144
+ data = _load_stats()
145
+ rows = _aggregate_rows(data)
146
+
147
+ if "--summary" in args:
148
+ summary = _build_summary(rows)
149
+ if "--json" in args:
150
+ print(json.dumps(summary, indent=2))
40
151
  else:
41
- print("{}")
152
+ _print_summary(summary)
153
+ return
154
+
155
+ # --json
156
+ if "--json" in args:
157
+ print(STATS_FILE.read_text(encoding="utf-8") if STATS_FILE.is_file() else "{}")
42
158
  return
43
159
 
44
160
  # Default: pretty-print table
@@ -53,39 +169,10 @@ def main() -> None:
53
169
  print("========================")
54
170
  print()
55
171
 
56
- with open(STATS_FILE, encoding="utf-8") as f:
57
- data: dict = json.load(f)
58
-
59
- if not data:
172
+ if not rows:
60
173
  print("No invocations recorded.")
61
174
  return
62
175
 
63
- # Handle both formats: {skill: {count, last_used}} and {loop_runs: [...]}
64
- if "loop_runs" in data:
65
- runs = data["loop_runs"]
66
- if not runs:
67
- print("No invocations recorded.")
68
- return
69
- # Aggregate loop_runs by command
70
- agg: dict[str, dict] = {}
71
- for run in runs:
72
- cmd = run.get("command", "unknown")
73
- iters = run.get("iterations", [])
74
- if cmd not in agg:
75
- agg[cmd] = {"count": 0, "last_used": "unknown"}
76
- agg[cmd]["count"] += len(iters) if iters else 1
77
- started = run.get("started_at", "")
78
- if started > agg[cmd]["last_used"]:
79
- agg[cmd]["last_used"] = started
80
- rows = sorted(agg.items(), key=lambda x: x[1]["count"], reverse=True)
81
- else:
82
- # Original format: {skill_name: {count, last_used}}
83
- skill_data = {k: v for k, v in data.items() if isinstance(v, dict)}
84
- if not skill_data:
85
- print("No invocations recorded.")
86
- return
87
- rows = sorted(skill_data.items(), key=lambda x: x[1].get("count", 0), reverse=True)
88
-
89
176
  print(f"{'Skill':<30} {'Count':>6} {'Last Used':<20}")
90
177
  print("-" * 60)
91
178
  for name, info in rows:
@@ -42,9 +42,9 @@ VALID_HOOK_EVENTS = frozenset({
42
42
  # Core lifecycle
43
43
  "SessionStart", "SessionEnd", "UserPromptSubmit", "Notification",
44
44
  # Tool lifecycle
45
- "PreToolUse", "PostToolUse",
45
+ "PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch",
46
46
  # Turn lifecycle
47
- "Stop", "StopFailure",
47
+ "Stop", "StopFailure", "UserPromptExpansion",
48
48
  # Subagent lifecycle
49
49
  "SubagentStart", "SubagentStop",
50
50
  # Compaction
@@ -61,11 +61,67 @@ VALID_HOOK_EVENTS = frozenset({
61
61
  "Setup", "InstructionsLoaded",
62
62
  })
63
63
 
64
+ VALID_HOOK_TYPES = frozenset({
65
+ "command",
66
+ "http",
67
+ "prompt",
68
+ "agent",
69
+ "mcp_tool",
70
+ })
71
+
72
+ HOOK_TYPE_EVENTS = {
73
+ "agent": frozenset({"Stop", "SubagentStop"}),
74
+ "prompt": frozenset({
75
+ "PreToolUse",
76
+ "PostToolUse",
77
+ "PostToolUseFailure",
78
+ "PostToolBatch",
79
+ "UserPromptSubmit",
80
+ "UserPromptExpansion",
81
+ "Stop",
82
+ "SubagentStop",
83
+ }),
84
+ }
85
+
86
+ HOOK_REQUIRED_FIELDS = {
87
+ "command": ("command",),
88
+ "http": ("url",),
89
+ "prompt": ("prompt",),
90
+ "agent": ("agent",),
91
+ "mcp_tool": ("server", "tool", "arguments"),
92
+ }
93
+
64
94
  VALID_KB_CATEGORIES = frozenset({
65
95
  "reference", "howto", "procedures", "troubleshooting", "best-practices",
66
96
  "planning",
67
97
  })
68
98
 
99
+ VALID_RULE_CATEGORIES = frozenset({
100
+ "coding-style",
101
+ "testing",
102
+ "security",
103
+ "performance",
104
+ "git-workflow",
105
+ "patterns",
106
+ "frameworks",
107
+ })
108
+
109
+ COMMON_RULE_CATEGORIES = frozenset({
110
+ "coding-style",
111
+ "testing",
112
+ "security",
113
+ "performance",
114
+ "git-workflow",
115
+ })
116
+
117
+ LANGUAGE_RULE_CATEGORIES = frozenset({
118
+ "coding-style",
119
+ "testing",
120
+ "security",
121
+ "patterns",
122
+ "frameworks",
123
+ })
124
+
69
125
  PLANNED_ASSETS = [
70
126
  "app/.claude-plugin/plugin.json",
71
127
  "scripts/doctor.py",
@@ -354,8 +410,49 @@ def validate_legacy_commands(tk_dir: Path, vr: ValidationResult) -> None:
354
410
  print()
355
411
 
356
412
 
413
+ def _validate_hook_handler(event: str, hook: dict, vr: ValidationResult) -> None:
414
+ """Validate one hooks.json handler object."""
415
+ hook_type = hook.get("type")
416
+ if not hook_type:
417
+ vr.error(f"{event}: hook entry missing type")
418
+ return
419
+
420
+ if hook_type not in VALID_HOOK_TYPES:
421
+ vr.error(f"Unsupported hook handler type '{hook_type}' for event {event}")
422
+ return
423
+
424
+ allowed_events = HOOK_TYPE_EVENTS.get(hook_type)
425
+ if allowed_events is not None and event not in allowed_events:
426
+ vr.error(f"Hook type '{hook_type}' is not supported for event {event}")
427
+
428
+ for field in HOOK_REQUIRED_FIELDS.get(hook_type, ()):
429
+ if field not in hook:
430
+ vr.error(f"{event}: hook type '{hook_type}' missing required field '{field}'")
431
+
432
+
433
+ def _validate_hook_entries(event: str, entries: object, vr: ValidationResult) -> None:
434
+ """Validate hooks.json matcher entries for one event."""
435
+ if not isinstance(entries, list):
436
+ vr.error(f"{event}: expected list of hook matcher entries")
437
+ return
438
+
439
+ for index, entry in enumerate(entries):
440
+ if not isinstance(entry, dict):
441
+ vr.error(f"{event}[{index}]: expected hook matcher entry object")
442
+ continue
443
+ hooks = entry.get("hooks")
444
+ if not isinstance(hooks, list) or not hooks:
445
+ vr.error(f"{event}[{index}]: missing non-empty hooks list")
446
+ continue
447
+ for hook_index, hook in enumerate(hooks):
448
+ if not isinstance(hook, dict):
449
+ vr.error(f"{event}[{index}].hooks[{hook_index}]: expected hook object")
450
+ continue
451
+ _validate_hook_handler(event, hook, vr)
452
+
453
+
357
454
  def validate_hook_events(tk_dir: Path, vr: ValidationResult) -> None:
358
- """Validate hook event names in hooks.json."""
455
+ """Validate hook event names and handler shapes in hooks.json."""
359
456
  print("## Hook Events")
360
457
  hooks_file = tk_dir / "app" / "hooks.json"
361
458
 
@@ -373,9 +470,10 @@ def validate_hook_events(tk_dir: Path, vr: ValidationResult) -> None:
373
470
  return
374
471
 
375
472
  hooks = data.get("hooks", {})
376
- for event in hooks:
473
+ for event, entries in hooks.items():
377
474
  if event in VALID_HOOK_EVENTS:
378
475
  print(f" OK: {event}")
476
+ _validate_hook_entries(event, entries, vr)
379
477
  else:
380
478
  vr.error(f"Unknown hook event: {event}")
381
479
 
@@ -386,6 +484,63 @@ def validate_hook_events(tk_dir: Path, vr: ValidationResult) -> None:
386
484
  print()
387
485
 
388
486
 
487
+ def validate_language_rules(tk_dir: Path, vr: ValidationResult) -> None:
488
+ """Validate structured language-rule directories under app/rules."""
489
+ print("## Language Rules")
490
+ rules_dir = tk_dir / "app" / "rules"
491
+ rule_count = 0
492
+ rule_errors = 0
493
+
494
+ if not rules_dir.is_dir():
495
+ vr.error("app/rules directory not found")
496
+ print()
497
+ return
498
+
499
+ for rule_dir in sorted(p for p in rules_dir.iterdir() if p.is_dir()):
500
+ language = rule_dir.name
501
+ expected = COMMON_RULE_CATEGORIES if language == "common" else LANGUAGE_RULE_CATEGORIES
502
+ seen: set[str] = set()
503
+
504
+ for rule_file in sorted(rule_dir.glob("*.md")):
505
+ rel = str(rule_file.relative_to(tk_dir))
506
+ rule_count += 1
507
+ if not _has_frontmatter(rule_file):
508
+ vr.error(f"{rel} - Missing YAML frontmatter")
509
+ rule_errors += 1
510
+ continue
511
+
512
+ fm_lines = _parse_frontmatter_lines(rule_file)
513
+ for field in ("language", "category", "version"):
514
+ if not _fm_has(fm_lines, field):
515
+ vr.error(f"{rel} - Missing required field: {field}")
516
+ rule_errors += 1
517
+
518
+ rule_language = _fm_field(fm_lines, "language").strip()
519
+ if rule_language and rule_language != language:
520
+ vr.error(f"{rel} language '{rule_language}' does not match directory '{language}'")
521
+ rule_errors += 1
522
+
523
+ category = _fm_field(fm_lines, "category").strip()
524
+ if category:
525
+ seen.add(category)
526
+ if category not in VALID_RULE_CATEGORIES:
527
+ vr.error(f"{rel} has invalid rule category '{category}'")
528
+ rule_errors += 1
529
+ if rule_file.stem != category:
530
+ vr.error(f"{rel} filename does not match category '{category}'")
531
+ rule_errors += 1
532
+
533
+ for category in sorted(expected - seen):
534
+ vr.error(f"app/rules/{language} missing required rule category: {category}")
535
+ rule_errors += 1
536
+
537
+ if rule_errors == 0:
538
+ print(f" OK: {rule_count} rule files validated")
539
+ else:
540
+ print(f" Found: {rule_count} rule files ({rule_errors} with errors)")
541
+ print()
542
+
543
+
389
544
  def validate_planned_assets(tk_dir: Path, vr: ValidationResult) -> None:
390
545
  """Validate that planned assets exist and are non-empty."""
391
546
  print("## Planned Assets")
@@ -781,6 +936,7 @@ def _run_all_checks(tk_dir: Path, vr: ValidationResult) -> tuple[int, int, str]:
781
936
  skill_count = validate_skills(tk_dir, vr)
782
937
  validate_legacy_commands(tk_dir, vr)
783
938
  validate_hook_events(tk_dir, vr)
939
+ validate_language_rules(tk_dir, vr)
784
940
  validate_planned_assets(tk_dir, vr)
785
941
  validate_plugin_packs(tk_dir, vr)
786
942
  validate_kb_documents(tk_dir, vr)