@softspark/ai-toolkit 4.2.4 → 4.3.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 +67 -0
- package/README.md +10 -9
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/_hook-io.sh +64 -0
- package/app/hooks/_locate-toolkit.sh +39 -0
- package/app/hooks/_search-capability.sh +46 -0
- package/app/hooks/commit-quality.sh +3 -1
- package/app/hooks/config-desync-guard.sh +95 -0
- package/app/hooks/governance-capture.sh +5 -3
- package/app/hooks/guard-config.sh +5 -3
- package/app/hooks/guard-destructive.sh +3 -1
- package/app/hooks/guard-path.sh +8 -1
- package/app/hooks/instructions-audit.sh +43 -0
- package/app/hooks/post-tool-use.sh +20 -8
- package/app/hooks/quality-gate.sh +47 -0
- package/app/hooks/revert-guard.sh +84 -0
- package/app/hooks/search-tracker.sh +18 -0
- package/app/hooks/session-start.sh +14 -2
- package/app/hooks/stop-search-check.sh +37 -0
- package/app/hooks/test-cohesion-map.json +77 -0
- package/app/hooks/test-cohesion.sh +93 -0
- package/app/hooks/track-usage.sh +3 -1
- package/app/hooks/user-prompt-submit.sh +33 -6
- package/app/hooks.json +65 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +8 -8
- package/bin/ai-toolkit.js +43 -0
- package/kb/procedures/release-verification-sop.md +2 -2
- package/kb/reference/architecture-overview.md +1 -1
- package/kb/reference/extension-api.md +77 -11
- package/kb/reference/hooks-catalog.md +149 -24
- package/kb/reference/mcp-templates.md +6 -4
- package/kb/reference/unique-features.md +11 -5
- package/llms-full.txt +163 -32
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.py +13 -0
- package/scripts/generate_augment_hooks.py +5 -1
- package/scripts/generate_codex_hooks.py +2 -0
- package/scripts/generate_cursor_hooks.py +6 -0
- package/scripts/generate_gemini_hooks.py +5 -1
- package/scripts/generate_windsurf_hooks.py +6 -0
- package/scripts/inject_mcp_cli.py +514 -0
- package/scripts/install.py +2 -1
- package/scripts/install_steps/hooks.py +5 -0
- package/scripts/install_steps/markers.py +40 -0
- package/scripts/mcp_sources.py +162 -0
- package/scripts/merge-hooks.py +10 -1
- package/scripts/paths.py +2 -0
- package/scripts/plugin_schema.py +4 -0
- package/scripts/session_state.py +150 -0
- package/scripts/test_cohesion.py +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,73 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.3.0 - inject-mcp extension API (2026-05-12)
|
|
11
|
+
|
|
12
|
+
Minor release. Closes the asymmetry between `inject-rule` / `inject-hook` and MCP servers by adding `inject-mcp` and `remove-mcp` as first-class members of the extension API. External tools (rag-mcp, jira-mcp, custom integrations) can now register their MCP templates the same way they register rules and hooks -- from a local file or HTTPS URL, with full editor propagation and auto-refresh on `ai-toolkit update`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`ai-toolkit inject-mcp <file|url> [--name <name>] [--force]`** - Inject an external MCP server template into `~/.mcp.json` (toolkit source-of-truth) and propagate it to every editor with a `global_path` (Claude, Cursor, Codex, Gemini, Windsurf, Cline, Augment, Copilot). Servers in `~/.mcp.json` are tagged with `_source` for idempotent re-injection; native editor configs receive the same servers without `_source`.
|
|
17
|
+
- **`ai-toolkit remove-mcp <name>`** - Strip all servers tagged with the given source from `~/.mcp.json` and every editor config, unregister URL sources, and remove cached template files.
|
|
18
|
+
- **`--name <name>` flag** - Override the auto-derived source name for both local files and URLs. Required when filename stem is generic (e.g., `mcp-template.json` → `--name rag-mcp`).
|
|
19
|
+
- **`--force` flag** - Overwrite servers tagged with a different `_source`. Without `--force`, collisions exit with code 3. Entries tagged `"_source": "ai-toolkit"` are protected even with `--force`.
|
|
20
|
+
- **URL fetch + cache + auto-refresh** - HTTPS templates are cached in `~/.softspark/ai-toolkit/mcp-templates/external/<name>.json` and registered in `sources.json` with sha256 pin. On every `ai-toolkit update`, URL-sourced templates are re-fetched and re-injected; cached version is used on fetch failure.
|
|
21
|
+
- **`scripts/mcp_sources.py`** - Source registry for external MCP templates, mirroring `hook_sources.py`.
|
|
22
|
+
- **`scripts/inject_mcp_cli.py`** - CLI entry point implementing both inject and remove modes.
|
|
23
|
+
- **`refresh_url_mcp()` in `install_steps/markers.py`** - Update flow integration, called from `install.py` alongside `refresh_url_hooks()`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`paths.py`** - Added `MCP_TEMPLATES_DIR` and `EXTERNAL_MCP_DIR` constants.
|
|
28
|
+
- **`bin/ai-toolkit.js`** - Registered `inject-mcp` / `remove-mcp` handlers and help text.
|
|
29
|
+
- **`kb/reference/extension-api.md`** - Documented inject-mcp / remove-mcp + flags. Version bumped to 1.5.0.
|
|
30
|
+
- **`kb/reference/mcp-templates.md`** - Added External Templates section pointing to inject-mcp. Version bumped to 1.2.0.
|
|
31
|
+
|
|
32
|
+
### Tests
|
|
33
|
+
|
|
34
|
+
- **`tests/test_inject_mcp.bats`** - 15 bats cases covering local-file inject, URL fetch via fixture, `--name` override, `--force` collision override, ai-toolkit source protection, idempotent re-inject, editor propagation to Cursor (JSON) and Codex (TOML), `_source` strip from native configs, sources.json registry, `--remove` cleanup, HTTPS-only enforcement.
|
|
35
|
+
|
|
36
|
+
### Verification
|
|
37
|
+
|
|
38
|
+
- 15/15 bats cases passing in `test_inject_mcp.bats`.
|
|
39
|
+
- E2E smoke test: injecting an external `mcp-template.json` writes 9 files (`~/.mcp.json` + 8 editor configs) with `_source` only in source-of-truth and stripped from native configs.
|
|
40
|
+
- No regressions in `test_inject_hook.bats` or `test_mcp_manager.bats`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## v4.2.5 - Hook safety and no-RAG compatibility (2026-05-12)
|
|
45
|
+
|
|
46
|
+
Patch release. Hardens Claude Code hook enforcement while keeping the toolkit safe for users who do not have RAG/MCP search providers installed.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Capability-aware search hooks** - Added shared search capability detection so search-first enforcement uses RAG/MCP when available and degrades to guidance when no provider is installed.
|
|
51
|
+
- **Revert guard** - Added destructive Git restore/checkout protection that blocks unsafe rollback commands when they would discard unrelated user work.
|
|
52
|
+
- **Test cohesion hook** - Added source-to-test mapping for focused Bats verification after relevant edits, with project-level override support.
|
|
53
|
+
- **Hook payload adapter** - Added shared hook input/output helpers for consistent Claude, Gemini, Augment, Codex, Cursor, and Windsurf hook payload handling.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **Hook installation** - Installs hook JSON maps and runtime helper scripts needed by the new hooks into the global toolkit runtime directory.
|
|
58
|
+
- **Generated editor configs** - Regenerated multi-editor hook guidance and project agent surfaces after the hook expansion.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **No-RAG installs** - Search-first hooks no longer block users who installed the toolkit without `rag-mcp` or another supported search provider.
|
|
63
|
+
- **Doctor coverage** - `ai-toolkit doctor` now verifies the new hook helper scripts and runtime hook files.
|
|
64
|
+
|
|
65
|
+
### Ecosystem
|
|
66
|
+
|
|
67
|
+
- Refreshed `benchmarks/ecosystem-doctor-snapshot.json` after class-A documentation drift only: upstream docs changed content hashes without heading or marker deltas.
|
|
68
|
+
|
|
69
|
+
### Verification
|
|
70
|
+
|
|
71
|
+
- `npm view @softspark/ai-toolkit version`: latest published version is `4.2.4`; `4.2.5` is available.
|
|
72
|
+
- Local Claude test confirmed RAG-backed prompts still call `rag-mcp` when present.
|
|
73
|
+
- Local Claude destructive-revert scenario was refused before data loss.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
10
77
|
## v4.2.4 - GitHub Actions Node 24 readiness (2026-05-12)
|
|
11
78
|
|
|
12
79
|
Patch release. Removes GitHub Actions Node 20 action-runtime deprecation warnings from CI, publish, and reusable action surfaces.
|
package/README.md
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
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.3.0
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Minor release. Closes the asymmetry between `inject-rule` / `inject-hook` and MCP servers -- external tools (rag-mcp, jira-mcp, custom integrations) can now register MCP templates the same way they register rules and hooks.
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- **
|
|
15
|
+
- **`ai-toolkit inject-mcp <file|url>`**: register an external MCP template into `~/.mcp.json` + every editor with a `global_path` (Claude, Cursor, Codex, Gemini, Windsurf, Cline, Augment, Copilot) in one command.
|
|
16
|
+
- **`ai-toolkit remove-mcp <name>`**: strip injected servers from `~/.mcp.json` and every editor config in one command.
|
|
17
|
+
- **URL templates + auto-refresh**: HTTPS-sourced templates are cached and re-fetched on every `ai-toolkit update`, just like URL rules and hooks.
|
|
18
|
+
- **`--name` and `--force` flags**: explicit source naming for both files and URLs, plus collision-safe overwrites that still protect `ai-toolkit` built-in entries.
|
|
18
19
|
|
|
19
20
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
20
21
|
|
|
@@ -140,14 +141,14 @@ ai-toolkit/
|
|
|
140
141
|
│ ├── agents/ # 44 agent definitions
|
|
141
142
|
│ ├── skills/ # 107 skills (task / hybrid / knowledge)
|
|
142
143
|
│ ├── rules/ # Auto-injected into your CLAUDE.md
|
|
143
|
-
│ ├── hooks/ # Hook scripts (
|
|
144
|
+
│ ├── hooks/ # Hook scripts (28 entries, 14 lifecycle events)
|
|
144
145
|
│ ├── plugins/ # 11 experimental plugin packs (opt-in)
|
|
145
146
|
│ ├── output-styles/ # System prompt output style overrides
|
|
146
147
|
│ ├── constitution.md # 6 immutable safety articles
|
|
147
148
|
│ └── ARCHITECTURE.md # Full system design
|
|
148
149
|
├── kb/ # Reference docs, procedures, plans
|
|
149
150
|
├── scripts/ # Validation, install, evaluation scripts
|
|
150
|
-
├── tests/ # Bats test suite (
|
|
151
|
+
├── tests/ # Bats test suite (1131 tests)
|
|
151
152
|
└── CHANGELOG.md
|
|
152
153
|
```
|
|
153
154
|
|
|
@@ -159,7 +160,7 @@ ai-toolkit/
|
|
|
159
160
|
|
|
160
161
|
**Machine-enforced constitution** — 6-article safety constitution enforced via `PreToolUse` hooks that actually block `rm -rf`, `DROP TABLE`, and irreversible operations. Not just documentation.
|
|
161
162
|
|
|
162
|
-
**
|
|
163
|
+
**28 lifecycle hooks** — Executable scripts across 14 events (SessionStart → SessionEnd, plus InstructionsLoaded + ConfigChange). Guards, governance, quality gates, session persistence, MCP health checks, revert protection, test-cohesion enforcement, search-first discipline. See [Hooks Catalog](kb/reference/hooks-catalog.md).
|
|
163
164
|
|
|
164
165
|
**Security scanning** — `/skill-audit` for code-level risks, `/cve-scan` for dependency CVEs. Both CI-ready with exit codes.
|
|
165
166
|
|
|
@@ -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": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SoftSpark",
|
|
7
7
|
"url": "https://github.com/softspark"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _hook-io.sh — shared JSON input/output helpers for multi-editor hooks.
|
|
3
|
+
#
|
|
4
|
+
# Source this file from hook scripts. It expects the caller to set INPUT to the
|
|
5
|
+
# hook payload JSON before calling helper functions.
|
|
6
|
+
|
|
7
|
+
hook_json() {
|
|
8
|
+
local filter="$1"
|
|
9
|
+
echo "${INPUT:-}" | jq -r "$filter" 2>/dev/null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
hook_tool_name() {
|
|
13
|
+
local value
|
|
14
|
+
value=$(hook_json '.tool_name // .toolInfo.name // .tool_info.name // .tool // empty')
|
|
15
|
+
[ -n "$value" ] && [ "$value" != "null" ] && printf '%s\n' "$value"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
hook_prompt() {
|
|
19
|
+
hook_json '.prompt // .tool_info.user_prompt // .user_prompt // .input // empty'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
hook_command() {
|
|
23
|
+
hook_json '.tool_input.command // .tool_input.command_line // .tool_info.command // .tool_info.command_line // .command // empty'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hook_file_path() {
|
|
27
|
+
hook_json '
|
|
28
|
+
.tool_input.file_path //
|
|
29
|
+
.tool_input.path //
|
|
30
|
+
.tool_info.file_path //
|
|
31
|
+
.tool_info.path //
|
|
32
|
+
(.file_changes[0].path? // empty) //
|
|
33
|
+
empty
|
|
34
|
+
'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
hook_old_content() {
|
|
38
|
+
hook_json '(.tool_input.old_string // .tool_input.old_str_1 // .tool_info.old_string // .tool_info.old_str_1 // .tool_info.edits[0].old_string // "")'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
hook_new_content() {
|
|
42
|
+
hook_json '(.tool_input.new_string // .tool_input.new_str_1 // .tool_input.content // .tool_info.new_string // .tool_info.new_str_1 // .tool_info.content // .tool_info.edits[0].new_string // .file_changes[0].content? // "")'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
hook_emit_context() {
|
|
46
|
+
local message="$1"
|
|
47
|
+
if [ "${AI_TOOLKIT_HOOK_FORMAT:-}" = "json" ]; then
|
|
48
|
+
jq -nc --arg msg "$message" \
|
|
49
|
+
'{"hookSpecificOutput":{"additionalContext":$msg},"suppressOutput":true}'
|
|
50
|
+
else
|
|
51
|
+
printf '%s\n' "$message"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hook_emit_block() {
|
|
56
|
+
local event="$1"
|
|
57
|
+
local reason="$2"
|
|
58
|
+
if [ "${AI_TOOLKIT_HOOK_FORMAT:-}" = "json" ]; then
|
|
59
|
+
jq -nc --arg event "$event" --arg reason "$reason" \
|
|
60
|
+
'{"hookSpecificOutput":{"hookEventName":$event,"decision":"block","reason":$reason},"decision":"block","reason":$reason,"suppressOutput":false}'
|
|
61
|
+
else
|
|
62
|
+
jq -nc --arg reason "$reason" '{"decision":"block","reason":$reason}'
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _locate-toolkit.sh — shared toolkit-root locator for hooks needing scripts/.
|
|
3
|
+
#
|
|
4
|
+
# Source this file to get $TOOLKIT_DIR set to the ai-toolkit checkout root.
|
|
5
|
+
# Resolution order:
|
|
6
|
+
# 1. $AI_TOOLKIT_DIR env override (used by tests)
|
|
7
|
+
# 2. npm global install: $(npm root -g)/@softspark/ai-toolkit
|
|
8
|
+
# 3. Fallback: installed runtime at ~/.softspark/ai-toolkit
|
|
9
|
+
# 4. Fallback: parent of the hook script directory (repo checkout)
|
|
10
|
+
#
|
|
11
|
+
# Sets:
|
|
12
|
+
# TOOLKIT_DIR — absolute path to toolkit root, or empty if not findable.
|
|
13
|
+
|
|
14
|
+
_ai_toolkit_hook_dir() {
|
|
15
|
+
local src="${BASH_SOURCE[1]:-$0}"
|
|
16
|
+
(cd "$(dirname "$src")" 2>/dev/null && pwd)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if [ -n "${AI_TOOLKIT_DIR:-}" ] && [ -d "$AI_TOOLKIT_DIR" ]; then
|
|
20
|
+
TOOLKIT_DIR="$AI_TOOLKIT_DIR"
|
|
21
|
+
else
|
|
22
|
+
TOOLKIT_DIR="$(npm root -g 2>/dev/null)/@softspark/ai-toolkit"
|
|
23
|
+
if [ ! -d "$TOOLKIT_DIR" ]; then
|
|
24
|
+
TOOLKIT_DIR="$HOME/.softspark/ai-toolkit"
|
|
25
|
+
fi
|
|
26
|
+
if [ ! -d "$TOOLKIT_DIR" ]; then
|
|
27
|
+
_hook_dir="$(_ai_toolkit_hook_dir)"
|
|
28
|
+
if [ -f "$_hook_dir/../app/hooks.json" ]; then
|
|
29
|
+
TOOLKIT_DIR="$(cd "$_hook_dir/.." 2>/dev/null && pwd)"
|
|
30
|
+
else
|
|
31
|
+
TOOLKIT_DIR="$(cd "$_hook_dir/../.." 2>/dev/null && pwd)"
|
|
32
|
+
fi
|
|
33
|
+
unset _hook_dir
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
[ -d "$TOOLKIT_DIR" ] || TOOLKIT_DIR=""
|
|
38
|
+
unset -f _ai_toolkit_hook_dir
|
|
39
|
+
export TOOLKIT_DIR
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _search-capability.sh — determine whether strict search-first enforcement is safe.
|
|
3
|
+
#
|
|
4
|
+
# Default is conservative: do not block users unless a search provider is
|
|
5
|
+
# explicitly configured or strict mode is requested.
|
|
6
|
+
|
|
7
|
+
ai_toolkit_search_first_mode() {
|
|
8
|
+
printf '%s\n' "${AI_TOOLKIT_SEARCH_FIRST:-${CLAUDE_SEARCH_FIRST:-auto}}" \
|
|
9
|
+
| tr '[:upper:]' '[:lower:]'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
ai_toolkit_has_search_provider() {
|
|
13
|
+
local mode
|
|
14
|
+
mode="$(ai_toolkit_search_first_mode)"
|
|
15
|
+
case "$mode" in
|
|
16
|
+
off|false|0|disabled|none)
|
|
17
|
+
return 1
|
|
18
|
+
;;
|
|
19
|
+
strict|required|require|on|true|1)
|
|
20
|
+
return 0
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
[ -n "${AI_TOOLKIT_SEARCH_PROVIDER:-}" ] && return 0
|
|
25
|
+
[ -n "${CLAUDE_SEARCH_PROVIDER:-}" ] && return 0
|
|
26
|
+
|
|
27
|
+
local candidates=(
|
|
28
|
+
"$PWD/.mcp.json"
|
|
29
|
+
"$PWD/.claude/mcp.json"
|
|
30
|
+
"$PWD/.cursor/mcp.json"
|
|
31
|
+
"$PWD/.gemini/settings.json"
|
|
32
|
+
"$HOME/.claude.json"
|
|
33
|
+
"$HOME/.claude/settings.json"
|
|
34
|
+
"$HOME/.codex/config.toml"
|
|
35
|
+
"$HOME/.gemini/settings.json"
|
|
36
|
+
)
|
|
37
|
+
local path
|
|
38
|
+
for path in "${candidates[@]}"; do
|
|
39
|
+
[ -f "$path" ] || continue
|
|
40
|
+
if grep -Eqi 'rag[-_]?mcp|smart_query|hybrid_search_kb|crag_search|multi_hop_search|websearch|web-fetch|web_fetch|web_search' "$path"; then
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# Non-blocking: always exits 0 (warnings only).
|
|
6
6
|
|
|
7
7
|
INPUT=$(cat)
|
|
8
|
-
|
|
8
|
+
# shellcheck source=_hook-io.sh
|
|
9
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
10
|
+
COMMAND=$(hook_command)
|
|
9
11
|
|
|
10
12
|
if [ -z "$COMMAND" ]; then
|
|
11
13
|
exit 0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# config-desync-guard.sh — Warn when ~/.claude/settings.json drifts from
|
|
3
|
+
# the ai-toolkit source manifest (app/hooks.json).
|
|
4
|
+
#
|
|
5
|
+
# Fires on: ConfigChange
|
|
6
|
+
# Matcher: user_settings
|
|
7
|
+
# Non-blocking by design: exits 0 even on drift. We never want to block a
|
|
8
|
+
# user's legitimate settings edit. Output goes to stderr so Claude Code
|
|
9
|
+
# surfaces it as a notice.
|
|
10
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
|
|
11
|
+
#
|
|
12
|
+
# Override (silence advisory): CLAUDE_SKIP_CONFIG_DESYNC=1
|
|
13
|
+
|
|
14
|
+
# shellcheck source=_profile-check.sh
|
|
15
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
16
|
+
# shellcheck source=_locate-toolkit.sh
|
|
17
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
18
|
+
|
|
19
|
+
[ "${CLAUDE_SKIP_CONFIG_DESYNC:-0}" = "1" ] && exit 0
|
|
20
|
+
[ -z "$TOOLKIT_DIR" ] && exit 0
|
|
21
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
22
|
+
|
|
23
|
+
SETTINGS="$HOME/.claude/settings.json"
|
|
24
|
+
SOURCE="$TOOLKIT_DIR/app/hooks.json"
|
|
25
|
+
[ -f "$SETTINGS" ] || exit 0
|
|
26
|
+
[ -f "$SOURCE" ] || exit 0
|
|
27
|
+
|
|
28
|
+
# Compare the set of ai-toolkit-tagged commands in each file.
|
|
29
|
+
DIFF=$(python3 - "$SETTINGS" "$SOURCE" <<'PY'
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def toolkit_commands(path):
|
|
35
|
+
try:
|
|
36
|
+
data = json.load(open(path))
|
|
37
|
+
except Exception:
|
|
38
|
+
return set()
|
|
39
|
+
hooks = data.get("hooks", {}) if isinstance(data, dict) else {}
|
|
40
|
+
out = set()
|
|
41
|
+
for event, entries in hooks.items():
|
|
42
|
+
if not isinstance(entries, list):
|
|
43
|
+
continue
|
|
44
|
+
for entry in entries:
|
|
45
|
+
if not isinstance(entry, dict):
|
|
46
|
+
continue
|
|
47
|
+
tagged_top = entry.get("_source") == "ai-toolkit"
|
|
48
|
+
for hook in entry.get("hooks", []):
|
|
49
|
+
if not isinstance(hook, dict):
|
|
50
|
+
continue
|
|
51
|
+
tagged_inner = hook.get("_source") == "ai-toolkit"
|
|
52
|
+
if not (tagged_top or tagged_inner):
|
|
53
|
+
continue
|
|
54
|
+
cmd = hook.get("command", "")
|
|
55
|
+
out.add((event, entry.get("matcher", ""), cmd))
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
installed = toolkit_commands(sys.argv[1])
|
|
60
|
+
source = toolkit_commands(sys.argv[2])
|
|
61
|
+
|
|
62
|
+
missing = source - installed # in source, not installed (toolkit got new hooks)
|
|
63
|
+
extra = installed - source # in installed, not source (stale toolkit hook)
|
|
64
|
+
|
|
65
|
+
if not (missing or extra):
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
if missing:
|
|
69
|
+
print(f" Missing from settings.json ({len(missing)}):")
|
|
70
|
+
for event, matcher, cmd in sorted(missing):
|
|
71
|
+
print(f" - [{event}] {cmd}")
|
|
72
|
+
if extra:
|
|
73
|
+
print(f" Stale in settings.json ({len(extra)}):")
|
|
74
|
+
for event, matcher, cmd in sorted(extra):
|
|
75
|
+
print(f" - [{event}] {cmd}")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
PY
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if [ $? -ne 0 ] && [ -n "$DIFF" ]; then
|
|
81
|
+
cat >&2 <<EOF
|
|
82
|
+
config-desync-guard: ai-toolkit hooks drifted from source manifest.
|
|
83
|
+
|
|
84
|
+
$DIFF
|
|
85
|
+
|
|
86
|
+
Fix: re-install canonical state:
|
|
87
|
+
ai-toolkit update
|
|
88
|
+
or auto-repair:
|
|
89
|
+
ai-toolkit doctor --fix
|
|
90
|
+
|
|
91
|
+
Silence advisory (one-off): CLAUDE_SKIP_CONFIG_DESYNC=1
|
|
92
|
+
EOF
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
exit 0
|
|
@@ -14,7 +14,9 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
14
14
|
SESSION="${CLAUDE_SESSION_ID:-unknown}"
|
|
15
15
|
|
|
16
16
|
INPUT=$(cat)
|
|
17
|
-
|
|
17
|
+
# shellcheck source=_hook-io.sh
|
|
18
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
19
|
+
TOOL_NAME=$(hook_tool_name)
|
|
18
20
|
|
|
19
21
|
if [ -z "$TOOL_NAME" ]; then
|
|
20
22
|
TOOL_NAME="${CLAUDE_TOOL_NAME:-unknown}"
|
|
@@ -39,7 +41,7 @@ log_event() {
|
|
|
39
41
|
|
|
40
42
|
case "$TOOL_NAME" in
|
|
41
43
|
Bash)
|
|
42
|
-
COMMAND=$(
|
|
44
|
+
COMMAND=$(hook_command)
|
|
43
45
|
[ -z "$COMMAND" ] && exit 0
|
|
44
46
|
|
|
45
47
|
# Check for dangerous command patterns
|
|
@@ -64,7 +66,7 @@ case "$TOOL_NAME" in
|
|
|
64
66
|
;;
|
|
65
67
|
|
|
66
68
|
Write|Edit|MultiEdit)
|
|
67
|
-
FILE_PATH=$(
|
|
69
|
+
FILE_PATH=$(hook_file_path)
|
|
68
70
|
[ -z "$FILE_PATH" ] && exit 0
|
|
69
71
|
|
|
70
72
|
BASENAME=$(basename "$FILE_PATH")
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# Exit 2 = block the tool call. Stderr message goes to Claude as feedback.
|
|
6
6
|
|
|
7
7
|
INPUT=$(cat)
|
|
8
|
-
|
|
8
|
+
# shellcheck source=_hook-io.sh
|
|
9
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
10
|
+
FILE_PATH=$(hook_file_path)
|
|
9
11
|
|
|
10
12
|
if [ -z "$FILE_PATH" ]; then
|
|
11
13
|
exit 0
|
|
@@ -30,8 +32,8 @@ case "$BASENAME" in
|
|
|
30
32
|
BLOCKED=1 ;;
|
|
31
33
|
pyproject.toml)
|
|
32
34
|
# Only block if editing [tool.ruff] section — check new_string/content for ruff markers
|
|
33
|
-
CONTENT=$(
|
|
34
|
-
OLD=$(
|
|
35
|
+
CONTENT=$(hook_new_content)
|
|
36
|
+
OLD=$(hook_old_content)
|
|
35
37
|
if printf '%s\n%s' "$OLD" "$CONTENT" | grep -qi '\[tool\.ruff'; then
|
|
36
38
|
BLOCKED=1
|
|
37
39
|
fi
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# Exit 2 = block the command. Stderr message goes to Claude as feedback.
|
|
6
6
|
|
|
7
7
|
INPUT=$(cat)
|
|
8
|
-
|
|
8
|
+
# shellcheck source=_hook-io.sh
|
|
9
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
10
|
+
COMMAND=$(hook_command)
|
|
9
11
|
|
|
10
12
|
if [ -z "$COMMAND" ]; then
|
|
11
13
|
# Fallback: try CLAUDE_TOOL_INPUT env var
|
package/app/hooks/guard-path.sh
CHANGED
|
@@ -29,7 +29,14 @@ ALL_PATHS=$(echo "$INPUT" | jq -r '
|
|
|
29
29
|
.tool_input.source,
|
|
30
30
|
.tool_input.destination,
|
|
31
31
|
.tool_input.command,
|
|
32
|
-
|
|
32
|
+
.tool_input.command_line,
|
|
33
|
+
(.tool_input.paths[]? // empty),
|
|
34
|
+
.tool_info.file_path,
|
|
35
|
+
.tool_info.path,
|
|
36
|
+
.tool_info.command,
|
|
37
|
+
.tool_info.command_line,
|
|
38
|
+
(.tool_info.paths[]? // empty),
|
|
39
|
+
(.file_changes[]?.path // empty)
|
|
33
40
|
] | map(select(. != null and . != "")) | .[]
|
|
34
41
|
' 2>/dev/null)
|
|
35
42
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# instructions-audit.sh — Log which CLAUDE.md / rules / skills loaded.
|
|
3
|
+
#
|
|
4
|
+
# Fires on: InstructionsLoaded
|
|
5
|
+
# Matcher: all
|
|
6
|
+
# Non-blocking: always exits 0.
|
|
7
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
|
|
8
|
+
#
|
|
9
|
+
# Provides an audit trail so we can see whether a CLAUDE.md rule actually
|
|
10
|
+
# entered the agent's context (vs. silently dropped due to token budget,
|
|
11
|
+
# nested include limits, or path-glob misses).
|
|
12
|
+
#
|
|
13
|
+
# Output: ~/.softspark/ai-toolkit/state/loaded-instructions.log
|
|
14
|
+
# Schema: ISO-8601 ts \t memory_type \t load_reason \t file_path
|
|
15
|
+
|
|
16
|
+
# shellcheck source=_profile-check.sh
|
|
17
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat 2>/dev/null)
|
|
20
|
+
[ -z "$INPUT" ] && exit 0
|
|
21
|
+
|
|
22
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // empty' 2>/dev/null)
|
|
23
|
+
MEMORY_TYPE=$(echo "$INPUT" | jq -r '.memory_type // "unknown"' 2>/dev/null)
|
|
24
|
+
LOAD_REASON=$(echo "$INPUT" | jq -r '.load_reason // "unknown"' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
27
|
+
|
|
28
|
+
LOG_DIR="$HOME/.softspark/ai-toolkit/state"
|
|
29
|
+
LOG_FILE="$LOG_DIR/loaded-instructions.log"
|
|
30
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || exit 0
|
|
31
|
+
|
|
32
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
33
|
+
printf '%s\t%s\t%s\t%s\n' "$TS" "$MEMORY_TYPE" "$LOAD_REASON" "$FILE_PATH" >> "$LOG_FILE"
|
|
34
|
+
|
|
35
|
+
# Cap log at 2000 lines to avoid unbounded growth.
|
|
36
|
+
if [ -f "$LOG_FILE" ]; then
|
|
37
|
+
line_count=$(wc -l < "$LOG_FILE" 2>/dev/null | tr -d ' ')
|
|
38
|
+
if [ -n "$line_count" ] && [ "$line_count" -gt 2000 ]; then
|
|
39
|
+
tail -1500 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exit 0
|
|
@@ -7,31 +7,43 @@
|
|
|
7
7
|
|
|
8
8
|
# shellcheck source=_profile-check.sh
|
|
9
9
|
source "$(dirname "$0")/_profile-check.sh"
|
|
10
|
+
# shellcheck source=_locate-toolkit.sh
|
|
11
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
12
|
+
# shellcheck source=_hook-io.sh
|
|
13
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
10
14
|
|
|
11
15
|
# Read from stdin (Claude Code passes JSON with .tool_name, .tool_input)
|
|
12
16
|
INPUT=$(cat)
|
|
13
|
-
TOOL_NAME=$(
|
|
14
|
-
|
|
17
|
+
TOOL_NAME=$(hook_tool_name)
|
|
18
|
+
[ -z "$TOOL_NAME" ] && TOOL_NAME="unknown"
|
|
19
|
+
FILE_PATH=$(hook_file_path)
|
|
20
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
# Append edit to session state (used by revert-guard, test-cohesion, quality-gate).
|
|
23
|
+
if [ -n "$FILE_PATH" ] && [ -n "$TOOLKIT_DIR" ] && command -v python3 >/dev/null 2>&1; then
|
|
24
|
+
python3 "$TOOLKIT_DIR/scripts/session_state.py" append \
|
|
25
|
+
--tool "$TOOL_NAME" --path "$FILE_PATH" \
|
|
26
|
+
${SESSION_ID:+--session-id "$SESSION_ID"} >/dev/null 2>&1 || true
|
|
27
|
+
fi
|
|
15
28
|
|
|
16
29
|
if [ -z "$FILE_PATH" ]; then
|
|
17
|
-
|
|
30
|
+
hook_emit_context "PostToolUse: ${TOOL_NAME} completed. Consider validating lint, tests, and docs if behavior changed."
|
|
18
31
|
exit 0
|
|
19
32
|
fi
|
|
20
33
|
|
|
21
34
|
case "$FILE_PATH" in
|
|
22
35
|
*.md|*.txt)
|
|
23
|
-
|
|
36
|
+
hook_emit_context "PostToolUse: updated ${FILE_PATH}. If behavior or workflow changed, refresh related docs and examples."
|
|
24
37
|
;;
|
|
25
38
|
*.bats|*.test.*|*.spec.*)
|
|
26
|
-
|
|
39
|
+
hook_emit_context "PostToolUse: updated test file ${FILE_PATH}. Run the most relevant targeted test command next."
|
|
27
40
|
;;
|
|
28
41
|
*.json|*.yml|*.yaml|*.toml)
|
|
29
|
-
|
|
42
|
+
hook_emit_context "PostToolUse: updated config file ${FILE_PATH}. Validate syntax and any generated artifacts affected by this change."
|
|
30
43
|
;;
|
|
31
44
|
*)
|
|
32
|
-
|
|
45
|
+
hook_emit_context "PostToolUse: updated ${FILE_PATH}. If behavior changed, run validation, targeted tests, and update docs if needed."
|
|
33
46
|
;;
|
|
34
47
|
esac
|
|
35
48
|
|
|
36
49
|
exit 0
|
|
37
|
-
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
# shellcheck source=_profile-check.sh
|
|
9
9
|
source "$(dirname "$0")/_profile-check.sh"
|
|
10
|
+
# shellcheck source=_locate-toolkit.sh
|
|
11
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
10
12
|
|
|
11
13
|
run_required() {
|
|
12
14
|
local label="$1"
|
|
@@ -35,6 +37,51 @@ require_command() {
|
|
|
35
37
|
return 1
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
cohesion_for_session_edits() {
|
|
41
|
+
# Run cohesion-mapped tests for every path touched this session.
|
|
42
|
+
# Used by projects that ship a test-cohesion-map.json (e.g. ai-toolkit).
|
|
43
|
+
[ -z "$TOOLKIT_DIR" ] && return 0
|
|
44
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
45
|
+
|
|
46
|
+
local edits
|
|
47
|
+
edits=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list 2>/dev/null)
|
|
48
|
+
[ -z "$edits" ] && return 0
|
|
49
|
+
|
|
50
|
+
# shellcheck disable=SC2206 # word-splitting is intentional, paths have no spaces
|
|
51
|
+
local edits_array=()
|
|
52
|
+
while IFS= read -r line; do
|
|
53
|
+
[ -n "$line" ] && edits_array+=("$line")
|
|
54
|
+
done <<<"$edits"
|
|
55
|
+
[ "${#edits_array[@]}" -eq 0 ] && return 0
|
|
56
|
+
|
|
57
|
+
local commands
|
|
58
|
+
commands=$(python3 "$TOOLKIT_DIR/scripts/test_cohesion.py" resolve \
|
|
59
|
+
--changed-paths "${edits_array[@]}" --repo-root "$PWD" 2>/dev/null)
|
|
60
|
+
[ -z "$commands" ] && return 0
|
|
61
|
+
|
|
62
|
+
local tmp
|
|
63
|
+
tmp="$(mktemp "${TMPDIR:-/tmp}/ai-toolkit-cohesion.XXXXXX")"
|
|
64
|
+
while IFS= read -r cmd; do
|
|
65
|
+
[ -z "$cmd" ] && continue
|
|
66
|
+
if ! bash -c "$cmd" >"$tmp" 2>&1; then
|
|
67
|
+
echo "QUALITY GATE FAILED: cohesion tests failed." >&2
|
|
68
|
+
echo "Command: $cmd" >&2
|
|
69
|
+
tail -25 "$tmp" >&2
|
|
70
|
+
rm -f "$tmp"
|
|
71
|
+
exit 2
|
|
72
|
+
fi
|
|
73
|
+
done <<<"$commands"
|
|
74
|
+
rm -f "$tmp"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Cohesion-driven branch (ai-toolkit-style repos): runs ONLY tests mapped to
|
|
78
|
+
# files edited in this session via .claude/test-cohesion-map.json (or the
|
|
79
|
+
# toolkit default at app/hooks/test-cohesion-map.json).
|
|
80
|
+
if [ -f .claude/test-cohesion-map.json ] || \
|
|
81
|
+
{ [ -f app/hooks.json ] && [ -d tests ] && [ -f scripts/validate.py ]; }; then
|
|
82
|
+
cohesion_for_session_edits
|
|
83
|
+
fi
|
|
84
|
+
|
|
38
85
|
if [ -f pyproject.toml ] || [ -f setup.py ]; then
|
|
39
86
|
if require_command ruff; then
|
|
40
87
|
run_required "ruff found errors" "head -30" ruff check .
|