diff --git a/bash/functions/infra/detect_fleet_context.md b/bash/functions/infra/detect_fleet_context.md new file mode 100644 index 00000000..ba9b7b26 --- /dev/null +++ b/bash/functions/infra/detect_fleet_context.md @@ -0,0 +1,98 @@ +--- +name: detect_fleet_context +kind: function +lang: bash +domain: infra +version: 1.0.0 +purity: impure +signature: "detect_fleet_context() -> JSON {in_fleet,in_tmux,socket,session,source}" +description: "Detecta de forma robusta si el proceso corre dentro de una flota tmux FleetView, derivando socket y sesion de $TMUX (senal fiable) en vez de $FLEET_SOCKET (fragil, a veces vacia en un claude resumido/relanzado). Salida JSON con in_fleet/in_tmux/socket/session/source." +tags: [orchestration, fleet, tmux, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: error_go_core +imports: [] +tested: false +file_path: "bash/functions/infra/detect_fleet_context.sh" +params: + - name: "(ninguno)" + desc: "No recibe argumentos. Lee el entorno ($TMUX, con fallback a $FLEET_SOCKET/$FLEET_SESSION) y consulta el servidor tmux." +output: "JSON en stdout: {\"in_fleet\":bool, \"in_tmux\":bool, \"socket\":str, \"session\":str, \"source\":\"tmux|fleet_socket|none\"}. in_tmux=true basta para lanzar una window; in_fleet es la senal semantica de 'estoy en una flota'." +--- + +# detect_fleet_context + +Detecta el contexto de flota del proceso actual sin depender de `$FLEET_SOCKET`. + +## Por que existe + +La deteccion de "estoy en una flota FleetView" dependia de la variable de +entorno `$FLEET_SOCKET`, que `launch_fleetclaude` exporta con +`tmux set-environment -g`. Esa variable solo llega a los procesos que tmux +arranca **despues** de setearla: un `claude` relanzado o resumido a mano puede +no heredarla y `$FLEET_SOCKET` queda vacia, aunque ese claude SI viva en una +window de la flota. Cuando eso pasa, el modo orquestador cae al fallback kitty +(`launch_claude_agent_kitty`) y lanza ejecutores en terminales sueltas en vez de +como windows de la flota. + +La senal **fiable** es `$TMUX`: todo proceso dentro de tmux la tiene SIEMPRE, con +el formato `/tmp/tmux-/,,`. De ahi se extrae +el socket (basename del path antes de la primera coma) y, con +`tmux -L display-message -p '#{session_name}'`, la sesion actual. + +## Salida + +```json +{"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"} +``` + +| Campo | Significado | +|---|---| +| `in_fleet` | Heuristica de "estoy en una flota". `true` si en tmux Y (socket/sesion casan `fleet`, O hay window `fleetview`, O la sesion tiene >= 2 windows). | +| `in_tmux` | `true` si el proceso esta dentro de tmux. Basta para lanzar una window (mejor que caer a kitty). | +| `socket` | Socket tmux derivado de `$TMUX` (o de `$FLEET_SOCKET` en fallback). | +| `session` | Sesion tmux actual resuelta con `display-message` (fallback a `$FLEET_SESSION` o al socket). | +| `source` | `tmux` (derivado de `$TMUX`), `fleet_socket` (fallback), o `none`. | + +## Ejemplo + +```bash +# Dentro de una window de la flota fleet3: +bash bash/functions/infra/detect_fleet_context.sh +# {"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"} + +# Fuera de tmux, sin FLEET_SOCKET: +env -u TMUX -u FLEET_SOCKET bash bash/functions/infra/detect_fleet_context.sh +# {"in_fleet":false,"in_tmux":false,"socket":"","session":"","source":"none"} + +# Parsear el socket con jq para pasarlo a spawn_fleet_agent: +ctx=$(bash bash/functions/infra/detect_fleet_context.sh) +sock=$(printf '%s' "$ctx" | jq -r .socket) +``` + +## Cuando usarla + +Antes de lanzar un ejecutor de la flota: llama a esta funcion para saber si +estas dentro de una flota tmux. Si `in_tmux=true`, lanza con `spawn_fleet_agent` +(que ya la usa para auto-detectar el socket); NUNCA caigas a kitty. Tambien la +usa el hook `hook_fleet_state_inject.sh` para recordarle al orquestador el socket +de su flota cada turno. + +## Gotchas + +- Es **impura**: consulta el servidor tmux (`display-message`, `list-windows`). + No modifica estado. +- `in_fleet` es **heuristico** a proposito. Para LANZAR basta `in_tmux=true` + (lanzar una window en cualquier tmux supera a una kitty suelta). `in_fleet` es + solo la senal semantica que consume el hook y la doctrina. +- Fallback `source=fleet_socket`: si `$TMUX` no esta pero `$FLEET_SOCKET` si, + devuelve `socket`/`session` de esas vars con `in_tmux=false`. Un + `tmux -L new-window` puede seguir funcionando si el servidor existe, + aunque el caller no este attached. +- No requiere `jq` ni python: emite el JSON con `printf`, para poder ser el + detector base que invocan hooks y otras funciones bash. +- Si `tmux` no esta instalado y `$TMUX` esta seteada (raro), `socket` se deriva + igual de `$TMUX` pero `session` cae al fallback y `in_fleet` no se puede afinar + por windows. diff --git a/bash/functions/infra/detect_fleet_context.sh b/bash/functions/infra/detect_fleet_context.sh new file mode 100644 index 00000000..786c4de4 --- /dev/null +++ b/bash/functions/infra/detect_fleet_context.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# detect_fleet_context — detecta de forma robusta si el proceso actual corre +# dentro de una sesion tmux de una flota FleetView, derivando el socket y la +# sesion de la variable de entorno $TMUX (senal fiable) en vez de depender de +# $FLEET_SOCKET (que a veces viene vacia en el entorno de un claude resumido o +# relanzado, aunque ese claude SI viva en una window de la flota). +# +# Por que $TMUX y no $FLEET_SOCKET: +# launch_fleetclaude exporta FLEET_SOCKET/FLEET_SESSION con `tmux +# set-environment -g`. Esa variable solo llega a los procesos que tmux arranca +# DESPUES de setearla; un claude relanzado o resumido a mano puede no heredarla +# y entonces $FLEET_SOCKET queda vacia. En cambio, todo proceso que corre +# dentro de tmux tiene SIEMPRE $TMUX seteada, con el formato: +# /tmp/tmux-/,, +# De ahi se extrae el socket (basename del path antes de la primera coma) y, +# con `tmux -L display-message -p '#{session_name}'`, la sesion +# actual. Eso identifica el contexto fleet sin depender de $FLEET_SOCKET. +# +# Salida: JSON en stdout con los campos: +# in_fleet : true|false — heuristica de "estoy en una flota" (ver criterio). +# in_tmux : true|false — estoy dentro de tmux (basta para lanzar una window). +# socket : nombre del socket tmux derivado ("" si no hay). +# session : nombre de la sesion tmux actual ("" si no se resuelve). +# source : "tmux" | "fleet_socket" | "none" — de donde se derivo el contexto. +# +# Criterio de "flota reconocible" (in_fleet): estar en tmux (in_tmux) Y que se +# cumpla al menos uno, de mas fiable a menos: +# 1. el socket o la sesion casan el patron de flota (contienen "fleet"), o +# 2. existe una window llamada "fleetview" (la TUI de la flota), o +# 3. la sesion tiene >= 2 windows (una flota agrupa varios agentes en windows). +# Es heuristico a proposito: para LANZAR un ejecutor basta con in_tmux (lanzar +# una window en cualquier tmux es mejor que caer a una kitty suelta); in_fleet es +# la senal semantica que consume el hook del orquestador y la doctrina. +# +# Funcion IMPURA: lee el entorno y consulta el servidor tmux (display-message, +# list-windows). No modifica estado. Degrada limpio: si tmux no esta o falla +# cualquier consulta, devuelve los campos que pueda y nunca aborta con error. +set -euo pipefail +IFS=$' \t\n' + +detect_fleet_context() { + local socket="" session="" source="none" + local in_tmux="false" in_fleet="false" + + if [[ -n "${TMUX:-}" ]]; then + in_tmux="true" + source="tmux" + # $TMUX = /tmp/tmux-/,, + # Socket = basename del path antes de la primera coma. + local tmux_path="${TMUX%%,*}" + socket="$(basename "$tmux_path" 2>/dev/null || true)" + # Sesion actual: tmux resuelve el cliente via $TMUX. -L fija el socket. + if command -v tmux >/dev/null 2>&1 && [[ -n "$socket" ]]; then + session="$(tmux -L "$socket" display-message -p '#{session_name}' 2>/dev/null || true)" + fi + # Fallback de sesion si display-message no resolvio nada. + [[ -z "$session" ]] && session="${FLEET_SESSION:-$socket}" + elif [[ -n "${FLEET_SOCKET:-}" ]]; then + # No estamos en tmux pero hay FLEET_SOCKET exportada: usarla como ultimo + # recurso (un claude que perdio $TMUX pero conserva la env del perfil). + in_tmux="false" + source="fleet_socket" + socket="${FLEET_SOCKET}" + session="${FLEET_SESSION:-$socket}" + fi + + # Heuristica in_fleet: solo tiene sentido si estamos en tmux. + if [[ "$in_tmux" == "true" && -n "$socket" ]]; then + local sl="${socket,,}" sesl="${session,,}" + if [[ "$sl" == *fleet* || "$sesl" == *fleet* ]]; then + in_fleet="true" + elif command -v tmux >/dev/null 2>&1; then + # Construir el target de sesion sin trucos de expansion fragiles. + local -a tgt=() + [[ -n "$session" ]] && tgt=(-t "$session") + # window "fleetview" presente => flota. + if tmux -L "$socket" list-windows "${tgt[@]}" \ + -F '#{window_name}' 2>/dev/null | grep -qx 'fleetview'; then + in_fleet="true" + else + # >= 2 windows => agrupacion tipo flota. + local nwin + nwin="$(tmux -L "$socket" list-windows "${tgt[@]}" \ + -F x 2>/dev/null | wc -l | tr -d ' ')" + [[ "${nwin:-0}" -ge 2 ]] && in_fleet="true" + fi + fi + fi + + # JSON sin dependencias (jq/python no requeridos: este es el detector base). + printf '{"in_fleet":%s,"in_tmux":%s,"socket":"%s","session":"%s","source":"%s"}\n' \ + "$in_fleet" "$in_tmux" "$socket" "$session" "$source" + return 0 +} + +# Permitir ejecutar el archivo directamente (no solo como funcion sourced). +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + detect_fleet_context "$@" +fi