@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.
- package/CHANGELOG.md +33 -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/kb/procedures/release-verification-sop.md +2 -2
- package/kb/reference/architecture-overview.md +1 -1
- package/kb/reference/hooks-catalog.md +149 -24
- 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/install_steps/hooks.py +5 -0
- package/scripts/merge-hooks.py +10 -1
- 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,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)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
10
|
|
|
11
|
-
## What's New in v4.2.
|
|
11
|
+
## What's New in v4.2.5
|
|
12
12
|
|
|
13
|
-
Patch release
|
|
13
|
+
Patch release hardening Claude Code hooks while keeping installs safe for users without RAG/MCP.
|
|
14
14
|
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
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 (
|
|
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 (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
|
-
**
|
|
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
|
+
"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
|
-
|
|
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 .
|