feat(infra): auto-commit con 29 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 02:06:44 +02:00
parent 47fac22230
commit ca1bf5a59b
29 changed files with 2148 additions and 11 deletions
+13 -4
View File
@@ -59,15 +59,24 @@ ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
insert_call() {
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
# Politica issue 0087: command_snippet solo se rellena cuando function_id
# esta vacio. Si la call golpea una funcion del registry, su ID y
# tool_used bastan; no duplicamos el comando.
if [ -n "$fn_id" ]; then snippet=""; fi
# Redact common secrets antes de persistir
snippet=$(printf '%s' "$snippet" \
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
| head -c 200)
fn_esc=$(sql_escape "$fn_id")
tu_esc=$(sql_escape "$tool_used")
ec_esc=$(sql_escape "$ERROR_CLASS")
es_esc=$(sql_escape "$ERROR_SNIPPET")
sid_esc=$(sql_escape "$SESSION_ID")
ah_esc=$(sql_escape "$ARGS_HASH")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc',$TS);" 2>/dev/null || true
snip_esc=$(sql_escape "$snippet")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
}
insert_code_write() {
@@ -204,7 +213,7 @@ case "$TOOL_NAME" in
TOOL_USED="sqlite_direct"
fi
insert_call "$FN_ID" "$TOOL_USED"
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
# ---- Violation rules ----
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES)
# del registry como additionalContext en cada turno del usuario.
#
# Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h).
# Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo.
#
# NUNCA bloquea: si algo falla, emite contexto vacio y sale 0.
set -uo pipefail
CACHE_DIR="${HOME}/.cache/fn_registry"
CACHE_FILE="${CACHE_DIR}/capabilities.txt"
TTL_SECONDS=3600
# Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then
printf '%s' "${CLAUDE_PROJECT_DIR}"
return 0
fi
return 1
}
# Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean
cat >/dev/null 2>&1 || true
ROOT=$(resolve_root) || exit 0
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
# Cache freshness check
need_refresh=1
if [ -f "$CACHE_FILE" ]; then
now=$(date +%s)
mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
age=$((now - mtime))
if [ "$age" -lt "$TTL_SECONDS" ]; then
need_refresh=0
fi
fi
if [ "$need_refresh" -eq 1 ]; then
# Regenerate: call fn doctor capabilities --emit-claude-md and process
raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true)
if [ -z "$raw" ]; then
exit 0
fi
# Extract top 5 from each section using awk.
# Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines".
line=$(printf '%s\n' "$raw" | awk '
BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; }
/^## .*Top 20/ { sec="TOP"; next }
/^## .*Fresh/ { sec="FRESH"; next }
/^## .*Pipelines/ { sec="PIPE"; next }
/^## / { sec=""; next }
/^- `/ {
# extract first backticked token
s = $0
sub(/^- `/, "", s)
i = index(s, "`")
if (i == 0) next
id = substr(s, 1, i-1)
if (sec == "TOP" && n_top < 5) { tops[n_top++] = id }
if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id }
if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id }
}
END {
out = "CAPABILITIES (cache 1h):"
if (n_top > 0) {
line = " TOP: " tops[0]
for (i=1; i<n_top; i++) line = line ", " tops[i]
out = out "\n" line
}
if (n_fresh > 0) {
line = " FRESH (7d): " fresh[0]
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
out = out "\n" line
}
if (n_pipe > 0) {
line = " PIPELINES: " pipes[0]
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
out = out "\n" line
}
print out
}
')
if [ -z "$line" ]; then
exit 0
fi
printf '%s\n' "$line" >"$CACHE_FILE" 2>/dev/null || exit 0
fi
# Emit cached content as additionalContext
if [ ! -s "$CACHE_FILE" ]; then
exit 0
fi
ctx=$(cat "$CACHE_FILE")
if command -v jq >/dev/null 2>&1; then
jq -n --arg ctx "$ctx" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
else
# Fallback: print raw text (Claude Code prints stdout as context too)
printf '%s\n' "$ctx"
fi
exit 0
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# PreToolUse hook: sugiere funciones del registry cuando un comando Bash
# inline probablemente reinventa una funcion existente (issue 0087).
#
# Llama a `./fn match "<cmd>"` con timeout 200ms. Si encaja con alta
# confianza, imprime un <system-reminder> a stderr para que Claude Code
# lo lea como recordatorio. NUNCA bloquea la tool — exit 0 siempre.
set -euo pipefail
# ---- Always exit 0, no matter what ----
trap 'exit 0' ERR
# ---- Resolve registry root (walks up from cwd) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
FN_BIN="$ROOT/fn"
[ -x "$FN_BIN" ] || exit 0
# ---- Read stdin JSON ----
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
[ "$TOOL_NAME" = "Bash" ] || exit 0
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
[ -z "$CMD" ] && exit 0
# Single-line for matching against denylist patterns
CMD_FLAT=$(printf '%s' "$CMD" | tr '\n' ' ')
# ---- Denylist (skip antes de llamar fn match para ahorrar el invoke) ----
# Comandos demasiado cortos -> trivial
CMD_LEN=${#CMD_FLAT}
[ "$CMD_LEN" -lt 20 ] && exit 0
# Trivial single-utility commands
case "$CMD_FLAT" in
"ls"|"ls "*|"cd"|"cd "*|"pwd"|"pwd "*|"cat"|"cat "*|"echo"|"echo "*)
exit 0 ;;
"grep"|"grep "*|"head"|"head "*|"tail"|"tail "*|"wc"|"wc "*)
exit 0 ;;
"mkdir"|"mkdir "*|"rm"|"rm "*|"mv"|"mv "*|"cp"|"cp "*)
exit 0 ;;
"git"|"git "*)
exit 0 ;;
"go"|"go "*)
# go build / go test corrientes — el agente ya los maneja
exit 0 ;;
esac
# Comandos que ya usan el registry: ./fn ..., fn run ..., mcp__registry__*
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])\./fn([[:space:]]|$)'; then
exit 0
fi
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])fn[[:space:]]+(run|search|show|code|uses|doctor|index|match|list|add|proposal|sync|ops|check)'; then
exit 0
fi
# Pure-cd (movement only, no logic)
if printf '%s' "$CMD_FLAT" | grep -qE '^[[:space:]]*cd[[:space:]]+[^&|;]+$'; then
exit 0
fi
# ---- Llamar fn match con timeout 200ms ----
command -v timeout >/dev/null 2>&1 || exit 0
# Truncar el comando a algo razonable para fn match (evitar args huge)
CMD_TRUNC=$(printf '%s' "$CMD_FLAT" | head -c 500)
MATCH_JSON=$(timeout 0.2 "$FN_BIN" match "$CMD_TRUNC" --format json --top 3 2>/dev/null) || exit 0
[ -z "$MATCH_JSON" ] && exit 0
# ---- Parsear JSON ----
HIGH_CONF=$(printf '%s' "$MATCH_JSON" | jq -r '.high_confidence // false' 2>/dev/null || echo "false")
TOP_ID=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].id // ""' 2>/dev/null || echo "")
TOP_SCORE=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].score // 0' 2>/dev/null || echo "0")
TOP_SIG=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].signature // ""' 2>/dev/null || echo "")
TOP_SNIP=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].snippet // ""' 2>/dev/null || echo "")
[ -z "$TOP_ID" ] && exit 0
# Trigger condition: (high_confidence==true OR score>=0.85) AND score>=0.6
# - high_confidence requires top1/top2 gap > 1.5 (set por fn match)
# - score>=0.85 cubre matches muy fuertes donde el gap es modesto
SCORE_HI=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.85) ? "1" : "0" }')
SCORE_MIN=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.6) ? "1" : "0" }')
[ "$SCORE_MIN" = "1" ] || exit 0
if [ "$HIGH_CONF" != "true" ] && [ "$SCORE_HI" != "1" ]; then
exit 0
fi
# Truncar snippet a 100 chars y limpiar saltos de linea
SNIP_SHORT=$(printf '%s' "$TOP_SNIP" | tr '\n' ' ' | head -c 100)
# Formatear score con 2 decimales
SCORE_FMT=$(awk -v s="$TOP_SCORE" 'BEGIN{ printf "%.2f", s+0 }')
# ---- Emitir <system-reminder> a stderr ----
cat >&2 <<EOF
<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function.
USE: ./fn run $TOP_ID -> $TOP_SIG
SNIPPET: $SNIP_SHORT
Confidence: $SCORE_FMT. If you proceed inline, the violation will be logged.
</system-reminder>
EOF
exit 0
# Test manual:
# echo '{"tool_name":"Bash","tool_input":{"command":"taskkill.exe /IM registry_dashboard.exe /F"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
#
# Casos silenciosos:
# echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
# echo '{"tool_name":"Bash","tool_input":{"command":"./fn run filter_slice_go_core 1 2 3"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh