feat(infra): auto-commit con 8 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 15:35:59 +02:00
parent 6f4b440762
commit 46954d8584
8 changed files with 444 additions and 22 deletions
+7 -4
View File
@@ -3,10 +3,10 @@ name: kill_fleet_agent
kind: function kind: function
lang: bash lang: bash
domain: infra domain: infra
version: 1.0.0 version: 1.1.0
purity: impure purity: impure
signature: "kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]" signature: "kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]"
description: "Cierre limpio y dirigido de UN ejecutor de la flota tmux. Dado un sessionId (exacto o prefijo) o un PID, manda SIGTERM al proceso claude del ejecutor (cierre limpio, recuperable con claude --resume) y cierra su window tmux (kill-window) en el socket del perfil FleetView. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Guards de seguridad: NUNCA mata a un agente con role=orchestrator (leido de su goal.json) ni a la sesion que invoca la funcion (resuelve su propio PID de claude por los ancestros de /proc). Por defecto EJECUTA; --dry-run imprime el plan sin tocar nada. Es el cierre dirigido a UN agente, frente a reboot_all_claudes que opera sobre toda la flota." description: "Cierre limpio y dirigido de UN ejecutor de la flota tmux. Dado un sessionId (exacto o prefijo) o un PID, manda SIGTERM al proceso claude del ejecutor (cierre limpio, recuperable con claude --resume) y cierra su window tmux en el socket del perfil FleetView. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Tres guards de seguridad: NUNCA mata a un agente con role=orchestrator (leido de su goal.json); NUNCA a la sesion que invoca la funcion (resuelve su propio PID de claude por los ancestros de /proc); y NUNCA cierra la window que aloja la TUI fleetview o la window 'console' con kill-window (eso se llevaria el panel de control por delante) — en ese caso cierra SOLO el pane del target con kill-pane y preserva la TUI. Por defecto EJECUTA; --dry-run imprime el plan (incluida la accion kill-pane vs kill-window) sin tocar nada. Es el cierre dirigido a UN agente, frente a reboot_all_claudes que opera sobre toda la flota."
tags: [fleet, claude-fleet, orchestration, tmux, kill, infra] tags: [fleet, claude-fleet, orchestration, tmux, kill, infra]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
@@ -17,6 +17,7 @@ tests:
- "golden: ejecutor por sessionId, PID y prefijo se resuelve y dry-run imprime el plan" - "golden: ejecutor por sessionId, PID y prefijo se resuelve y dry-run imprime el plan"
- "guard: matar un role=orchestrator devuelve rc=3 y se niega" - "guard: matar un role=orchestrator devuelve rc=3 y se niega"
- "guard: matar la sesion actual (self) devuelve rc=3 y se niega" - "guard: matar la sesion actual (self) devuelve rc=3 y se niega"
- "guard3: predicado _fleet_window_hosts_tui detecta window 'console' o pane fleetview"
- "error: target no resuelto rc=2; sin target rc=2" - "error: target no resuelto rc=2; sin target rc=2"
test_file_path: "bash/functions/infra/kill_fleet_agent_test.sh" test_file_path: "bash/functions/infra/kill_fleet_agent_test.sh"
params: params:
@@ -55,11 +56,13 @@ Cierra de forma dirigida UN ejecutor de la flota tmux: SIGTERM al proceso `claud
- **Impura y destructiva**: manda SIGTERM y cierra una window tmux. Por defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado `met`); usa `--dry-run` para inspeccionar antes. - **Impura y destructiva**: manda SIGTERM y cierra una window tmux. Por defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado `met`); usa `--dry-run` para inspeccionar antes.
- **Guard anti-orquestador**: si el goal.json del target tiene `role=orchestrator`, rehúsa con exit 3. Evita decapitar la flota por error. El `role` se lee de `~/.claude/goals/<sessionId>.json` (lo escribe `mark_claude_role`). - **Guard anti-orquestador**: si el goal.json del target tiene `role=orchestrator`, rehúsa con exit 3. Evita decapitar la flota por error. El `role` se lee de `~/.claude/goals/<sessionId>.json` (lo escribe `mark_claude_role`).
- **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 suicido"). Es el equivalente dirigido de la regla "nunca `pkill claude`". - **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 suicido"). Es el equivalente dirigido de la regla "nunca `pkill claude`".
- **Resolución de la window**: usa `tmux -L <socket> list-panes -a` y casa `pane_pid == PID`. Funciona porque `spawn_fleet_agent` arranca el ejecutor con `exec claude`, así el `pane_pid` ES el PID de claude. Si no hay socket/tmux, la window queda "(no resuelta)" y solo se manda el SIGTERM (best-effort, no falla). - **Guard 3 — anti-TUI/console (no decapitar el panel)**: antes de cerrar nada, comprueba si la window del target **aloja la TUI fleetview** (algún pane corre el binario `fleetview`) o se llama **`console`**. El layout FleetView mete la TUI y un Claude en la misma window `console`, y los focus-swaps (`join-pane`) pueden meter al ejecutor target en esa window; un `kill-window` ahí se llevaría la TUI por delante (causa del fallo descrito en `fleetview` v0.4.3). En ese caso la función NO usa `kill-window`: manda el SIGTERM al claude y cierra **solo su pane** con `kill-pane`, preservando el pane de la TUI. El plan (y el `--dry-run`) lo refleja como `accion: kill-pane … (aloja la TUI/console)` vs `accion: kill-window …`. El predicado es la función interna `_fleet_window_hosts_tui` (testeada). Se mantiene inline (no función propia del registry) por estar acoplada a este flujo y para no dejar una capacidad huérfana (KISS).
- **Resolución de la window y el pane**: usa `tmux -L <socket> list-panes -a` y casa `pane_pid == PID`, capturando `window_id`, `pane_id` y `window_name`. Funciona porque `spawn_fleet_agent` arranca el ejecutor con `exec claude`, así el `pane_pid` ES el PID de claude. Si no hay socket/tmux, la window queda "(no resuelta)" y solo se manda el SIGTERM (best-effort, no falla).
- **SIGTERM, no SIGKILL**: cierre limpio para que Claude Code persista su sesión; el trabajo se puede retomar con `claude --resume <sessionId>`. - **SIGTERM, no SIGKILL**: cierre limpio para que Claude Code persista su sesión; el trabajo se puede retomar con `claude --resume <sessionId>`.
- **Requiere `jq`** para leer los JSON de sessions/goals. - **Requiere `jq`** para leer los JSON de sessions/goals.
- **Overrides de entorno solo para tests**: `FN_FLEET_SESSIONS_DIR`, `FN_FLEET_GOALS_DIR` y `FN_FLEET_SELF_PID` redirigen los directorios y fuerzan el PID propio; no usarlos en operación normal. - **Overrides de entorno solo para tests**: `FN_FLEET_SESSIONS_DIR`, `FN_FLEET_GOALS_DIR` y `FN_FLEET_SELF_PID` redirigen los directorios y fuerzan el PID propio; no usarlos en operación normal.
## Capability growth log ## Capability growth log
(v1.0.0 — sin cambios todavía.) - v1.1.0 (2026-06-24) — **Guard 3 anti-TUI/console** (elimina un gotcha conocido). Antes, si un focus-swap metía al ejecutor target en la window `console` (la que aloja la TUI fleetview), `kill-window` cerraba la TUI por error. Ahora, cuando la window del target aloja la TUI (pane `fleetview`) o se llama `console`, se cierra solo el pane del target con `kill-pane` y la TUI sobrevive; el resto de windows siguen cerrándose con `kill-window`. Predicado interno `_fleet_window_hosts_tui` con tests. Es la causa raíz que complementa el auto-respawn de la TUI (`supervise_fleetview_tui`).
- v1.0.0 — versión inicial.
+76 -10
View File
@@ -26,6 +26,25 @@
set -euo pipefail set -euo pipefail
IFS=$' \t\n' IFS=$' \t\n'
# Predicado (puro respecto a tmux): dada una window — su nombre y el texto de sus
# panes en formato "<pane_pid> <pane_current_command>" (una linea por pane) —
# decide si esa window ALOJA la TUI fleetview o es la window 'console' del perfil.
# Si es asi, cerrar la window entera con kill-window se llevaria la TUI por
# delante; el caller debe cerrar solo el pane del target con kill-pane.
# - Nombre de window 'console' = la window del panel FleetView por convencion
# del launcher (y a donde el focus-swap ancla la TUI, ver fleetview v0.4.3).
# - Algun pane corre el binario 'fleetview' (pane_current_command) = la TUI
# vive ahi aunque la window se haya renombrado.
# Devuelve 0 si aloja la TUI/console, 1 si no.
_fleet_window_hosts_tui() {
local window_name="${1:-}" panes_text="${2:-}"
[[ "$window_name" == "console" ]] && return 0
if printf '%s\n' "$panes_text" | awk '{print $2}' | grep -qx 'fleetview'; then
return 0
fi
return 1
}
kill_fleet_agent() { kill_fleet_agent() {
local target="" socket="" dry=0 local target="" socket="" dry=0
@@ -155,27 +174,65 @@ USAGE
fi fi
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Resolver la window tmux del PID en el socket (pane_pid == claude por el # Resolver la window tmux Y el pane del PID en el socket (pane_pid == claude
# `exec claude` de spawn_fleet_agent). Best-effort: vacio si no hay socket. # por el `exec claude` de spawn_fleet_agent). Capturamos window_id, pane_id y
# window_name juntos. Best-effort: vacio si no hay socket.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
local window="" local window="" pane="" wname=""
if command -v tmux >/dev/null 2>&1; then if command -v tmux >/dev/null 2>&1; then
window="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{window_id}' 2>/dev/null \ local line
| awk -v p="$pid" '$1==p {print $2; exit}' || true)" line="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{window_id} #{pane_id} #{window_name}' 2>/dev/null \
| awk -v p="$pid" '$1==p {print $2, $3, $4; exit}' || true)"
if [[ -n "$line" ]]; then
window="$(awk '{print $1}' <<<"$line")"
pane="$(awk '{print $2}' <<<"$line")"
wname="$(awk '{print $3}' <<<"$line")"
fi
fi
# -----------------------------------------------------------------------
# Guard 3 — anti-TUI/console: si la window del target aloja la TUI fleetview
# o es la window 'console' del perfil, NO cerramos la window entera (eso se
# llevaria la TUI), sino solo el pane del target con kill-pane. El layout
# FleetView mete la TUI y un Claude en la misma window 'console', y los
# focus-swaps (join-pane) pueden meter al ejecutor target en esa window.
# -----------------------------------------------------------------------
local hosts_tui=0
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
local panes_text
panes_text="$(tmux -L "$socket" list-panes -t "$window" -F '#{pane_pid} #{pane_current_command}' 2>/dev/null || true)"
if _fleet_window_hosts_tui "$wname" "$panes_text"; then
hosts_tui=1
fi
fi
# Accion sobre la window/pane segun lo resuelto y el Guard 3.
local action
if [[ -z "$window" ]]; then
action="solo SIGTERM (window no resuelta)"
elif [[ "$hosts_tui" -eq 1 ]]; then
if [[ -n "$pane" ]]; then
action="kill-pane $pane (window '${wname:-$window}' aloja la TUI/console; se preserva la TUI)"
else
action="solo SIGTERM (window '${wname:-$window}' aloja la TUI y no se resolvio el pane; window preservada)"
fi
else
action="kill-window $window"
fi fi
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Plan (se imprime siempre). # Plan (se imprime siempre).
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
echo "kill_fleet_agent — target: $target PID: $pid sessionId: ${sid:-?} role: ${role:-executor} socket: $socket window: ${window:-(no resuelta)}" echo "kill_fleet_agent — target: $target PID: $pid sessionId: ${sid:-?} role: ${role:-executor} socket: $socket window: ${window:-(no resuelta)} pane: ${pane:-?} accion: $action"
if [[ "$dry" -eq 1 ]]; then if [[ "$dry" -eq 1 ]]; then
echo "DRY-RUN: no se ha matado el proceso ni cerrado la window." echo "DRY-RUN: no se ha matado el proceso ni cerrado nada."
return 0 return 0
fi fi
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Ejecutar: SIGTERM al claude (cierre limpio) + kill-window (idempotente). # Ejecutar: SIGTERM al claude (cierre limpio) + cierre de pane/window segun
# el Guard 3 (idempotente).
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
if kill -0 "$pid" 2>/dev/null; then if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true
@@ -185,8 +242,17 @@ USAGE
fi fi
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
tmux -L "$socket" kill-window -t "$window" 2>/dev/null || true if [[ "$hosts_tui" -eq 1 ]]; then
echo "kill_fleet_agent: window $window cerrada en el socket $socket." if [[ -n "$pane" ]]; then
tmux -L "$socket" kill-pane -t "$pane" 2>/dev/null || true
echo "kill_fleet_agent: pane $pane cerrado (window '${wname:-$window}' aloja la TUI; window preservada)."
else
echo "kill_fleet_agent: window '${wname:-$window}' aloja la TUI pero no se resolvio el pane; solo SIGTERM (window preservada)."
fi
else
tmux -L "$socket" kill-window -t "$window" 2>/dev/null || true
echo "kill_fleet_agent: window $window cerrada en el socket $socket."
fi
fi fi
return 0 return 0
@@ -104,6 +104,24 @@ set -e
assert_rc "error: sin target devuelve rc=2" 2 "$rc" assert_rc "error: sin target devuelve rc=2" 2 "$rc"
assert_contains "error: mensaje falta target" "falta el target" "$out" assert_contains "error: mensaje falta target" "falta el target" "$out"
# --- Test 7 (Guard 3 predicado): _fleet_window_hosts_tui ---
# La window 'console' SIEMPRE se considera que aloja la TUI (no se cierra entera).
assert_predicate() {
local test_name="$1" expected="$2"; shift 2
set +e
_fleet_window_hosts_tui "$@"; local rc=$?
set -e
assert_rc "$test_name" "$expected" "$rc"
}
# Nombre de window 'console' -> aloja TUI (rc 0), aunque ningun pane sea fleetview.
assert_predicate "guard3: window 'console' aloja la TUI" 0 "console" $'1234 claude\n5678 bash'
# Algun pane corre 'fleetview' -> aloja TUI (rc 0), aunque la window no sea console.
assert_predicate "guard3: pane fleetview aloja la TUI" 0 "claude" $'1111 bash\n2222 fleetview'
# Ni console ni fleetview -> NO aloja la TUI (rc 1): kill-window normal.
assert_predicate "guard3: window normal no aloja la TUI" 1 "claude" $'3333 claude\n4444 bash'
# Substring que contiene 'fleetview' pero no es el comando exacto -> NO matchea (grep -qx).
assert_predicate "guard3: comando 'fleetviewer' no falsea positivo" 1 "work" $'7777 fleetviewer'
echo "---" echo "---"
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1 [[ $FAIL -eq 0 ]] || exit 1
+25 -7
View File
@@ -3,10 +3,10 @@ name: launch_fleetclaude
kind: function kind: function
lang: bash lang: bash
domain: infra domain: infra
version: "1.4.0" version: "1.5.0"
purity: impure purity: impure
signature: "launch_fleetclaude [--cwd <dir>] [--bin <path>] [--session <name>] [--reuse] [--cols <n>]" signature: "launch_fleetclaude [--cwd <dir>] [--bin <path>] [--session <name>] [--reuse] [--cols <n>]"
description: "Entrypoint de FleetView: abre una ventana kitty con una sesion tmux (socket aislado por perfil) de dos panes (TUI fleetview a la izquierda, claude --dangerously-skip-permissions a la derecha) para centralizar la flota de Claudes. Soporta PERFILES multiples: sin --session/--reuse cada invocacion abre un perfil nuevo (fleet, fleet2, fleet3, ...) con su propia flota; inyecta FLEET_SOCKET/FLEET_SESSION a la TUI para que cada panel vea solo sus Claudes. Instala atajos alt+flechas/alt+enter/alt+n que controlan la TUI desde cualquier pane, y fija el ancho del sidebar con hooks." description: "Entrypoint de FleetView: abre una ventana kitty con una sesion tmux (socket aislado por perfil) de dos panes (TUI fleetview a la izquierda, claude --dangerously-skip-permissions a la derecha) para centralizar la flota de Claudes. El pane de la TUI corre dentro del bucle supervisor supervise_fleetview_tui, que la relanza si muere (crash/panic/kill), asi el panel de control NUNCA se pierde. Soporta PERFILES multiples: sin --session/--reuse cada invocacion abre un perfil nuevo (fleet, fleet2, fleet3, ...) con su propia flota; inyecta FLEET_SOCKET/FLEET_SESSION a la TUI para que cada panel vea solo sus Claudes. Instala atajos alt+flechas/alt+enter/alt+n que controlan la TUI desde cualquier pane, y fija el ancho del sidebar con hooks."
tags: [claude-fleet, infra, kitty, tmux, claude, fleetview, launcher] tags: [claude-fleet, infra, kitty, tmux, claude, fleetview, launcher]
params: params:
- name: --cwd - name: --cwd
@@ -20,7 +20,8 @@ params:
- name: --cols - name: --cols
desc: "Ancho en columnas del pane izquierdo (la TUI). Opcional. Default: 40." desc: "Ancho en columnas del pane izquierdo (la TUI). Opcional. Default: 40."
output: "Crea/reutiliza una sesion tmux detached con dos panes y lanza una ventana kitty 'FleetView' adjunta a ella, desacoplada del shell padre (setsid). Imprime el estado por stdout. Sin valor de retorno; exit 0 en exito." output: "Crea/reutiliza una sesion tmux detached con dos panes y lanza una ventana kitty 'FleetView' adjunta a ella, desacoplada del shell padre (setsid). Imprime el estado por stdout. Sin valor de retorno; exit 0 en exito."
uses_functions: [] uses_functions:
- supervise_fleetview_tui_bash_infra
uses_types: [] uses_types: []
returns: [] returns: []
returns_optional: false returns_optional: false
@@ -83,10 +84,20 @@ al retomar el trabajo en el repo `fn_registry`.
TTY, reutiliza la terminal actual con `exec tmux attach`. TTY, reutiliza la terminal actual con `exec tmux attach`.
- **kitty detached (setsid)**: la ventana se lanza con `setsid ... &` para - **kitty detached (setsid)**: la ventana se lanza con `setsid ... &` para
sobrevivir al cierre de la terminal que la invoco. No bloquea al shell padre. sobrevivir al cierre de la terminal que la invoco. No bloquea al shell padre.
- **`exec` en los panes**: tanto la TUI como `claude` se lanzan con `exec`, asi - **TUI bajo supervisor (auto-respawn)**: el pane izquierdo NO corre un
que al terminar el proceso el pane se cierra en vez de dejar una shell zombie `exec fleetview` de una sola vida, sino `supervise_fleetview_tui` (bucle que
colgando. Excepcion: el fallback cuando `fleetview` no esta compilado deja una relanza la TUI si muere por crash/panic/kill). Asi el panel de control nunca se
shell interactiva a proposito (para que veas el mensaje y puedas compilar). pierde por un fallo puntual. El supervisor para limpio con su sentinel
(`touch ~/.claude/fleet/tui_stop_<perfil>` y deja salir la TUI) o se rinde si la
TUI entra en crash-loop; en ambos casos el pane cae a una shell viva (no se
cierra solo) para inspeccionar. Es la mitad "auto-recuperacion" del par de
fixes que blindan FleetView; la otra es el Guard 3 anti-TUI/console de
`kill_fleet_agent` (la causa raiz del cierre accidental). Si el script del
supervisor no estuviera en disco, cae al `exec fleetview` clasico.
- **`exec` en los demas panes**: `claude` (orquestador e idle) se lanza con
`exec`, asi que al terminar el proceso el pane se cierra en vez de dejar una
shell zombie. Excepcion: el fallback cuando `fleetview` no esta compilado deja
una shell interactiva a proposito (para que veas el mensaje y puedas compilar).
- **Requiere fleetview compilado**: el default `--bin` apunta a - **Requiere fleetview compilado**: el default `--bin` apunta a
`<repo>/apps/fleetview/fleetview`. Si ese binario no existe, el pane izquierdo `<repo>/apps/fleetview/fleetview`. Si ese binario no existe, el pane izquierdo
muestra `cd apps/fleetview && go build -o fleetview .` en lugar de fallar en muestra `cd apps/fleetview && go build -o fleetview .` en lugar de fallar en
@@ -113,6 +124,13 @@ al retomar el trabajo en el repo `fn_registry`.
## Capability growth log ## Capability growth log
- v1.5.0 (2026-06-24) — **auto-respawn de la TUI**. El pane izquierdo ya no corre
`exec fleetview` (una sola vida), sino el bucle supervisor
`supervise_fleetview_tui`, que relanza la TUI si muere (crash/panic/kill de su
proceso o pane). Asi el panel de control NUNCA se pierde por un fallo puntual.
Parada voluntaria via sentinel; crash-loop guard para no relanzar en bucle
cerrado. Complementa el Guard 3 anti-TUI/console de `kill_fleet_agent` (causa
raiz del cierre accidental). Nueva dependencia: `supervise_fleetview_tui_bash_infra`.
- v1.4.0 (2026-06-18) — **perfiles multiples**. Socket+sesion tmux ya no son el - v1.4.0 (2026-06-18) — **perfiles multiples**. Socket+sesion tmux ya no son el
fijo `fleet`: cada perfil tiene los suyos (mismo nombre). Sin `--session`/ fijo `fleet`: cada perfil tiene los suyos (mismo nombre). Sin `--session`/
`--reuse`, cada invocacion abre el primer perfil libre (`fleet`, `fleet2`, ...), `--reuse`, cada invocacion abre el primer perfil libre (`fleet`, `fleet2`, ...),
+16 -1
View File
@@ -170,7 +170,22 @@ USAGE
envpfx="FLEET_SOCKET=$(printf '%q' "$session") FLEET_SESSION=$(printf '%q' "$session")" envpfx="FLEET_SOCKET=$(printf '%q' "$session") FLEET_SESSION=$(printf '%q' "$session")"
local left_cmd local left_cmd
if [[ -x "$bin" ]]; then if [[ -x "$bin" ]]; then
left_cmd="$envpfx exec $(printf '%q' "$bin")" # NO un `exec fleetview` de una sola vida: lo envolvemos en el bucle
# supervisor supervise_fleetview_tui, que relanza la TUI si muere (crash,
# panic, kill de su proceso o de su pane). Asi el panel de control de la
# flota NUNCA se pierde por un fallo puntual. El supervisor para limpio
# con su sentinel (touch ~/.claude/fleet/tui_stop_<perfil>) o se rinde si
# la TUI entra en crash-loop; en ambos casos cae a una shell viva.
local sup="$repo_root/bash/functions/infra/supervise_fleetview_tui.sh"
if [[ -f "$sup" ]]; then
# bash <sup> (no exec): al volver el supervisor (sentinel o crash-loop)
# caemos a una shell viva para que el mensaje siga visible y se pueda
# inspeccionar/relanzar. El env aplica al supervisor y a su hijo TUI.
left_cmd="$envpfx bash $(printf '%q' "$sup") --bin $(printf '%q' "$bin") --socket $(printf '%q' "$session"); exec \"\$SHELL\""
else
# Fallback si falta el supervisor en disco: comportamiento clasico.
left_cmd="$envpfx exec $(printf '%q' "$bin")"
fi
else else
# Fallback claro: instruye como compilar la TUI y deja una shell viva. # Fallback claro: instruye como compilar la TUI y deja una shell viva.
left_cmd="echo 'fleetview no compilado: cd apps/fleetview && go build -o fleetview .'; exec \"\$SHELL\"" left_cmd="echo 'fleetview no compilado: cd apps/fleetview && go build -o fleetview .'; exec \"\$SHELL\""
@@ -0,0 +1,67 @@
---
name: supervise_fleetview_tui
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "supervise_fleetview_tui --bin <path> [--socket <s>] [--sentinel <path>] [--backoff <s>] [--min-uptime <s>] [--max-fast-exits <n>]"
description: "Bucle supervisor que mantiene viva la TUI fleetview: lanza el binario y, si sale (crash, panic, kill de su proceso o pane), lo relanza tras un backoff, para que el panel de control de la flota NUNCA se pierda por un fallo puntual. Es la pieza que hace resiliente al pane izquierdo de la sesion tmux FleetView (lo invoca launch_fleetclaude). Dos valvulas de escape evitan el respawn infinito: un fichero centinela (touch <sentinel> => parada voluntaria al siguiente ciclo) y un crash-loop guard (si la TUI sale demasiado rapido muchas veces seguidas, el supervisor se rinde con rc=3 en vez de quemar CPU relanzando un binario roto)."
tags: [fleet, claude-fleet, orchestration, fleetview, tui, supervisor, resilience, infra]
uses_functions: []
uses_types: []
error_type: error_go_core
file_path: "bash/functions/infra/supervise_fleetview_tui.sh"
tested: true
tests:
- "golden: tras salir el binario, el supervisor lo relanza (respawn observable)"
- "sentinel: tocar el fichero centinela para el bucle limpio (rc=0) y lo consume"
- "crash-loop: salidas rapidas seguidas >= max_fast_exits hacen que se rinda (rc=3)"
- "error: sin --bin rc=1; binario no ejecutable rc=1"
test_file_path: "bash/functions/infra/supervise_fleetview_tui_test.sh"
params:
- name: --bin
desc: "Ruta al binario fleetview a supervisar. Obligatorio. Si no es ejecutable, sale con rc=1 con instruccion de compilado."
- name: --socket
desc: "Socket del perfil FleetView. Solo fija el nombre del sentinel por defecto. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada."
- name: --sentinel
desc: "Ruta del fichero centinela de parada voluntaria. Si existe tras una salida de la TUI, se borra y el bucle termina. Default: $HOME/.claude/fleet/tui_stop_<socket>."
- name: --backoff
desc: "Segundos de espera antes de relanzar la TUI tras una salida. Default: 1."
- name: --min-uptime
desc: "Umbral en segundos para considerar una salida 'rapida' (sospecha de crash-loop). Un arranque que dura >= este valor resetea el contador. Default: 2."
- name: --max-fast-exits
desc: "Numero de salidas rapidas seguidas tras las que el supervisor se rinde (crash-loop guard) en vez de seguir relanzando. Default: 5."
output: "No retorna valor; corre indefinidamente relanzando la TUI. Sale 0 ante parada voluntaria (sentinel), 1 ante uso incorrecto / binario no ejecutable, 3 cuando el crash-loop guard se rinde. Imprime una linea por cada relanzamiento o parada."
---
# supervise_fleetview_tui
Bucle supervisor de la TUI `fleetview`. Corre el binario y, cada vez que sale (crash, panic, `kill` de su proceso, cierre de su pane), lo **relanza** tras un pequeño backoff. Hace que el panel de control de la flota — el pane izquierdo de la sesión tmux FleetView — **nunca se pierda** por un fallo puntual. `launch_fleetclaude` lo usa como comando del pane izquierdo en vez de un `exec fleetview` de una sola vida.
## Ejemplo
```bash
# Como lo invoca el launcher en el pane izquierdo (relanza la TUI si muere):
FLEET_SOCKET=fleet bash bash/functions/infra/supervise_fleetview_tui.sh \
--bin apps/fleetview/fleetview --socket fleet
# Pararlo voluntariamente desde otra terminal: tocar el sentinel y dejar salir la TUI.
touch ~/.claude/fleet/tui_stop_fleet
```
## Cuando usarla
Úsala como wrapper del binario `fleetview` siempre que quieras que la TUI sobreviva a un crash o a un `kill` accidental de su proceso/pane (p. ej. un `kill_fleet_agent` que cierre la window que la aloja). Es la mitad "auto-recuperación" del par de fixes que blindan FleetView; la otra mitad es el Guard 3 anti-TUI/console de `kill_fleet_agent` (la causa raíz). No la uses para supervisar Claudes (esos se relanzan con `claude --resume`, no en bucle ciego).
## Gotchas
- **Impura y de larga duración**: corre indefinidamente. Está pensada para vivir en un pane tmux con TTY, no como systemd service (la TUI necesita PTY; el watcher de fleetview sí es systemd `Restart=always`).
- **Crash-loop guard**: si la TUI sale en menos de `--min-uptime` segundos, `--max-fast-exits` veces seguidas, el supervisor se **rinde** (rc=3) en vez de relanzar para siempre un binario roto. Ajusta los umbrales si tu arranque es legítimamente lento.
- **Sentinel = única parada voluntaria limpia**: `touch <sentinel>` y deja que la TUI salga; al siguiente ciclo el supervisor ve el fichero, lo borra y termina. Sin sentinel, **relanza siempre** (es el objetivo: que no se pierda). Un sentinel huérfano de una sesión previa se limpia al arrancar para no parar de inmediato.
- **El sentinel por defecto depende del socket**: `~/.claude/fleet/tui_stop_<socket>`. Dos perfiles (`fleet`, `fleet2`) tienen sentinels distintos, así parar uno no para el otro.
- **No supervisa Claudes**: su contrato es solo la TUI. Relanzar un Claude en bucle ciego perdería su sesión; los Claudes se recuperan con `claude --resume`.
## Capability growth log
(v1.0.0 — sin cambios todavía.)
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# supervise_fleetview_tui — bucle supervisor que mantiene viva la TUI fleetview.
#
# Lanza el binario fleetview y, si sale (crash, panic, kill de su proceso o de su
# pane), lo relanza tras un pequeno backoff. Asi el panel de control de la flota
# NUNCA se pierde por un fallo puntual: es la pieza que hace resiliente al pane
# izquierdo de la sesion tmux FleetView (lo invoca launch_fleetclaude).
#
# Dos valvulas de escape para no hacer respawn infinito:
# - Sentinel file: si tras una salida existe el fichero centinela, se borra y
# el bucle termina (parada voluntaria solicitada por el usuario). El default
# es $HOME/.claude/fleet/tui_stop_<socket>; pararla a mano: `touch <sentinel>`
# y dejar que la TUI salga (o matar su proceso).
# - Crash-loop guard: si la TUI sale demasiado rapido (uptime < min_uptime
# segundos) muchas veces seguidas (>= max_fast_exits), el supervisor se rinde
# y devuelve != 0, para no quemar CPU relanzando un binario roto en caliente.
# Un arranque que dura >= min_uptime resetea el contador.
#
# Funcion IMPURA: lanza un proceso en bucle y lee/escribe un fichero centinela.
#
# Overrides de entorno (testabilidad, no para uso normal):
# FLEET_SOCKET socket del perfil; fija el nombre del sentinel por defecto.
set -euo pipefail
IFS=$' \t\n'
supervise_fleetview_tui() {
local bin="" socket="" sentinel="" backoff=1 min_uptime=2 max_fast_exits=5
while [[ $# -gt 0 ]]; do
case "$1" in
--bin) shift; bin="${1:-}" ;;
--socket) shift; socket="${1:-}" ;;
--sentinel) shift; sentinel="${1:-}" ;;
--backoff) shift; backoff="${1:-1}" ;;
--min-uptime) shift; min_uptime="${1:-2}" ;;
--max-fast-exits) shift; max_fast_exits="${1:-5}" ;;
-h|--help)
cat <<'USAGE'
Uso: supervise_fleetview_tui --bin <path> [opciones]
Bucle supervisor: corre el binario fleetview y lo relanza si sale, para que el
panel de la flota nunca se pierda por un crash/kill puntual.
Opciones:
--bin <path> Ruta al binario fleetview (obligatorio).
--socket <s> Socket del perfil FleetView. Default: $FLEET_SOCKET o "fleet".
--sentinel <path> Fichero centinela de parada voluntaria.
Default: $HOME/.claude/fleet/tui_stop_<socket>.
--backoff <s> Segundos de espera antes de relanzar. Default: 1.
--min-uptime <s> Umbral (s) para considerar una salida "rapida". Default: 2.
--max-fast-exits <n> Salidas rapidas seguidas tras las que el supervisor se
rinde (crash-loop guard). Default: 5.
-h, --help Esta ayuda.
Parar el bucle a mano: `touch <sentinel>` y dejar que la TUI salga (o matar su
proceso); en el siguiente ciclo el supervisor ve el sentinel, lo borra y termina.
Salida: 0 parada voluntaria (sentinel); 1 binario no ejecutable / uso incorrecto;
3 el supervisor se rindio por crash-loop (demasiadas salidas rapidas seguidas).
USAGE
return 0 ;;
--*)
echo "supervise_fleetview_tui: opcion desconocida '$1' (usa -h)" >&2
return 1 ;;
*)
if [[ -z "$bin" ]]; then
bin="$1"
else
echo "supervise_fleetview_tui: argumento extra '$1' (bin ya es '$bin')" >&2
return 1
fi ;;
esac
shift
done
[[ -z "$bin" ]] && {
echo "supervise_fleetview_tui: falta --bin <path> al binario fleetview. Usa -h." >&2
return 1
}
[[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"
[[ -z "$sentinel" ]] && sentinel="$HOME/.claude/fleet/tui_stop_${socket}"
mkdir -p "$(dirname "$sentinel")" 2>/dev/null || true
if [[ ! -x "$bin" ]]; then
echo "supervise_fleetview_tui: binario '$bin' no es ejecutable. Compila la TUI: cd apps/fleetview && go build -o fleetview ." >&2
return 1
fi
# Limpiar un sentinel huerfano de una sesion anterior, para no parar al arrancar.
[[ -f "$sentinel" ]] && rm -f "$sentinel" 2>/dev/null || true
local fast_exits=0
while true; do
local start end uptime code
start=$(date +%s)
set +e
"$bin"
code=$?
set -e
end=$(date +%s)
uptime=$(( end - start ))
# Valvula 1 — parada voluntaria por sentinel.
if [[ -f "$sentinel" ]]; then
rm -f "$sentinel" 2>/dev/null || true
echo "[fleetview: parada solicitada via sentinel ($sentinel) — fin del supervisor]"
return 0
fi
# Valvula 2 — crash-loop guard.
if [[ "$uptime" -lt "$min_uptime" ]]; then
fast_exits=$(( fast_exits + 1 ))
else
fast_exits=0
fi
if [[ "$fast_exits" -ge "$max_fast_exits" ]]; then
echo "[fleetview: $fast_exits salidas rapidas seguidas (ultimo code=$code) — el supervisor se rinde para no hacer respawn infinito. Inspecciona el binario y relanza.]" >&2
return 3
fi
echo "[fleetview salio (code=$code, uptime=${uptime}s) — relanzando en ${backoff}s. Para parar: touch $sentinel, o Ctrl-C.]"
sleep "$backoff"
done
}
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
supervise_fleetview_tui "$@"
fi
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Tests para supervise_fleetview_tui. Usa un binario falso (un script) que cuenta
# sus invocaciones, para verificar respawn, crash-loop guard y sentinel sin correr
# la TUI real.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/supervise_fleetview_tui.sh"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
assert_eq() {
local test_name="$1" expected="$2" actual="$3"
if [[ "$actual" == "$expected" ]]; then
echo "PASS: $test_name ($actual)"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected '$expected', got '$actual'"
FAIL=$((FAIL+1))
fi
}
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
COUNTER="$TMP/runs"
SENTINEL="$TMP/sentinel"
# --- Test 1 (crash-loop guard): binario que sale rapido siempre se rinde a las N ---
# Fake bin: registra una linea por invocacion y sale 1 inmediato.
FAKE_FAST="$TMP/fake_fast.sh"
cat > "$FAKE_FAST" <<EOF
#!/usr/bin/env bash
echo run >> "$COUNTER"
exit 1
EOF
chmod +x "$FAKE_FAST"
: > "$COUNTER"
set +e
out=$(supervise_fleetview_tui --bin "$FAKE_FAST" --backoff 0 --min-uptime 100 \
--max-fast-exits 3 --sentinel "$SENTINEL" 2>&1); rc=$?
set -e
runs=$(wc -l < "$COUNTER" | tr -d ' ')
assert_eq "crash-loop: se rinde con rc=3" 3 "$rc"
assert_eq "crash-loop: corrio exactamente 3 veces" 3 "$runs"
assert_contains "crash-loop: mensaje de rendicion" "el supervisor se rinde" "$out"
# --- Test 2 (golden respawn + sentinel): relanza tras salir, para via sentinel ---
# Fake bin: en la 2a invocacion crea el sentinel, luego sale. Prueba que:
# (a) tras la 1a salida RELANZA (respawn) -> hay 2a invocacion (golden).
# (b) al ver el sentinel, PARA (no hay 3a invocacion).
FAKE_SENT="$TMP/fake_sent.sh"
cat > "$FAKE_SENT" <<EOF
#!/usr/bin/env bash
echo run >> "$COUNTER"
n=\$(wc -l < "$COUNTER" | tr -d ' ')
if [[ "\$n" -ge 2 ]]; then
touch "$SENTINEL"
fi
exit 1
EOF
chmod +x "$FAKE_SENT"
: > "$COUNTER"
rm -f "$SENTINEL"
set +e
out=$(supervise_fleetview_tui --bin "$FAKE_SENT" --backoff 0 --min-uptime 0 \
--max-fast-exits 99 --sentinel "$SENTINEL" 2>&1); rc=$?
set -e
runs=$(wc -l < "$COUNTER" | tr -d ' ')
assert_eq "golden: relanzo tras morir (2 invocaciones)" 2 "$runs"
assert_eq "sentinel: para limpio con rc=0" 0 "$rc"
assert_contains "sentinel: mensaje de parada voluntaria" "parada solicitada via sentinel" "$out"
assert_eq "sentinel: el fichero se consume (borrado)" "no" "$([[ -f "$SENTINEL" ]] && echo si || echo no)"
assert_contains "golden: registra el respawn" "relanzando" "$out"
# --- Test 3 (error): falta --bin ---
set +e
out=$(supervise_fleetview_tui --backoff 0 2>&1); rc=$?
set -e
assert_eq "error: sin --bin devuelve rc=1" 1 "$rc"
assert_contains "error: mensaje falta --bin" "falta --bin" "$out"
# --- Test 4 (error): binario no ejecutable ---
set +e
out=$(supervise_fleetview_tui --bin "$TMP/no_existe" 2>&1); rc=$?
set -e
assert_eq "error: binario no ejecutable rc=1" 1 "$rc"
assert_contains "error: mensaje no ejecutable" "no es ejecutable" "$out"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1