--- name: fleet_send_text kind: function lang: bash domain: infra version: 1.0.0 purity: impure signature: "fleet_send_text \"\" [--socket ] [--no-enter] [--retries N] [--dry-run]" description: "Empuja texto a UN agente de la flota tmux de forma fiable, resolviendo su pane_id (%N) ESTABLE FRESCO justo antes de enviar. Es el reemplazo del nudge antiguo del orquestador, que apuntaba al window_id (@N) leido del JSON de la flota: ese @N MIGRA cuando el focus-swap de FleetView (break-pane + join-pane) recrea windows, asi que enviar al @N viejo (cacheado por el bloque FLEET-STATE o leido un instante antes) mandaba el texto al window equivocado o a otro agente. fleet_send_text resuelve sessionId -> PID (sessions/.json) -> el pane cuyo proceso (o un ancestro suyo en /proc) es pane_pid, leyendo tmux list-panes -a en el momento del envio, y usa el pane_id (%N) que NO migra. Ademas manda el texto literal (send-keys -l) y el Enter en invocaciones SEPARADAS, verificando con capture-pane que el texto aparecio en el input antes de pulsar Enter; reintenta si no aparece. Guards: NO envia a tu propio pane; error claro si el target no resuelve a un pane vivo. Por defecto EJECUTA; --dry-run imprime el plan sin enviar." tags: [fleet, claude-fleet, orchestration, tmux, nudge, send-keys, infra] uses_functions: [] uses_types: [] error_type: error_go_core file_path: "bash/functions/infra/fleet_send_text.sh" tested: true tests: - "golden: envio por PID resuelve el pane_id estable, inyecta el texto y se verifica via capture-pane" - "edge: tras break-pane (focus-swap) el pane_id NO migra y el reenvio sigue llegando" - "edge: resolucion por prefijo de sessionId (sessions/.json) entrega el texto" - "edge: --dry-run no inyecta nada y reporta status=dry-run" - "error: sessionId no resuelto rc=2; falta texto rc=2; PID sin pane vivo rc=4" - "guard: enviar a la sesion actual (self) rc=3" test_file_path: "bash/functions/infra/fleet_send_text_test.sh" params: - name: target desc: "Primer arg posicional: sessionId del agente (exacto o prefijo) o su PID (todo digitos). Por sessionId se busca en sessions/*.json el que case y su archivo (.json) da el PID; por PID se usa directo." - name: texto desc: "Segundo arg posicional: el texto a inyectar en el input del agente (entre comillas)." - name: --socket desc: "Socket tmux del perfil FleetView donde vive el pane. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada." - name: --no-enter desc: "Deja el texto en el input sin pulsar Enter (no hace submit). Por defecto envia el Enter en una invocacion separada tras el texto." - name: --retries desc: "Numero de reintentos si el texto no aparece en el pane tras el send (default 2). Cada reintento limpia el input con C-u antes de reenviar." - name: --dry-run desc: "Imprime el plan (PID, sessionId, pane, socket) y NO envia nada. Sin esto, ejecuta." output: "Imprime una linea de plan (target, PID, sessionId, socket, pane resuelto, modo de envio) y una linea final parseable 'pane=%N intento=N status=ok|dry-run'. Exit 0 ok/dry-run; 2 uso incorrecto o target no resuelto a PID; 3 guard (target es la sesion actual); 4 no se encontro pane vivo para el target; 5 enviado pero no verificado tras los reintentos." --- # fleet_send_text Empuja texto al input de **un** agente de la flota tmux de forma fiable. Resuelve el `pane_id` (`%N`) **estable** del agente **fresco** justo antes de enviar (nunca cachea el `window_id` `@N`, que migra con el focus-swap), manda el texto literal y el `Enter` en invocaciones **separadas**, y verifica con `capture-pane` que el texto llegó antes de hacer submit. Es el reemplazo del patrón de nudge antiguo (`tmux send-keys -t `), que fallaba "a veces" porque enviaba al window equivocado tras un focus-swap. ## Ejemplo ```bash # Nudge a un ejecutor estancado por sessionId (el orquestador lo llama tras detectar ESTANCADO): ./fn run fleet_send_text 32945650-a4e1-472b-90c9-5b38ef60a463 \ "Sigues idle con tu DoD-contrato sin cerrar. Falta: el error path con evidencia. Cierralo o reporta el bloqueo." \ --socket "$FLEET_SOCKET" # Por prefijo de sessionId, en el socket por defecto ($FLEET_SOCKET o "fleet"): ./fn run fleet_send_text 32945650 "Recuerda pushear la rama antes de cerrar." # Dejar texto en el input sin hacer submit (--no-enter), o solo ver el plan (--dry-run): ./fn run fleet_send_text 48213 "borrador..." --no-enter ./fn run fleet_send_text 48213 "texto" --dry-run ``` ## Cuando usarla Úsala desde el modo orquestador siempre que necesites **inyectar texto en el input de un agente** de la flota: el **nudge** a un `ESTANCADO`, el aviso de un gap concreto a un ejecutor cuyo cierre falló la verificación, o cualquier mensaje dirigido. Sustituye al `tmux send-keys -t ` manual. Resuelve el target por sessionId (exacto o prefijo) o por PID. **Solo a idle/ESTANCADO; jamás a un agente en `waiting`/`preguntando`** (esos te reclaman a ti, no un empujón del bot). Para *cerrar* un ejecutor verificado `met` no es esto: usa `kill_fleet_agent`. ## Gotchas - **El bug que arregla — el `window_id` (`@N`) MIGRA**: el focus-swap de FleetView (`tmux_swap_window_into_console.go`) trae el claude objetivo a la console con `break-pane` + `join-pane`, lo que **recrea windows** y cambia el `@N` del agente (`@32` → `@34`). El bloque `FLEET-STATE` y el JSON de la flota pueden traer un `@N` ya viejo. Enviar a ese `@N` manda el texto al window equivocado o a otro agente. Esta función NUNCA usa `@N`: resuelve el `pane_id` (`%N`), que se **preserva** durante toda la vida del pane aunque el pane se mueva de window. Verificado en test: tras `break-pane` el `window_id` pasa de `@0` a `@1` pero el `pane_id` sigue `%0` y el envío sigue llegando. - **Resolución fresca**: el mapa `pane_pid → pane_id` se lee con `tmux -L list-panes -a` **en el momento del envío**, no se cachea. La resolución sube por los ancestros de `/proc` desde el PID del agente hasta casar un `pane_pid`: cubre tanto `exec claude` (pane_pid == claude pid, match directo, como hace `spawn_fleet_agent`) como un claude lanzado bajo un shell (pane_pid == shell ancestro). - **Texto y Enter separados**: el texto va con `send-keys -l` (literal, sin interpretar nombres de tecla), luego `sleep 0.3`, y el `Enter` en una **invocación aparte**. Mandar texto+Enter juntos hace que el TUI de Claude Code a veces no interprete el Enter como submit. La verificación con `capture-pane` se hace **antes** del Enter (tras el submit el TUI vacía el input y no se podría comprobar). Si el texto no aparece, limpia el input con `C-u` y reintenta (`--retries`, default 2). - **Impura**: inyecta teclas en un pane ajeno. Por defecto EJECUTA; usa `--dry-run` para inspeccionar el plan antes. - **Guard anti-self**: resuelve el PID de `claude` de la sesión actual subiendo por los ancestros de `/proc`; si el target coincide, rehúsa con exit 3 ("No me autoenvio"). - **Verificación por fragmento ancla**: comprueba que aparezcan los primeros 24 caracteres del texto (no el texto completo) para no dar falso negativo cuando el input del TUI wrapea un mensaje largo en varias líneas. - **Socket**: si no pasas `--socket`, usa `$FLEET_SOCKET` o `"fleet"`. Si el agente no está en ese socket, no se encontrará el pane (exit 4).