Files
fn_registry/.claude/scripts/hook_fleet_state_inject.sh
T
agent 03fc0461fa feat(hook): inyectar CONTEXTO FLEET con socket/session al orquestador
hook_fleet_state_inject.sh ahora, ademas de MODO ORQUESTADOR, llama a
detect_fleet_context (por $TMUX) e inyecta una linea CONTEXTO FLEET con
socket/session + recordatorio de usar spawn_fleet_agent (nunca kitty) cuando
in_fleet=true. No depende del venv (solo bash+tmux) y se emite antes del bloque
FLEET-STATE. Degrada limpio: si el detector falta o $TMUX esta vacia, no emite
la linea y el turno sigue intacto.
2026-06-21 21:50:32 +02:00

96 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
# Hook UserPromptSubmit: inyecta el estado de la flota al Claude orquestador.
#
# En el modo /orquestador, el Claude principal gestiona una flota de agentes y
# necesita enterarse de forma reactiva cuando uno cambia de estado: termina
# (DICE_TERMINADO), reclama una decision (RECLAMA) o se estanca (ESTANCADO).
# El watcher de fleetview escribe esas transiciones a la cola JSONL
# ~/.claude/fleet/events.jsonl. Este hook hace un peek de esa cola en cada turno
# y emite un bloque "FLEET-STATE:" para que el orquestador vea los cambios
# pendientes sin tener que drenar la cola a mano.
#
# Entrada (stdin JSON del hook UserPromptSubmit): { session_id, cwd, ... }
# El stdout de este script se inyecta como additionalContext en el turno.
#
# Solo el orquestador recibe el feed: se identifica leyendo el campo `role` de
# ~/.claude/goals/<session_id>.json (lo marca `mark_claude_role`). Cualquier
# sesion que no sea role=orchestrator termina en silencio (sin stdout).
#
# El peek usa advance=False: NO mueve el cursor de la cola. El orquestador sigue
# viendo los mismos eventos pendientes cada turno hasta que los consume
# explicitamente con `./fn run drain_fleet_events` (que si avanza el cursor).
#
# Degradacion limpia: si falta jq/python/venv, si la cola no existe, o si el
# watcher esta caido, el hook nunca rompe el turno (siempre exit 0).
set -u
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null)
[ -z "$SESSION_ID" ] && exit 0
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
ROLE=""
[ -f "$GOAL_FILE" ] && ROLE=$(jq -r '.role // ""' "$GOAL_FILE" 2>/dev/null)
# Solo el orquestador recibe el feed de la flota. Resto: silencio total.
[ "$ROLE" != "orchestrator" ] && exit 0
# Reanclar el rol en cada turno: el modo /orquestador no debe depender solo de
# que su prompt (.claude/commands/orquestador.md) siga en contexto. Este
# recordatorio se reinyecta aunque el watcher este caido o falte el venv (la
# guarda de abajo saldria con exit 0 sin emitir FLEET-STATE). Se emite SOLO para
# role=orchestrator: las sesiones sin goal.json o sin ese rol ya salieron arriba
# con exit 0 y stdout vacio, asi que el path limpio queda intacto.
printf '%s\n' "MODO ORQUESTADOR activo (role=orchestrator)."
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/fn_registry}"
# Contexto de flota: recordarle al orquestador en que socket/sesion tmux vive,
# para que lance ejecutores con spawn_fleet_agent (auto-detecta el socket) y
# NUNCA caiga a kitty estando dentro de la flota. La deteccion va por $TMUX
# (senal fiable), no por $FLEET_SOCKET (a veces vacia en un claude resumido/
# relanzado). No necesita venv ni python: solo bash + tmux. Degrada limpio: si
# el detector falta o falla, simplemente no se emite la linea (turno intacto).
DETECTOR="$PROJECT_DIR/bash/functions/infra/detect_fleet_context.sh"
if [ -f "$DETECTOR" ]; then
CTX=$(bash "$DETECTOR" 2>/dev/null || true)
IN_FLEET=$(printf '%s' "$CTX" | sed -n 's/.*"in_fleet":\(true\|false\).*/\1/p')
F_SOCKET=$(printf '%s' "$CTX" | sed -n 's/.*"socket":"\([^"]*\)".*/\1/p')
F_SESSION=$(printf '%s' "$CTX" | sed -n 's/.*"session":"\([^"]*\)".*/\1/p')
if [ "$IN_FLEET" = "true" ]; then
printf 'CONTEXTO FLEET: estas dentro de la fleet tmux socket=%s session=%s. Lanza ejecutores con spawn_fleet_agent (auto-detecta el socket) — NUNCA kitty/launch_claude_agent_kitty estando aqui.\n' "$F_SOCKET" "$F_SESSION"
fi
fi
PY="$PROJECT_DIR/python/.venv/bin/python3"
{ [ -x "$PY" ] && [ -d "$PROJECT_DIR/python/functions" ]; } || exit 0
OUT=$(FN_PROJECT_DIR="$PROJECT_DIR" timeout 8 "$PY" - <<'PYEOF' 2>/dev/null
import os
import sys
root = os.environ.get("FN_PROJECT_DIR", os.path.expanduser("~/fn_registry"))
sys.path.insert(0, os.path.join(root, "python", "functions"))
events = os.path.join(os.path.expanduser("~"), ".claude", "fleet", "events.jsonl")
try:
from infra.drain_fleet_events import drain_fleet_events
from infra.summarize_fleet_transitions import summarize_fleet_transitions
if not os.path.exists(events):
# Watcher nunca arranco o cola borrada: diagnostico explicito.
print("FLEET-STATE: cola del watcher no disponible (events.jsonl ausente)")
else:
drained = drain_fleet_events(advance=False) # peek: NO mueve el cursor
print(summarize_fleet_transitions(drained.get("by_classification", {})))
except Exception:
# Funciones no indexadas, cola corrupta, etc.: degradar sin romper el turno.
pass
PYEOF
)
[ -n "$OUT" ] && printf '%s\n' "$OUT"
exit 0