feat: spawn de ejecutores en la flota tmux (flow 0012, gap 1 cerrado)
- spawn_fleet_agent (bash/functions/infra): lanza un Claude como window de la sesion tmux de un perfil fleet (no kitty suelta), con --skill para arrancar en un modo (ej. /orquestador), --prompt-file para ejecutores autocontenidos, y --role para marcar el goal.json via mark_claude_role. Asi ejecutores y orquestador viven en la flota, visibles en fleetview y conmutables con /fleet focus. - skill /orquestador: paso 2 ahora prefiere spawn_fleet_agent sobre kitty cuando se opera dentro de un perfil fleet ($FLEET_SOCKET seteado); tabla de funciones del grupo actualizada. Validado en vivo: el orquestador arranca en la flota fleet2 en modo (MODO ORQUESTADOR activo), role=orchestrator marcado, pinneado arriba en la TUI; los 9 ejecutores existentes intactos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,26 @@ queda en telemetría):
|
||||
el comando canónico exacto y devuelve el log donde se ve el arranque. Valida que el dir y el
|
||||
prompt_file existan y que kitty esté instalado.
|
||||
|
||||
#### En la flota tmux (PREFERIDO cuando operas en un perfil fleet)
|
||||
|
||||
Si estás dentro de un perfil FleetView (variable `$FLEET_SOCKET` seteada — eres el orquestador de
|
||||
una flota tmux montada con `launch_fleetclaude`), **NO lances kitties sueltas**: lanza cada
|
||||
ejecutor como una **window de la flota tmux** con `spawn_fleet_agent`, para que viva en la flota,
|
||||
se vea en la TUI `fleetview` y sea conmutable con `/fleet focus`:
|
||||
|
||||
```bash
|
||||
./fn run spawn_fleet_agent --socket "$FLEET_SOCKET" --session "$FLEET_SESSION" \
|
||||
--cwd <dir-aislado> --prompt-file /tmp/orq_<slug>.md --title "<subtarea>"
|
||||
# devuelve el window_id; despues escribe el DoD-contrato del ejecutor:
|
||||
./fn run set_dod_contract <sessionId-del-ejecutor> "<DoD golden+edge+error>" pending
|
||||
```
|
||||
|
||||
- `spawn_fleet_agent_bash_infra` crea la window tmux + arranca claude con el prompt autocontenido
|
||||
(o `--skill <name>` para arrancar en un modo), y con `--role executor|orchestrator` marca su
|
||||
`goal.json` (via `mark_claude_role`). El aislamiento git (sub-repo / worktree / scope) sigue
|
||||
imponiéndose en el prompt igual que con kitty.
|
||||
- Usa kitty (arriba) solo para secundarios que deban vivir **fuera** de un perfil fleet.
|
||||
|
||||
### 3. Aislamiento git obligatorio por secundario (regla de oro)
|
||||
|
||||
**Dos Claudes en el MISMO working tree comparten `HEAD` y el índice; sus `git checkout` se
|
||||
@@ -303,6 +323,8 @@ El orquestador no hace polling caro: drena la cola **cuando actúa** (cuando la
|
||||
| `drain_fleet_events_py_infra` | Consumir la cola de transiciones del watcher (`~/.claude/fleet/events.jsonl`), agrupada por clasificación + urgentes |
|
||||
| `classify_fleet_termination_go_infra` | Clasificar el estado de terminación de un agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO) — lo usa el watcher |
|
||||
| `list_claude_fleet_go_infra` | Fleet tipado con goal/phase/`dod_contract`/`dod_status`/`role` + window tmux (alimenta `/fleet` y el watcher) |
|
||||
| `spawn_fleet_agent_bash_infra` | Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty cuando hay perfil fleet |
|
||||
| `mark_claude_role_py_infra` | Marcar `role` (orchestrator/executor) en el goal.json de un Claude resolviendo PID→sessionId |
|
||||
|
||||
## Ejemplo end-to-end
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: spawn_fleet_agent
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.0.0
|
||||
purity: impure
|
||||
signature: "spawn_fleet_agent --socket <s> --session <s> --cwd <dir> [--prompt-file <f> | --skill <name>] [--role orchestrator|executor] [--title <t>]"
|
||||
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."
|
||||
tags: [fleet, claude-fleet, orchestration, tmux, infra]
|
||||
uses_functions:
|
||||
- mark_claude_role_py_infra
|
||||
uses_types: []
|
||||
error_type: error_go_core
|
||||
file_path: "bash/functions/infra/spawn_fleet_agent.sh"
|
||||
tested: false
|
||||
params:
|
||||
- name: --socket
|
||||
desc: "Socket tmux del perfil FleetView (ej. fleet, fleet2). El perfil debe estar ya montado (sesion viva)."
|
||||
- name: --session
|
||||
desc: "Nombre de la sesion tmux dentro del socket (normalmente igual al socket)."
|
||||
- name: --cwd
|
||||
desc: "Directorio de trabajo del nuevo Claude. Default: PWD."
|
||||
- name: --prompt-file
|
||||
desc: "Ruta a un archivo cuyo contenido sera el primer prompt del Claude (prompt autocontenido del ejecutor). El cat lo hace el shell del pane, admite multi-linea."
|
||||
- name: --skill
|
||||
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: --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."
|
||||
---
|
||||
|
||||
# spawn_fleet_agent
|
||||
|
||||
Lanza un Claude dentro de un perfil FleetView (sesion tmux de un socket aislado) como una window nueva, para que forme parte de la flota visible en la TUI `fleetview` y conmutable con `/fleet focus`. Es la pieza que hace que los ejecutores —y el orquestador— vivan en la flota tmux en vez de en kitties dispersas (flow 0012, Fase 3).
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Meter el ORQUESTADOR en la flota actual (arranca en modo + se pinea arriba):
|
||||
./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \
|
||||
--skill orquestador --role orchestrator --title orquestador
|
||||
|
||||
# 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"
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando el orquestador (o el launcher) necesita arrancar un Claude que debe vivir EN la flota tmux: un ejecutor con tarea, o el propio orquestador. Usala en lugar de `launch_claude_agent_kitty_bash_infra` siempre que ya exista un perfil fleet montado y quieras ver/conmutar el agente desde `fleetview` y `/fleet`. Tras lanzar un ejecutor, escribe su DoD-contrato con `set_dod_contract`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- 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/<PID>.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).
|
||||
- `--skill` envia `/<name>` 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.
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/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/<PID>.json).
|
||||
set -euo pipefail
|
||||
IFS=$' \t\n'
|
||||
|
||||
spawn_fleet_agent() {
|
||||
local socket="" session="" cwd="" prompt_file="" skill="" role="" 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:-}" ;;
|
||||
--title) shift; title="${1:-claude}" ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Uso: spawn_fleet_agent --socket <s> --session <s> --cwd <dir> [opciones]
|
||||
|
||||
Lanza un Claude como window nueva en la sesion tmux <session> del socket <socket>
|
||||
(un perfil FleetView ya montado). Imprime el window_id creado.
|
||||
|
||||
Opciones:
|
||||
--prompt-file <f> 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 <name> Primer prompt = "/<name>" (invoca un skill al arrancar, ej.
|
||||
--skill orquestador para arrancar en modo orquestador).
|
||||
--role <r> Marca el goal.json del nuevo Claude: orchestrator|executor
|
||||
(via mark_claude_role, en background). Sin esto, executor
|
||||
implicito.
|
||||
--title <t> 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:
|
||||
spawn_fleet_agent --socket fleet2 --session fleet2 --cwd ~/fn_registry \
|
||||
--prompt-file /tmp/orq_health.md --title "kanban-health"
|
||||
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: "/<skill>". 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 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/<PID>.json para resolver el sessionId.
|
||||
if [[ -n "$role" ]]; 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
|
||||
) >/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
|
||||
Reference in New Issue
Block a user