diff --git a/.claude/commands/orquestador.md b/.claude/commands/orquestador.md index 2ac64f50..382dc4d1 100644 --- a/.claude/commands/orquestador.md +++ b/.claude/commands/orquestador.md @@ -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 --prompt-file /tmp/orq_.md --title "" +# devuelve el window_id; despues escribe el DoD-contrato del ejecutor: +./fn run set_dod_contract "" pending +``` + +- `spawn_fleet_agent_bash_infra` crea la window tmux + arranca claude con el prompt autocontenido + (o `--skill ` 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 diff --git a/bash/functions/infra/spawn_fleet_agent.md b/bash/functions/infra/spawn_fleet_agent.md new file mode 100644 index 00000000..5e7a419b --- /dev/null +++ b/bash/functions/infra/spawn_fleet_agent.md @@ -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 --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." +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/.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 `/` 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 new file mode 100644 index 00000000..a513543b --- /dev/null +++ b/bash/functions/infra/spawn_fleet_agent.sh @@ -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/.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 --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. + --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: + 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: "/". 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/.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