From b6ad1a3a153a7699b74ce9b8bfdeb6cd1512fccb Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 21 Jun 2026 13:28:07 +0200 Subject: [PATCH] feat(infra): spawn_fleet_agent --parent atribuye el ejecutor a su orquestador Nuevo flag opcional --parent: persiste parent_orchestrator en el goal.json del Claude recien lanzado (via mark_claude_parent, en background). Habilita el routing del watcher de fleetview, que asi sabe a que pane de orquestador empujar el aviso de cierre del ejecutor. El bloque background ahora cubre --role y/o --parent encadenados secuencialmente (primero role, luego parent) para evitar la carrera de lectura-modificacion-escritura sobre el goal.json. Retro-compatible: sin --parent, el spawn se comporta igual que antes. Bump 1.0.0 -> 1.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- bash/functions/infra/spawn_fleet_agent.md | 15 +++++++++--- bash/functions/infra/spawn_fleet_agent.sh | 29 ++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/bash/functions/infra/spawn_fleet_agent.md b/bash/functions/infra/spawn_fleet_agent.md index 5e7a419b..6cd7c2b8 100644 --- a/bash/functions/infra/spawn_fleet_agent.md +++ b/bash/functions/infra/spawn_fleet_agent.md @@ -3,13 +3,14 @@ name: spawn_fleet_agent kind: function lang: bash domain: infra -version: 1.0.0 +version: 1.1.0 purity: impure -signature: "spawn_fleet_agent --socket --session --cwd [--prompt-file | --skill ] [--role orchestrator|executor] [--title ]" -description: "Lanza un Claude como window nueva dentro de la sesion tmux de un perfil FleetView (socket aislado), opcionalmente en modo orquestador (skill embebida como primer prompt) y marcado con un role en su goal.json. Es la forma de que un ejecutor o el propio orquestador VIVAN en la flota tmux (visibles en la TUI fleetview, conmutables con /fleet focus) en vez de en kitties sueltas. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de un perfil fleet ya montado. Imprime el window_id creado." +signature: "spawn_fleet_agent --socket --session --cwd [--prompt-file | --skill ] [--role orchestrator|executor] [--parent ] [--title ]" +description: "Lanza un Claude como window nueva dentro de la sesion tmux de un perfil FleetView (socket aislado), opcionalmente en modo orquestador (skill embebida como primer prompt), marcado con un role en su goal.json y atribuido a su orquestador padre. Es la forma de que un ejecutor o el propio orquestador VIVAN en la flota tmux (visibles en la TUI fleetview, conmutables con /fleet focus) en vez de en kitties sueltas. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de un perfil fleet ya montado. Con --parent escribe parent_orchestrator en el goal.json del nuevo Claude (via mark_claude_parent) para que el watcher de fleetview rutee sus avisos al orquestador que lo lanzo. Imprime el window_id creado." tags: [fleet, claude-fleet, orchestration, tmux, infra] uses_functions: - mark_claude_role_py_infra + - mark_claude_parent_py_infra uses_types: [] error_type: error_go_core file_path: "bash/functions/infra/spawn_fleet_agent.sh" @@ -27,6 +28,8 @@ params: desc: "Nombre de un skill a invocar como primer prompt (ej. orquestador -> envia '/orquestador'). Tiene prioridad sobre --prompt-file." - name: --role desc: "Role a escribir en el goal.json del nuevo Claude: orchestrator | executor. Se aplica via mark_claude_role en background. Sin esto, executor implicito." + - name: --parent + desc: "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. Opcional; sin esto el aviso no se atribuye a un orquestador concreto." - name: --title desc: "Nombre de la window tmux creada. Default: claude." output: "Imprime por stdout el window_id (ej. @7) de la window tmux creada. Exit 0 ok; 1 error de entorno (tmux ausente, sesion inexistente, prompt-file inexistente); 2 uso incorrecto." @@ -46,6 +49,11 @@ Lanza un Claude dentro de un perfil FleetView (sesion tmux de un socket aislado) # Lanzar un EJECUTOR con tarea autocontenida en la misma flota: ./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \ --prompt-file /tmp/orq_health.md --title "kanban-health" + +# Ejecutor atribuido a SU orquestador padre (habilita el routing del watcher): +./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \ + --prompt-file /tmp/orq_health.md --title "kanban-health" \ + --parent 32945650-a4e1-472b-90c9-5b38ef60a463 ``` ## Cuando usarla @@ -56,6 +64,7 @@ Cuando el orquestador (o el launcher) necesita arrancar un Claude que debe vivir - El perfil (socket+session) debe estar **ya montado** (`launch_fleetclaude` primero); si la sesion no existe, falla con exit 1. - El `--role` se aplica en **background**: el `sessionId` del nuevo Claude no existe hasta que Claude escribe `~/.claude/sessions/.json` (unos segundos). `mark_claude_role` espera ese archivo. Si el arranque es muy lento, el role puede tardar en aparecer; es no-fatal (el agente simplemente no se pinea/identifica hasta entonces). +- El `--parent` se aplica igual en **background** via `mark_claude_parent` (misma espera del `sessions/.json`). Cuando se pasan `--role` y `--parent` juntos se encadenan **secuencialmente** en el mismo subshell (primero role, luego parent) para que la segunda escritura lea el goal ya con la primera clave puesta — sin carrera de lectura-modificacion-escritura. Es no-fatal: si el sessions JSON no aparece a tiempo, el `parent_orchestrator` simplemente no se escribe. - `--skill` envia `/` como primer prompt: depende de que Claude Code interprete el primer argumento como invocacion de slash command (verificado con `/orquestador`). - El nuevo Claude hereda `FLEET_SOCKET`/`FLEET_SESSION` del entorno del server tmux (que `launch_fleetclaude` fija con `set-environment`), asi apunta al perfil correcto. - `--dangerously-skip-permissions` siempre (los agentes de la flota trabajan desatendidos); riesgo asumido como en el resto del modo orquestador. diff --git a/bash/functions/infra/spawn_fleet_agent.sh b/bash/functions/infra/spawn_fleet_agent.sh index a513543b..7da67cbe 100644 --- a/bash/functions/infra/spawn_fleet_agent.sh +++ b/bash/functions/infra/spawn_fleet_agent.sh @@ -15,7 +15,7 @@ set -euo pipefail IFS=$' \t\n' spawn_fleet_agent() { - local socket="" session="" cwd="" prompt_file="" skill="" role="" title="claude" + local socket="" session="" cwd="" prompt_file="" skill="" role="" parent="" title="claude" while [[ $# -gt 0 ]]; do case "$1" in @@ -25,6 +25,7 @@ spawn_fleet_agent() { --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' @@ -42,15 +43,20 @@ Opciones: --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: + # 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" + --prompt-file /tmp/orq_health.md --title "kanban-health" \ + --parent 32945650-a4e1-472b-90c9-5b38ef60a463 USAGE return 0 ;; *) @@ -98,17 +104,24 @@ USAGE "exec claude --dangerously-skip-permissions" C-m fi - # Marcar role 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 luego - # espera a que aparezca sessions/.json para resolver el sessionId. - if [[ -n "$role" ]]; then + # 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) - [[ -n "$pid" ]] && "$fn_bin" run mark_claude_role "$pid" "$role" >/dev/null 2>&1 + 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