orquestador.md + orchestration.md: la deteccion de 'estoy en una flota' se hace por $TMUX (via detect_fleet_context), NO por $FLEET_SOCKET (fragil). kitty es fallback SOLO cuando in_tmux=false. spawn_fleet_agent auto-detecta el socket (ya no hace falta pasar --socket/--session). Documenta la linea CONTEXTO FLEET del hook y anade detect_fleet_context al catalogo del grupo orchestration.
23 KiB
Maquinaria del modo orquestador: vigilancia reactiva de la flota
Esta regla recoge la maquinaria estable del modo /orquestador (.claude/commands/orquestador.md):
cómo se sigue la flota, cómo se consume la cola del watcher, cómo se clasifica cada agente y qué
política se aplica a cada clasificación, el verificador adversarial de cierres, el auto-kill, el
nudge, el splitter, la cadencia, y el catálogo de funciones del registry del grupo orchestration.
El comando /orquestador se queda con la doctrina y el flujo de cada turno; el detalle operativo
vive aquí para que el prompt del comando sea corto y la maquinaria no se diluya. El cerebro reactivo
de esta regla corresponde al flow 0012.
Seguir la flota — listado y tiempo
La fuente de verdad del mapeo PID→sessionId→cwd son los archivos ~/.claude/sessions/<PID>.json
(memoria claude-session-pid-mapping). Para listar la flota de Claudes vivos:
./fn run list_claude_agents # tabla: PID, STATUS, ETIME, KITTY, SELF, SESSION_ID, CWD
./fn run list_claude_agents --json # para parsear y decidir
list_claude_agents_bash_infra([--json] [--exclude-current])— cruzapgrep -x claudecon lossessions/<PID>.json(con validación anti-PID-reciclado), marca tu propia sesión comoSELF, y reporta cwd + sessionId de cada secundario (para retomar conclaude --resume <sessionId>).
Flota tipada (goal/phase/window/age) — usa el binario fleetview, NO fn run. La flota con
goal, phase, status, tmux_window y age/idle_seconds la da el CLI de la app fleetview:
apps/fleetview/fleetview list --json # flota tipada: session_id, goal, phase, status, tmux_window, age, idle_seconds
apps/fleetview/fleetview list # tabla legible (incluye columna AGE)
Nota: NO uses ./fn run list_claude_fleet — list_claude_fleet_go_infra es una función Go con
tests, así que fn run la despacha como go test (corre la suite, no imprime la flota). La vía
ejecutable es el binario apps/fleetview/fleetview (el atajo /fleet del humano envuelve este mismo
CLI). El JSON de fleetview list ya incluye role/dod_contract/dod_status (además de
tmux_window): el binario los serializa directamente ("" cuando el goal.json no los declara,
ver apps/fleetview/cli.go). El tool MCP fleet_list (ver abajo) además rellena los que el binario
deje vacíos leyéndolos del sidecar ~/.claude/goals/<session_id>.json, así que con el MCP nunca te
faltan. Ya no hace falta leer el sidecar a mano salvo que uses el binario crudo y el campo venga vacío.
Tiempo — usa el de ACTIVIDAD, no el del proceso. Para "cuánto lleva cada agente" usa la columna
AGE de fleetview list (o age/idle_seconds en --json): es el tiempo desde su última
actividad (proxy de cuánto lleva sin avanzar / en su estado), lo útil para detectar estancados. El
etime de list_claude_agents es la vida del proceso (cuánto lleva la terminal abierta, p.ej.
8h) — NO es el tiempo de la tarea; nunca lo reportes como progreso.
Vía preferida: tools MCP fleet_* (orchestrator_mcp)
El MCP orchestrator (registrado en .mcp.json como orchestrator, binario
apps/orchestrator_mcp/orchestrator_mcp) expone la maquinaria de la flota como 6 tools que
envuelven las mismas funciones del registry. En una sesión con orchestrator_mcp conectado,
prefiere los tools mcp__orchestrator__fleet_* sobre ./fn run: tienen permisos pre-aprobados,
devuelven salida estructurada y se registran en la telemetría como cualquier MCP (regla
registry_calls.md). El ./fn run (o el binario fleetview para el listado) sigue siendo el
fallback CLI cuando el MCP no está conectado. Mapa de cada operación de la flota a su tool:
| Operación de la flota | Tool MCP (preferido) | Fallback ./fn run / binario |
|---|---|---|
| Listar la flota tipada (session_id, goal, phase, status, role, dod_contract, dod_status, tmux_window, age, idle_seconds) | mcp__orchestrator__fleet_list |
apps/fleetview/fleetview list --json (NO ./fn run list_claude_fleet) |
| Drenar la cola de transiciones del watcher (agrupada por clasificación + urgentes) | mcp__orchestrator__fleet_drain (advance true consume, false hace peek) |
./fn run drain_fleet_events |
| Clasificar el estado de terminación de UN agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO) | mcp__orchestrator__fleet_classify |
(Go con tests; lo consume el watcher, no se invoca a mano) |
Escribir el DoD-contrato fijo (dod_contract/dod_status) en el goal.json de un agente |
mcp__orchestrator__fleet_set_dod |
./fn run set_dod_contract |
| Cerrar dirigido UN ejecutor (auto-kill: SIGTERM + kill-window, con guards) | mcp__orchestrator__fleet_kill (dry_run para ver el plan) |
./fn run kill_fleet_agent |
Lanzar un ejecutor como window de la flota tmux (con parent para el push) |
mcp__orchestrator__fleet_spawn |
./fn run spawn_fleet_agent |
Ventaja extra de fleet_list: expone role/dod_contract/dod_status directamente (y rellena los
vacíos desde el sidecar goal.json), así que la regla "No te vigiles a ti mismo" se resuelve sin leer
el sidecar a mano — filtra por el role que ya trae cada fila.
Mantén una tabla de seguimiento, una fila por secundario, y actualízala en cada turno:
| slug | título kitty | PID | cwd / dir aislado | rama | log | report | estado |
|---|---|---|---|---|---|---|---|
| docs | fn_registry · docs | 3637133 | /tmp/orq_docs_wt | orq/docs | /tmp/orq_docs_kitty.log | reports/00NN-…-docs.md | en curso |
Cuando un secundario parezca terminado, confirma: ¿pusheó la rama? ¿escribió el report? Lee el
report (reports/), revisa los commits de su rama (git -C <dir> log --oneline).
El cerebro reactivo: vigilar la salud por el DoD
Seguir la flota no es solo "¿quién vive?". Es vigilar la salud por el DoD: cada agente termina lo
que empieza, o sabes por qué no. La métrica es el throughput de DoD cumplidos, no el número de
agentes vivos — 30 agentes que no cierran nada no sirven. La fuente es la cola del watcher embebido
en fleetview (~/.claude/fleet/events.jsonl): una línea por transición de estado de un agente
(edge-triggered, sin ruido de nivel). El orquestador la drena cada vez que actúa y aplica una política
por clasificación.
DoD-contrato fijo al lanzar (regla dura)
Ningún secundario arranca sin DoD-contrato: el criterio de aceptación FIJO contra el que se evalúa
su terminación. Es distinto del campo dod del statusline (texto corto identificativo de la
terminal). Desde 2026-06-21 ese dod ya NO se regenera con un LLM en cada turno: el hook
goal_refine.sh que lo reescribía con haiku por prompt quedó desactivado (amplificaba el rate-limit
compartido). El objetivo+DoD inicial los fija goal_autogen.sh una sola vez por terminal; a partir
de ahí son fijos y el usuario los ajusta a mano con objetivo: ... / dod: .... El criterio que
clasifica la flota es dod_contract + dod_status (lo escribe set_dod_contract, sin LLM), no ese
dod móvil. Tras lanzar y conocer el sessionId:
./fn run set_dod_contract <sessionId> "Golden: <caso feliz+evidencia>. Edge: <2 bordes>. Error: <1 fallo manejado>." pending
El contrato sigue dod_quality.md (golden + edge + error con evidencia ejecutable), no un checkbox
vago. Sin él, el agente es MAL_LANZADO.
Push automático: el bloque FLEET-STATE
No hace falta acordarse de drenar para enterarse de un cambio. El hook UserPromptSubmit
hook_fleet_state_inject.sh (registrado en .claude/settings.local.json) inyecta en CADA turno del
orquestador —solo cuando la sesión es role=orchestrator— una línea recordatorio del rol
(MODO ORQUESTADOR activo (role=orchestrator)., que reancla el modo aunque su prompt se haya
diluido del contexto) seguida de un bloque resumen de las transiciones pendientes del watcher:
FLEET-STATE: terminados=[<sid>:<goal>…] reclaman=[…] estancados=[…] (drain con ./fn run drain_fleet_events para consumir)
Si no hay cambios emite FLEET-STATE: sin cambios; si el watcher está caído o el events.jsonl no
existe, degrada limpio sin romper el turno (la línea de rol se sigue emitiendo). El bloque es solo un
aviso (hace peek, no avanza el cursor): para consumir las transiciones y aplicar la política por
clasificación sigues drenando (abajo). El resumen lo produce summarize_fleet_transitions_py_infra
sobre el feed del watcher.
Además, el mismo hook inyecta una línea CONTEXTO FLEET cuando detecta (vía
detect_fleet_context_bash_infra, leyendo $TMUX, no $FLEET_SOCKET) que el orquestador vive
dentro de una flota tmux:
CONTEXTO FLEET: estás dentro de la fleet tmux socket=<X> session=<Y>. Lanza ejecutores con spawn_fleet_agent (auto-detecta el socket) — NUNCA kitty/launch_claude_agent_kitty estando aquí.
Es el recordatorio que evita el bug de caer a kitty cuando $FLEET_SOCKET viene vacía pese a estar
en la flota: la detección de contexto se hace por $TMUX (señal fiable que todo proceso dentro de
tmux tiene siempre), no por $FLEET_SOCKET (a veces ausente en un claude resumido/relanzado). Esta
parte del hook no necesita venv ni python (solo bash + tmux) y se emite antes del bloque
FLEET-STATE; si el detector falta o $TMUX está vacía, simplemente no se emite la línea (turno
intacto).
Gotcha conocido: el bloque FLEET-STATE (peek pasivo) lista transiciones de TODA la flota, incluidas
las de otros orquestadores y sus ejecutores. Si hay más de un orquestador activo, filtra por tu propia
familia de agentes (los que tú lanzaste) — igual que en "No te vigiles a ti mismo" más abajo. El push
activo (siguiente apartado) sí está ya ruteado por familia.
Push activo del watcher — send-keys dirigido (routing por parent_orchestrator)
Además del aviso pasivo en cada turno, el watcher de fleetview empuja activamente: cuando un
ejecutor transita a DICE_TERMINADO, hace tmux send-keys directamente al pane del orquestador que
lo lanzó, para que el cierre no espere a tu siguiente turno. El ruteo se resuelve por la clave
parent_orchestrator del goal.json del ejecutor — la que escribe spawn_fleet_agent --parent <tu-sessionId>. Por eso lanza siempre tus ejecutores con --parent: sin esa clave el watcher no
sabe a qué pane mandar el aviso y el cierre queda solo en el peek pasivo (toda la flota). Con
--parent, cada familia de agentes avisa a su propio orquestador y desaparece el ruido cruzado entre
orquestadores.
Indicador "idle nuevo sin ver" en la TUI fleetview
La TUI fleetview marca de forma distinguible los ejecutores que acaban de quedar idle y que aún no
has atendido (idle nuevo sin ver), para que el humano y el orquestador localicen de un vistazo qué
agentes reclaman acción frente a los que ya están en seguimiento. Es la señal visual hermana del push
del watcher: el push te lo trae a la terminal, el indicador lo resalta en la lista. Úsalo como
disparador para drenar la cola y aplicar la política por clasificación (verificar DICE_TERMINADO,
nudge a ESTANCADO).
Drenar la cola
./fn run drain_fleet_events # consume nuevos (avanza cursor), agrupa por clasificación, marca urgentes
./fn run drain_fleet_events --advance false # peek sin consumir (inspección)
Devuelve {total_new, events, by_classification, urgent, cursor}. La clasificación de cada agente la
produce classify_fleet_termination (pura) desde su estado (status + phase + dod_contract +
dod_status + segundos ociosos).
No te vigiles a ti mismo. Al procesar la cola, ignora los eventos de tu propia sesión y de
cualquier agente con role=orchestrator. El role ya viene en cada fila de fleet_list (y de
fleetview list --json), así que filtras directamente por ese campo. Solo si usas el binario crudo y
la fila trae role vacío, cae al sidecar del goal de cada session_id:
# Preferido: filtrar por el role que ya trae fleet_list / fleetview list --json.
apps/fleetview/fleetview list --json | jq -r '.[] | select((.role // "executor") != "orchestrator") | .session_id'
# Fallback solo si el binario dejó role vacío en alguna fila:
jq -r '.role // "executor"' ~/.claude/goals/<session_id>.json # "orchestrator" => ignóralo
El orquestador no tiene dod_contract y aparecería como MAL_LANZADO — es ruido, no un ejecutor que
vigilar. Solo actúas sobre los ejecutores (role=executor o sin role).
Políticas por clasificación
| Transición a… | Qué hace el orquestador |
|---|---|
RECLAMA (urgent) |
Escalar a la persona: resumen corto de QUÉ decisión se necesita + /fleet focus <sid> para llevarla al agente. Si no está presente, PushNotification. NUNCA decidir tú por ella en un RECLAMA. |
DICE_TERMINADO |
Lanzar verificador independiente (abajo). No confiar en el autodeclarado. Si met → cerrar con kill_fleet_agent (auto-kill, libera el slot idle). |
ESTANCADO |
Nudge al agente (abajo). Solo idle; jamás waiting. |
MAL_LANZADO |
Escribir dod_contract retroactivo (set_dod_contract) o re-lanzar con DoD. |
TRABAJANDO |
No molestar. |
GONE |
Limpiar de la tabla de seguimiento (terminó o murió; si tenía DoD sin cumplir, anótalo). |
Verificador — cierre de DICE_TERMINADO (cero auto-aprobación)
Cuando un agente se autodeclara terminado, no se confía: lanzas un verificador independiente
del ejecutor (Agent efímero), que compara el report del ejecutor (en reports/, con evidencia
ejecutable) contra su dod_contract:
Agent(subagent_type="general-purpose", prompt:
"Verifica de forma ADVERSARIAL si el trabajo cumple su DoD-contrato. NO ejecutaste tú la tarea.
DoD-contrato: <contract>
Report del ejecutor: <ruta del reports/NNNN-*.md>
Comprueba CADA cláusula (golden + edge + error) contra la evidencia citada en el report; re-ejecuta
los comandos de verificación si puedes. Devuelve {verdict: met|failed, gaps: [...], evidence: [...]}.
Por defecto failed si la evidencia no respalda una cláusula.")
El verificador (y el splitter y las búsquedas con Explore) son la única excepción autorizada al
Agent tool dentro del modo: utilidades internas read-only del propio orquestador, que devuelven un
resultado y mueren sin que el humano las gestione como agentes de la flota. Jamás se usa el Agent tool
para ejecutar una sub-tarea (ver paso 8 del comando).
met→ el orquestador marcaset_dod_contract <sid> "<contract>" met, informa a la persona y cierra el ejecutor para liberar el slot idle conkill_fleet_agent(regla de auto-kill, abajo).failed→ nudge al ejecutor con el gap concreto (no cerrar).set_dod_contract <sid> "<contract>" failed(vuelve a pending tras el nudge si reabre trabajo).
Auto-kill — cerrar el ejecutor tras verificar met (libera el slot idle)
Un ejecutor verificado met no se deja vivo en reposo: se cierra de inmediato para que no se
acumule en la flota ocupando un slot idle. En cuanto el verificador devuelve met y has marcado
set_dod_contract <sid> "<contract>" met, ciérralo:
./fn run kill_fleet_agent <sessionId> --socket "$FLEET_SOCKET"
kill_fleet_agent_bash_infra manda SIGTERM al proceso claude del ejecutor (cierre limpio,
recuperable luego con claude --resume <sessionId>) y cierra su window tmux (kill-window). Trae
guards que lo hacen seguro de invocar programáticamente:
- No mata a un
role=orchestrator(lo lee delgoal.json): nunca decapitas la flota por error. - No se mata a sí mismo: rechaza el target si es la sesión que invoca (equivalente dirigido de la
regla "nunca
pkill claude", paso 6 del comando). - Acepta el target por
sessionId(exacto o prefijo) o por PID. Usa--dry-runpara ver el plan sin tocar nada.
Esto cierra el ciclo del modo: lanzas con --parent → el watcher te avisa del DICE_TERMINADO →
verificas → kill_fleet_agent libera el slot. No uses pkill/killall ni kill a pelo para esto:
kill_fleet_agent resuelve la window y aplica los guards.
Nudge — ESTANCADO
Agente idle con dod_contract sin cumplir y sin actividad > umbral (10 min). Empújalo a cerrar SU DoD
inyectando en su pane tmux:
tmux -L "${FLEET_SOCKET:-fleet}" send-keys -t <window_id> \
"Sigues idle con tu DoD-contrato sin cerrar. Falta: <gap>. Cierra el golden+edge+error con evidencia, o reporta el bloqueo concreto." Enter
El window_id es el campo tmux_window (p.ej. @20) de apps/fleetview/fleetview list --json:
apps/fleetview/fleetview list --json | jq -r '.[] | select(.session_id|startswith("<sid>")) | .tmux_window'
Solo a idle/ESTANCADO. JAMÁS a un agente en waiting/preguntando — esos te reclaman a TI, no un
empujón del bot.
Splitter — tarea demasiado grande
Si una sub-tarea sigue siendo grande para un solo agente, antes de lanzarla pásala por un splitter
(Agent efímero) que devuelve un plan de sub-tareas atómicas, cada una con su dod_contract y sus
dependencias:
Agent(subagent_type="Plan", prompt:
"Descompón esta tarea en sub-tareas ATÓMICAS, cada una cerrable por UN agente en una sesión, con
su propio DoD-contrato (golden+edge+error) y dependencias (cuáles son paralelas y cuáles
secuenciales). Máximo 6 sub-tareas. Tarea: <...>. Devuelve [{tarea, dod_contract, deps:[...]}].")
El orquestador lanza un ejecutor por sub-tarea respetando las dependencias (paralelas a la vez, secuenciales encadenadas), siempre dentro del tope de fan-out (ver "Tope de fan-out" abajo).
Tope de fan-out (regla dura)
Máximo 6 ejecutores role=executor activos simultáneos por orquestador. Si se alcanza el tope,
el orquestador NO lanza más: encola las sub-tareas restantes y las despacha a medida que un slot
se libera — un slot se libera cuando un ejecutor se verifica met y se cierra con kill_fleet_agent
(auto-kill). El conteo es de la familia propia (ejecutores con tu parent_orchestrator), no de
toda la flota; resuélvelo con el routing por parent_orchestrator, igual que el push activo.
Por qué un número duro y no "los que hagan falta": ya hubo el caso de 30 agentes que no cerraban
nada y, al competir todos por el mismo rate-limit compartido, hubo que desactivar goal_refine
(el hook que reescribía el dod con un LLM por prompt). Más ejecutores no es más throughput: el
cuello de botella es el rate-limit compartido y los DoD que nadie cierra, no el número de procesos.
Cadencia
El orquestador no hace polling caro: drena la cola cuando actúa (cuando la persona le habla) y,
para vigilancia desatendida, con un heartbeat largo (ScheduleWakeup 20-30 min) o cuando el watcher
empuja un urgente. Lo urgente (RECLAMA) sube al instante; el resto (cierres, estancados) se procesa
en lote.
Funciones del registry del grupo orchestration
| Función | Para qué |
|---|---|
launch_claude_agent_kitty_bash_infra |
Lanzar un secundario en kitty con prompt autónomo + --dangerously-skip-permissions |
list_claude_agents_bash_infra |
Listar la flota de Claudes vivos (PID, sessionId, cwd, status, kitty) para seguirla |
reboot_all_claudes_bash_infra |
Reiniciar/parar la flota retomando sesiones; --exclude-current para no tocarte |
set_dod_contract_py_infra |
Escribir el DoD-contrato fijo (dod_contract/dod_status) en el goal.json de un secundario al lanzarlo |
drain_fleet_events_py_infra |
Consumir la cola de transiciones del watcher (~/.claude/fleet/events.jsonl), agrupada por clasificación + urgentes |
summarize_fleet_transitions_py_infra |
Resumir las transiciones del feed en una línea (terminados/reclaman/estancados); alimenta el bloque FLEET-STATE que el hook UserPromptSubmit inyecta cada turno |
classify_fleet_termination_go_infra |
Clasificar el estado de terminación de un agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO) — lo usa el watcher |
list_claude_fleet_go_infra |
Fleet tipado con goal/phase/role + dod_contract/dod_status + tmux_window (alimenta /fleet, el watcher y el tool fleet_list). Invócala por el tool mcp__orchestrator__fleet_list (preferido) o el binario apps/fleetview/fleetview list --json, NUNCA por ./fn run (la despacha como go test). El JSON del CLI ya expone role/dod_contract/dod_status ("" si el goal.json no los declara); el tool MCP además rellena los vacíos desde ~/.claude/goals/<session_id>.json |
detect_fleet_context_bash_infra |
Detectar si estás en una flota tmux derivando socket/session de $TMUX (señal fiable), con fallback a $FLEET_SOCKET. Devuelve JSON {in_fleet,in_tmux,socket,session,source}. Lo usan spawn_fleet_agent (auto-detección de socket) y el hook (línea CONTEXTO FLEET) para no caer a kitty estando en la flota |
spawn_fleet_agent_bash_infra |
Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty siempre que estés en tmux. Auto-detecta socket/session de $TMUX (vía detect_fleet_context) si no se pasan --socket/--session (los explícitos priman). --parent <tu-sessionId> atribuye el ejecutor a ti y habilita el push activo del watcher |
mark_claude_role_py_infra |
Marcar role (orchestrator/executor) en el goal.json de un Claude resolviendo PID→sessionId |
mark_claude_parent_py_infra |
Marcar parent_orchestrator (sessionId del orquestador que lo lanzó) en el goal.json de un ejecutor resolviendo PID→sessionId. Lo invoca spawn_fleet_agent --parent; habilita el routing del watcher al pane del orquestador padre |
kill_fleet_agent_bash_infra |
Cierre dirigido de UN ejecutor: SIGTERM al claude + kill-window de su window tmux. Guards anti-orquestador y anti-self. Lo usa el orquestador para liberar el slot idle tras verificar met (auto-kill) |
notify_desktop_go_infra |
Notificación de escritorio del fleet (notify-send --app-name=fleetview, degradación silenciosa si no hay notify-send). La usa el orquestador/watcher para avisar a la persona de un RECLAMA u otro evento urgente cuando no está mirando la terminal |
Cómo invocarlas. Las Bash y Python del grupo se lanzan con ./fn run <id> [args] (verificado:
list_claude_agents, drain_fleet_events, reboot_all_claudes, set_dod_contract,
mark_claude_role, mark_claude_parent, kill_fleet_agent, launch_claude_agent_kitty,
spawn_fleet_agent, detect_fleet_context). Las Go con tests NO: ./fn run las despacha como go test. Por eso
list_claude_fleet_go_infra se usa por el binario apps/fleetview/fleetview list --json, y
classify_fleet_termination_go_infra la consume el watcher embebido en fleetview (no se invoca a
mano).
Relación con otras reglas
.claude/commands/orquestador.md— la doctrina y el flujo de cada turno del modo; esta regla es su maquinaria operativa..claude/rules/autonomous_loop.md—fn-orquestador(Agent tool, sandbox no-interactivo). Es lo que el modo orquestador no es..claude/rules/apps_subrepo.md— apps/analyses/projects son sub-repos Gitea (apps/*gitignored): el aislamiento natural y el gotcha degit initantes de limpiar un worktree con una app nueva..claude/rules/reports.md+.claude/rules/dod_quality.md— qué entrega cada secundario: report con evidencia ejecutable + gaps..claude/rules/delegation.md+.claude/rules/registry_calls.md— los secundarios siguen registry-first y delegan afn-constructor.- Memorias:
lanzar-agentes-skip-permissions,multi-agent-git-race-same-repo,claude-session-pid-mapping,prefiere-kitty-terminal.