@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# revert-guard.sh — Block git revert/restore on files edited in this session.
|
|
3
|
+
#
|
|
4
|
+
# Fires on: PreToolUse
|
|
5
|
+
# Matcher: Bash
|
|
6
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
|
|
7
|
+
#
|
|
8
|
+
# Constitution Art. VI.2 ("Fix Every Found Bug"): when a session has produced
|
|
9
|
+
# edits, a `git checkout/restore/reset --hard/clean -fd` on those files is
|
|
10
|
+
# almost always discarding work-in-progress instead of fixing the root cause.
|
|
11
|
+
# Block it unless the user explicitly opted in.
|
|
12
|
+
#
|
|
13
|
+
# Escape hatch: set CLAUDE_REVERT_OK=1 (per-call) to bypass.
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 allow command
|
|
16
|
+
# 2 block (per Claude Code hooks spec)
|
|
17
|
+
|
|
18
|
+
# shellcheck source=_profile-check.sh
|
|
19
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
20
|
+
# shellcheck source=_locate-toolkit.sh
|
|
21
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
22
|
+
# shellcheck source=_hook-io.sh
|
|
23
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
COMMAND=$(hook_command)
|
|
27
|
+
|
|
28
|
+
[ -z "$COMMAND" ] && exit 0
|
|
29
|
+
[ "${CLAUDE_REVERT_OK:-0}" = "1" ] && exit 0
|
|
30
|
+
[ -z "$TOOLKIT_DIR" ] && exit 0
|
|
31
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
32
|
+
|
|
33
|
+
# Strip leading words so we can match the git verb.
|
|
34
|
+
# Only act on commands whose first token is `git`.
|
|
35
|
+
read -r FIRST _rest <<<"$COMMAND"
|
|
36
|
+
[ "$FIRST" = "git" ] || exit 0
|
|
37
|
+
|
|
38
|
+
# Helper: emit block message on stderr and exit 2.
|
|
39
|
+
_block() {
|
|
40
|
+
local reason="$1"
|
|
41
|
+
local detail="$2"
|
|
42
|
+
cat >&2 <<EOF
|
|
43
|
+
revert-guard: BLOCKED — $reason
|
|
44
|
+
Constitution Art. VI.2: do not revert work-in-progress. Fix the root cause instead.
|
|
45
|
+
|
|
46
|
+
Affected: $detail
|
|
47
|
+
|
|
48
|
+
If you really need to discard changes, re-run with:
|
|
49
|
+
CLAUDE_REVERT_OK=1 $COMMAND
|
|
50
|
+
EOF
|
|
51
|
+
exit 2
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# git reset --hard / git clean -fd: clobber-style, scope = all session edits.
|
|
55
|
+
if printf '%s' "$COMMAND" | grep -Eq 'reset[[:space:]]+(-+[A-Za-z-]+[[:space:]]+)*--hard|clean[[:space:]]+-[A-Za-z]*[df]'; then
|
|
56
|
+
edited=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list 2>/dev/null | head -10)
|
|
57
|
+
if [ -n "$edited" ]; then
|
|
58
|
+
_block "destructive 'git reset --hard' / 'git clean' with session edits in tree" "$edited"
|
|
59
|
+
fi
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# git checkout / git restore: only block when `--` separator is present
|
|
64
|
+
# (file-restore form). Bare `git checkout branch-name` is allowed.
|
|
65
|
+
if printf '%s' "$COMMAND" | grep -Eq '(checkout|restore)([[:space:]].*)?[[:space:]]--[[:space:]]'; then
|
|
66
|
+
files=$(printf '%s' "$COMMAND" | awk -F' -- ' '{print $2}')
|
|
67
|
+
[ -z "$files" ] && exit 0
|
|
68
|
+
blocked=""
|
|
69
|
+
for f in $files; do
|
|
70
|
+
# Resolve relative paths against $PWD.
|
|
71
|
+
case "$f" in
|
|
72
|
+
/*) abs="$f" ;;
|
|
73
|
+
*) abs="$PWD/$f" ;;
|
|
74
|
+
esac
|
|
75
|
+
if python3 "$TOOLKIT_DIR/scripts/session_state.py" was-edited "$abs" >/dev/null 2>&1; then
|
|
76
|
+
blocked="$blocked $f"
|
|
77
|
+
fi
|
|
78
|
+
done
|
|
79
|
+
if [ -n "$blocked" ]; then
|
|
80
|
+
_block "restoring file(s) edited in this session" "$blocked"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# search-tracker.sh — Clear search-required flag when a search tool runs.
|
|
3
|
+
#
|
|
4
|
+
# Fires on: PostToolUse
|
|
5
|
+
# Matcher: mcp__rag-mcp__smart_query|mcp__rag-mcp__hybrid_search_kb|mcp__rag-mcp__crag_search|mcp__rag-mcp__multi_hop_search|WebSearch|WebFetch
|
|
6
|
+
# Non-blocking: always exits 0.
|
|
7
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
|
|
8
|
+
#
|
|
9
|
+
# Paired with user-prompt-submit.sh (sets the flag) and stop-search-check.sh
|
|
10
|
+
# (blocks Stop when flag still present). Together they implement the Step 0
|
|
11
|
+
# "search before answering" rule from global CLAUDE.md.
|
|
12
|
+
|
|
13
|
+
# shellcheck source=_profile-check.sh
|
|
14
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
15
|
+
|
|
16
|
+
FLAG="$HOME/.softspark/ai-toolkit/state/search-required.flag"
|
|
17
|
+
rm -f "$FLAG" 2>/dev/null
|
|
18
|
+
exit 0
|
|
@@ -8,9 +8,21 @@
|
|
|
8
8
|
echo "MANDATORY: Before answering ANY technical question, apply ALL rules from your CLAUDE.md files (global + project). Follow the exact order of operations defined there. Do NOT skip mandatory steps even if you think you already know the answer."
|
|
9
9
|
echo "REMINDER: When writing features or fixing bugs, ensure tests cover the changes. When modifying API, config, or setup, update relevant documentation. Propose these steps to the user — do not silently skip them."
|
|
10
10
|
|
|
11
|
+
# shellcheck source=_locate-toolkit.sh
|
|
12
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
13
|
+
|
|
14
|
+
# 1a. Reset per-session edit state (used by revert-guard, test-cohesion, quality-gate)
|
|
15
|
+
SESSION_ID_INPUT=""
|
|
16
|
+
if [ ! -t 0 ]; then
|
|
17
|
+
STDIN_PAYLOAD="$(cat)"
|
|
18
|
+
SESSION_ID_INPUT="$(printf '%s' "$STDIN_PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)"
|
|
19
|
+
fi
|
|
20
|
+
if [ -n "$TOOLKIT_DIR" ] && command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
python3 "$TOOLKIT_DIR/scripts/session_state.py" reset \
|
|
22
|
+
${SESSION_ID_INPUT:+--session-id "$SESSION_ID_INPUT"} >/dev/null 2>&1 || true
|
|
23
|
+
fi
|
|
24
|
+
|
|
11
25
|
# 2. Check for updates (cached, max once per 24h, non-blocking)
|
|
12
|
-
TOOLKIT_DIR="$(npm root -g 2>/dev/null)/@softspark/ai-toolkit"
|
|
13
|
-
[ ! -d "$TOOLKIT_DIR" ] && TOOLKIT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
14
26
|
VERSION_MSG=$(python3 "$TOOLKIT_DIR/scripts/version_check.py" 2>/dev/null)
|
|
15
27
|
if [ -n "$VERSION_MSG" ]; then
|
|
16
28
|
echo "$VERSION_MSG"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# stop-search-check.sh — Block Stop when search-first rule was skipped.
|
|
3
|
+
#
|
|
4
|
+
# Fires on: Stop
|
|
5
|
+
# Matcher: all
|
|
6
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
|
|
7
|
+
#
|
|
8
|
+
# If user-prompt-submit.sh set the search-required flag and no search tool
|
|
9
|
+
# was invoked (search-tracker.sh would have cleared it), this hook continues
|
|
10
|
+
# the conversation with a JSON `decision: block` payload so Claude reads the
|
|
11
|
+
# reminder and calls smart_query() before the next response.
|
|
12
|
+
#
|
|
13
|
+
# Override (one-off): CLAUDE_SKIP_SEARCH_FIRST=1
|
|
14
|
+
|
|
15
|
+
# shellcheck source=_profile-check.sh
|
|
16
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
17
|
+
# shellcheck source=_hook-io.sh
|
|
18
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
19
|
+
# shellcheck source=_search-capability.sh
|
|
20
|
+
source "$(dirname "$0")/_search-capability.sh"
|
|
21
|
+
|
|
22
|
+
[ "${CLAUDE_SKIP_SEARCH_FIRST:-0}" = "1" ] && exit 0
|
|
23
|
+
|
|
24
|
+
FLAG="$HOME/.softspark/ai-toolkit/state/search-required.flag"
|
|
25
|
+
[ -f "$FLAG" ] || exit 0
|
|
26
|
+
|
|
27
|
+
if ! ai_toolkit_has_search_provider; then
|
|
28
|
+
rm -f "$FLAG" 2>/dev/null
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Read the original prompt (line 2 of the flag file) for the reminder.
|
|
33
|
+
PROMPT_PREVIEW=$(sed -n '2p' "$FLAG" 2>/dev/null | head -c 200)
|
|
34
|
+
rm -f "$FLAG" 2>/dev/null # one-shot; do not loop forever
|
|
35
|
+
|
|
36
|
+
hook_emit_block "Stop" "Step 0 violated: you responded to a technical prompt without calling smart_query() or hybrid_search_kb() first. Per global CLAUDE.md GOLDEN RULE: search KB BEFORE answering. Prompt was: \"${PROMPT_PREVIEW}...\". Now: call the search tool, then continue your reply. Override (one-off): CLAUDE_SKIP_SEARCH_FIRST=1."
|
|
37
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"match": "app/hooks/quality-gate.sh",
|
|
4
|
+
"tests": ["tests/test_hooks.bats"],
|
|
5
|
+
"runner": "bats"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"match": "app/hooks/quality-check.sh",
|
|
9
|
+
"tests": ["tests/test_hooks.bats"],
|
|
10
|
+
"runner": "bats"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"match": "app/hooks/revert-guard.sh",
|
|
14
|
+
"tests": ["tests/test_revert_guard_hook.bats"],
|
|
15
|
+
"runner": "bats"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"match": "app/hooks/test-cohesion.sh",
|
|
19
|
+
"tests": ["tests/test_test_cohesion_hook.bats"],
|
|
20
|
+
"runner": "bats"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"match": "app/hooks/config-desync-guard.sh",
|
|
24
|
+
"tests": ["tests/test_config_desync_guard.bats"],
|
|
25
|
+
"runner": "bats"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"match": "app/hooks/instructions-audit.sh",
|
|
29
|
+
"tests": ["tests/test_instructions_audit.bats"],
|
|
30
|
+
"runner": "bats"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"match": "app/hooks/user-prompt-submit.sh",
|
|
34
|
+
"tests": ["tests/test_hooks.bats"],
|
|
35
|
+
"runner": "bats"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"match": "app/hooks/*.sh",
|
|
39
|
+
"tests": ["tests/test_hooks.bats", "tests/test_hooks_per_editor.bats"],
|
|
40
|
+
"runner": "bats"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"match": "app/hooks.json",
|
|
44
|
+
"tests": ["tests/test_hooks.bats", "tests/test_merge_hooks_statusline.bats", "tests/test_hooks_per_editor.bats"],
|
|
45
|
+
"runner": "bats"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"match": "scripts/merge-hooks.py",
|
|
49
|
+
"tests": ["tests/test_merge_hooks_statusline.bats", "tests/test_inject_hook.bats"],
|
|
50
|
+
"runner": "bats"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"match": "scripts/session_state.py",
|
|
54
|
+
"tests": ["tests/test_session_state.bats"],
|
|
55
|
+
"runner": "bats"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"match": "scripts/test_cohesion.py",
|
|
59
|
+
"tests": ["tests/test_test_cohesion_resolver.bats"],
|
|
60
|
+
"runner": "bats"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"match": "scripts/inject_hook_cli.py",
|
|
64
|
+
"tests": ["tests/test_inject_hook.bats"],
|
|
65
|
+
"runner": "bats"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"match": "scripts/inject_rule_cli.py",
|
|
69
|
+
"tests": ["tests/test_inject.bats"],
|
|
70
|
+
"runner": "bats"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"match": "scripts/validate.py",
|
|
74
|
+
"tests": ["tests/test_validate.bats", "tests/test_validate_negative.bats"],
|
|
75
|
+
"runner": "bats"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-cohesion.sh — Run related tests after edits, block if they fail.
|
|
3
|
+
#
|
|
4
|
+
# Fires on: PostToolUse
|
|
5
|
+
# Matcher: Edit|MultiEdit|Write
|
|
6
|
+
# Skipped when TOOLKIT_HOOK_PROFILE=minimal or CLAUDE_HOOK_BOOTSTRAP=1.
|
|
7
|
+
#
|
|
8
|
+
# Constitution Art. VI.3 ("Tests and Docs Follow Behavior"): when source code
|
|
9
|
+
# changes, the matching tests must pass before the agent can continue. This
|
|
10
|
+
# hook resolves a per-project test-cohesion map and runs only the related
|
|
11
|
+
# test files (NOT the full suite).
|
|
12
|
+
#
|
|
13
|
+
# Map lookup (first found wins):
|
|
14
|
+
# 1. $PWD/.claude/test-cohesion-map.json (project-local)
|
|
15
|
+
# 2. $TOOLKIT_DIR/app/hooks/test-cohesion-map.json (toolkit default)
|
|
16
|
+
#
|
|
17
|
+
# Escape hatches:
|
|
18
|
+
# CLAUDE_HOOK_BOOTSTRAP=1 bypass (used when editing the hook itself)
|
|
19
|
+
# CLAUDE_SKIP_COHESION=1 one-off bypass
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 no map / no match / tests passed
|
|
23
|
+
# 2 related test command failed
|
|
24
|
+
|
|
25
|
+
# shellcheck source=_profile-check.sh
|
|
26
|
+
source "$(dirname "$0")/_profile-check.sh"
|
|
27
|
+
# shellcheck source=_locate-toolkit.sh
|
|
28
|
+
source "$(dirname "$0")/_locate-toolkit.sh"
|
|
29
|
+
# shellcheck source=_hook-io.sh
|
|
30
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
31
|
+
|
|
32
|
+
[ "${CLAUDE_HOOK_BOOTSTRAP:-0}" = "1" ] && exit 0
|
|
33
|
+
[ "${CLAUDE_SKIP_COHESION:-0}" = "1" ] && exit 0
|
|
34
|
+
|
|
35
|
+
INPUT=$(cat)
|
|
36
|
+
FILE_PATH=$(hook_file_path)
|
|
37
|
+
|
|
38
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
39
|
+
[ -z "$TOOLKIT_DIR" ] && exit 0
|
|
40
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
41
|
+
|
|
42
|
+
REPO_ROOT="$PWD"
|
|
43
|
+
# Prefer git repo root when available; fall back to PWD.
|
|
44
|
+
if command -v git >/dev/null 2>&1; then
|
|
45
|
+
git_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
46
|
+
[ -n "$git_root" ] && REPO_ROOT="$git_root"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Resolve symlinks on both sides so the cohesion-map glob can match files
|
|
50
|
+
# under symlinked roots (e.g. /tmp -> /private/tmp on macOS).
|
|
51
|
+
REAL_FILE=$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$FILE_PATH" 2>/dev/null)
|
|
52
|
+
REAL_REPO=$(python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$REPO_ROOT" 2>/dev/null)
|
|
53
|
+
[ -z "$REAL_FILE" ] && REAL_FILE="$FILE_PATH"
|
|
54
|
+
[ -z "$REAL_REPO" ] && REAL_REPO="$REPO_ROOT"
|
|
55
|
+
|
|
56
|
+
COMMANDS=$(python3 "$TOOLKIT_DIR/scripts/test_cohesion.py" resolve \
|
|
57
|
+
--changed-paths "$REAL_FILE" --repo-root "$REAL_REPO" 2>/dev/null)
|
|
58
|
+
[ -z "$COMMANDS" ] && exit 0
|
|
59
|
+
|
|
60
|
+
# Run each resolved command. Block on first failure.
|
|
61
|
+
LOG_DIR="$HOME/.softspark/ai-toolkit/state"
|
|
62
|
+
mkdir -p "$LOG_DIR" 2>/dev/null
|
|
63
|
+
LOG_FILE="$LOG_DIR/test-cohesion-last.log"
|
|
64
|
+
|
|
65
|
+
cd "$REAL_REPO" || exit 0
|
|
66
|
+
FAILED=""
|
|
67
|
+
while IFS= read -r cmd; do
|
|
68
|
+
[ -z "$cmd" ] && continue
|
|
69
|
+
if ! bash -c "$cmd" >"$LOG_FILE" 2>&1; then
|
|
70
|
+
FAILED="$cmd"
|
|
71
|
+
break
|
|
72
|
+
fi
|
|
73
|
+
done <<<"$COMMANDS"
|
|
74
|
+
|
|
75
|
+
if [ -n "$FAILED" ]; then
|
|
76
|
+
cat >&2 <<EOF
|
|
77
|
+
test-cohesion: BLOCKED — tests for ${FILE_PATH} failed.
|
|
78
|
+
Constitution Art. VI.3: tests must pass before continuing.
|
|
79
|
+
|
|
80
|
+
Command:
|
|
81
|
+
$FAILED
|
|
82
|
+
|
|
83
|
+
Last log (truncated):
|
|
84
|
+
$(tail -20 "$LOG_FILE")
|
|
85
|
+
|
|
86
|
+
Fix the failing test OR update it to match the new behavior (Art. VI.3).
|
|
87
|
+
Override (one-off): CLAUDE_SKIP_COHESION=1
|
|
88
|
+
EOF
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
[ "${AI_TOOLKIT_HOOK_FORMAT:-}" = "json" ] || echo "test-cohesion: related tests passed for ${FILE_PATH}"
|
|
93
|
+
exit 0
|
package/app/hooks/track-usage.sh
CHANGED
|
@@ -11,7 +11,9 @@ STATS_FILE="${HOME}/.softspark/ai-toolkit/stats.json"
|
|
|
11
11
|
|
|
12
12
|
# Read prompt from stdin (Claude Code passes JSON with .prompt field)
|
|
13
13
|
INPUT=$(cat)
|
|
14
|
-
|
|
14
|
+
# shellcheck source=_hook-io.sh
|
|
15
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
16
|
+
PROMPT_TEXT=$(hook_prompt)
|
|
15
17
|
[ -z "$PROMPT_TEXT" ] && exit 0
|
|
16
18
|
|
|
17
19
|
# Only track if prompt starts with a slash command
|
|
@@ -7,21 +7,48 @@
|
|
|
7
7
|
|
|
8
8
|
# shellcheck source=_profile-check.sh
|
|
9
9
|
source "$(dirname "$0")/_profile-check.sh"
|
|
10
|
+
# shellcheck source=_hook-io.sh
|
|
11
|
+
source "$(dirname "$0")/_hook-io.sh"
|
|
12
|
+
# shellcheck source=_search-capability.sh
|
|
13
|
+
source "$(dirname "$0")/_search-capability.sh"
|
|
10
14
|
|
|
11
15
|
# Read prompt from stdin (Claude Code passes JSON with .prompt field)
|
|
12
16
|
INPUT=$(cat)
|
|
13
|
-
PROMPT_TEXT=$(
|
|
17
|
+
PROMPT_TEXT=$(hook_prompt)
|
|
14
18
|
LOWERED="$(printf '%s' "$PROMPT_TEXT" | tr '[:upper:]' '[:lower:]')"
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
# Heuristic search-required flag (paired with search-tracker.sh + Stop check).
|
|
21
|
+
# Set only when a search provider is detectable or strict mode is requested.
|
|
22
|
+
# Cleared by search-tracker.sh after smart_query / hybrid_search_kb / web runs.
|
|
23
|
+
PROMPT_LEN=${#PROMPT_TEXT}
|
|
24
|
+
STATE_DIR="$HOME/.softspark/ai-toolkit/state"
|
|
25
|
+
FLAG="$STATE_DIR/search-required.flag"
|
|
26
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
27
|
+
if [ "$PROMPT_LEN" -gt 30 ] && \
|
|
28
|
+
[ "${CLAUDE_SKIP_SEARCH_FIRST:-0}" != "1" ] && \
|
|
29
|
+
ai_toolkit_has_search_provider; then
|
|
30
|
+
printf '%s\n%s\n' "$(date -u +%s)" "$PROMPT_TEXT" > "$FLAG" 2>/dev/null
|
|
31
|
+
else
|
|
32
|
+
rm -f "$FLAG" 2>/dev/null
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if ai_toolkit_has_search_provider; then
|
|
36
|
+
CONTEXT_MSG="STOP. Execute Step 0 before responding: check your CLAUDE.md for search-first rules. If search-first rules exist and a search tool is available, call the required search tool NOW — before any other tool or text output."
|
|
37
|
+
else
|
|
38
|
+
CONTEXT_MSG="Search-first note: no ai-toolkit search provider was detected, so search-first enforcement is advisory only. Use available local evidence; do not call unavailable RAG/MCP tools."
|
|
39
|
+
fi
|
|
17
40
|
|
|
18
41
|
if printf '%s' "$LOWERED" | grep -Eq 'architecture|design|migration|deploy|rollback|refactor|plugin|workflow'; then
|
|
19
|
-
|
|
42
|
+
CONTEXT_MSG="$CONTEXT_MSG
|
|
43
|
+
UserPromptSubmit: task looks architectural or multi-step. Use plan mode, define success criteria, and validate before marking done."
|
|
20
44
|
elif printf '%s' "$LOWERED" | grep -Eq 'bug|error|fail|failing|incident|outage|debug'; then
|
|
21
|
-
|
|
45
|
+
CONTEXT_MSG="$CONTEXT_MSG
|
|
46
|
+
UserPromptSubmit: debugging request detected. Gather evidence first, then propose the smallest safe fix and targeted tests."
|
|
22
47
|
else
|
|
23
|
-
|
|
48
|
+
CONTEXT_MSG="$CONTEXT_MSG
|
|
49
|
+
UserPromptSubmit: apply KB-first research, keep changes minimal, and update tests/docs when behavior changes."
|
|
24
50
|
fi
|
|
25
51
|
|
|
26
|
-
|
|
52
|
+
hook_emit_context "$CONTEXT_MSG"
|
|
27
53
|
|
|
54
|
+
exit 0
|
package/app/hooks.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "
|
|
42
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/notify-waiting.sh\""
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -84,6 +84,16 @@
|
|
|
84
84
|
"command": "\"$HOME/.softspark/ai-toolkit/hooks/commit-quality.sh\""
|
|
85
85
|
}
|
|
86
86
|
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"_source": "ai-toolkit",
|
|
90
|
+
"matcher": "Bash",
|
|
91
|
+
"hooks": [
|
|
92
|
+
{
|
|
93
|
+
"type": "command",
|
|
94
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/revert-guard.sh\""
|
|
95
|
+
}
|
|
96
|
+
]
|
|
87
97
|
}
|
|
88
98
|
],
|
|
89
99
|
"UserPromptSubmit": [
|
|
@@ -128,6 +138,26 @@
|
|
|
128
138
|
"command": "\"$HOME/.softspark/ai-toolkit/hooks/governance-capture.sh\""
|
|
129
139
|
}
|
|
130
140
|
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"_source": "ai-toolkit",
|
|
144
|
+
"matcher": "Edit|MultiEdit|Write",
|
|
145
|
+
"hooks": [
|
|
146
|
+
{
|
|
147
|
+
"type": "command",
|
|
148
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/test-cohesion.sh\""
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"_source": "ai-toolkit",
|
|
154
|
+
"matcher": "mcp__rag-mcp__smart_query|mcp__rag-mcp__hybrid_search_kb|mcp__rag-mcp__crag_search|mcp__rag-mcp__multi_hop_search|mcp__rag-mcp__verify_answer|WebSearch|WebFetch",
|
|
155
|
+
"hooks": [
|
|
156
|
+
{
|
|
157
|
+
"type": "command",
|
|
158
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/search-tracker.sh\""
|
|
159
|
+
}
|
|
160
|
+
]
|
|
131
161
|
}
|
|
132
162
|
],
|
|
133
163
|
"Stop": [
|
|
@@ -160,6 +190,16 @@
|
|
|
160
190
|
"command": "\"$HOME/.softspark/ai-toolkit/hooks/quality-gate.sh\""
|
|
161
191
|
}
|
|
162
192
|
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"_source": "ai-toolkit",
|
|
196
|
+
"matcher": "",
|
|
197
|
+
"hooks": [
|
|
198
|
+
{
|
|
199
|
+
"type": "command",
|
|
200
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/stop-search-check.sh\""
|
|
201
|
+
}
|
|
202
|
+
]
|
|
163
203
|
}
|
|
164
204
|
],
|
|
165
205
|
"TaskCompleted": [
|
|
@@ -243,6 +283,30 @@
|
|
|
243
283
|
}
|
|
244
284
|
]
|
|
245
285
|
}
|
|
286
|
+
],
|
|
287
|
+
"InstructionsLoaded": [
|
|
288
|
+
{
|
|
289
|
+
"_source": "ai-toolkit",
|
|
290
|
+
"matcher": "",
|
|
291
|
+
"hooks": [
|
|
292
|
+
{
|
|
293
|
+
"type": "command",
|
|
294
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/instructions-audit.sh\""
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"ConfigChange": [
|
|
300
|
+
{
|
|
301
|
+
"_source": "ai-toolkit",
|
|
302
|
+
"matcher": "user_settings",
|
|
303
|
+
"hooks": [
|
|
304
|
+
{
|
|
305
|
+
"type": "command",
|
|
306
|
+
"command": "\"$HOME/.softspark/ai-toolkit/hooks/config-desync-guard.sh\""
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
}
|
|
246
310
|
]
|
|
247
311
|
},
|
|
248
312
|
"statusLine": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"last_run": "2026-05-
|
|
2
|
+
"last_run": "2026-05-12T19:04:22Z",
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"tools": {
|
|
5
5
|
"aider": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"augment": {
|
|
27
|
-
"docs_hash": "
|
|
27
|
+
"docs_hash": "ceaf27b7126f44dd",
|
|
28
28
|
"headings": [
|
|
29
29
|
"Agent",
|
|
30
30
|
"Chat",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"claude-code": {
|
|
60
|
-
"docs_hash": "
|
|
60
|
+
"docs_hash": "6ea2ec6ef023c1bd",
|
|
61
61
|
"headings": [
|
|
62
62
|
"Claude Code overview",
|
|
63
63
|
"Documentation Index",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
"codex-cli": {
|
|
140
|
-
"docs_hash": "
|
|
140
|
+
"docs_hash": "c8fb328bf1954fc2",
|
|
141
141
|
"headings": [
|
|
142
142
|
"About",
|
|
143
143
|
"Contributing",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"version": "codex-cli 0.130.0"
|
|
190
190
|
},
|
|
191
191
|
"cursor": {
|
|
192
|
-
"docs_hash": "
|
|
192
|
+
"docs_hash": "787203a4eee076f7",
|
|
193
193
|
"headings": [],
|
|
194
194
|
"markers": {
|
|
195
195
|
".cursor/rules": false,
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
}
|
|
206
206
|
},
|
|
207
207
|
"gemini-cli": {
|
|
208
|
-
"docs_hash": "
|
|
208
|
+
"docs_hash": "26555623f610ff8e",
|
|
209
209
|
"headings": [
|
|
210
210
|
"Breadcrumbs",
|
|
211
211
|
"Directory actions",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
}
|
|
245
245
|
},
|
|
246
246
|
"github-copilot": {
|
|
247
|
-
"docs_hash": "
|
|
247
|
+
"docs_hash": "1566c05f84f5c1a7",
|
|
248
248
|
"headings": [
|
|
249
249
|
"About Copilot auto model selection",
|
|
250
250
|
"About Copilot integrations",
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
}
|
|
354
354
|
},
|
|
355
355
|
"windsurf": {
|
|
356
|
-
"docs_hash": "
|
|
356
|
+
"docs_hash": "7a8a7569f81eba1b",
|
|
357
357
|
"headings": [
|
|
358
358
|
"Advanced",
|
|
359
359
|
"App Deploys",
|
package/bin/ai-toolkit.js
CHANGED
|
@@ -69,6 +69,8 @@ const COMMANDS = {
|
|
|
69
69
|
'remove-rule': 'Unregister a rule from ~/.softspark/ai-toolkit/rules/ and remove its block from CLAUDE.md',
|
|
70
70
|
'inject-hook': 'Inject external hooks (file or URL) into ~/.claude/settings.json (URL hooks auto-refresh on update)',
|
|
71
71
|
'remove-hook': 'Remove injected hooks by source name from ~/.claude/settings.json (also unregisters URL source)',
|
|
72
|
+
'inject-mcp': 'Inject external MCP template (file or URL) into ~/.mcp.json + all editor MCP configs (URL templates auto-refresh on update)',
|
|
73
|
+
'remove-mcp': 'Remove injected MCP servers by source name from ~/.mcp.json and all editor configs',
|
|
72
74
|
validate: 'Verify toolkit integrity',
|
|
73
75
|
doctor: 'Check install health, hooks, and artifact drift',
|
|
74
76
|
eject: 'Export standalone config (no symlinks, no toolkit dependency)',
|
|
@@ -257,6 +259,14 @@ function showHelp() {
|
|
|
257
259
|
console.log('\nOptions for remove-hook:');
|
|
258
260
|
console.log(' <source-name> Source tag to remove (also unregisters URL source if present)');
|
|
259
261
|
console.log(' [target-dir] Target dir containing .claude/settings.json (default: $HOME)');
|
|
262
|
+
console.log('\nOptions for inject-mcp:');
|
|
263
|
+
console.log(' <template-file-or-url> Path to JSON file or HTTPS URL with {"mcpServers": {...}}');
|
|
264
|
+
console.log(' [target-dir] Target dir for .mcp.json + editor configs (default: $HOME)');
|
|
265
|
+
console.log(' --name <name> Override source name (default: filename/URL stem)');
|
|
266
|
+
console.log(' --force Overwrite servers tagged with a different _source');
|
|
267
|
+
console.log('\nOptions for remove-mcp:');
|
|
268
|
+
console.log(' <source-name> Source tag to remove (also unregisters URL source and cleans editor configs)');
|
|
269
|
+
console.log(' [target-dir] Target dir containing .mcp.json (default: $HOME)');
|
|
260
270
|
console.log('\nOptions for add-rule:');
|
|
261
271
|
console.log(' <rule-file> Path to .md rule file or HTTPS URL to register globally');
|
|
262
272
|
console.log(' [rule-name] Override rule name (default: filename/URL stem without .md)');
|
|
@@ -417,6 +427,37 @@ function handleRemoveHook(args) {
|
|
|
417
427
|
run(scriptPath('inject_hook_cli.py'), ['--remove', sourceName, targetDir]);
|
|
418
428
|
}
|
|
419
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Handle `ai-toolkit inject-mcp` -- injects external MCP template (file or URL) into .mcp.json
|
|
432
|
+
* and propagates to all editor MCP configs.
|
|
433
|
+
* @param {string[]} args
|
|
434
|
+
*/
|
|
435
|
+
function handleInjectMcp(args) {
|
|
436
|
+
const source = args[0];
|
|
437
|
+
if (!source) {
|
|
438
|
+
console.error('Usage: ai-toolkit inject-mcp <template-file-or-url> [target-dir] [--name <name>] [--force]');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const isUrl = source.startsWith('https://') || source.startsWith('http://');
|
|
442
|
+
const resolvedSource = isUrl ? source : path.resolve(CWD, source);
|
|
443
|
+
const remaining = args.slice(1);
|
|
444
|
+
run(scriptPath('inject_mcp_cli.py'), [resolvedSource, ...remaining]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handle `ai-toolkit remove-mcp` -- removes injected MCP servers by source name.
|
|
449
|
+
* @param {string[]} args
|
|
450
|
+
*/
|
|
451
|
+
function handleRemoveMcp(args) {
|
|
452
|
+
const sourceName = args[0];
|
|
453
|
+
if (!sourceName) {
|
|
454
|
+
console.error('Usage: ai-toolkit remove-mcp <template-source-name> [target-dir]');
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
const targetDir = args[1] || process.env.HOME;
|
|
458
|
+
run(scriptPath('inject_mcp_cli.py'), ['--remove', sourceName, targetDir]);
|
|
459
|
+
}
|
|
460
|
+
|
|
420
461
|
/**
|
|
421
462
|
* Handle `ai-toolkit mcp` -- delegates to mcp_manager.py with subcommand.
|
|
422
463
|
* @param {string[]} args
|
|
@@ -566,6 +607,8 @@ const SPECIAL_HANDLERS = {
|
|
|
566
607
|
'add-rule': handleAddRule,
|
|
567
608
|
'inject-hook': handleInjectHook,
|
|
568
609
|
'remove-hook': handleRemoveHook,
|
|
610
|
+
'inject-mcp': handleInjectMcp,
|
|
611
|
+
'remove-mcp': handleRemoveMcp,
|
|
569
612
|
'llms-txt': (_args) => generateLlmsTxt(),
|
|
570
613
|
'antigravity-rules': (_args) => run(scriptPath('generate_antigravity.py'), [CWD]),
|
|
571
614
|
'cursor-mdc': (_args) => run(scriptPath('generate_cursor_mdc.py'), [CWD]),
|
|
@@ -144,7 +144,7 @@ ai-toolkit doctor
|
|
|
144
144
|
- Environment: node, bash, python3, bats
|
|
145
145
|
- Global Install: .claude exists, agents/skills symlinks (0 broken), settings.json hooks
|
|
146
146
|
- Hook Scripts: all present and executable
|
|
147
|
-
- Hook Configuration:
|
|
147
|
+
- Hook Configuration: 14 events registered
|
|
148
148
|
- Generated Artifacts: AGENTS.md, llms.txt, llms-full.txt
|
|
149
149
|
- Planned Assets: plugin.json, benchmarks, plugin packs
|
|
150
150
|
- Benchmark Freshness: < 30 days
|
|
@@ -194,7 +194,7 @@ python3 scripts/audit_skills.py --ci
|
|
|
194
194
|
|
|
195
195
|
**Verify validate.py:**
|
|
196
196
|
- [ ] Agents >= 44, Skills >= 99, Tests >= 900
|
|
197
|
-
- [ ] Hook events:
|
|
197
|
+
- [ ] Hook events: 14, Hook scripts: >= 30
|
|
198
198
|
- [ ] Plugin packs >= 10, KB documents >= 20
|
|
199
199
|
- [ ] `Errors: 0 | Warnings: 0` → `VALIDATION PASSED`
|
|
200
200
|
|
|
@@ -264,7 +264,7 @@ Agents (code-reviewer, debugger, devops-implementer, ...)
|
|
|
264
264
|
|
|
265
265
|
## Quality Hooks
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
28 entries across 14 lifecycle events. See [hooks-catalog.md](hooks-catalog.md) for full details.
|
|
268
268
|
|
|
269
269
|
| Hook | Trigger | Script | Action |
|
|
270
270
|
|------|---------|--------|--------|
|