334a71eed1
- mark_claude_role (python/functions/infra): resuelve PID->sessionId esperando sessions/<PID>.json y escribe role en el goal.json sin pisar el resto. 4 tests. - launch_fleetclaude: el pane derecho arranca el ORQUESTADOR con el skill /orquestador embebido como primer prompt; tras arrancar, mark_claude_role le pone role=orchestrator (en background, no-fatal) para que la TUI lo pinee arriba; ademas siembra 1 ejecutor idle inicial en su propia window. - skill /orquestador: regla 'no te vigiles a ti mismo' (ignora en la cola su propia sesion y cualquier role=orchestrator). Validado en vivo (perfil aislado): claude /orquestador entra en modo, role marcado, idle sembrado, pin correcto, fleet2 intacto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
313 lines
15 KiB
Bash
313 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# launch_fleetclaude — Entrypoint MVP de FleetView.
|
|
#
|
|
# Abre UNA ventana kitty corriendo una sesion tmux de dos panes:
|
|
# - pane izquierdo: la TUI 'fleetview' (la flota de Claudes centralizada).
|
|
# - pane derecho: 'claude --dangerously-skip-permissions'.
|
|
#
|
|
# Objetivo: dejar de tener N ventanas kitty dispersas y centralizar el control
|
|
# de los Claudes en una sola ventana.
|
|
#
|
|
# Funcion IMPURA: lanza procesos (tmux + kitty) con efectos secundarios.
|
|
# - Crea/reusa una sesion tmux detached llamada <session> (idempotente).
|
|
# - Lanza una ventana kitty desacoplada del shell padre (setsid) para que
|
|
# sobreviva al cierre de la terminal que la invoco.
|
|
# - No toca atajos de teclado ni kitty.conf.
|
|
set -euo pipefail
|
|
IFS=$' \t\n'
|
|
|
|
launch_fleetclaude() {
|
|
local cwd=""
|
|
local bin=""
|
|
local session="fleet"
|
|
local cols=52
|
|
local explicit_session=0 # 1 si el usuario pasó --session <name> a mano
|
|
local reuse=0 # 1 si el usuario pidió --reuse (reattach al perfil principal)
|
|
local T="" # socket tmux aislado; se fija al resolver el perfil
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Parseo de argumentos
|
|
# -----------------------------------------------------------------------
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--cwd)
|
|
shift
|
|
cwd="${1:-}"
|
|
;;
|
|
--bin)
|
|
shift
|
|
bin="${1:-}"
|
|
;;
|
|
--session)
|
|
shift
|
|
session="${1:-}"
|
|
explicit_session=1
|
|
;;
|
|
--reuse)
|
|
reuse=1
|
|
;;
|
|
--cols)
|
|
shift
|
|
cols="${1:-40}"
|
|
;;
|
|
-h|--help)
|
|
cat <<'USAGE'
|
|
Uso: launch_fleetclaude [opciones]
|
|
|
|
Abre una ventana kitty con una sesion tmux de dos panes: la TUI fleetview a la
|
|
izquierda y 'claude --dangerously-skip-permissions' a la derecha.
|
|
|
|
Cada PERFIL de FleetView es un socket+sesion tmux aislados (su propia flota de
|
|
Claudes). Sin --session ni --reuse, cada invocacion abre un perfil NUEVO: usa
|
|
el primer nombre libre de la secuencia fleet, fleet2, fleet3, ... Asi puedes
|
|
tener varias FleetView abiertas a la vez, cada una con su flota independiente.
|
|
|
|
Opciones:
|
|
--cwd <dir> Directorio de trabajo de los panes.
|
|
Default: raiz del repo fn_registry (derivada dinamicamente).
|
|
--bin <path> Ruta al binario de la TUI fleetview.
|
|
Default: <repo>/apps/fleetview/fleetview
|
|
--session <name> Fija el perfil (socket+sesion) por nombre exacto; reutiliza
|
|
el existente si ya esta vivo. Sin esta opcion, perfil auto.
|
|
--reuse Reattach al perfil principal 'fleet' en vez de abrir uno
|
|
nuevo (vuelve al comportamiento idempotente clasico).
|
|
--cols <n> Ancho (columnas) del pane izquierdo. Default: 40.
|
|
-h, --help Muestra esta ayuda.
|
|
|
|
Ejemplos:
|
|
launch_fleetclaude # perfil nuevo (fleet, luego fleet2, ...)
|
|
launch_fleetclaude --reuse # reattach a la flota principal 'fleet'
|
|
launch_fleetclaude --session trabajo # perfil con nombre fijo 'trabajo'
|
|
launch_fleetclaude --cwd ~/fn_registry --cols 50
|
|
USAGE
|
|
return 0
|
|
;;
|
|
*)
|
|
echo "launch_fleetclaude: opcion desconocida: '$1' (usa -h)" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Derivar la raiz del repo fn_registry dinamicamente (NO hardcodear paths
|
|
# de usuario). Estrategia: subir desde la ubicacion del script con
|
|
# 'git rev-parse --show-toplevel'; fallbacks razonables si no aplica.
|
|
# -----------------------------------------------------------------------
|
|
local script_dir repo_root=""
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# El script vive en <repo>/bash/functions/infra/, asi que la raiz son 3
|
|
# niveles arriba; pero preferimos git para robustez.
|
|
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
if [[ -z "$repo_root" ]]; then
|
|
# Fallback 1: navegacion relativa desde la ubicacion del script.
|
|
repo_root="$(cd "$script_dir/../../.." 2>/dev/null && pwd || true)"
|
|
fi
|
|
if [[ -z "$repo_root" ]]; then
|
|
# Fallback 2: variable de entorno del registry o el cwd actual.
|
|
repo_root="${FN_REGISTRY_ROOT:-$PWD}"
|
|
fi
|
|
|
|
# Defaults derivados de la raiz del repo.
|
|
[[ -z "$cwd" ]] && cwd="$repo_root"
|
|
[[ -z "$bin" ]] && bin="$repo_root/apps/fleetview/fleetview"
|
|
|
|
# Validar cwd: si no existe, caer al repo_root.
|
|
if [[ ! -d "$cwd" ]]; then
|
|
echo "launch_fleetclaude: --cwd '$cwd' no existe; usando '$repo_root'." >&2
|
|
cwd="$repo_root"
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Comprobar herramientas necesarias.
|
|
# -----------------------------------------------------------------------
|
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
echo "launch_fleetclaude: tmux no esta instalado." >&2
|
|
return 1
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Resolver el PERFIL (socket+sesion tmux comparten nombre).
|
|
#
|
|
# - --session <name> -> usa ese nombre exacto (reutiliza si ya vive).
|
|
# - --reuse -> usa 'fleet' (el perfil principal), idempotente.
|
|
# - sin nada -> perfil NUEVO: primer nombre libre de la secuencia
|
|
# fleet, fleet2, fleet3, ... Asi abrir FleetView con
|
|
# uno ya abierto arranca otra flota, no la reusa.
|
|
#
|
|
# "Libre" = no hay un server tmux con esa sesion (has-session falla). Un
|
|
# perfil cerrado libera su nombre, asi que tras cerrar 'fleet' el siguiente
|
|
# lanzamiento vuelve a 'fleet'.
|
|
# -----------------------------------------------------------------------
|
|
if [[ "$explicit_session" -eq 0 && "$reuse" -eq 0 ]]; then
|
|
local base="$session" n=1 cand
|
|
while :; do
|
|
if [[ "$n" -eq 1 ]]; then cand="$base"; else cand="${base}${n}"; fi
|
|
if ! tmux -L "$cand" has-session -t "$cand" 2>/dev/null; then
|
|
session="$cand"
|
|
break
|
|
fi
|
|
n=$((n + 1))
|
|
done
|
|
echo "launch_fleetclaude: perfil nuevo '$session'."
|
|
fi
|
|
# A partir de aqui el socket aislado es el del perfil resuelto.
|
|
T="tmux -L $session"
|
|
# Nota: kitty NO se exige aqui. La ruta interactiva (TTY) reutiliza la
|
|
# terminal actual con `exec tmux attach` y no necesita kitty. Solo la
|
|
# ruta sin-TTY (abrir ventana nueva con setsid kitty) lo requiere, y ahi
|
|
# se comprueba justo antes de usarlo.
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Comando para el pane izquierdo:
|
|
# - Si el binario fleetview existe -> ejecutarlo (exec, sin shell colgado).
|
|
# - Si NO existe -> mensaje claro + shell interactiva (no falla en silencio).
|
|
# -----------------------------------------------------------------------
|
|
# La TUI necesita saber a qué perfil pertenece: se lo pasamos por entorno
|
|
# (FLEET_SOCKET/FLEET_SESSION), que main.go lee con fallback a "fleet".
|
|
local envpfx
|
|
envpfx="FLEET_SOCKET=$(printf '%q' "$session") FLEET_SESSION=$(printf '%q' "$session")"
|
|
local left_cmd
|
|
if [[ -x "$bin" ]]; then
|
|
left_cmd="$envpfx exec $(printf '%q' "$bin")"
|
|
else
|
|
# 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\""
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Montar la sesion tmux SOLO si no existe (idempotencia). Socket aislado $T.
|
|
#
|
|
# Targeting por PANE ID (%0/%1), no por indice (console.0). El socket
|
|
# -L fleet sigue leyendo ~/.tmux.conf; si el usuario tiene
|
|
# `base-index 1` / `pane-base-index 1` (muy comun), el primer pane es el
|
|
# indice 1 y cualquier referencia a console.0 falla con
|
|
# "can't find pane: 0". Los pane ID son estables e inmunes al base-index.
|
|
# -----------------------------------------------------------------------
|
|
local left_pane right_pane
|
|
if $T has-session -t "$session" 2>/dev/null; then
|
|
echo "launch_fleetclaude: la sesion tmux '$session' ya existe; reutilizandola."
|
|
else
|
|
echo "launch_fleetclaude: creando sesion tmux '$session' en '$cwd'."
|
|
|
|
# Sesion detached con ventana 'console'. Capturamos el pane ID del pane
|
|
# izquierdo (la TUI fleetview, o el fallback claro).
|
|
left_pane=$($T new-session -d -s "$session" -n console -c "$cwd" -P -F '#{pane_id}')
|
|
$T send-keys -t "$left_pane" "$left_cmd" C-m
|
|
|
|
# pane derecho = el ORQUESTADOR de la flota: un Claude que arranca ya en
|
|
# modo orquestador invocando el skill /orquestador como primer prompt. Es
|
|
# el Claude con el que el humano habla; vigila la flota por su DoD.
|
|
right_pane=$($T split-window -h -t "$left_pane" -c "$cwd" -P -F '#{pane_id}')
|
|
$T send-keys -t "$right_pane" "exec claude --dangerously-skip-permissions '/orquestador'" C-m
|
|
|
|
# Fijar el ancho del pane izquierdo en columnas.
|
|
$T resize-pane -t "$left_pane" -x "$cols"
|
|
|
|
# Foco inicial en el pane del orquestador (derecha).
|
|
$T select-pane -t "$right_pane"
|
|
|
|
# Marcar el orquestador con role=orchestrator en su goal.json para que la
|
|
# TUI lo pinee arriba (estrella). El sessionId no se conoce hasta que
|
|
# Claude escribe sessions/<PID>.json; mark_claude_role resuelve
|
|
# PID->sessionId esperando ese archivo. En background (no bloquea el
|
|
# arranque) y con sleep para que el `exec claude` reemplace al shell antes
|
|
# de leer el pane_pid. Fallo = no-fatal (el orquestador no se pinea).
|
|
if [[ -x "$repo_root/fn" ]]; then
|
|
( sleep 1
|
|
orch_pid=$($T display-message -p -t "$right_pane" '#{pane_pid}' 2>/dev/null || true)
|
|
[[ -n "$orch_pid" ]] && "$repo_root/fn" run mark_claude_role "$orch_pid" orchestrator >/dev/null 2>&1
|
|
) >/dev/null 2>&1 &
|
|
disown 2>/dev/null || true
|
|
fi
|
|
|
|
# Sembrar 1 ejecutor idle: una window detached con un claude normal,
|
|
# listo para recibir tarea del orquestador. Aparece en la TUI bajo el
|
|
# orquestador (role executor por defecto). Hereda FLEET_SOCKET/SESSION
|
|
# del server (set-environment), asi apunta a este perfil.
|
|
local idle_pane
|
|
idle_pane=$($T new-window -d -t "$session" -n claude -c "$cwd" -P -F '#{pane_id}')
|
|
$T send-keys -t "$idle_pane" "exec claude --dangerously-skip-permissions" C-m
|
|
fi
|
|
|
|
# Si reutilizamos sesion (o por seguridad), derivar el pane ID de la TUI:
|
|
# el primer pane de la ventana 'console' (orden por indice) es el izquierdo.
|
|
if [[ -z "$left_pane" ]]; then
|
|
left_pane=$($T list-panes -t "$session":console -F '#{pane_id}' 2>/dev/null | head -n1)
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Atajos globales (alt+*) en el socket aislado: redirigen la tecla al pane
|
|
# de la TUI (console.0) ESTES DONDE ESTES, para controlar la flota sin salir
|
|
# del pane de Claude. La TUI (fleetview) es quien interpreta Up/Down/Enter/n.
|
|
# `bind -n` = tabla root (sin prefijo). Idempotente: re-set en cada lanzamiento.
|
|
# -----------------------------------------------------------------------
|
|
$T bind -n M-Up send-keys -t "$left_pane" Up
|
|
$T bind -n M-Down send-keys -t "$left_pane" Down
|
|
$T bind -n M-Enter send-keys -t "$left_pane" Enter
|
|
$T bind -n M-n send-keys -t "$left_pane" n
|
|
$T bind -n M-0 send-keys -t "$left_pane" n
|
|
$T bind -n M-k send-keys -t "$left_pane" k
|
|
$T bind -n M-r send-keys -t "$left_pane" r
|
|
$T bind -n M-u send-keys -t "$left_pane" u
|
|
$T bind -n M-h send-keys -t "$left_pane" h
|
|
$T bind -n M-R send-keys -t "$left_pane" R
|
|
$T bind -n M-Left send-keys -t "$left_pane" Escape
|
|
$T bind -n M-q send-keys -t "$left_pane" Q
|
|
# Entorno del perfil en el server tmux: respawn-pane (alt+R, recompila la TUI)
|
|
# y los Claude nuevos heredan FLEET_SOCKET/FLEET_SESSION para apuntar al
|
|
# socket correcto aunque no sea el default "fleet".
|
|
$T set-environment -g FLEET_SOCKET "$session"
|
|
$T set-environment -g FLEET_SESSION "$session"
|
|
# Raton: enruta clicks/rueda al pane bajo el cursor; la TUI los interpreta.
|
|
$T set -g mouse on
|
|
# Al salir un Claude (exit / Ctrl-D / kill), cerrar su window en vez de
|
|
# dejarla muerta ("dead" pane) en la sesion.
|
|
$T set -g remain-on-exit off
|
|
|
|
# Estetica neutra: sin el verde fosforo por defecto de tmux. Status bar gris y
|
|
# bordes de pane gris tenue, iguales en activo e inactivo (separacion simple,
|
|
# sin resaltado de enfoque).
|
|
$T set -g status-style "bg=colour236,fg=colour250"
|
|
$T set -g pane-border-style "fg=colour238"
|
|
$T set -g pane-active-border-style "fg=colour240"
|
|
|
|
# Mantener el ancho del sidebar (pane 0) cuando kitty redimensiona la ventana
|
|
# tras el attach, o cuando se conmuta de Claude (window-linked / layout change).
|
|
$T set-hook -g client-resized "resize-pane -t $left_pane -x $cols"
|
|
$T set-hook -g window-layout-changed "resize-pane -t $left_pane -x $cols"
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Lanzar kitty adjuntando la sesion, DESACOPLADA del shell padre con
|
|
# setsid, para que no muera al cerrar la terminal invocadora.
|
|
# (Mismo patron que reboot_all_claudes para relanzar terminales.)
|
|
# -----------------------------------------------------------------------
|
|
# Adjuntar la sesion:
|
|
# - Terminal interactiva y FUERA de tmux: convertir ESA terminal en el
|
|
# panel FleetView (exec reemplaza el proceso; al hacer detach vuelve la
|
|
# shell). Asi `fleetclaude` no abre otra ventana: usa la actual.
|
|
# - DENTRO de tmux (o sin TTY: atajo de escritorio, cron, script): abrir
|
|
# una ventana kitty nueva desacoplada (setsid). No hacemos `attach`
|
|
# anidado dentro de otra sesion tmux (rompe / da el warning de nesting).
|
|
if [ -t 0 ] && [ -t 1 ] && [ -z "${TMUX:-}" ]; then
|
|
exec tmux -L "$session" attach -t "$session"
|
|
fi
|
|
# Ruta ventana-nueva: necesitamos kitty para abrirla.
|
|
if ! command -v kitty >/dev/null 2>&1; then
|
|
echo "launch_fleetclaude: kitty no esta instalado (necesario para abrir ventana nueva)." >&2
|
|
echo "launch_fleetclaude: lanzalo desde una terminal interactiva fuera de tmux, o instala kitty." >&2
|
|
return 1
|
|
fi
|
|
setsid kitty --title "FleetView ($session)" -e tmux -L "$session" attach -t "$session" </dev/null >/dev/null 2>&1 &
|
|
disown 2>/dev/null || true
|
|
|
|
echo "launch_fleetclaude: ventana kitty 'FleetView ($session)' adjunta al perfil '$session'."
|
|
return 0
|
|
}
|
|
|
|
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
launch_fleetclaude "$@"
|
|
fi
|