118d5d36d3
El orquestador no se enteraba de los cambios de estado de su flota: el drenado
era manual y el peek documentado `./fn run drain_fleet_events --advance false`
devolvia un falso `{total_new:0, cursor:0}` porque `fn run` mapea los argumentos
posicionalmente y no parsea flags `--nombre valor` (events_path acababa valiendo
"--advance", una ruta inexistente).
- drain_fleet_events: nuevo helper _normalize_fn_run_flags que renormaliza el
patron `--advance <bool>` aplanado por `fn run`, de modo que el peek funciona
directo desde la CLI sin tocar el runner de Go. Bump 1.1.0 + growth log + tests
del normalizador (unit y end-to-end por HOME).
- summarize_fleet_transitions (nueva, pure, grupo claude-fleet): resume el dict
by_classification de drain en un bloque de una linea con las tres categorias
accionables (terminados / reclaman / estancados), dedup por session_id y
truncado de objetivo.
- hook_fleet_state_inject.sh (UserPromptSubmit): si la sesion es role=orchestrator
(leido de ~/.claude/goals/<session_id>.json), hace peek de la cola sin mover el
cursor y emite el bloque FLEET-STATE cada turno. Degrada limpio si el watcher
esta caido, la cola no existe o la sesion no es orquestador.
El registro del hook va en .claude/settings.local.json (gitignored, fuera de este
commit). Pendiente, lo integra otro agente: documentar el bloque FLEET-STATE en
.claude/commands/orquestador.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.9 KiB
Bash
Executable File
70 lines
2.9 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
|
|
|
|
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
|