#!/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/.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}" 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