@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +10 -9
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/hooks/_hook-io.sh +64 -0
  5. package/app/hooks/_locate-toolkit.sh +39 -0
  6. package/app/hooks/_search-capability.sh +46 -0
  7. package/app/hooks/commit-quality.sh +3 -1
  8. package/app/hooks/config-desync-guard.sh +95 -0
  9. package/app/hooks/governance-capture.sh +5 -3
  10. package/app/hooks/guard-config.sh +5 -3
  11. package/app/hooks/guard-destructive.sh +3 -1
  12. package/app/hooks/guard-path.sh +8 -1
  13. package/app/hooks/instructions-audit.sh +43 -0
  14. package/app/hooks/post-tool-use.sh +20 -8
  15. package/app/hooks/quality-gate.sh +47 -0
  16. package/app/hooks/revert-guard.sh +84 -0
  17. package/app/hooks/search-tracker.sh +18 -0
  18. package/app/hooks/session-start.sh +14 -2
  19. package/app/hooks/stop-search-check.sh +37 -0
  20. package/app/hooks/test-cohesion-map.json +77 -0
  21. package/app/hooks/test-cohesion.sh +93 -0
  22. package/app/hooks/track-usage.sh +3 -1
  23. package/app/hooks/user-prompt-submit.sh +33 -6
  24. package/app/hooks.json +65 -1
  25. package/benchmarks/ecosystem-doctor-snapshot.json +8 -8
  26. package/bin/ai-toolkit.js +43 -0
  27. package/kb/procedures/release-verification-sop.md +2 -2
  28. package/kb/reference/architecture-overview.md +1 -1
  29. package/kb/reference/extension-api.md +77 -11
  30. package/kb/reference/hooks-catalog.md +149 -24
  31. package/kb/reference/mcp-templates.md +6 -4
  32. package/kb/reference/unique-features.md +11 -5
  33. package/llms-full.txt +163 -32
  34. package/manifest.json +1 -1
  35. package/package.json +1 -1
  36. package/scripts/doctor.py +13 -0
  37. package/scripts/generate_augment_hooks.py +5 -1
  38. package/scripts/generate_codex_hooks.py +2 -0
  39. package/scripts/generate_cursor_hooks.py +6 -0
  40. package/scripts/generate_gemini_hooks.py +5 -1
  41. package/scripts/generate_windsurf_hooks.py +6 -0
  42. package/scripts/inject_mcp_cli.py +514 -0
  43. package/scripts/install.py +2 -1
  44. package/scripts/install_steps/hooks.py +5 -0
  45. package/scripts/install_steps/markers.py +40 -0
  46. package/scripts/mcp_sources.py +162 -0
  47. package/scripts/merge-hooks.py +10 -1
  48. package/scripts/paths.py +2 -0
  49. package/scripts/plugin_schema.py +4 -0
  50. package/scripts/session_state.py +150 -0
  51. 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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-107-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1051%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1131%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.2.4
11
+ ## What's New in v4.3.0
12
12
 
13
- Patch release removing Node 20 action-runtime warnings from GitHub Actions.
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
- - **CI action runtime**: `ci.yml`, `publish.yml`, and the reusable `action.yml` now use `actions/setup-node@v6`.
16
- - **Generated guidance**: CI templates and CI/CD skill examples now use current GitHub Actions versions.
17
- - **Release readiness**: the next publish run executes on the Node 24-compatible action runtime.
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 (22 entries, 12 lifecycle events)
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 (1051 tests)
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
- **22 lifecycle hooks** — Executable scripts across 12 events (SessionStart → SessionEnd). Guards, governance, quality gates, session persistence, MCP health checks. See [Hooks Catalog](kb/reference/hooks-catalog.md).
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.2.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
- COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
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
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
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=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
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=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
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
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
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=$(echo "$INPUT" | jq -r '(.tool_input.new_string // .tool_input.content // "")' 2>/dev/null)
34
- OLD=$(echo "$INPUT" | jq -r '(.tool_input.old_string // "")' 2>/dev/null)
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
- COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
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
@@ -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
- (.tool_input.paths[]? // empty)
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=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null)
14
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
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
- echo "PostToolUse: ${TOOL_NAME} completed. Consider validating lint, tests, and docs if behavior changed."
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
- echo "PostToolUse: updated ${FILE_PATH}. If behavior or workflow changed, refresh related docs and examples."
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
- echo "PostToolUse: updated test file ${FILE_PATH}. Run the most relevant targeted test command next."
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
- echo "PostToolUse: updated config file ${FILE_PATH}. Validate syntax and any generated artifacts affected by this change."
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
- echo "PostToolUse: updated ${FILE_PATH}. If behavior changed, run validation, targeted tests, and update docs if needed."
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 .