diff --git a/.claude/helpers/foxhunt-audit-router.sh b/.claude/helpers/foxhunt-audit-router.sh new file mode 100755 index 000000000..8ce848878 --- /dev/null +++ b/.claude/helpers/foxhunt-audit-router.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# foxhunt-audit-router.sh +# +# PostToolUse hook router. Reads Claude Code tool-input JSON from stdin, +# inspects the file path being edited, and emits at most one one-line hint +# to stdout suggesting which foxhunt-* auditor agents to dispatch. +# +# Contract: +# - Completes in <200 ms. +# - Never spawns agents itself. +# - Always exits 0 (never blocks). +# - One suggestion per (path, agent) per session, tracked in +# .claude/.foxhunt-audit-state (gitignored, cleared at SessionStart). + +set -u # not -e; we want to exit 0 even on errors + +STATE_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/.foxhunt-audit-state" +mkdir -p "$(dirname "$STATE_FILE")" 2>/dev/null || exit 0 +touch "$STATE_FILE" 2>/dev/null || exit 0 + +# Read tool input from stdin; bail if not JSON or no file_path +INPUT="$(cat 2>/dev/null || true)" +[ -z "$INPUT" ] && exit 0 + +# Extract file path. Claude Code sends keys like "tool_input": {"file_path": "..."} +# Be lenient: try multiple JSON shapes via grep/sed (avoid jq dep). +PATH_FOUND="$(printf '%s' "$INPUT" | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"file_path"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')" +[ -z "$PATH_FOUND" ] && exit 0 + +# Make path repo-relative if absolute +REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" +case "$PATH_FOUND" in + "$REPO_ROOT"/*) REL="${PATH_FOUND#$REPO_ROOT/}" ;; + /*) REL="$PATH_FOUND" ;; + *) REL="$PATH_FOUND" ;; +esac + +# Routing table: build SUGGEST list of agent names +SUGGEST=() +case "$REL" in + *.cu) SUGGEST+=("gpu-contract-auditor") ;; + *cuda/*.rs|*/cuda/*) SUGGEST+=("gpu-contract-auditor") ;; + crates/*/build.rs) SUGGEST+=("gpu-contract-auditor") ;; +esac +case "$REL" in + */experience_kernels.cu|*reward*.rs|*reward*.cu|*controller*.rs|*trader_*.rs) + SUGGEST+=("reward-controller-auditor") ;; +esac +case "$REL" in + *sp*_isv_slots.rs) SUGGEST+=("isv-discipline-auditor") ;; + crates/ml-*/*.rs|crates/ml-*/**/*.rs) SUGGEST+=("isv-discipline-auditor") ;; +esac +case "$REL" in + docs/superpowers/specs/*.md|docs/plans/*.md) + SUGGEST+=("sp-critical-reviewer") ;; +esac +case "$REL" in + MEMORY.md|*memory/pearl_*.md|*memory/feedback_*.md) + SUGGEST+=("memory-curator") ;; +esac +# Default for any rust file in crates/ services/ bin/ that didn't match above +case "$REL" in + crates/*.rs|crates/**/*.rs|services/*.rs|services/**/*.rs|bin/*.rs|bin/**/*.rs) + # Add code-hygiene unconditionally for Rust source + SUGGEST+=("code-hygiene-auditor") ;; +esac + +[ "${#SUGGEST[@]}" -eq 0 ] && exit 0 + +# Dedupe SUGGEST and filter against state file (one suggestion per (path, agent) per session) +EMIT=() +for AGENT in "${SUGGEST[@]}"; do + KEY="$REL|$AGENT" + if ! grep -Fxq "$KEY" "$STATE_FILE" 2>/dev/null; then + echo "$KEY" >> "$STATE_FILE" + # Dedupe within this invocation too + case " ${EMIT[*]:-} " in *" $AGENT "*) ;; *) EMIT+=("$AGENT") ;; esac + fi +done + +[ "${#EMIT[@]}" -eq 0 ] && exit 0 + +# Emit single-line hint, prefixed for grep +JOINED="$(printf '%s, ' "${EMIT[@]}")"; JOINED="${JOINED%, }" +echo "[foxhunt-audit] $REL — suggest: $JOINED" +exit 0 diff --git a/.claude/helpers/foxhunt-audit-session-start.sh b/.claude/helpers/foxhunt-audit-session-start.sh new file mode 100755 index 000000000..3f13ad6a9 --- /dev/null +++ b/.claude/helpers/foxhunt-audit-session-start.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Clear foxhunt-audit dedup state at session start. +STATE_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/.foxhunt-audit-state" +: > "$STATE_FILE" 2>/dev/null || true +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 99423beb4..0ac0e1de8 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,6 +21,15 @@ "timeout": 15 } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/helpers/foxhunt-audit-session-start.sh\"", + "timeout": 5 + } + ] } ], "SessionEnd": [ @@ -33,6 +42,18 @@ } ] } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/helpers/foxhunt-audit-router.sh\"", + "timeout": 2 + } + ] + } ] }, "permissions": { diff --git a/.gitignore b/.gitignore index cb44aa089..22c61a293 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,6 @@ services/*/load_tests/*.json !services/*/load_tests/package.json .playwright-mcp/ crates/ml/ml/ + +# Foxhunt audit hook dedup state (cleared at SessionStart) +.claude/.foxhunt-audit-state