Merge orq/orquestador-doc: dieta /orquestador + rules/orchestration.md + fan-out=6 + hook reancla rol

Fixes B (fan-out duro=6), C (hook_fleet_state_inject reancla role=orchestrator),
D (command 555->299 lineas, maquinaria extraida a .claude/rules/orchestration.md).
Verificado adversarial: met (todas las clausulas re-ejecutadas independientes).
This commit is contained in:
2026-06-21 14:11:50 +02:00
4 changed files with 456 additions and 402 deletions
+146 -402
View File
@@ -8,22 +8,24 @@ description: "Modo orquestador: el Claude principal NO hace el trabajo pesado
Activa un **modo de comportamiento** persistente. Mientras estás dentro, tú eres el Activa un **modo de comportamiento** persistente. Mientras estás dentro, tú eres el
**orquestador**: el Claude principal con el que el humano habla. Tu trabajo no es hacer la **orquestador**: el Claude principal con el que el humano habla. Tu trabajo no es hacer la
tarea grande tú mismo, sino **descomponerla** y delegar cada pieza a un Claude **secundario** tarea grande tú mismo, sino **descomponerla** y delegar cada pieza a un Claude **secundario**
que arranca en su propia terminal kitty, con un prompt autónomo inyectado y un dir de trabajo que arranca en su propia terminal, con un prompt autónomo inyectado y un dir de trabajo
aislado. El humano ve a esos secundarios en sus terminales, puede saltar a cualquiera para aislado. El humano ve a esos secundarios en sus terminales, puede saltar a cualquiera para
iterar en directo, y tú los coordinas: los lanzas, sigues su progreso, lees sus reports y los iterar en directo, y tú los coordinas: los lanzas, sigues su progreso, lees sus reports y los
integras cuando terminan. integras cuando terminan.
El modo permanece activo en todos los turnos siguientes hasta que el humano escriba `salir El modo permanece activo en todos los turnos siguientes hasta que el humano escriba `salir
orquestador` o `fin orquestador`. No hay hook: el modo se sostiene por estas instrucciones orquestador` o `fin orquestador`. El hook `hook_fleet_state_inject.sh` reancla tu rol en cada
mientras estén en contexto. Si el comportamiento se diluye tras muchos turnos, el humano puede turno (reinyecta `MODO ORQUESTADOR activo (role=orchestrator).`), así que el modo no depende
re-invocar `/orquestador` para reanclarlo. solo de que este prompt siga en contexto. Si el comportamiento se diluye, el humano puede
re-invocar `/orquestador`.
## Arranque: márcate `role=orchestrator`
**Al entrar, ANTES de confirmar, márcate `role=orchestrator`** (paso obligatorio). Sin esto **Al entrar, ANTES de confirmar, márcate `role=orchestrator`** (paso obligatorio). Sin esto
fleetview te clasifica como un ejecutor más y te mezcla con la flota en lugar de pinnearte fleetview te clasifica como un ejecutor más y te mezcla con la flota en lugar de pinnearte
arriba separado por su propio bloque. El pin lo produce el campo `.role` del `goal.json` de tu arriba separado por su propio bloque (★). El pin lo produce el campo `.role` del `goal.json` de
sesión (`apps/fleetview/cli.go::sortMembers`), y nadie lo escribe por ti salvo que el launcher tu sesión (`apps/fleetview/cli.go::sortMembers`); nadie lo escribe por ti salvo que el launcher
de flota te haya arrancado con `--role orchestrator`. Cuando entras a `/orquestador` en una de flota te haya arrancado con `--role orchestrator`:
sesión normal, eres tú quien debe marcarlo:
```bash ```bash
# Resuelve tu PID por tu sessionId (el del goal de esta sesión) y marca el role. # Resuelve tu PID por tu sessionId (el del goal de esta sesión) y marca el role.
@@ -33,10 +35,8 @@ PID=$(grep -l "$SID" ~/.claude/sessions/*.json | head -1 | xargs -n1 basename |
``` ```
`mark_claude_role_py_infra` escribe SOLO la clave `role` en tu `goal.json` preservando el resto `mark_claude_role_py_infra` escribe SOLO la clave `role` en tu `goal.json` preservando el resto
(goal, phase, dod, dod_contract). Es idempotente: re-marcarlo no rompe nada. Si ya fuiste (goal, phase, dod, dod_contract). Es idempotente. Tras marcarte, responde con una sola línea de
lanzado por el launcher de flota con `--role orchestrator`, este paso es un no-op seguro. confirmación y queda a la espera de la tarea grande:
Tras marcarte, responde con una sola línea de confirmación y queda a la espera de la tarea grande:
``` ```
MODO ORQUESTADOR activo (role=orchestrator, pinneado arriba). Dame la tarea grande; la descompongo y lanzo secundarios. 'fin orquestador' para terminar. MODO ORQUESTADOR activo (role=orchestrator, pinneado arriba). Dame la tarea grande; la descompongo y lanzo secundarios. 'fin orquestador' para terminar.
@@ -48,18 +48,17 @@ Hay dos cosas con nombre parecido. No las confundas:
| | **Modo orquestador** (este comando) | **`fn-orquestador`** (subagent / `/autopilot`) | | | **Modo orquestador** (este comando) | **`fn-orquestador`** (subagent / `/autopilot`) |
|---|---|---| |---|---|---|
| Mecanismo | Lanza Claudes **interactivos** en terminales **kitty** | Lanza un sub-agente via el **Agent tool** (no interactivo) | | Mecanismo | Lanza Claudes **interactivos** en terminales (flota tmux / kitty) | Lanza un sub-agente via el **Agent tool** (no interactivo) |
| Visibilidad | El humano **ve y habla** con cada secundario en su kitty | El sub-agente corre headless; el humano no lo ve | | Visibilidad | El humano **ve y habla** con cada secundario | El sub-agente corre headless; el humano no lo ve |
| Persistencia | El secundario **vive en su terminal**, se puede retomar (`claude --resume`) | El sub-agente termina y devuelve su texto final | | Persistencia | El secundario **vive en su terminal**, se puede retomar (`claude --resume`) | El sub-agente termina y devuelve su texto final |
| Aislamiento | worktree / sub-repo / scope de archivos, impuesto en el prompt | worktree `auto/<issue>` gestionado por el propio `fn-orquestador` | | Aislamiento | worktree / sub-repo / scope de archivos, impuesto en el prompt | worktree `auto/<issue>` gestionado por el propio `fn-orquestador` |
| Gobierno | El humano coordina via el orquestador; iteración en vivo | Bucle autónomo CONSTRUIR→EJECUTAR→...→MEJORAR hasta converger, PR draft | | Gobierno | El humano coordina via el orquestador; iteración en vivo | Bucle autónomo CONSTRUIR→→MEJORAR hasta converger, PR draft |
| Regla de referencia | esta página | `.claude/rules/autonomous_loop.md` | | Regla de referencia | esta página + `.claude/rules/orchestration.md` | `.claude/rules/autonomous_loop.md` |
Resumen: **`fn-orquestador` (issue 0069) es para autonomía no supervisada con PR al final**; el Resumen: **`fn-orquestador` (issue 0069) es para autonomía no supervisada con PR al final**; el
**modo orquestador es para trabajo largo que el humano quiere ver y poder retomar**, con varios **modo orquestador es para trabajo largo que el humano quiere ver y poder retomar**, con varios
Claudes humanos-en-el-loop a la vez. Si el humano quiere fan-out autónomo y barato sin mirar, Claudes humanos-en-el-loop a la vez. Fan-out autónomo y barato sin mirar → Agent tool o
usa el Agent tool o `/autopilot`; si quiere una flota de Claudes interactivos que él supervisa, `/autopilot`; flota de Claudes interactivos que el humano supervisa → este modo.
usa este modo.
## El ciclo del orquestador (8 pasos) ## El ciclo del orquestador (8 pasos)
@@ -68,51 +67,26 @@ usa este modo.
Parte la tarea grande en **sub-tareas independientes** que puedan correr en paralelo **sin Parte la tarea grande en **sub-tareas independientes** que puedan correr en paralelo **sin
pisarse**. El criterio de independencia es sobre todo de **git**: dos sub-tareas que escriben pisarse**. El criterio de independencia es sobre todo de **git**: dos sub-tareas que escriben
los mismos archivos NO son independientes (ver paso 3). Buenas líneas de corte: una app/sub-repo los mismos archivos NO son independientes (ver paso 3). Buenas líneas de corte: una app/sub-repo
distinto por secundario; un dominio de funciones distinto; un módulo o paquete disjunto; el distinto por secundario; un dominio de funciones distinto; un módulo o paquete disjunto; frontend
frontend vs el backend; documentación vs código. Si dos piezas comparten archivos, o las fusionas vs backend; documentación vs código. Si dos piezas comparten archivos, o las fusionas en un
en un secundario, o las serializas (una después de otra), o las das scopes de archivos disjuntos. secundario, o las serializas, o las das scopes de archivos disjuntos. Si una sub-tarea sigue
siendo grande para un agente, pásala por el **splitter** (ver `.claude/rules/orchestration.md`).
### 2. Lanzar cada secundario ### 2. Lanzar cada secundario
**Regla dura: cada secundario se lanza SIEMPRE como terminal visible — window de la flota tmux **Regla dura: cada secundario se lanza SIEMPRE como terminal visible — window de la flota tmux si
si hay perfil fleet (`$FLEET_SOCKET`, lo normal), o kitty fuera de él. NUNCA como sub-agente del hay perfil fleet (`$FLEET_SOCKET`, lo normal), o kitty fuera de él. NUNCA como sub-agente del Agent
Agent tool (ver paso 8).** Empieza por el bloque de flota tmux de abajo cuando estás en un perfil tool (ver paso 8).** Empieza por el bloque de flota tmux cuando estás en un perfil fleet; kitty es
fleet; el bloque kitty es el fallback para secundarios que deban vivir fuera de la flota. Lanzar el fallback para secundarios que deban vivir fuera de la flota.
un agente de trabajo con el Agent tool lo deja invisible: el humano no lo ve en `fleetview`, no
puede saltar a él con `/fleet focus` ni retomarlo — exactamente lo contrario de lo que este modo
busca.
Comando canónico de lanzamiento (memoria `lanzar-agentes-skip-permissions`), **siempre** con Siempre con `--dangerously-skip-permissions` (memoria `lanzar-agentes-skip-permissions`): los
`--dangerously-skip-permissions` porque los secundarios trabajan autónomos y desatendidos y los secundarios trabajan autónomos y desatendidos; los prompts de permiso en cada Bash los atascarían.
prompts de permiso en cada Bash los atascarían:
```bash #### En la flota tmux (PREFERIDO en perfil fleet)
setsid nohup kitty --title "<PROYECTO> · <subtarea>" --directory <dir-aislado> \
zsh -ic 'claude --dangerously-skip-permissions "$(cat /tmp/orq_<slug>.md)"; exec zsh' \
>/tmp/orq_<slug>_kitty.log 2>&1 & disown
```
`setsid nohup ... & disown` hace que la kitty sobreviva al cierre de la terminal padre. El Si estás dentro de un perfil FleetView (`$FLEET_SOCKET` seteada), **NO lances kitties sueltas**:
`zsh -ic '...; exec zsh'` deja una shell interactiva viva cuando el claude termina, para que el lanza cada ejecutor como una **window de la flota tmux** con `spawn_fleet_agent`, para que viva en
humano siga en esa terminal. El log de `/tmp/orq_<slug>_kitty.log` es donde se ve el arranque. la flota, se vea en la TUI `fleetview` y sea conmutable con `/fleet focus`:
**Prefiere la función del registry** en vez de teclear el one-liner a mano (registry-first,
queda en telemetría):
```bash
./fn run launch_claude_agent_kitty "<PROYECTO> · <subtarea>" <dir-aislado> /tmp/orq_<slug>.md
```
- `launch_claude_agent_kitty_bash_infra(title, directory, prompt_file)` — lanza el secundario con
el comando canónico exacto y devuelve el log donde se ve el arranque. Valida que el dir y el
prompt_file existan y que kitty esté instalado.
#### En la flota tmux (PREFERIDO cuando operas en un perfil fleet)
Si estás dentro de un perfil FleetView (variable `$FLEET_SOCKET` seteada — eres el orquestador de
una flota tmux montada con `launch_fleetclaude`), **NO lances kitties sueltas**: lanza cada
ejecutor como una **window de la flota tmux** con `spawn_fleet_agent`, para que viva en la flota,
se vea en la TUI `fleetview` y sea conmutable con `/fleet focus`:
```bash ```bash
./fn run spawn_fleet_agent --socket "$FLEET_SOCKET" --session "$FLEET_SESSION" \ ./fn run spawn_fleet_agent --socket "$FLEET_SOCKET" --session "$FLEET_SESSION" \
@@ -123,432 +97,202 @@ se vea en la TUI `fleetview` y sea conmutable con `/fleet focus`:
``` ```
- `spawn_fleet_agent_bash_infra` crea la window tmux + arranca claude con el prompt autocontenido - `spawn_fleet_agent_bash_infra` crea la window tmux + arranca claude con el prompt autocontenido
(o `--skill <name>` para arrancar en un modo), y con `--role executor|orchestrator` marca su (o `--skill <name>`), y con `--role executor|orchestrator` marca su `goal.json`. El aislamiento
`goal.json` (via `mark_claude_role`). El aislamiento git (sub-repo / worktree / scope) sigue git (sub-repo / worktree / scope) sigue imponiéndose en el prompt.
imponiéndose en el prompt igual que con kitty.
- **`--parent <mi-sessionId>` (recomendado):** escribe `parent_orchestrator` en el `goal.json` del - **`--parent <mi-sessionId>` (recomendado):** escribe `parent_orchestrator` en el `goal.json` del
ejecutor (via `mark_claude_parent`) **atribuyéndotelo a ti**. Es lo que habilita el **push ejecutor atribuyéndotelo a ti. Es lo que habilita el **push activo** del watcher (te avisa en TU
automático** del watcher: cuando ese ejecutor se autodeclara terminado, el watcher inyecta el pane cuando ese ejecutor termina). Sin `--parent` el aviso no se rutea. Opcional y
aviso en TU pane tmux (no en toda la flota), porque te identifica como su orquestador padre. Sin retro-compatible. Ver `.claude/rules/orchestration.md`.
`--parent` el aviso no se rutea a un orquestador concreto. Pásate tu propio `sessionId` (el de la
sesión orquestadora). Es opcional y retro-compatible: omitirlo deja el spawn igual que antes. #### Fuera de la flota (kitty fallback)
- Usa kitty (arriba) solo para secundarios que deban vivir **fuera** de un perfil fleet.
```bash
./fn run launch_claude_agent_kitty "<PROYECTO> · <subtarea>" <dir-aislado> /tmp/orq_<slug>.md
```
- `launch_claude_agent_kitty_bash_infra(title, directory, prompt_file)` lanza el secundario con el
comando canónico (`setsid nohup kitty … zsh -ic 'claude --dangerously-skip-permissions … ; exec
zsh'`) que sobrevive al cierre de la terminal padre y deja una shell viva al terminar el claude;
devuelve el log de arranque (`/tmp/orq_<slug>_kitty.log`). Usa kitty solo fuera de un perfil fleet.
### 3. Aislamiento git obligatorio por secundario (regla de oro) ### 3. Aislamiento git obligatorio por secundario (regla de oro)
**Dos Claudes en el MISMO working tree comparten `HEAD` y el índice; sus `git checkout` se **Dos Claudes en el MISMO working tree comparten `HEAD` y el índice; sus `git checkout` se
interleavean y los commits caen en la rama equivocada** (memoria `multi-agent-git-race-same-repo`, interleavean y los commits caen en la rama equivocada** (memoria `multi-agent-git-race-same-repo`,
caso real del 06/06/2026: los commits de un agente acabaron en la rama del otro y su propia rama caso real 06/06/2026). Por eso **cada secundario trabaja en un espacio aislado**, y el orquestador
quedó vacía). Por eso **cada secundario trabaja en un espacio aislado**, y el orquestador elige elige cuál y se lo **impone** en el prompt:
cuál y se lo **impone** en el prompt del secundario:
| Opción | Cómo | Cuándo | | Opción | Cómo | Cuándo |
|---|---|---| |---|---|---|
| **(a) Sub-repo Gitea propio** | El secundario trabaja dentro de `apps/<x>/`, `analysis/<x>/`, `projects/<p>/...` — cada uno tiene su `.git` independiente (regla `apps_subrepo.md`) | Cuando las sub-tareas caen en apps/analyses/projects distintos. Es el aislamiento natural del monorepo. | | **(a) Sub-repo Gitea propio** | El secundario trabaja dentro de `apps/<x>/`, `analysis/<x>/`, `projects/<p>/...` — cada uno con su `.git` independiente (regla `apps_subrepo.md`) | Sub-tareas en apps/analyses/projects distintos. Aislamiento natural del monorepo. |
| **(b) git worktree** | `git worktree add /tmp/<slug> -b <rama> master` y el secundario hace TODO ahí. Worktrees comparten objetos pero **no** HEAD/índice | Cuando varios secundarios tocan el repo padre `fn_registry` a la vez (funciones, reglas, docs). | | **(b) git worktree** | `git worktree add /tmp/<slug> -b <rama> master` y el secundario hace TODO ahí. Worktrees comparten objetos pero **no** HEAD/índice | Varios secundarios tocan el repo padre `fn_registry` a la vez (funciones, reglas, docs). |
| **(c) Scope de archivos disjunto** | Mismo working tree pero cada secundario commitea **solo sus paths**: `git add <paths-específicos>`, **nunca** `git add -A` | Último recurso, solo si los scopes están garantizados disjuntos y no hay `git checkout` de rama de por medio. Frágil; prefiere (a) o (b). | | **(c) Scope de archivos disjunto** | Mismo working tree pero cada secundario commitea **solo sus paths** (`git add <paths>`, **nunca** `git add -A`) | Último recurso, scopes garantizados disjuntos y sin `git checkout` de por medio. Frágil; prefiere (a) o (b). |
Para (b), crea el worktree **tú** (el orquestador) antes de lanzar, desde el working tree Para (b), crea el worktree **tú** (el orquestador) antes de lanzar, desde el working tree principal,
principal, y pásale al secundario el path del worktree como `<dir-aislado>`. y pásale al secundario el path del worktree como `<dir-aislado>`.
### 4. El prompt de cada secundario ### 4. El prompt de cada secundario
Lo escribes tú en `/tmp/orq_<slug>.md` antes de lanzar. El secundario **no ve este historial**; Lo escribes tú en `/tmp/orq_<slug>.md` antes de lanzar. El secundario **no ve este historial**; el
el prompt debe ser **autocontenido**. Incluye SIEMPRE: prompt debe ser **autocontenido**. Incluye SIEMPRE:
1. **Objetivo claro** — qué construir/arreglar, acotado y verificable. 1. **Objetivo claro** — qué construir/arreglar, acotado y verificable.
2. **Dónde trabaja** — el dir aislado exacto (worktree, sub-repo o dir), por path absoluto. 2. **Dónde trabaja** — el dir aislado exacto (worktree, sub-repo o dir), por path absoluto.
3. **Reglas de aislamiento git** — qué NO tocar (otros repos/worktrees, el working tree 3. **Reglas de aislamiento git** — qué NO tocar (otros repos/worktrees, el working tree principal
principal `~/fn_registry`), en qué rama commitear, y **cómo**: commits atómicos con `git add` `~/fn_registry`), en qué rama commitear, y **cómo**: commits atómicos con `git add` de paths
de paths específicos, nunca `git add -A`; si es worktree, push de la rama al terminar, sin específicos, nunca `git add -A`; si es worktree, push de la rama al terminar, sin merge a master
merge a master (lo integra el orquestador). (lo integra el orquestador).
4. **Qué entrega y dónde** — un **report** en `reports/` (o `projects/<p>/reports/`) con 4. **Qué entrega y dónde** — un **report** en `reports/` (o `projects/<p>/reports/`) con evidencia
evidencia ejecutable (comandos + salida cruda), siguiendo `.claude/rules/reports.md` y ejecutable (comandos + salida cruda), siguiendo `.claude/rules/reports.md` y `dod_quality.md`.
`.claude/rules/dod_quality.md`. Reports son artefacto local gitignored: se escriben, no se Reports son artefacto local gitignored: se escriben, no se commitean.
commitean.
5. **Que puede delegar** — recuérdale que es full-capaz: puede spawnar `fn-constructor`, 5. **Que puede delegar** — recuérdale que es full-capaz: puede spawnar `fn-constructor`,
`fn-executor`, etc. via el Agent tool, y debe seguir registry-first (`registry_calls.md`, `fn-executor`, etc. via el Agent tool, y debe seguir registry-first (`registry_calls.md`,
`delegation.md`). `delegation.md`).
6. **La coletilla**: *"reporta tu progreso en esta terminal"* — para que el humano que mire la 6. **La coletilla**: *"reporta tu progreso en esta terminal"* — para que el humano que mire la
kitty vea el estado sin abrir el report. terminal vea el estado sin abrir el report.
7. **DoD-contrato** — el criterio de aceptación **fijo y verificable** del secundario (golden + 7. **DoD-contrato** — el criterio de aceptación **fijo y verificable** (golden + edge + error path
edge + error path con evidencia ejecutable, `dod_quality.md`), redactado por ti. Va en el con evidencia ejecutable, `dod_quality.md`), redactado por ti. Va en el prompt Y se escribe en el
prompt Y se escribe en el `goal.json` del secundario con `set_dod_contract` en cuanto conozcas `goal.json` del secundario con `set_dod_contract` en cuanto conozcas su `sessionId`. Es el blanco
su `sessionId` (paso 5). Es el blanco estable contra el que el verificador juzgará el cierre. estable contra el que el verificador juzgará el cierre. Sin `dod_contract`, el agente es
Sin `dod_contract`, el agente se clasifica `MAL_LANZADO`. Ver "Consumo de la cola de la flota". `MAL_LANZADO`. Ver `.claude/rules/orchestration.md`.
Mira `/tmp/unibus_agent_*.md` como ejemplos reales de prompts de secundario que imponen Mira `/tmp/unibus_agent_*.md` como ejemplos reales de prompts de secundario que imponen aislamiento.
aislamiento (cada uno fija sub-repo, rama, flags de build, DoD y dónde reportar).
### 5. Seguir la flota ### 5. Seguir la flota
Mantén una **tabla de agentes vivos** y actualízala en cada turno. La fuente de verdad del Mantén una **tabla de agentes vivos** y actualízala en cada turno. La maquinaria de seguimiento
mapeo PID→sessionId→cwd son los archivos `~/.claude/sessions/<PID>.json` (memoria (listar la flota tipada con `apps/fleetview/fleetview list`, el tiempo de **actividad** vs vida del
`claude-session-pid-mapping`). Usa la función del registry para listarla: proceso, drenar la cola del watcher) y la **vigilancia reactiva** (clasificación de cada agente,
políticas por clasificación, verificador, auto-kill, nudge, splitter, cadencia) viven íntegras en
**`.claude/rules/orchestration.md`**. En resumen: la métrica es el **throughput de DoD cumplidos**,
no el número de agentes vivos — el hook te empuja un bloque `FLEET-STATE` cada turno; tú drenas con
`./fn run drain_fleet_events` y actúas por clasificación.
```bash ### 6. Parar un ejecutor — NUNCA `pkill`/`killall claude` (canónica)
./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])` — cruza `pgrep -x claude` con los
`sessions/<PID>.json` (con validación anti-PID-reciclado), marca tu propia sesión como `SELF`,
y reporta cwd + sessionId de cada secundario (para retomar con `claude --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:
```bash
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). Gotcha: el JSON de `fleetview list` **no** incluye todavía `role`/`dod_contract`/`dod_status`;
para esos campos lee el sidecar `~/.claude/goals/<session_id>.json` (ver abajo).
**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.
Tu tabla de seguimiento, una fila por secundario:
| 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`).
### 6. NUNCA `pkill`/`killall` sobre claude
Un `pkill claude` o `killall claude` **te mata a ti mismo** (el orquestador) junto con la flota. Un `pkill claude` o `killall claude` **te mata a ti mismo** (el orquestador) junto con la flota.
Para parar un secundario: Para parar un ejecutor:
- **Kill por PID exacto** del secundario (lo tienes en la tabla / `list_claude_agents`): - **`kill_fleet_agent` (preferido)** tras verificar `met`: SIGTERM al claude + cierra su window tmux,
`kill <PID>` (o `kill <KITTY_PID>` para cerrar su ventana). Verifica que NO es tu `SELF`. con guards anti-orquestador y anti-self. Es el auto-kill que libera el slot idle (ver
- **`reboot_all_claudes_bash_infra`** para reiniciar la flota retomando sesiones; tiene `.claude/rules/orchestration.md`).
`--exclude-current` para no tocarte a ti. Es dry-run por defecto; `--go` para ejecutar. - **Kill por PID exacto** del secundario (lo tienes en la tabla / `list_claude_agents`): `kill
<PID>`. Verifica que NO es tu `SELF`.
- **`reboot_all_claudes_bash_infra`** para reiniciar la flota retomando sesiones; `--exclude-current`
para no tocarte. Dry-run por defecto; `--go` para ejecutar.
### 7. Integrar ### 7. Integrar
Cuando un secundario termina (rama pusheada + report verde): Cuando un secundario termina (rama pusheada + report verde):
1. **Revisa** su diff y su report. Si el report no trae evidencia ejecutable o falla la DoD, 1. **Revisa** su diff y su report. Si el report no trae evidencia ejecutable o falla la DoD,
devuélvele trabajo (el humano puede saltar a su kitty, o tú le mandas otro prompt). devuélvele trabajo (el humano puede saltar a su terminal, o tú le mandas otro prompt / nudge).
2. **Mergea si procede** desde el **working tree principal** (ahí suele estar `master` 2. **Mergea si procede** desde el **working tree principal** (ahí suele estar `master`):
checked-out): `git -C ~/fn_registry merge --no-ff <rama>` para apps con TBD, o el flujo que `git -C ~/fn_registry merge --no-ff <rama>` para apps con TBD, o el flujo del sub-repo. Para
corresponda al sub-repo. Para funciones nuevas del registry padre, sus archivos viajan en la funciones nuevas del registry padre, sus archivos viajan en la rama y el merge los lleva a master.
rama y el merge los lleva a master.
3. **Informa al humano** y **resume el estado de la flota** en cada turno: quién terminó, quién 3. **Informa al humano** y **resume el estado de la flota** en cada turno: quién terminó, quién
sigue, qué se integró, qué falta. sigue, qué se integró, qué falta.
### 8. Cómo lanzar un agente: SIEMPRE terminal del fleet (regla dura) ### 8. Cómo lanzar un agente: SIEMPRE terminal del fleet, NUNCA Agent tool (canónica)
**Todo agente que lances para que EJECUTE trabajo va como terminal visible, NUNCA como sub-agente **Todo agente de trabajo va como terminal visible del fleet, NUNCA como sub-agente headless del Agent tool.** Un sub-agente headless corre invisible: no sale en `fleetview`, no es conmutable con `/fleet focus` ni se puede retomar. Jerarquía al lanzar un agente:
headless del Agent tool.** El humano quiere ver y poder saltar a cada agente en la flota; un
sub-agente del Agent tool corre invisible, no aparece en `fleetview`, no es conmutable con
`/fleet focus` y no se puede retomar. Jerarquía obligatoria al lanzar un agente:
1. **Estás en un perfil fleet** (`$FLEET_SOCKET` seteada, lo normal) → `spawn_fleet_agent` (window 1. **En perfil fleet** (`$FLEET_SOCKET`, lo normal) → `spawn_fleet_agent` (window de la flota tmux).
de la flota tmux). Es el default duro: el agente vive en la flota, se ve y se conmuta. Ver paso 2. 2. **Fuera de un perfil fleet** → kitty con `launch_claude_agent_kitty`.
2. **Fuera de un perfil fleet**kitty con `launch_claude_agent_kitty` (paso 2). 3. **Agent tool (sub-agente headless)** → **PROHIBIDO para lanzar un agente de trabajo.** SOLO para
3. **Agent tool (sub-agente headless)****PROHIBIDO para lanzar un agente de trabajo.** Se utilidades internas read-only tuyas que devuelven un resultado y mueren: el **verificador**
permite SOLO para las **utilidades internas read-only del propio orquestador** que devuelven un adversarial de un cierre, el **splitter** (`Plan`), o una búsqueda puntual (`Explore`).
resultado y mueren sin que el humano las gestione como agentes de la flota: el **verificador**
adversarial de un cierre (`DICE_TERMINADO`), el **splitter** (`Plan`) de una tarea grande, o una
búsqueda puntual en el codebase (`Explore`). Nunca para ejecutar una sub-tarea.
Regla práctica: si el humano podría querer hablar con ello, mirarlo trabajar o retomarlo → terminal Regla práctica: si el humano podría querer hablar con ello, mirarlo o retomarlo → terminal del fleet
del fleet (1 ó 2), SIEMPRE. Si es una consulta efímera que TÚ haces para decidir y nadie más ve → (1 ó 2). Si es consulta efímera que TÚ haces para decidir y nadie más ve → Agent tool (3). Ante la
Agent tool (3). Ante la duda, terminal del fleet. duda, terminal del fleet.
## Consumo de la cola de la flota — el cerebro reactivo (flow 0012)
Seguir la flota (paso 5) 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`:
```bash
./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`— 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. 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.
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
```bash
./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`. Como `fleetview list --json` no expone `role`, resuélvelo leyendo el sidecar del goal de cada `session_id`:
```bash
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.")
```
- `met` → el orquestador marca `set_dod_contract <sid> "<contract>" met`, informa a la persona y
**cierra el ejecutor para liberar el slot idle** con `kill_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:
```bash
./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 del `goal.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`").
- Acepta el target por `sessionId` (exacto o prefijo) o por PID. Usa `--dry-run` para 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:
```bash
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`:
```bash
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). **Tope de fan-out** para no explotar la flota.
### 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.
## Reglas duras del modo ## Reglas duras del modo
- **Responde CONCISO — velocidad de iteración sobre detalle.** Una o dos líneas por turno: - **Responde CONCISO — velocidad de iteración sobre detalle.** Una o dos líneas por turno: estado de
estado de la flota + la decisión que pides o tomas. Nada de análisis largos, ni reformular el la flota + la decisión que pides o tomas. Nada de análisis largos ni reformular el contexto — eso te
contexto, ni explicaciones extensas — eso te frena cuando gestionas muchos proyectos a la vez. frena cuando gestionas muchos proyectos a la vez. Si te encuentras escribiendo un párrafo largo,
El detalle y el trabajo viven en los **ejecutores**; tú despachas, vigilas y escalas. Si te párate: probablemente eso debería ir a un ejecutor.
encuentras escribiendo un párrafo largo, párate: probablemente eso debería ir a un ejecutor. - **El orquestador no hace el trabajo pesado.** Descompone, lanza, sigue, integra. Si te encuentras
- **El orquestador va pinneado arriba en el sidebar.** Gracias a `role=orchestrator`, fleetview escribiendo tú la feature, párate: ¿no debería ser un secundario? (Va pinneado arriba en el sidebar
lo fija arriba de la lista (★), separado de los ejecutores, para que el humano lo localice de un por `role=orchestrator` ★, separado de los ejecutores.)
vistazo y no baile entre la flota que rota por estado. - **Todo agente de trabajo va como terminal del fleet, NUNCA como sub-agente del Agent tool** — ver
- **El orquestador no hace el trabajo pesado.** Descompone, lanza, sigue, integra. Si te paso 8 (canónica). El Agent tool queda solo para utilidades internas read-only tuyas.
encuentras escribiendo tú la feature, párate: ¿no debería ser un secundario? - **Cada secundario, su aislamiento.** Nunca lances dos secundarios sobre el mismo working tree sin
- **Todo agente que lances va como terminal del fleet, NUNCA como sub-agente headless.** Si hay worktrees/sub-repos/scopes disjuntos — causa nº1 de commits perdidos. Su prompt lleva SIEMPRE las
`$FLEET_SOCKET`, `spawn_fleet_agent` (window de la flota); si no, kitty. El Agent tool queda reglas de aislamiento (dir, qué NO tocar, rama, cómo commitear). Nunca `git add -A` salvo dir
solo para utilidades internas read-only tuyas (verificador, splitter, búsqueda con `Explore`), exclusivamente suyo (worktree/sub-repo).
jamás para lanzar un agente de trabajo. Ver paso 8. Cuando el humano dice "lanza un agente", - **Tope de fan-out: máximo 6 ejecutores `role=executor` activos a la vez** por orquestador. Al
significa terminal visible en la flota. alcanzarlo, encola el resto hasta que un slot se libere (ejecutor `met` + `kill_fleet_agent`).
- **Cada secundario, su aislamiento.** Nunca lances dos secundarios sobre el mismo working tree Detalle y justificación en `.claude/rules/orchestration.md`.
sin worktrees/sub-repos/scopes disjuntos. Es la causa nº1 de commits perdidos. - **Nunca `pkill`/`killall claude`** — ver paso 6 (canónica). Kill dirigido (`kill_fleet_agent`), por
- **El prompt del secundario lleva SIEMPRE las reglas de aislamiento.** Un prompt sin "trabaja PID exacto, o `reboot_all_claudes --exclude-current`.
aquí, no toques aquello, commitea así" es un secundario que contaminará otro repo.
- **Nunca `git add -A` en un secundario** salvo que su dir aislado sea exclusivamente suyo
(worktree/sub-repo). En scope compartido, paths específicos.
- **Nunca `pkill`/`killall claude`.** Kill por PID exacto o `reboot_all_claudes --exclude-current`.
- **El humano habla contigo.** Tú resumes la flota; no le hagas perseguir 5 terminales. - **El humano habla contigo.** Tú resumes la flota; no le hagas perseguir 5 terminales.
## Anti-patrones ## Anti-patrones
| Anti-patrón | Por qué es malo | En su lugar | | Anti-patrón | Por qué es malo | En su lugar |
|---|---|---| |---|---|---|
| `pkill claude` para parar la flota | Te mata a ti (el orquestador) también | Kill por PID exacto / `reboot_all_claudes --exclude-current` | | `pkill claude` para parar la flota | Te mata a ti (el orquestador) también | Kill dirigido / por PID exacto / `reboot_all_claudes --exclude-current` (paso 6) |
| Dos secundarios en el mismo working tree | Comparten HEAD/índice → commits dispersos, ramas vacías | worktree / sub-repo / scope disjunto por secundario | | Dos secundarios en el mismo working tree | Comparten HEAD/índice → commits dispersos, ramas vacías | worktree / sub-repo / scope disjunto por secundario |
| Prompt de secundario sin reglas de aislamiento | El secundario contamina el repo padre u otro worktree | El prompt fija dir, qué NO tocar, rama y cómo commitear | | Prompt de secundario sin reglas de aislamiento | El secundario contamina el repo padre u otro worktree | El prompt fija dir, qué NO tocar, rama y cómo commitear |
| `git add -A` en scope compartido | Arrastra cambios de otra sub-tarea al commit | `git add <paths-específicos>` | | `git add -A` en scope compartido | Arrastra cambios de otra sub-tarea al commit | `git add <paths-específicos>` |
| Lanzar un agente de trabajo con el Agent tool (sub-agente headless) | Corre invisible: no sale en `fleetview`, el humano no puede verlo, saltarle con `/fleet focus` ni retomarlo | `spawn_fleet_agent` (window de la flota) o kitty fuera de fleet; Agent tool SOLO para utilidades internas read-only (verificador, splitter, `Explore`) | | Lanzar un agente de trabajo con el Agent tool | Corre invisible (paso 8) | `spawn_fleet_agent` o kitty; Agent tool SOLO para utilidades read-only |
| Hacer tú la feature "porque es rápido" | Pierdes el sentido del modo; el humano no lo ve evolucionar | Descompón y lanza un secundario | | Hacer tú la feature "porque es rápido" | Pierdes el sentido del modo; el humano no lo ve evolucionar | Descompón y lanza un secundario |
| Lanzar sin `--dangerously-skip-permissions` | El secundario se atasca pidiendo permiso en cada Bash | Siempre `--dangerously-skip-permissions` (riesgo asumido) | | Lanzar sin `--dangerously-skip-permissions` | El secundario se atasca pidiendo permiso en cada Bash | Siempre `--dangerously-skip-permissions` (riesgo asumido) |
| Mergear desde el dir del secundario | Master suele estar en el working tree principal; colisión de HEAD | Mergear desde `~/fn_registry` | | Mergear desde el dir del secundario | Master suele estar en el working tree principal; colisión de HEAD | Mergear desde `~/fn_registry` |
## Funciones del registry que usa este modo (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` + `tmux_window` (alimenta `/fleet` y el watcher). **Invócala por el binario `apps/fleetview/fleetview list --json`**, NUNCA por `./fn run` (la despacha como `go test`). El JSON del CLI aún no expone `role`/`dod_contract`/`dod_status`; léelos de `~/.claude/goals/<session_id>.json` |
| `spawn_fleet_agent_bash_infra` | Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty cuando hay perfil fleet. `--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) |
**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`). 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).
## Ejemplo end-to-end ## Ejemplo end-to-end
Tarea grande: *"añade un endpoint `/api/health` al backend de la app `kanban` y, en paralelo, Tarea grande: *"añade un endpoint `/api/health` al backend de la app `kanban` y, en paralelo,
documenta el grupo de capacidad `deploy` en `docs/capabilities/deploy.md`"*. Dos piezas documenta el grupo de capacidad `deploy` en `docs/capabilities/deploy.md`"*. Dos piezas
independientes: una toca el sub-repo `apps/kanban` (su propio `.git`), la otra toca el repo independientes: una toca el sub-repo `apps/kanban` (su propio `.git`), la otra toca el repo padre
padre `fn_registry` (docs). Aislamiento natural distinto para cada una. `fn_registry` (docs). Aislamiento natural distinto para cada una.
```bash ```bash
# 1. Descomponer → 2 secundarios independientes: # 1. Descomponer → 2 secundarios independientes:
# A) health endpoint → sub-repo apps/kanban (aislamiento (a)) # A) health endpoint → sub-repo apps/kanban (aislamiento (a))
# B) doc capability → worktree del padre (aislamiento (b)) # B) doc capability → worktree del padre (aislamiento (b))
# 2. Preparar aislamiento de B (worktree del padre; A ya está aislado por su sub-repo): # 2. Preparar aislamiento de B (A ya está aislado por su sub-repo):
git -C ~/fn_registry worktree add /tmp/orq_capdoc -b orq/cap-deploy master git -C ~/fn_registry worktree add /tmp/orq_capdoc -b orq/cap-deploy master
# 3. Escribir los prompts autónomos (autocontenidos, con reglas de aislamiento): # 3. Escribir los prompts autónomos (autocontenidos, con reglas de aislamiento + DoD-contrato):
# /tmp/orq_health.md → "trabaja en apps/kanban (sub-repo propio), rama issue/health, # /tmp/orq_health.md → trabaja en apps/kanban (sub-repo propio), rama issue/health, push, report.
# commits atómicos de tus paths, push al terminar, report en reports/. No toques el # /tmp/orq_capdoc.md → trabaja SOLO en /tmp/orq_capdoc (worktree), rama orq/cap-deploy, push, report.
# repo padre. Reporta tu progreso en esta terminal."
# /tmp/orq_capdoc.md → "trabaja SOLO en /tmp/orq_capdoc (worktree), rama orq/cap-deploy,
# toca solo docs/capabilities/deploy.md, git add de ese path, push al terminar, report
# en reports/. No toques ~/fn_registry. Reporta tu progreso en esta terminal."
# 4. Lanzar ambos secundarios (cada uno su kitty, su dir aislado): # 4. Lanzar ambos (window de la flota si hay $FLEET_SOCKET; aquí kitty fallback). Tras conocer su
./fn run launch_claude_agent_kitty "kanban · health endpoint" \ # sessionId, escribe su DoD-contrato con set_dod_contract.
~/fn_registry/apps/kanban /tmp/orq_health.md ./fn run launch_claude_agent_kitty "kanban · health endpoint" ~/fn_registry/apps/kanban /tmp/orq_health.md
./fn run launch_claude_agent_kitty "fn_registry · doc deploy" \ ./fn run launch_claude_agent_kitty "fn_registry · doc deploy" /tmp/orq_capdoc /tmp/orq_capdoc.md
/tmp/orq_capdoc /tmp/orq_capdoc.md
# 5. Seguir la flota (cada turno): # 5. Seguir cada turno: drena FLEET-STATE, verifica DICE_TERMINADO, nudge a ESTANCADO, lee reports/ (maquinaria en orchestration.md).
./fn run list_claude_agents
# → tabla con los 2 secundarios vivos (PID, cwd, sessionId, status) + tu SELF.
# Lee /tmp/orq_*_kitty.log para el arranque; cuando terminen, lee sus reports/.
# 7. Integrar (desde el working tree principal): # 7. Integrar (desde el working tree principal):
git -C ~/fn_registry/apps/kanban merge --no-ff issue/health # sub-repo de la app git -C ~/fn_registry/apps/kanban merge --no-ff issue/health # sub-repo de la app
git -C ~/fn_registry merge --no-ff orq/cap-deploy # repo padre (la doc) git -C ~/fn_registry merge --no-ff orq/cap-deploy # repo padre (la doc)
git -C ~/fn_registry worktree remove /tmp/orq_capdoc # limpiar worktree git -C ~/fn_registry worktree remove /tmp/orq_capdoc # limpiar worktree
# Resumen al humano: A integrado (endpoint + test verde), B integrado (doc), flota vacía.
# Resumen al humano: A integrado (endpoint + test verde), B integrado (doc),
# flota vacía. Tarea grande hecha.
``` ```
## Salida del modo ## Salida del modo
Cuando el humano escriba `salir orquestador` o `fin orquestador`, cierra con un resumen de la Cuando el humano escriba `salir orquestador` o `fin orquestador`, cierra con un resumen de la flota:
flota: secundarios lanzados, cuáles terminaron e integraste, cuáles siguen vivos (con su kitty secundarios lanzados, cuáles terminaron e integraste, cuáles siguen vivos (con su terminal para que el
para que el humano decida), y los reports generados. Si quedan secundarios vivos, recuérdale que humano decida), y los reports generados. Si quedan secundarios vivos, recuérdale que
`list_claude_agents` los lista y que para pararlos es kill por PID exacto, nunca `pkill`. `list_claude_agents` los lista y que para pararlos es kill dirigido / por PID exacto, nunca `pkill`
(paso 6).
## Relación con otras reglas ## Relación con otras reglas
- `.claude/rules/autonomous_loop.md``fn-orquestador` (Agent tool, sandbox no-interactivo). Es - `.claude/rules/orchestration.md` — la maquinaria del modo: seguir la flota, watcher + cola,
lo que este modo **no** es; tenlas claras separadas. clasificación, políticas, verificador, auto-kill, nudge, splitter, cadencia, y el catálogo de
- `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*` funciones del grupo `orchestration`.
gitignored): el aislamiento natural (opción (a)) y el gotcha de `git init` antes de limpiar un - `.claude/rules/autonomous_loop.md` — `fn-orquestador` (Agent tool, sandbox no-interactivo). Es lo
worktree con una app nueva dentro. que este modo **no** es; tenlas claras separadas.
- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario: - `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*` gitignored):
report con evidencia ejecutable + gaps. el aislamiento natural (opción (a)) y el gotcha de `git init` antes de limpiar un worktree.
- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario.
- `.claude/rules/delegation.md` + `.claude/rules/registry_calls.md` — los secundarios siguen - `.claude/rules/delegation.md` + `.claude/rules/registry_calls.md` — los secundarios siguen
registry-first y delegan a `fn-constructor` igual que tú. registry-first y delegan a `fn-constructor` igual que tú.
- Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`, - Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`,
+1
View File
@@ -42,3 +42,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 35 | [llm_invocation.md](llm_invocation.md) | Invocacion de LLM: SIEMPRE `ask_llm` (grupo `claude-direct`, API directa, arranque 0), NUNCA `claude -p` (lento, cold start). One-shot/streaming/tool-loop + legacy `claude_stream_go_core` deprecado. | | 35 | [llm_invocation.md](llm_invocation.md) | Invocacion de LLM: SIEMPRE `ask_llm` (grupo `claude-direct`, API directa, arranque 0), NUNCA `claude -p` (lento, cold start). One-shot/streaming/tool-loop + legacy `claude_stream_go_core` deprecado. |
| 36 | [reports.md](reports.md) | Reports: reportes de trabajo como artefacto local (entregable de tarea con evidencia). Gitignored salvo `.gitkeep`, NO suben a Gitea ni se indexan (como vaults+playgrounds). Viven en `reports/` o `projects/<p>/reports/`. Convencion + plantilla. ADR 0006. | | 36 | [reports.md](reports.md) | Reports: reportes de trabajo como artefacto local (entregable de tarea con evidencia). Gitignored salvo `.gitkeep`, NO suben a Gitea ni se indexan (como vaults+playgrounds). Viven en `reports/` o `projects/<p>/reports/`. Convencion + plantilla. ADR 0006. |
| 37 | [flow_replay.md](flow_replay.md) | Flow replay: guardar un flujo web (login, reiniciar server, formulario) como funcion del registry. Patron grabar→destilar→reproducir con jerarquia HTTP puro > headless chromium > visible humanizado. Empieza por Nivel 1. Seguridad: HAR sensible, secrets a pass, acciones con efecto exigen confirmacion. Grupo `flow-replay`. Issue 0087. | | 37 | [flow_replay.md](flow_replay.md) | Flow replay: guardar un flujo web (login, reiniciar server, formulario) como funcion del registry. Patron grabar→destilar→reproducir con jerarquia HTTP puro > headless chromium > visible humanizado. Empieza por Nivel 1. Seguridad: HAR sensible, secrets a pass, acciones con efecto exigen confirmacion. Grupo `flow-replay`. Issue 0087. |
| 38 | [orchestration.md](orchestration.md) | Maquinaria del modo `/orquestador`: seguir la flota (fleetview, tiempo de actividad), cola del watcher (events.jsonl, push activo, FLEET-STATE), clasificacion (`classify_fleet_termination`), politicas por clasificacion, verificador adversarial de cierres, auto-kill (`kill_fleet_agent`), nudge, splitter, cadencia + catalogo de funciones del grupo `orchestration`. Tope de fan-out=6. Flow 0012. |
+301
View File
@@ -0,0 +1,301 @@
## 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:
```bash
./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])` — cruza `pgrep -x claude` con los
`sessions/<PID>.json` (con validación anti-PID-reciclado), marca tu propia sesión como `SELF`,
y reporta cwd + sessionId de cada secundario (para retomar con `claude --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:
```bash
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). Gotcha: el JSON de `fleetview list` **no** incluye todavía `role`/`dod_contract`/`dod_status`;
para esos campos lee el sidecar `~/.claude/goals/<session_id>.json` (ver abajo).
**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.
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`:
```bash
./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.
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
```bash
./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`. Como `fleetview list --json` no expone `role`, resuélvelo
leyendo el sidecar del goal de cada `session_id`:
```bash
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 marca `set_dod_contract <sid> "<contract>" met`, informa a la persona y
**cierra el ejecutor para liberar el slot idle** con `kill_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:
```bash
./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 del `goal.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-run` para 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:
```bash
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`:
```bash
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` + `tmux_window` (alimenta `/fleet` y el watcher). **Invócala por el binario `apps/fleetview/fleetview list --json`**, NUNCA por `./fn run` (la despacha como `go test`). El JSON del CLI aún no expone `role`/`dod_contract`/`dod_status`; léelos de `~/.claude/goals/<session_id>.json` |
| `spawn_fleet_agent_bash_infra` | Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty cuando hay perfil fleet. `--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) |
**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`). 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 de `git init` antes 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 a `fn-constructor`.
- Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`,
`claude-session-pid-mapping`, `prefiere-kitty-terminal`.
@@ -37,6 +37,14 @@ ROLE=""
# Solo el orquestador recibe el feed de la flota. Resto: silencio total. # Solo el orquestador recibe el feed de la flota. Resto: silencio total.
[ "$ROLE" != "orchestrator" ] && exit 0 [ "$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}" PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/fn_registry}"
PY="$PROJECT_DIR/python/.venv/bin/python3" PY="$PROJECT_DIR/python/.venv/bin/python3"
{ [ -x "$PY" ] && [ -d "$PROJECT_DIR/python/functions" ]; } || exit 0 { [ -x "$PY" ] && [ -d "$PROJECT_DIR/python/functions" ]; } || exit 0