@softspark/ai-toolkit 4.25.1 → 4.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +19 -10
- package/app/.claude-plugin/plugin.json +1 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +13 -13
- package/bin/ai-toolkit.js +60 -0
- package/kb/best-practices/README.md +1 -0
- package/kb/howto/README.md +1 -0
- package/kb/troubleshooting/README.md +5 -2
- package/kb/troubleshooting/plugin-double-load.md +97 -0
- package/llms-full.txt +110 -2
- package/llms.txt +1 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/_common.py +27 -0
- package/scripts/check_deps.py +2 -2
- package/scripts/doctor.py +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.27.0 — Doctor sees the Claude app plugin (2026-08-21)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Doctor check 11: plugin double-load.** `ai-toolkit doctor` now reads the
|
|
15
|
+
Claude Code plugin registry (`~/.claude/plugins/installed_plugins.json`) and
|
|
16
|
+
`enabledPlugins` in `~/.claude/settings.json`. When an uploaded Claude app
|
|
17
|
+
plugin is active next to the global install, both feed the same session:
|
|
18
|
+
Claude Code merges plugin hooks with user hooks without deduplication, so every
|
|
19
|
+
toolkit hook fires twice per event and skills and agents load twice. The check
|
|
20
|
+
warns with the hook count, and `--fix` disables the plugin for Claude Code
|
|
21
|
+
while leaving the global install authoritative. Previously this passed as a
|
|
22
|
+
healthy install with no signal at all.
|
|
23
|
+
- `kb/troubleshooting/plugin-double-load.md` documents the symptom, the debug-log
|
|
24
|
+
evidence, and the fix.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## v4.26.0 — Python floor is declared and enforced (2026-08-21)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Python preflight in the CLI.** `bin/ai-toolkit.js` now verifies `python3`
|
|
33
|
+
exists and reports at least 3.11 before spawning any script, and prints a
|
|
34
|
+
platform-specific fix (`brew install python@3.13` on macOS) instead of a
|
|
35
|
+
traceback from deep inside `scripts/`. The check is lazy and memoized, so
|
|
36
|
+
`help` and `--version` still run without a Python interpreter.
|
|
37
|
+
- **Matching floor check in `scripts/_common.py`.** Every entry point imports
|
|
38
|
+
`_common` first, so `python3 scripts/install.py` run directly fails with the
|
|
39
|
+
same message rather than a `TypeError` from whichever module happens to use
|
|
40
|
+
3.11+ syntax.
|
|
41
|
+
- **CI import sweep.** The `python-syntax` job now runs on a `['3.11', '3.13']`
|
|
42
|
+
matrix and imports every module under `scripts/` on each. `py_compile` only
|
|
43
|
+
catches syntax, which is exactly why a 3.10-only construct shipped: version-
|
|
44
|
+
gated runtime features surface on import, not on compile.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **`ai-toolkit update --local` crashed on macOS system Python.** `/usr/bin/python3`
|
|
49
|
+
is 3.9 on every Mac, and `scripts/mcp_editors.py` uses
|
|
50
|
+
`@dataclass(frozen=True, slots=True)` (3.10+), so the install path died with
|
|
51
|
+
`TypeError: dataclass() got an unexpected keyword argument 'slots'` while
|
|
52
|
+
importing `install_steps/ai_tools.py`. The interpreter is now rejected up
|
|
53
|
+
front with instructions.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **`scripts/check_deps.py` declares Python >= 3.11**, up from a `min_version`
|
|
58
|
+
of `3.8` that never matched what the scripts actually needed, and its reason
|
|
59
|
+
string calls out the macOS 3.9 trap.
|
|
60
|
+
- **Requirements are documented.** README gained a Requirements note above the
|
|
61
|
+
install snippet; `CLAUDE.md` records the three places the floor is declared
|
|
62
|
+
(`bin/ai-toolkit.js`, `scripts/_common.py`, `scripts/check_deps.py`) and the
|
|
63
|
+
CI matrix that guards it.
|
|
64
|
+
|
|
65
|
+
### Ecosystem
|
|
66
|
+
|
|
67
|
+
- Snapshot refreshed: 11 tools drifted, all class A (content reworded, no
|
|
68
|
+
heading delta). Claude Code 2.1.235 → 2.1.238 and Codex CLI 0.147.0 → 0.148.0
|
|
69
|
+
are upstream patch bumps with no new surface, so no generator changed.
|
|
70
|
+
|
|
71
|
+
### Tests
|
|
72
|
+
|
|
73
|
+
- Three CLI regression tests: a stale `python3` shim exits 1 with the version
|
|
74
|
+
message, a missing `python3` exits 1 instead of crashing, and `help` still
|
|
75
|
+
works with no interpreter on PATH. Test count: 1637 → 1640.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
10
79
|
## v4.25.1 — npm advisories count again (2026-08-19)
|
|
11
80
|
|
|
12
81
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,18 +6,23 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
10
|
|
|
11
|
-
## What's New in v4.
|
|
11
|
+
## What's New in v4.27.0
|
|
12
12
|
|
|
13
|
-
**v4.
|
|
13
|
+
**v4.27.0** teaches `doctor` about a collision it used to report as healthy:
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
- Uploading the Claude app plugin ZIP registers it under `~/.claude/plugins`,
|
|
16
|
+
which Claude Code reads too. The plugin then carries the same skills, agents,
|
|
17
|
+
and hooks as the global install.
|
|
18
|
+
- Claude Code merges plugin hooks with user hooks without deduplication, so
|
|
19
|
+
every toolkit hook fired twice per event and skills and agents loaded twice.
|
|
20
|
+
`doctor` printed `HEALTH CHECK PASSED` through all of it.
|
|
21
|
+
- New check 11 reports the collision with the hook count; `doctor --fix`
|
|
22
|
+
disables the plugin for Claude Code and leaves the global install
|
|
23
|
+
authoritative.
|
|
24
|
+
- `kb/troubleshooting/plugin-double-load.md` documents the debug-log evidence
|
|
25
|
+
and the verification steps.
|
|
21
26
|
|
|
22
27
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
23
28
|
|
|
@@ -40,6 +45,10 @@ See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
|
40
45
|
|
|
41
46
|
## Install
|
|
42
47
|
|
|
48
|
+
**Requirements:** Node.js >= 18 and Python >= 3.11.
|
|
49
|
+
|
|
50
|
+
> **macOS:** `/usr/bin/python3` is Python 3.9 and will not run the toolkit. Install a supported one with `brew install python@3.13` and make sure `which python3` no longer points at `/usr/bin/python3`.
|
|
51
|
+
|
|
43
52
|
```bash
|
|
44
53
|
# Option A: install globally (once per machine)
|
|
45
54
|
npm install -g @softspark/ai-toolkit
|
|
@@ -183,7 +192,7 @@ ai-toolkit/
|
|
|
183
192
|
│ └── ARCHITECTURE.md # Full system design
|
|
184
193
|
├── kb/ # Reference docs, procedures, plans
|
|
185
194
|
├── scripts/ # Validation, install, evaluation scripts
|
|
186
|
-
├── tests/ # Bats and Python test suite (
|
|
195
|
+
├── tests/ # Bats and Python test suite (1645 tests)
|
|
187
196
|
└── CHANGELOG.md
|
|
188
197
|
```
|
|
189
198
|
|
|
@@ -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.27.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"last_run": "2026-08-
|
|
2
|
+
"last_run": "2026-08-21T15:52:27Z",
|
|
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": "
|
|
27
|
+
"docs_hash": "516d8db28cb3ec63",
|
|
28
28
|
"headings": [
|
|
29
29
|
"Admin",
|
|
30
30
|
"Auggie CLI",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"claude-app": {
|
|
69
|
-
"docs_hash": "
|
|
69
|
+
"docs_hash": "cacc8b579baa5e86",
|
|
70
70
|
"headings": [
|
|
71
71
|
"Add global and folder instructions",
|
|
72
72
|
"Availability",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
"claude-code": {
|
|
110
|
-
"docs_hash": "
|
|
110
|
+
"docs_hash": "2b3d6b11c4be766f",
|
|
111
111
|
"headings": [
|
|
112
112
|
"Core concepts",
|
|
113
113
|
"Documentation Index",
|
|
@@ -173,10 +173,10 @@
|
|
|
173
173
|
"userConfig": false,
|
|
174
174
|
"workflows": true
|
|
175
175
|
},
|
|
176
|
-
"version": "2.1.
|
|
176
|
+
"version": "2.1.238 (Claude Code)"
|
|
177
177
|
},
|
|
178
178
|
"cline": {
|
|
179
|
-
"docs_hash": "
|
|
179
|
+
"docs_hash": "f93d3a8437c1165f",
|
|
180
180
|
"headings": [
|
|
181
181
|
"API Reference",
|
|
182
182
|
"Best Practices",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
}
|
|
224
224
|
},
|
|
225
225
|
"codex-cli": {
|
|
226
|
-
"docs_hash": "
|
|
226
|
+
"docs_hash": "df3b133739abfa24",
|
|
227
227
|
"headings": [
|
|
228
228
|
"API",
|
|
229
229
|
"API Reference",
|
|
@@ -362,10 +362,10 @@
|
|
|
362
362
|
"plugin marketplace": false,
|
|
363
363
|
"sandbox": true
|
|
364
364
|
},
|
|
365
|
-
"version": "codex-cli 0.
|
|
365
|
+
"version": "codex-cli 0.148.0"
|
|
366
366
|
},
|
|
367
367
|
"cursor": {
|
|
368
|
-
"docs_hash": "
|
|
368
|
+
"docs_hash": "2abfadb030c12d07",
|
|
369
369
|
"headings": [
|
|
370
370
|
"Agent",
|
|
371
371
|
"CLI",
|
|
@@ -397,7 +397,7 @@
|
|
|
397
397
|
}
|
|
398
398
|
},
|
|
399
399
|
"gemini-cli": {
|
|
400
|
-
"docs_hash": "
|
|
400
|
+
"docs_hash": "c66bb0647b70c7cf",
|
|
401
401
|
"headings": [
|
|
402
402
|
"Breadcrumbs",
|
|
403
403
|
"Directory actions",
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
"version": "0.55.1"
|
|
439
439
|
},
|
|
440
440
|
"github-copilot": {
|
|
441
|
-
"docs_hash": "
|
|
441
|
+
"docs_hash": "2e595a058e906644",
|
|
442
442
|
"headings": [
|
|
443
443
|
"About Copilot auto model selection",
|
|
444
444
|
"About Copilot automations",
|
|
@@ -502,7 +502,7 @@
|
|
|
502
502
|
}
|
|
503
503
|
},
|
|
504
504
|
"opencode": {
|
|
505
|
-
"docs_hash": "
|
|
505
|
+
"docs_hash": "3e9f45631e71a28c",
|
|
506
506
|
"headings": [
|
|
507
507
|
"Add features",
|
|
508
508
|
"Ask questions",
|
|
@@ -564,7 +564,7 @@
|
|
|
564
564
|
}
|
|
565
565
|
},
|
|
566
566
|
"windsurf": {
|
|
567
|
-
"docs_hash": "
|
|
567
|
+
"docs_hash": "b08c4f883c276129",
|
|
568
568
|
"headings": [
|
|
569
569
|
"Accounts",
|
|
570
570
|
"Advanced",
|
package/bin/ai-toolkit.js
CHANGED
|
@@ -12,6 +12,9 @@ const fs = require('fs');
|
|
|
12
12
|
const TOOLKIT_DIR = path.dirname(__dirname);
|
|
13
13
|
const CWD = process.cwd();
|
|
14
14
|
|
|
15
|
+
/** Minimum Python the toolkit scripts require: [major, minor]. */
|
|
16
|
+
const PYTHON_MIN = [3, 11];
|
|
17
|
+
|
|
15
18
|
if (!process.env.HOME) {
|
|
16
19
|
console.error('Error: HOME environment variable is not set');
|
|
17
20
|
process.exit(1);
|
|
@@ -139,6 +142,60 @@ function scriptPath(scriptName) {
|
|
|
139
142
|
return path.join(TOOLKIT_DIR, 'scripts', scriptName);
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
/** Memoized result of requirePython(); null until the first check runs. */
|
|
146
|
+
let pythonOk = null;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Verify `python3` exists and is >= PYTHON_MIN before any script is spawned.
|
|
150
|
+
* Without this the caller sees a raw traceback from deep inside scripts/
|
|
151
|
+
* (e.g. `dataclass() got an unexpected keyword argument 'slots'` on Python
|
|
152
|
+
* 3.9, which is what macOS ships as /usr/bin/python3).
|
|
153
|
+
* Exits with an actionable message when the interpreter is missing or too old.
|
|
154
|
+
*/
|
|
155
|
+
function requirePython() {
|
|
156
|
+
if (pythonOk) return;
|
|
157
|
+
const probe = spawnSync('python3', ['-c', 'import sys; sys.stdout.write("%d.%d.%d" % sys.version_info[:3])'], {
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
});
|
|
160
|
+
const want = PYTHON_MIN.join('.');
|
|
161
|
+
|
|
162
|
+
if (probe.error || probe.status !== 0) {
|
|
163
|
+
console.error(`Error: python3 not found on PATH — ai-toolkit requires Python >= ${want}`);
|
|
164
|
+
console.error(pythonHint());
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const found = probe.stdout.trim();
|
|
169
|
+
const [major, minor] = found.split('.').map(Number);
|
|
170
|
+
if (major < PYTHON_MIN[0] || (major === PYTHON_MIN[0] && minor < PYTHON_MIN[1])) {
|
|
171
|
+
console.error(`Error: ai-toolkit requires Python >= ${want}, found ${found}`);
|
|
172
|
+
console.error(pythonHint());
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
pythonOk = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Platform-specific instructions for installing a supported Python.
|
|
180
|
+
* @returns {string} Multi-line hint text
|
|
181
|
+
*/
|
|
182
|
+
function pythonHint() {
|
|
183
|
+
const lines = [];
|
|
184
|
+
if (process.platform === 'darwin') {
|
|
185
|
+
lines.push('macOS ships Python 3.9 as /usr/bin/python3. Install a newer one:');
|
|
186
|
+
lines.push(' brew install python@3.13');
|
|
187
|
+
lines.push('Then make sure it comes first on PATH (`which python3` should not be /usr/bin/python3).');
|
|
188
|
+
} else if (process.platform === 'win32') {
|
|
189
|
+
lines.push('Install Python 3.11+ and make sure `python3` resolves to it:');
|
|
190
|
+
lines.push(' winget install Python.Python.3.13');
|
|
191
|
+
} else {
|
|
192
|
+
lines.push('Install Python 3.11+ via your package manager, e.g.:');
|
|
193
|
+
lines.push(' sudo apt install python3.13 # Debian/Ubuntu');
|
|
194
|
+
lines.push(' sudo dnf install python3.13 # Fedora/RHEL');
|
|
195
|
+
}
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
/**
|
|
143
200
|
* Execute a generator script synchronously via python3, returning its stdout.
|
|
144
201
|
* Exits the process on failure.
|
|
@@ -147,6 +204,7 @@ function scriptPath(scriptName) {
|
|
|
147
204
|
* @returns {Buffer} stdout output
|
|
148
205
|
*/
|
|
149
206
|
function runGenerator(scriptName, extraArgs = []) {
|
|
207
|
+
requirePython();
|
|
150
208
|
try {
|
|
151
209
|
return execFileSync('python3', [scriptPath(scriptName), ...extraArgs], { cwd: TOOLKIT_DIR });
|
|
152
210
|
} catch (err) {
|
|
@@ -162,6 +220,7 @@ function runGenerator(scriptName, extraArgs = []) {
|
|
|
162
220
|
* @param {{ cwd?: string }} [opts={}] - Options (cwd override)
|
|
163
221
|
*/
|
|
164
222
|
function run(script, args = [], opts = {}) {
|
|
223
|
+
requirePython();
|
|
165
224
|
const result = spawnSync('python3', [script, ...args], {
|
|
166
225
|
stdio: 'inherit',
|
|
167
226
|
cwd: opts.cwd || CWD,
|
|
@@ -178,6 +237,7 @@ function run(script, args = [], opts = {}) {
|
|
|
178
237
|
* @param {...string} flags - Flags to pass: --rules, --hooks, --mcp
|
|
179
238
|
*/
|
|
180
239
|
function propagateGlobal(...flags) {
|
|
240
|
+
requirePython();
|
|
181
241
|
const result = spawnSync('python3', [scriptPath('propagate_global.py'), ...flags], {
|
|
182
242
|
stdio: 'inherit',
|
|
183
243
|
cwd: CWD,
|
|
@@ -4,6 +4,7 @@ service: ai-toolkit
|
|
|
4
4
|
category: best-practices
|
|
5
5
|
tags: [best-practices, guidelines]
|
|
6
6
|
last_updated: "2026-03-25"
|
|
7
|
+
description: "Section index for ai-toolkit best-practices. No documents are filed here yet; new ones follow the best-practices template."
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Best Practices
|
package/kb/howto/README.md
CHANGED
|
@@ -3,9 +3,12 @@ title: "Troubleshooting"
|
|
|
3
3
|
service: ai-toolkit
|
|
4
4
|
category: troubleshooting
|
|
5
5
|
tags: [troubleshooting, debugging]
|
|
6
|
-
last_updated: "2026-
|
|
6
|
+
last_updated: "2026-08-21"
|
|
7
|
+
description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Troubleshooting
|
|
10
11
|
|
|
11
|
-
Problem resolution guides.
|
|
12
|
+
Problem resolution guides.
|
|
13
|
+
|
|
14
|
+
- [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Claude App Plugin Loads Twice in Claude Code"
|
|
3
|
+
category: troubleshooting
|
|
4
|
+
service: ai-toolkit
|
|
5
|
+
tags: [plugin, claude-app, hooks, duplication, doctor]
|
|
6
|
+
created: "2026-08-21"
|
|
7
|
+
last_updated: "2026-08-21"
|
|
8
|
+
description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Claude App Plugin Loads Twice in Claude Code
|
|
12
|
+
|
|
13
|
+
## Symptom
|
|
14
|
+
|
|
15
|
+
After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
|
|
16
|
+
Code sessions get slower and every hook side effect appears twice: duplicate rows
|
|
17
|
+
in `governance.log`, duplicate session state writes, two Stop gates per turn.
|
|
18
|
+
`ai-toolkit doctor` reports a healthy install because it only inspected
|
|
19
|
+
`~/.claude` and `app/plugins`.
|
|
20
|
+
|
|
21
|
+
## Cause
|
|
22
|
+
|
|
23
|
+
The Claude app writes uploaded plugins into
|
|
24
|
+
`~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
|
|
25
|
+
`enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
|
|
26
|
+
as well, so Claude Code loads the plugin on top of the global install. The
|
|
27
|
+
bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
|
|
28
|
+
hooks without deduplication.
|
|
29
|
+
|
|
30
|
+
Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
|
|
31
|
+
`~/.claude/debug/latest`):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Loaded 111 unique skills (... user: 111 ...)
|
|
35
|
+
Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
|
|
36
|
+
Total plugin agents loaded: 44
|
|
37
|
+
Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `skills` manifest field adds to the default `skills/` directory instead of
|
|
41
|
+
replacing it, so the plugin contributes its 109 catalog skills plus the 2
|
|
42
|
+
app-only rule skills.
|
|
43
|
+
|
|
44
|
+
## Diagnosis
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ai-toolkit doctor
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Check 11 reports the collision:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
## 11. Plugin Double-Load
|
|
54
|
+
WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To see the duplication directly, count hook invocations per source in a fresh
|
|
58
|
+
session log:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Fix
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ai-toolkit doctor --fix
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That sets the plugin to `false` in `enabledPlugins` and leaves the global
|
|
71
|
+
install authoritative. Claude Code then logs
|
|
72
|
+
`enabled=false; will NOT register, plugin is disabled`, and plugin skills and
|
|
73
|
+
agents drop to 0.
|
|
74
|
+
|
|
75
|
+
The global install is the richer surface for Claude Code: it delivers rules as
|
|
76
|
+
real files under `~/.claude/rules/`, which are always in context, while the
|
|
77
|
+
plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
|
|
78
|
+
plugin also has no `session-context.sh` hook.
|
|
79
|
+
|
|
80
|
+
Keep the plugin enabled only when Claude Code has no global install, for example
|
|
81
|
+
a machine that runs the Claude app alone. In that case the plugin is the single
|
|
82
|
+
source and check 11 stays quiet.
|
|
83
|
+
|
|
84
|
+
## Verification
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
claude --debug -p "ok"
|
|
88
|
+
grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
|
|
92
|
+
`plugin is disabled` line.
|
|
93
|
+
|
|
94
|
+
## Related
|
|
95
|
+
|
|
96
|
+
- `kb/reference/global-install-model.md`
|
|
97
|
+
- `kb/procedures/maintenance-sop.md`
|
package/llms-full.txt
CHANGED
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
- [Unique Features & Differentiators](kb/reference/unique-features.md)
|
|
74
74
|
- [Windows Support](kb/reference/windows-support.md)
|
|
75
75
|
- [Troubleshooting](kb/troubleshooting/README.md)
|
|
76
|
+
- [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
|
|
76
77
|
|
|
77
78
|
## Skills
|
|
78
79
|
|
|
@@ -243,6 +244,7 @@ service: ai-toolkit
|
|
|
243
244
|
category: best-practices
|
|
244
245
|
tags: [best-practices, guidelines]
|
|
245
246
|
last_updated: "2026-03-25"
|
|
247
|
+
description: "Section index for ai-toolkit best-practices. No documents are filed here yet; new ones follow the best-practices template."
|
|
246
248
|
---
|
|
247
249
|
|
|
248
250
|
# Best Practices
|
|
@@ -5081,6 +5083,7 @@ service: ai-toolkit
|
|
|
5081
5083
|
category: howto
|
|
5082
5084
|
tags: [howto, guides]
|
|
5083
5085
|
last_updated: "2026-03-25"
|
|
5086
|
+
description: "Section index for ai-toolkit howto. No documents are filed here yet; new ones follow the howto template."
|
|
5084
5087
|
---
|
|
5085
5088
|
|
|
5086
5089
|
# How-To Guides
|
|
@@ -16128,12 +16131,117 @@ title: "Troubleshooting"
|
|
|
16128
16131
|
service: ai-toolkit
|
|
16129
16132
|
category: troubleshooting
|
|
16130
16133
|
tags: [troubleshooting, debugging]
|
|
16131
|
-
last_updated: "2026-
|
|
16134
|
+
last_updated: "2026-08-21"
|
|
16135
|
+
description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
|
|
16132
16136
|
---
|
|
16133
16137
|
|
|
16134
16138
|
# Troubleshooting
|
|
16135
16139
|
|
|
16136
|
-
Problem resolution guides.
|
|
16140
|
+
Problem resolution guides.
|
|
16141
|
+
|
|
16142
|
+
- [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
|
|
16143
|
+
|
|
16144
|
+
---
|
|
16145
|
+
|
|
16146
|
+
## kb/troubleshooting/plugin-double-load.md
|
|
16147
|
+
|
|
16148
|
+
---
|
|
16149
|
+
title: "Claude App Plugin Loads Twice in Claude Code"
|
|
16150
|
+
category: troubleshooting
|
|
16151
|
+
service: ai-toolkit
|
|
16152
|
+
tags: [plugin, claude-app, hooks, duplication, doctor]
|
|
16153
|
+
created: "2026-08-21"
|
|
16154
|
+
last_updated: "2026-08-21"
|
|
16155
|
+
description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
|
|
16156
|
+
---
|
|
16157
|
+
|
|
16158
|
+
# Claude App Plugin Loads Twice in Claude Code
|
|
16159
|
+
|
|
16160
|
+
## Symptom
|
|
16161
|
+
|
|
16162
|
+
After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
|
|
16163
|
+
Code sessions get slower and every hook side effect appears twice: duplicate rows
|
|
16164
|
+
in `governance.log`, duplicate session state writes, two Stop gates per turn.
|
|
16165
|
+
`ai-toolkit doctor` reports a healthy install because it only inspected
|
|
16166
|
+
`~/.claude` and `app/plugins`.
|
|
16167
|
+
|
|
16168
|
+
## Cause
|
|
16169
|
+
|
|
16170
|
+
The Claude app writes uploaded plugins into
|
|
16171
|
+
`~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
|
|
16172
|
+
`enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
|
|
16173
|
+
as well, so Claude Code loads the plugin on top of the global install. The
|
|
16174
|
+
bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
|
|
16175
|
+
hooks without deduplication.
|
|
16176
|
+
|
|
16177
|
+
Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
|
|
16178
|
+
`~/.claude/debug/latest`):
|
|
16179
|
+
|
|
16180
|
+
```
|
|
16181
|
+
Loaded 111 unique skills (... user: 111 ...)
|
|
16182
|
+
Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
|
|
16183
|
+
Total plugin agents loaded: 44
|
|
16184
|
+
Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
|
|
16185
|
+
```
|
|
16186
|
+
|
|
16187
|
+
The `skills` manifest field adds to the default `skills/` directory instead of
|
|
16188
|
+
replacing it, so the plugin contributes its 109 catalog skills plus the 2
|
|
16189
|
+
app-only rule skills.
|
|
16190
|
+
|
|
16191
|
+
## Diagnosis
|
|
16192
|
+
|
|
16193
|
+
```bash
|
|
16194
|
+
ai-toolkit doctor
|
|
16195
|
+
```
|
|
16196
|
+
|
|
16197
|
+
Check 11 reports the collision:
|
|
16198
|
+
|
|
16199
|
+
```
|
|
16200
|
+
## 11. Plugin Double-Load
|
|
16201
|
+
WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
|
|
16202
|
+
```
|
|
16203
|
+
|
|
16204
|
+
To see the duplication directly, count hook invocations per source in a fresh
|
|
16205
|
+
session log:
|
|
16206
|
+
|
|
16207
|
+
```bash
|
|
16208
|
+
grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
|
|
16209
|
+
```
|
|
16210
|
+
|
|
16211
|
+
## Fix
|
|
16212
|
+
|
|
16213
|
+
```bash
|
|
16214
|
+
ai-toolkit doctor --fix
|
|
16215
|
+
```
|
|
16216
|
+
|
|
16217
|
+
That sets the plugin to `false` in `enabledPlugins` and leaves the global
|
|
16218
|
+
install authoritative. Claude Code then logs
|
|
16219
|
+
`enabled=false; will NOT register, plugin is disabled`, and plugin skills and
|
|
16220
|
+
agents drop to 0.
|
|
16221
|
+
|
|
16222
|
+
The global install is the richer surface for Claude Code: it delivers rules as
|
|
16223
|
+
real files under `~/.claude/rules/`, which are always in context, while the
|
|
16224
|
+
plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
|
|
16225
|
+
plugin also has no `session-context.sh` hook.
|
|
16226
|
+
|
|
16227
|
+
Keep the plugin enabled only when Claude Code has no global install, for example
|
|
16228
|
+
a machine that runs the Claude app alone. In that case the plugin is the single
|
|
16229
|
+
source and check 11 stays quiet.
|
|
16230
|
+
|
|
16231
|
+
## Verification
|
|
16232
|
+
|
|
16233
|
+
```bash
|
|
16234
|
+
claude --debug -p "ok"
|
|
16235
|
+
grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
|
|
16236
|
+
```
|
|
16237
|
+
|
|
16238
|
+
Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
|
|
16239
|
+
`plugin is disabled` line.
|
|
16240
|
+
|
|
16241
|
+
## Related
|
|
16242
|
+
|
|
16243
|
+
- `kb/reference/global-install-model.md`
|
|
16244
|
+
- `kb/procedures/maintenance-sop.md`
|
|
16137
16245
|
|
|
16138
16246
|
---
|
|
16139
16247
|
|
package/llms.txt
CHANGED
|
@@ -73,3 +73,4 @@
|
|
|
73
73
|
- [Unique Features & Differentiators](kb/reference/unique-features.md)
|
|
74
74
|
- [Windows Support](kb/reference/windows-support.md)
|
|
75
75
|
- [Troubleshooting](kb/troubleshooting/README.md)
|
|
76
|
+
- [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
|
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.27.0",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/scripts/_common.py
CHANGED
|
@@ -20,8 +20,35 @@ from __future__ import annotations
|
|
|
20
20
|
import os
|
|
21
21
|
import platform
|
|
22
22
|
import shutil
|
|
23
|
+
import sys
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Python version floor
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Kept in sync with PYTHON_MIN in bin/ai-toolkit.js and the python3 entry in
|
|
30
|
+
# scripts/check_deps.py. Checked here because _common is imported first by
|
|
31
|
+
# every entry point, so a stale interpreter fails with this message instead of
|
|
32
|
+
# a traceback from whichever module happens to use 3.11+ syntax.
|
|
33
|
+
PYTHON_MIN = (3, 11)
|
|
34
|
+
|
|
35
|
+
if sys.version_info < PYTHON_MIN:
|
|
36
|
+
_want = ".".join(str(n) for n in PYTHON_MIN)
|
|
37
|
+
_found = ".".join(str(n) for n in sys.version_info[:3])
|
|
38
|
+
sys.stderr.write(
|
|
39
|
+
"Error: ai-toolkit requires Python >= {}, found {} ({})\n".format(
|
|
40
|
+
_want, _found, sys.executable
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
if sys.platform == "darwin":
|
|
44
|
+
sys.stderr.write(
|
|
45
|
+
"macOS ships Python 3.9 as /usr/bin/python3. Install a newer one:\n"
|
|
46
|
+
" brew install python@3.13\n"
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
sys.stderr.write("Install Python 3.11+ via your package manager.\n")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
25
52
|
# ---------------------------------------------------------------------------
|
|
26
53
|
# Re-exports from frontmatter module
|
|
27
54
|
# ---------------------------------------------------------------------------
|
package/scripts/check_deps.py
CHANGED
|
@@ -33,7 +33,7 @@ REQUIRED = [
|
|
|
33
33
|
{
|
|
34
34
|
"name": "python3",
|
|
35
35
|
"check": "python3",
|
|
36
|
-
"min_version": "3.
|
|
36
|
+
"min_version": "3.11",
|
|
37
37
|
"packages": {
|
|
38
38
|
"brew": "python3",
|
|
39
39
|
"apt": "python3",
|
|
@@ -45,7 +45,7 @@ REQUIRED = [
|
|
|
45
45
|
"choco": "python",
|
|
46
46
|
"scoop": "python",
|
|
47
47
|
},
|
|
48
|
-
"reason": "All toolkit scripts run on Python 3 (stdlib only, no pip needed)",
|
|
48
|
+
"reason": "All toolkit scripts run on Python 3.11+ (stdlib only, no pip needed). macOS /usr/bin/python3 is 3.9 and will not work.",
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"name": "git",
|
package/scripts/doctor.py
CHANGED
|
@@ -15,6 +15,8 @@ Checks:
|
|
|
15
15
|
7. Benchmark freshness
|
|
16
16
|
8. Stale rules
|
|
17
17
|
9. URL hook sources
|
|
18
|
+
10. Language rules drift (project-local)
|
|
19
|
+
11. Plugin double-load (Claude app plugin vs global install)
|
|
18
20
|
|
|
19
21
|
Exit codes:
|
|
20
22
|
0 all checks pass
|
|
@@ -44,6 +46,7 @@ HOOKS_DIR = _HOOKS_DIR
|
|
|
44
46
|
RULES_DIR = _RULES_DIR
|
|
45
47
|
EXTERNAL_HOOKS_DIR = _EXTERNAL_HOOKS_DIR
|
|
46
48
|
BENCHMARK_DASHBOARD = toolkit_dir / "benchmarks" / "ecosystem-dashboard.json"
|
|
49
|
+
PLUGIN_REGISTRY = CLAUDE_DIR / "plugins" / "installed_plugins.json"
|
|
47
50
|
|
|
48
51
|
VALID_EVENTS = frozenset({
|
|
49
52
|
"SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
|
|
@@ -668,6 +671,84 @@ def check_language_drift(dr: DiagResult) -> None:
|
|
|
668
671
|
dr.warn(f"{lang} detected but {skill} not injected — run: ai-toolkit install --local --lang {lang}")
|
|
669
672
|
|
|
670
673
|
|
|
674
|
+
# ---------------------------------------------------------------------------
|
|
675
|
+
# Check 11: Plugin Double-Load
|
|
676
|
+
# ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
def _registered_toolkit_plugins(registry: dict) -> list[str]:
|
|
679
|
+
"""Return ai-toolkit plugin keys from the Claude Code plugin registry."""
|
|
680
|
+
plugins = registry.get("plugins", {})
|
|
681
|
+
if not isinstance(plugins, dict):
|
|
682
|
+
return []
|
|
683
|
+
return [key for key in plugins if str(key).split("@", 1)[0] == "ai-toolkit"]
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
|
|
687
|
+
"""Warn when the Claude app plugin and the global install both feed Claude Code.
|
|
688
|
+
|
|
689
|
+
Uploading the ``claude-app export`` ZIP from the Claude app registers it under
|
|
690
|
+
``~/.claude/plugins``, which Claude Code reads as well. The plugin carries the
|
|
691
|
+
same skills, agents, and hooks as the global install, and Claude Code merges
|
|
692
|
+
plugin hooks with user hooks without deduplication, so every toolkit hook runs
|
|
693
|
+
twice per event.
|
|
694
|
+
"""
|
|
695
|
+
print()
|
|
696
|
+
print("## 11. Plugin Double-Load")
|
|
697
|
+
|
|
698
|
+
if not PLUGIN_REGISTRY.is_file():
|
|
699
|
+
dr.skip("no Claude Code plugin registry")
|
|
700
|
+
return
|
|
701
|
+
try:
|
|
702
|
+
registry = json.loads(PLUGIN_REGISTRY.read_text(encoding="utf-8"))
|
|
703
|
+
except (OSError, json.JSONDecodeError):
|
|
704
|
+
dr.warn(f"{PLUGIN_REGISTRY} is not valid JSON")
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
keys = _registered_toolkit_plugins(registry)
|
|
708
|
+
if not keys:
|
|
709
|
+
dr.ok("no ai-toolkit plugin registered in Claude Code")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
settings_json = CLAUDE_DIR / "settings.json"
|
|
713
|
+
try:
|
|
714
|
+
settings = json.loads(settings_json.read_text(encoding="utf-8"))
|
|
715
|
+
except (OSError, json.JSONDecodeError):
|
|
716
|
+
settings = {}
|
|
717
|
+
enabled = settings.get("enabledPlugins", {})
|
|
718
|
+
if not isinstance(enabled, dict):
|
|
719
|
+
enabled = {}
|
|
720
|
+
|
|
721
|
+
active = [key for key in keys if enabled.get(key, True)]
|
|
722
|
+
if not active:
|
|
723
|
+
dr.ok("ai-toolkit plugin registered but disabled for Claude Code")
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
hook_count, _ = _installed_toolkit_hook_count(settings)
|
|
727
|
+
if hook_count == 0:
|
|
728
|
+
dr.ok("ai-toolkit plugin active without global hooks (single source)")
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
for key in active:
|
|
732
|
+
dr.warn(
|
|
733
|
+
f"{key} is active next to the global install: {hook_count} toolkit hooks "
|
|
734
|
+
"fire twice per event and skills/agents load twice "
|
|
735
|
+
"(run: ai-toolkit doctor --fix)"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
if not fix_mode:
|
|
739
|
+
return
|
|
740
|
+
|
|
741
|
+
enabled.update({key: False for key in active})
|
|
742
|
+
settings["enabledPlugins"] = enabled
|
|
743
|
+
try:
|
|
744
|
+
settings_json.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
745
|
+
except OSError as exc:
|
|
746
|
+
dr.fail(f"could not disable plugin in settings.json: {exc}")
|
|
747
|
+
return
|
|
748
|
+
for key in active:
|
|
749
|
+
dr.fixed(f"disabled {key} for Claude Code (global install stays authoritative)")
|
|
750
|
+
|
|
751
|
+
|
|
671
752
|
# ---------------------------------------------------------------------------
|
|
672
753
|
# Main
|
|
673
754
|
# ---------------------------------------------------------------------------
|
|
@@ -691,6 +772,7 @@ def main() -> None:
|
|
|
691
772
|
check_stale_rules(dr, fix_mode)
|
|
692
773
|
check_url_hooks(dr, fix_mode)
|
|
693
774
|
check_language_drift(dr)
|
|
775
|
+
check_plugin_double_load(dr, fix_mode)
|
|
694
776
|
|
|
695
777
|
# Summary
|
|
696
778
|
print("========================")
|