---
name: fleet-orchestrator-dod
id: 0012
status: pending
created: 2026-06-20
updated: 2026-06-20
priority: high
risk: medium
related_issues: []
apps:
  - fleetview
  - fleet_watcher
  - dag_engine
trigger: manual
schedule: ""
expected_runtime_s: 0
tags: [orchestration, fleet, dod, multi-agent, watcher]

# Contrato de evidencia DoD del sistema completo (las superficies observables que
# prueban que el meta-orquestador funciona, no solo que compila).
dod_evidence_schema:
  - id: watcher_events
    kind: cmd
    expected: "wc -l ~/.claude/fleet/events.jsonl > 0; cada linea es una TRANSICION de estado (edge), no un nivel repetido"
    required: true
  - id: dod_contract_on_spawn
    kind: cmd
    expected: "todo agente lanzado por el orquestador tiene dod_contract no vacio en su goal.json; spawn sin dod_contract se rechaza"
    required: true
  - id: verifier_verdict
    kind: log
    expected: "al cerrar un agente, existe un veredicto del verificador (met|failed) con evidencia citada; el verificador NO es el mismo agente que ejecuto la tarea"
    required: true
  - id: human_load_reduction
    kind: url
    expected: "con N>=10 agentes vivos, el orquestador presenta UN resumen agrupado por prioridad (no N mensajes sueltos); el humano responde solo lo que requiere decision"
    required: true
  - id: push_on_reclama
    kind: screenshot
    expected: "un agente que pasa a waiting/preguntando/bloqueado dispara PushNotification al movil en < 1 min"
    required: true
  - id: stall_nudge
    kind: log
    expected: "un agente idle con dod_contract incompleto y sin actividad N min recibe un nudge automatico (send-keys) registrado en fleet_events; jamas se nudgea a un agente en waiting"
    required: false
---

## Goal

Un meta-orquestador que permita a una persona manejar una flota de 20-30 agentes Claude hablando solo con uno. El orquestador no entra nunca en los detalles de cada agente: vigila estados, persigue que cada agente **termine lo que empieza** (cumpla un DoD-contrato fijo), y solo escala a la persona lo que requiere su decisión. La métrica de salud es el **throughput de DoD cumplidos**, no el número de agentes vivos.

## Problema que resuelve

Hoy lanzar muchos Claudes produce saturación: N ventanas dispersas, N agentes que quedan idle sin cerrar nada, y la persona como cuello de botella revisando todo. El criterio de "terminado" tampoco existe de forma estable: el campo `dod` del `goal.json` lo reescribe el hook GOAL-TRACKER con cada prompt (es un resumen móvil), así que no hay un blanco fijo contra el que evaluar la terminación. Resultado: 30 agentes vivos que no resuelven nada.

## Arquitectura: 4 roles

El orquestador delega, nunca ejecuta (regla `orquestador-delega-no-ejecuta`). Reparto:

```
orquestador  (la persona habla SOLO con el; solo vigila, agrupa y escala)
  ├── splitter      agente EFIMERO. Tarea grande -> la parte en sub-tareas
  │                 atomicas (paralelas o secuenciales), cada una con su
  │                 dod_contract pequeno y verificable. Tope de fan-out.
  ├── ejecutores    Claudes INTERACTIVOS en la flota tmux (los 20-30 que la
  │                 persona ve). Cada uno con UNA tarea y UN dod_contract.
  └── verificador   agente EFIMERO e INDEPENDIENTE del ejecutor. Al cierre:
                    compara lo hecho contra el dod_contract -> met | failed
                    con evidencia citada. Cero auto-aprobacion.
```

Distinción dura: **splitter y verificador son agentes efímeros** (subagentes vía Agent tool / SDK: corren, devuelven un resultado estructurado y mueren). NO ocupan slot en la flota visible. La flota que la persona maneja = solo **ejecutores con tarea**. La maquinaria de verificación y descomposición es invisible para ella.

El reparto de coste: **el watcher vigila (barato, sin LLM, siempre activo); el orquestador y los agentes efímeros piensan (caro, solo cuando hay algo que decidir).**

## Modelo de datos: DoD-contrato fijo

En el `goal.json` de cada agente conviven dos campos distintos:

- `dod` (ya existe) — resumen móvil que el hook GOAL-TRACKER reescribe con cada prompt. Se queda como está.
- `dod_contract` (NUEVO, FIJO) — criterio de aceptación con evidencia ejecutable, escrito UNA vez al lanzar el agente y nunca reescrito por hooks. Es el blanco estable contra el que se evalúa "terminado".
- `dod_status` (NUEVO) — `pending | met | failed`, lo actualiza el verificador.

El hook GOAL-TRACKER debe respetar `dod_contract`/`dod_status` (solo reescribe `dod`). Spawn sin `dod_contract` se rechaza: ningún agente arranca sin saber cuándo habrá terminado.

## Máquina de terminación (lo que el watcher clasifica, mecánico, sin LLM)

| Estado del agente | Clasificación | Acción |
|---|---|---|
| `waiting` / phase `preguntando`/`bloqueado` | RECLAMA | escalar a la persona (push inmediato) |
| `idle` + phase `hecho` | DICE-TERMINADO | orquestador lanza verificador contra `dod_contract` |
| `idle` + phase≠hecho + sin actividad N min | ESTANCADO | nudge automático: "cierra tu DoD" |
| `busy` + phase `haciendo`/`testeando` | TRABAJANDO | no molestar |
| sin `dod_contract` | MAL LANZADO | bloquear / re-lanzar con DoD |

"Dar por terminado al hablar con ellos": cuando la persona se enruta a un ejecutor, lo primero es cerrar su `dod_contract` — si el verificador dice met, se cierra/reasigna; si quedó a medias, se empuja a terminar antes de abrir nada nuevo.

## Fases de construcción

### Fase 1 — watcher (cerebro barato, sin LLM) — DENTRO de fleetview [HECHO 2026-06-20]

Decisión: NO es un daemon aparte. fleetview ya es un proceso vivo que pollea la flota cada segundo y vive mientras la sesión tmux fleet (y por tanto la flota) exista. El watcher se embebe ahí (KISS). En cada refresco de la TUI:
1. Snapshot del fleet (`list_claude_fleet`, con `dod_contract`/`dod_status`/`role`).
2. Clasifica cada agente con `classify_fleet_termination` (función pura del registry).
3. Diff contra el snapshot anterior (en memoria) -> transiciones (edge-triggered, no nivel).
4. Escribe un evento por transición en la cola JSONL `~/.claude/fleet/events.jsonl` (sin SQLite/CGO — KISS). Línea: `{ts, session_id, pid, from, to, goal, phase, urgent}`.
5. Marca `urgent=true` en transición a RECLAMA. El push real al móvil lo hace el orquestador (Fase 2) leyendo la cola; el watcher solo marca.

Estado: modelo de datos (`DodContract`/`DodStatus`/`Role` en `ClaudeFleet`) + `classify_fleet_termination` + watcher embebido (`watcher.go`) — construidos y testeados (7 tests del watcher + 34 del clasificador). **Validado en vivo 2026-06-20**: tras relanzar fleetview con el binario nuevo, una transición real (inyectar `dod_status=met` en un agente idle → `MAL_LANZADO`→`DICE_TERMINADO`) quedó escrita como una línea en `~/.claude/fleet/events.jsonl`; el `goal.json` se restauró. Las 3 capas DoD de Fase 1 cumplidas.

Hallazgo: toda la flota lanzada hasta hoy clasifica `MAL_LANZADO` (ningún `dod_contract` escrito todavía). Es el comportamiento correcto (regla "ningún agente sin DoD") y lo que Fase 3 corrige al escribir `dod_contract` en el spawn.

DoD Fase 1:
- Golden: un agente pasa busy->idle -> aparece 1 evento `DICE-TERMINADO` o `ESTANCADO` en `fleet_events`.
- Edge 1: el mismo agente sigue idle 10 ticks -> NO se duplica el evento (edge, no nivel).
- Edge 2: un agente pasa a waiting -> evento RECLAMA + push en < 1 min.
- Error 1: goal.json corrupto/ausente -> el agente se clasifica MAL LANZADO sin crash del watcher.
- Vida: 7 días corriendo, 0 crashes (`journalctl`/log), cola sin huecos.

### Fase 2 — orquestador-Claude reactivo + verificador + splitter

Extiende el skill `/orquestador`. NO hace polling. Despierta por: la persona | heartbeat largo (ScheduleWakeup 20-30 min) | push del watcher. Al despertar:
1. Vacía `fleet_events`, agrupa por prioridad (RECLAMA > DICE-TERMINADO > ESTANCADO) y por ámbito.
2. Para DICE-TERMINADO: lanza un **verificador** (Agent efímero) que compara el output del ejecutor con su `dod_contract` -> met/failed+evidencia. met -> autocierra y reporta; failed -> nudge al ejecutor con el gap o escala.
3. Para ESTANCADO: nudge (send-keys) bajo política (solo idle con DoD pendiente; jamás waiting).
4. Para RECLAMA: presenta a la persona UN resumen corto con la decisión concreta que se necesita. Usa `/fleet focus` para saltarla al agente elegido.

DoD Fase 2:
- Golden: un agente DICE-TERMINADO con DoD realmente cumplido -> verificador met -> autocierre + reporte, sin intervención humana.
- Edge 1: agente DICE-TERMINADO con DoD a medias -> verificador failed -> nudge con el gap, no se cierra.
- Edge 2: 10 agentes con eventos a la vez -> un solo resumen agrupado, no 10 mensajes.
- Error 1: verificador no puede leer el output -> reporta "no evaluable", escala, no autocierra en falso.
- Vida: 7 días gestionando flota real; la persona responde solo decisiones, no enrutamiento.

Estado [CONSTRUIDO 2026-06-20]: primitivas `drain_fleet_events` (consume la cola, 7 tests) y `set_dod_contract` (escribe el DoD-contrato fijo, 5 tests) creadas, indexadas y validadas (set_dod_contract en vivo). Skill `/orquestador` evolucionado con la sección "Consumo de la cola de la flota": DoD-contrato obligatorio al lanzar, drenar, políticas por clasificación, verificador independiente (lee el report vs dod_contract), splitter con tope de fan-out, cadencia. Pendiente (capa Vida): uso real del modo — el verificador y el splitter son prompts de Agent en el skill, aún no ejercitados en un cierre real (requiere un agente con report terminado).

### Fase 3 — spawn dentro de la flota + splitter

Extiende `/orquestador` para lanzar ejecutores con `TmuxNewClaudeWindow` (socket fleet) en vez de kitties sueltas, escribiendo `dod_contract` en el `goal.json` del nuevo agente y un prompt con el DoD claro. Antes de spawnar, si la tarea se estima grande, pasa por el **splitter** (Agent efímero) que devuelve un plan de sub-tareas con dependencias; el orquestador spawna un ejecutor por sub-tarea (paralelas a la vez, secuenciales encadenadas).

DoD Fase 3:
- Golden: una tarea atómica -> 1 ejecutor en la flota con `dod_contract` escrito; `/fleet` lo lista.
- Edge 1: una tarea grande -> splitter devuelve >=2 sub-tareas, cada una con su `dod_contract`; se spawnan respetando deps.
- Edge 2: tope de fan-out -> el splitter nunca genera más de K sub-agentes de golpe (sin explosión).
- Error 1: spawn sin `dod_contract` -> rechazado con mensaje claro.
- Vida: 7 días lanzando trabajo real por esta vía.

Estado [PARCIAL 2026-06-20]: el **launcher** (`launch_fleetclaude`) ya arranca el orquestador con el skill `/orquestador` embebido como primer prompt (validado en vivo: entra en modo), le pone `role=orchestrator` con `mark_claude_role` (la TUI lo pinea arriba con ★, validado), y siembra 1 ejecutor idle inicial en la flota tmux. El `dod_contract` al spawn está en el skill (Fase 2). **Gap 1 CERRADO [2026-06-20]**: `spawn_fleet_agent` lanza ejecutores (y el orquestador) como windows de la flota tmux del perfil (no kitties sueltas), con `--skill` para arrancar en modo, `--prompt-file` para ejecutores autocontenidos y `--role` (via `mark_claude_role`). El skill `/orquestador` (paso 2) ahora prefiere `spawn_fleet_agent` sobre kitty cuando hay `$FLEET_SOCKET`. Validado en vivo: el orquestador arrancó en `fleet2` en modo, `role=orchestrator`, pinneado arriba, sin perder los 9 ejecutores existentes. **Gaps restantes** (capa Vida): el verificador independiente y el splitter están descritos en el skill pero aún no ejercitados en un cierre/descomposición real (requiere un ciclo completo de tarea con report).

## Pre-requisitos

- Sesión tmux fleet activa (perfil `launch_fleetclaude`); `/fleet` operativo (flow previo).
- PushNotification configurado (Remote Control activo en el móvil).
- dag_engine activo para schedule del watcher (regla `dag-engine-over-cron`).

## Acceptance

- [ ] `dod_contract` fijo escrito al spawn y respetado por el hook GOAL-TRACKER.
- [ ] watcher edge-triggered con eventos en `fleet_events` + push en RECLAMA.
- [ ] verificador independiente del ejecutor, con veredicto+evidencia.
- [ ] splitter con tope de fan-out para tareas grandes.
- [ ] orquestador presenta resumen agrupado, no N mensajes; usa `/fleet focus`.
- [ ] la persona maneja >=10 agentes respondiendo solo decisiones.

## Definition of Done

- [ ] **Mecánica**: watcher + funciones del registry compilan, `fn doctor` verde, sin drift `uses_functions`.
- [ ] **Cobertura**: cada fase con su golden + >=2 edge + >=1 error path, evidencia ejecutable (ver DoD por fase).
- [ ] **Vida útil**: >=7 días de uso real gestionando flota, 0 crashes del watcher, 0 "done" falsos detectados (verificador funciona).
- [ ] **Carga humana**: medible reducción — la persona responde decisiones, no enrutamiento ni vigilancia.
- [ ] **Secrets**: cero credenciales fuera de pass/vaults; el watcher no loguea contenido de sesiones, solo estados/transiciones.

## Notas (onboarding)

Para usarlo: lanzas trabajo por el orquestador (no abres Claudes a mano). Cada tarea recibe un `dod_contract`. El watcher vigila en background y empuja al móvil cuando un agente te reclama. Cuando vuelves, el orquestador te da un resumen agrupado y te lleva (`/fleet focus`) solo a lo que necesita tu decisión; lo demás (verificar cierres, empujar estancados, dividir tareas grandes) lo hace solo con agentes efímeros. La flota que ves = ejecutores con tarea; la maquinaria de verificación/división es invisible.

Relación con otras reglas: `dod_quality` (las 3 capas + verificador independiente), `orquestador-delega-no-ejecuta` (el orquestador no ejecuta), `dag-engine-over-cron` (schedule del watcher), `autonomous_loop` (fn-orquestador autónomo es el primo no-interactivo de este flujo), y el flow previo de `/fleet`/`fleetview` (la base de datos de estado).
