#!/usr/bin/env bash # spawn_fleet_agent — lanza un Claude como window nueva dentro de la sesion tmux # de un perfil FleetView (socket aislado), opcionalmente en modo orquestador # (skill embebida) y marcado con un role en su goal.json. # # Es la forma de que un ejecutor —o el propio orquestador— VIVA en la flota tmux # (visible en la TUI fleetview, conmutable con /fleet focus), en vez de en una # kitty suelta. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de # un perfil fleet ya montado. # # Funcion IMPURA: crea procesos (tmux window + claude) y, si se pide --role, # marca el goal.json del nuevo Claude via mark_claude_role (en background, porque # el sessionId no existe hasta que Claude escribe sessions/.json). set -euo pipefail IFS=$' \t\n' spawn_fleet_agent() { local socket="" session="" cwd="" prompt_file="" skill="" role="" parent="" title="claude" while [[ $# -gt 0 ]]; do case "$1" in --socket) shift; socket="${1:-}" ;; --session) shift; session="${1:-}" ;; --cwd) shift; cwd="${1:-}" ;; --prompt-file) shift; prompt_file="${1:-}" ;; --skill) shift; skill="${1:-}" ;; --role) shift; role="${1:-}" ;; --parent) shift; parent="${1:-}" ;; --title) shift; title="${1:-claude}" ;; -h|--help) cat <<'USAGE' Uso: spawn_fleet_agent --socket --session --cwd [opciones] Lanza un Claude como window nueva en la sesion tmux del socket (un perfil FleetView ya montado). Imprime el window_id creado. Opciones: --prompt-file Primer prompt del Claude = contenido del archivo (prompt autocontenido del ejecutor). El cat lo hace el shell del pane, asi que admite prompts multi-linea. --skill Primer prompt = "/" (invoca un skill al arrancar, ej. --skill orquestador para arrancar en modo orquestador). --role Marca el goal.json del nuevo Claude: orchestrator|executor (via mark_claude_role, en background). Sin esto, executor implicito. --parent sessionId del orquestador que lanza este ejecutor. Si se pasa, escribe parent_orchestrator en el goal.json del nuevo Claude (via mark_claude_parent, en background) para que el watcher de fleetview rutee sus avisos al orquestador padre. --title Nombre de la window tmux. Default: claude. Ejemplos: # Orquestador en la flota actual: spawn_fleet_agent --socket fleet2 --session fleet2 --cwd ~/fn_registry \ --skill orquestador --role orchestrator --title orquestador # Ejecutor con tarea autocontenida, atribuido a su orquestador padre: spawn_fleet_agent --socket fleet2 --session fleet2 --cwd ~/fn_registry \ --prompt-file /tmp/orq_health.md --title "kanban-health" \ --parent 32945650-a4e1-472b-90c9-5b38ef60a463 USAGE return 0 ;; *) echo "spawn_fleet_agent: opcion desconocida '$1' (usa -h)" >&2 return 2 ;; esac shift done [[ -z "$socket" || -z "$session" ]] && { echo "spawn_fleet_agent: --socket y --session son obligatorios" >&2 return 2 } [[ -z "$cwd" ]] && cwd="$PWD" command -v tmux >/dev/null 2>&1 || { echo "spawn_fleet_agent: tmux no esta instalado" >&2 return 1 } if ! tmux -L "$socket" has-session -t "$session" 2>/dev/null; then echo "spawn_fleet_agent: la sesion '$session' no existe en el socket '$socket' (lanza la flota primero)" >&2 return 1 fi # Window nueva detached + claude. send-keys con exec para que el pane_pid sea # el de claude (no el del shell), necesario para mark_claude_role. local win_pane win_id win_pane=$(tmux -L "$socket" new-window -d -t "$session" -n "$title" -c "$cwd" -P -F '#{pane_id}') if [[ -n "$skill" ]]; then # Skill como primer prompt: "/". Claude Code lo interpreta como # invocacion de slash command al arrancar. tmux -L "$socket" send-keys -t "$win_pane" \ "exec claude --dangerously-skip-permissions '/$skill'" C-m elif [[ -n "$prompt_file" ]]; then [[ -f "$prompt_file" ]] || { echo "spawn_fleet_agent: --prompt-file '$prompt_file' no existe" >&2 return 1 } # El cat lo ejecuta el shell del pane: admite prompts multi-linea. tmux -L "$socket" send-keys -t "$win_pane" \ "exec claude --dangerously-skip-permissions \"\$(cat $(printf '%q' "$prompt_file"))\"" C-m else tmux -L "$socket" send-keys -t "$win_pane" \ "exec claude --dangerously-skip-permissions" C-m fi # Marcar role y/o parent_orchestrator en background (no-fatal). El sleep da # tiempo a que el `exec claude` reemplace al shell antes de leer el pane_pid; # mark_claude_role / mark_claude_parent luego esperan a que aparezca # sessions/.json para resolver el sessionId. Se encadenan SECUENCIALMENTE # en el mismo subshell (primero role, luego parent) para que el segundo lea el # goal ya con la primera clave escrita y no haya carrera de # lectura-modificacion-escritura entre ambos. if [[ -n "$role" || -n "$parent" ]]; then local repo_root fn_bin repo_root="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel 2>/dev/null || echo "${FN_REGISTRY_ROOT:-$HOME/fn_registry}")" fn_bin="$repo_root/fn" if [[ -x "$fn_bin" ]]; then ( sleep 1 pid=$(tmux -L "$socket" display-message -p -t "$win_pane" '#{pane_pid}' 2>/dev/null || true) if [[ -n "$pid" ]]; then [[ -n "$role" ]] && "$fn_bin" run mark_claude_role "$pid" "$role" >/dev/null 2>&1 [[ -n "$parent" ]] && "$fn_bin" run mark_claude_parent "$pid" "$parent" >/dev/null 2>&1 fi ) >/dev/null 2>&1 & disown 2>/dev/null || true fi fi win_id=$(tmux -L "$socket" display-message -p -t "$win_pane" '#{window_id}' 2>/dev/null || true) echo "$win_id" return 0 } # Permitir ejecutar el archivo directamente (no solo como funcion sourced). if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then spawn_fleet_agent "$@" fi