@softspark/ai-toolkit 4.2.4 → 4.2.5

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 (43) hide show
  1. package/CHANGELOG.md +33 -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/kb/procedures/release-verification-sop.md +2 -2
  27. package/kb/reference/architecture-overview.md +1 -1
  28. package/kb/reference/hooks-catalog.md +149 -24
  29. package/kb/reference/unique-features.md +11 -5
  30. package/llms-full.txt +163 -32
  31. package/manifest.json +1 -1
  32. package/package.json +1 -1
  33. package/scripts/doctor.py +13 -0
  34. package/scripts/generate_augment_hooks.py +5 -1
  35. package/scripts/generate_codex_hooks.py +2 -0
  36. package/scripts/generate_cursor_hooks.py +6 -0
  37. package/scripts/generate_gemini_hooks.py +5 -1
  38. package/scripts/generate_windsurf_hooks.py +6 -0
  39. package/scripts/install_steps/hooks.py +5 -0
  40. package/scripts/merge-hooks.py +10 -1
  41. package/scripts/plugin_schema.py +4 -0
  42. package/scripts/session_state.py +150 -0
  43. package/scripts/test_cohesion.py +133 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.2.5 - Hook safety and no-RAG compatibility (2026-05-12)
11
+
12
+ Patch release. Hardens Claude Code hook enforcement while keeping the toolkit safe for users who do not have RAG/MCP search providers installed.
13
+
14
+ ### Added
15
+
16
+ - **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.
17
+ - **Revert guard** - Added destructive Git restore/checkout protection that blocks unsafe rollback commands when they would discard unrelated user work.
18
+ - **Test cohesion hook** - Added source-to-test mapping for focused Bats verification after relevant edits, with project-level override support.
19
+ - **Hook payload adapter** - Added shared hook input/output helpers for consistent Claude, Gemini, Augment, Codex, Cursor, and Windsurf hook payload handling.
20
+
21
+ ### Changed
22
+
23
+ - **Hook installation** - Installs hook JSON maps and runtime helper scripts needed by the new hooks into the global toolkit runtime directory.
24
+ - **Generated editor configs** - Regenerated multi-editor hook guidance and project agent surfaces after the hook expansion.
25
+
26
+ ### Fixed
27
+
28
+ - **No-RAG installs** - Search-first hooks no longer block users who installed the toolkit without `rag-mcp` or another supported search provider.
29
+ - **Doctor coverage** - `ai-toolkit doctor` now verifies the new hook helper scripts and runtime hook files.
30
+
31
+ ### Ecosystem
32
+
33
+ - Refreshed `benchmarks/ecosystem-doctor-snapshot.json` after class-A documentation drift only: upstream docs changed content hashes without heading or marker deltas.
34
+
35
+ ### Verification
36
+
37
+ - `npm view @softspark/ai-toolkit version`: latest published version is `4.2.4`; `4.2.5` is available.
38
+ - Local Claude test confirmed RAG-backed prompts still call `rag-mcp` when present.
39
+ - Local Claude destructive-revert scenario was refused before data loss.
40
+
41
+ ---
42
+
10
43
  ## v4.2.4 - GitHub Actions Node 24 readiness (2026-05-12)
11
44
 
12
45
  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-1116%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.2.4
11
+ ## What's New in v4.2.5
12
12
 
13
- Patch release removing Node 20 action-runtime warnings from GitHub Actions.
13
+ Patch release hardening Claude Code hooks while keeping installs safe for users without RAG/MCP.
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
+ - **Capability-aware search hooks**: search-first enforcement now degrades to guidance when no RAG/MCP provider is installed.
16
+ - **Revert safety**: destructive `git checkout`/`git restore` flows are blocked when they would discard unrelated user work.
17
+ - **Test cohesion**: source edits can trigger only their mapped Bats tests, with project-level override support.
18
+ - **Cross-editor hook payloads**: shared hook I/O helpers normalize Claude, Gemini, Augment, Codex, Cursor, and Windsurf payloads.
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 (1116 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.2.5",
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 .