From 5c253a26e2078e01e0248ee096df4542bafcaa5f Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 7 Jun 2026 19:32:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(infra):=20grupo=20orchestration=20?= =?UTF-8?q?=E2=80=94=20launch=5Fclaude=5Fagent=5Fkitty=20+=20list=5Fclaude?= =?UTF-8?q?=5Fagents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos funciones bash para la mecanica del modo orquestador (Claudes secundarios interactivos en kitty): - launch_claude_agent_kitty(title, directory, prompt_file): lanza un Claude Code secundario en su propia terminal kitty con un prompt autonomo inyectado y --dangerously-skip-permissions, detached (setsid nohup ... disown) para sobrevivir al cierre de la terminal padre. - list_claude_agents([--json] [--exclude-current]): lista la flota de Claudes vivos cruzando pgrep -x claude con ~/.claude/sessions/.json (con validacion anti-PID-reciclado por procStart), reportando PID, sessionId, cwd, status, etime y KITTY_PID. Reusa la logica de descubrimiento de reboot_all_claudes_bash_infra. Tag de grupo de capacidad: orchestration. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../infra/launch_claude_agent_kitty.md | 66 +++++ .../infra/launch_claude_agent_kitty.sh | 135 +++++++++ bash/functions/infra/list_claude_agents.md | 55 ++++ bash/functions/infra/list_claude_agents.sh | 265 ++++++++++++++++++ 4 files changed, 521 insertions(+) create mode 100644 bash/functions/infra/launch_claude_agent_kitty.md create mode 100755 bash/functions/infra/launch_claude_agent_kitty.sh create mode 100644 bash/functions/infra/list_claude_agents.md create mode 100755 bash/functions/infra/list_claude_agents.sh diff --git a/bash/functions/infra/launch_claude_agent_kitty.md b/bash/functions/infra/launch_claude_agent_kitty.md new file mode 100644 index 00000000..c57b0e18 --- /dev/null +++ b/bash/functions/infra/launch_claude_agent_kitty.md @@ -0,0 +1,66 @@ +--- +name: launch_claude_agent_kitty +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "launch_claude_agent_kitty(title: string, directory: string, prompt_file: string) -> string" +description: "Lanza un Claude Code secundario interactivo y persistente en su propia terminal kitty, con un prompt autonomo inyectado desde un archivo y --dangerously-skip-permissions. Mecanica del modo orquestador: un Claude principal descompone una tarea y lanza N secundarios, cada uno en su kitty, que el humano ve y puede retomar. La ventana sobrevive al cierre de la terminal padre (setsid nohup ... disown) y deja una shell interactiva viva cuando el claude termina (exec zsh)." +tags: [orchestration, agents, claude, kitty, agent, terminal, infra] +params: + - name: title + desc: "Titulo de la ventana kitty. Ej: 'fn_registry · subtarea X'. Tambien se sanitiza (minusculas, no-alfanumerico -> '_') para derivar el slug del archivo de log." + - name: directory + desc: "Directorio de trabajo AISLADO donde arranca el claude secundario (worktree git, sub-repo, o dir cualquiera). Debe existir; si no -> error exit 2. Usar un dir aislado: dos claudes en el mismo working tree comparten HEAD y dispersan commits." + - name: prompt_file + desc: "Ruta a un archivo .md con el prompt autonomo a inyectar (ej. /tmp/orq_.md). Debe existir y ser legible; si no -> error exit 2." +output: "Imprime en stdout el title, directory, prompt_file y la ruta del log (/tmp/orq__kitty.log) donde se ve el arranque. Exit 0 = lanzamiento disparado; exit 2 = argumentos invalidos; exit 1 = kitty no instalado." +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/launch_claude_agent_kitty.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/launch_claude_agent_kitty.sh + +# El orquestador prepara un worktree aislado y un archivo de prompt... +git worktree add /tmp/orq_docs_wt -b orq/docs +cat > /tmp/orq_docs.md <<'PROMPT' +Eres un agente secundario. Tu tarea: revisar y mejorar la documentacion del +dominio infra del registry. Trabaja SOLO en este worktree. Reporta al terminar. +PROMPT + +# ...y lanza un claude secundario en su propia kitty: +launch_claude_agent_kitty "fn_registry · docs" /tmp/orq_docs_wt /tmp/orq_docs.md +# -> abre una ventana kitty titulada "fn_registry · docs", arranca claude con +# el prompt inyectado, y deja /tmp/orq_fn_registry_docs_kitty.log con el arranque. +``` + +O directo via `fn run`: + +```bash +./fn run launch_claude_agent_kitty "fn_registry · docs" /tmp/orq_docs_wt /tmp/orq_docs.md +``` + +## Cuando usarla + +Cuando el orquestador quiere lanzar un Claude secundario **interactivo** en su propia terminal kitty para una sub-tarea que el humano quiere **ver y poder retomar**. A diferencia del `Agent` tool (sub-agente no interactivo, headless, cuyo output vuelve al padre y no deja terminal abierta), aqui cada secundario corre en una ventana visible que persiste: el humano observa el progreso en vivo y, cuando el claude termina, la shell sigue ahi para continuar manualmente o relanzar. + +## Gotchas + +- **kitty debe estar instalado.** Si `command -v kitty` falla -> exit 1 con mensaje claro. No hay fallback a otra terminal. +- **El `directory` debe ser AISLADO** (worktree git o sub-repo propio). Dos claudes apuntando al mismo working tree **comparten HEAD** y dispersan/cruzan los commits (memoria `multi-agent-git-race-same-repo`). El orquestador debe crear un worktree/clon por agente antes de llamar. +- **`--dangerously-skip-permissions` corre sin pedir confirmacion** a ninguna accion (memoria `lanzar-agentes-skip-permissions`). Es a proposito para agentes autonomos desatendidos, pero es un riesgo asumido: el secundario puede tocar el sistema sin gates. No lanzar sobre directorios sensibles. +- **El log de `/tmp/orq__kitty.log` es donde se ve el arranque** (errores de kitty/claude al iniciar). El `` deriva del `title` sanitizado; titulos distintos que colapsen al mismo slug sobrescriben el mismo log. +- **El PID reportado no es el de kitty.** Con `setsid` el `$!` es el del proceso setsid, no el de la ventana; por eso la funcion reporta el log en vez de un PID. Para encontrar la ventana despues: `pgrep -af kitty | grep `. +- **El prompt se inyecta con `"$(cat <prompt_file>)"` evaluado DENTRO de la kitty.** Si editas el `prompt_file` despues de lanzar pero antes de que la kitty arranque, el claude vera la version editada (se lee en el momento del arranque, no del lanzamiento). diff --git a/bash/functions/infra/launch_claude_agent_kitty.sh b/bash/functions/infra/launch_claude_agent_kitty.sh new file mode 100755 index 00000000..ca2fe815 --- /dev/null +++ b/bash/functions/infra/launch_claude_agent_kitty.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# launch_claude_agent_kitty — Lanza un Claude Code secundario interactivo y +# persistente en su propia terminal kitty, con un prompt autonomo inyectado +# desde un archivo. Es la mecanica de lanzamiento del "modo orquestador": un +# Claude principal descompone una tarea y lanza N secundarios, cada uno en su +# kitty, que el humano ve y puede retomar. +# +# Mecanismo: +# - setsid nohup kitty ... & disown -> la ventana sobrevive al cierre de la +# terminal padre (igual que reboot_all_claudes con setsid). +# - zsh -ic 'claude ...; exec zsh' -> al terminar el claude queda una shell +# interactiva viva para que el humano siga en esa terminal. +# - --dangerously-skip-permissions -> agente autonomo desatendido (sin +# confirmaciones). Riesgo asumido a proposito. +# - El prompt se inyecta con "$(cat <prompt_file>)" para no expandir nada en +# el shell del orquestador. +# - Log de arranque en /tmp/orq_<slug>_kitty.log, donde <slug> deriva del +# title (minusculas, no-alfanumerico -> '_'). +set -euo pipefail +IFS=$' \t\n' + +launch_claude_agent_kitty() { + # ----------------------------------------------------------------------- + # Ayuda / sin argumentos. + # ----------------------------------------------------------------------- + if [[ $# -eq 0 || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + cat <<'USAGE' +Uso: launch_claude_agent_kitty <title> <directory> <prompt_file> + +Lanza un Claude Code secundario interactivo y persistente en su propia +terminal kitty, con el prompt del archivo <prompt_file> inyectado y +--dangerously-skip-permissions (agente autonomo desatendido). + +Argumentos (los 3 obligatorios): + title Titulo de la ventana kitty. Ej: "fn_registry · subtarea X". + directory Directorio de trabajo AISLADO donde arranca el claude + secundario (worktree git, sub-repo, o dir cualquiera). Debe + existir. Usa un dir aislado: dos claudes en el mismo working + tree comparten HEAD y dispersan commits. + prompt_file Ruta a un archivo .md con el prompt autonomo a inyectar. + Debe existir y ser legible. + +Ejemplo: + launch_claude_agent_kitty "fn_registry · docs" /tmp/orq_docs_wt /tmp/orq_docs.md + +El log de arranque va a /tmp/orq_<slug>_kitty.log (slug derivado del title). +USAGE + return 0 + fi + + # ----------------------------------------------------------------------- + # Validacion de argumentos. + # ----------------------------------------------------------------------- + if [[ $# -ne 3 ]]; then + echo "launch_claude_agent_kitty: se requieren 3 argumentos <title> <directory> <prompt_file> (recibidos: $#). Usa -h." >&2 + return 2 + fi + + local title="$1" + local directory="$2" + local prompt_file="$3" + + if [[ -z "$title" ]]; then + echo "launch_claude_agent_kitty: <title> no puede estar vacio." >&2 + return 2 + fi + + if [[ ! -d "$directory" ]]; then + echo "launch_claude_agent_kitty: el directorio de trabajo no existe: '$directory'." >&2 + return 2 + fi + + if [[ ! -f "$prompt_file" ]]; then + echo "launch_claude_agent_kitty: el prompt_file no existe: '$prompt_file'." >&2 + return 2 + fi + + if [[ ! -r "$prompt_file" ]]; then + echo "launch_claude_agent_kitty: el prompt_file no es legible: '$prompt_file'." >&2 + return 2 + fi + + # ----------------------------------------------------------------------- + # Comprobar que kitty esta instalado. + # ----------------------------------------------------------------------- + if ! command -v kitty >/dev/null 2>&1; then + echo "launch_claude_agent_kitty: 'kitty' no esta instalado o no esta en el PATH." >&2 + return 1 + fi + + # ----------------------------------------------------------------------- + # Derivar el slug del title para el nombre del log. + # minusculas, todo no-alfanumerico -> '_', colapsar/recortar '_'. + # ----------------------------------------------------------------------- + local slug + slug="$(printf '%s' "$title" \ + | tr '[:upper:]' '[:lower:]' \ + | tr -c 'a-z0-9' '_' \ + | sed -E 's/_+/_/g; s/^_//; s/_$//')" + [[ -z "$slug" ]] && slug="agent" + + local log="/tmp/orq_${slug}_kitty.log" + + # ----------------------------------------------------------------------- + # Lanzar la kitty detached. El prompt se inyecta con "$(cat <prompt_file>)" + # ya escapado para que se evalue DENTRO de la kitty, no aqui. + # exec zsh deja una shell viva cuando el claude termina. + # ----------------------------------------------------------------------- + local inner + inner="claude --dangerously-skip-permissions \"\$(cat $(printf '%q' "$prompt_file"))\"; exec zsh" + + setsid nohup kitty \ + --title "$title" \ + --directory "$directory" \ + zsh -ic "$inner" \ + >"$log" 2>&1 & + disown 2>/dev/null || true + + # ----------------------------------------------------------------------- + # Reportar. Con setsid el $! es el PID de setsid, no el de kitty; basta + # con confirmar el lanzamiento y apuntar al log donde se ve el arranque. + # ----------------------------------------------------------------------- + echo "launch_claude_agent_kitty: claude secundario lanzado." + echo " title: $title" + echo " directory: $directory" + echo " prompt_file: $prompt_file" + echo " log: $log" + echo " (sigue el arranque con: tail -f $(printf '%q' "$log"))" + return 0 +} + +# Permitir ejecutar el archivo directamente (no solo como funcion sourced). +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + launch_claude_agent_kitty "$@" +fi diff --git a/bash/functions/infra/list_claude_agents.md b/bash/functions/infra/list_claude_agents.md new file mode 100644 index 00000000..0e5b2bad --- /dev/null +++ b/bash/functions/infra/list_claude_agents.md @@ -0,0 +1,55 @@ +--- +name: list_claude_agents +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "list_claude_agents([--json] [--exclude-current] [-h|--help])" +description: "Lista todas las instancias de Claude Code VIVAS cruzando pgrep -x claude con los archivos de estado por proceso ~/.claude/sessions/<PID>.json. Para cada claude vivo y validado devuelve PID, status (idle/busy), etime (tiempo de vida), KITTY_PID de su ventana kitty, sessionId y cwd. Es la herramienta de seguimiento de la flota del modo orquestador: el Claude principal ve que agentes secundarios siguen vivos, en que directorio trabajan y su sessionId para retomarlos con claude --resume." +tags: [orchestration, claude, session, fleet, kitty, infra, terminal-capture] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: "--json" + desc: "Imprime un array JSON (un objeto por agente con pid, session_id, cwd, status, etime, kitty_pid, self) en vez de la tabla legible. Pensado para que el agente parsee y decida cual retomar/parar." + - name: "--exclude-current" + desc: "Omite la propia sesion del listado. Detecta el claude propio subiendo por la cadena de ancestros de $$ hasta hallar un proceso con comm=claude. Sin esta opcion, la sesion actual se marca (columna SELF en tabla / self=true en JSON)." + - name: "-h|--help" + desc: "Muestra el uso y termina con exit 0." +output: "En modo tabla: una fila por claude vivo y validado con columnas PID, STATUS, ETIME, KITTY, SELF, SESSION_ID, CWD. En modo --json: array JSON con pid, session_id, cwd, status, etime, kitty_pid (null si no corre en kitty) y self. Si no hay claudes vivos imprime aviso (tabla) o [] (json) y exit 0. Exit 0 normal; exit 2 si flag invalido." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/list_claude_agents.sh" +notes: "Mecanismo (Claude Code 2.1.x sobre Linux + kitty): pgrep -x claude -> PIDs vivos; ~/.claude/sessions/<PID>.json -> sessionId/cwd/status/procStart (parseado con python3); validacion en tres capas: kill -0 <PID> exito, el JSON existe, y anti-PID-reciclado comparando procStart del JSON con el campo 22 de /proc/<PID>/stat (si difieren el JSON es huerfano de un PID reusado y se omite). KITTY_PID se saca del environ del proceso (tr '\\0' '\\n' < /proc/<PID>/environ | sed -n 's/^KITTY_PID=//p'). etime via ps -o etime= -p <PID>. Reusa la misma logica de descubrimiento y validacion que reboot_all_claudes_bash_infra. El codigo JSON va en python3 -c con los datos por stdin TSV (no heredoc) para no colisionar el stdin del pipe." +--- + +## Ejemplo + +```bash +# Tabla legible de la flota de Claudes vivos (PID, status, etime, kitty, sessionId, cwd). +./fn run list_claude_agents + +# Array JSON para parsear (decidir cual retomar con claude --resume <session_id>). +./fn run list_claude_agents --json + +# Omitir la propia sesion (ver solo los agentes secundarios). +./fn run list_claude_agents --exclude-current +``` + +## Cuando usarla + +Cuando el orquestador necesita ver la flota de Claudes secundarios vivos (PID, cwd, sessionId, status) para seguir su progreso o decidir cual retomar/parar. Lanzala al inicio de un ciclo de seguimiento para saber que agentes siguen activos y en que directorio trabaja cada uno; usa `--json` cuando vayas a programar la decision (filtrar por `status`, extraer `session_id` para un `claude --resume`). + +## Gotchas + +- **Requiere Claude Code >= 2.1.x.** Depende de que cada sesion escriba `~/.claude/sessions/<PID>.json` con los campos `sessionId`, `cwd`, `status`, `procStart`. Si una version futura cambia el formato, la funcion deja de mapear PID -> sessionId y omitira las sesiones. +- **Un JSON puede ser huerfano por PID reciclado.** El sistema operativo reusa PIDs; un `<PID>.json` viejo puede apuntar a un proceso `claude` distinto. Por eso se valida `procStart` del JSON contra el campo 22 de `/proc/<PID>/stat`; si no coincide la entrada se descarta. Sin esa validacion se reportarian agentes fantasma. +- **El titulo exacto de la ventana kitty no se recupera sin `kitty @`.** Se reporta el `KITTY_PID` (suficiente para identificar la ventana); mapearlo al titulo requeriria `kitty @ ls`, que solo funciona si el control remoto de kitty esta habilitado. KISS: se omite por defecto. Un claude que corra fuera de kitty (terminal integrado de un editor, etc.) sale con `KITTY` vacio `(none)` / `kitty_pid: null`. +- **Solo ve procesos del usuario actual.** `pgrep -x claude` y la lectura de `/proc/<PID>/{environ,stat}` solo cubren los claudes del propio usuario; no lista sesiones de otros usuarios del sistema. +- **`status` refleja el ultimo estado guardado en el JSON**, no necesariamente el instante exacto de la consulta (Claude actualiza el archivo al cambiar de estado). Pueden aparecer valores como `idle`, `busy` o `waiting`. diff --git a/bash/functions/infra/list_claude_agents.sh b/bash/functions/infra/list_claude_agents.sh new file mode 100755 index 00000000..a295c681 --- /dev/null +++ b/bash/functions/infra/list_claude_agents.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# list_claude_agents — Lista todas las instancias de Claude Code VIVAS cruzando +# pgrep -x claude con los archivos de estado por proceso ~/.claude/sessions/<PID>.json. +# Para cada claude vivo y validado reporta: PID, status (idle/busy), etime (tiempo de +# vida del proceso), KITTY_PID de la ventana kitty si corre en una, sessionId y cwd. +# Es la herramienta de "seguimiento de la flota" del modo orquestador: el Claude +# principal la usa para ver que agentes secundarios siguen vivos, en que directorio +# trabajan y su sessionId (para poder retomarlos con claude --resume <sessionId>). +# +# Mecanismo (Claude Code 2.1.x sobre Linux + kitty): +# - pgrep -x claude -> PIDs de las sesiones interactivas vivas. +# - ~/.claude/sessions/<PID>.json -> mapea PID a {sessionId, cwd, status, procStart}. +# - Anti-PID-reciclado: procStart del JSON debe coincidir con el campo 22 de +# /proc/<PID>/stat; ademas kill -0 <PID> debe tener exito y el JSON debe existir. +# - KITTY_PID del environ del proceso -> ventana kitty (titulo exacto requeriria +# 'kitty @ ls'; aqui se reporta el KITTY_PID, suficiente para identificarla). +# - etime via ps -o etime= -p <PID>. +set -euo pipefail +IFS=$' \t\n' + +list_claude_agents() { + local output="table" # table | json + local exclude_current=0 + + # ----------------------------------------------------------------------- + # Parseo de argumentos + # ----------------------------------------------------------------------- + while [[ $# -gt 0 ]]; do + case "$1" in + --json) + output="json" + ;; + --exclude-current) + exclude_current=1 + ;; + -h|--help) + cat <<'USAGE' +Uso: list_claude_agents [--json] [--exclude-current] + +Lista las instancias de Claude Code vivas y validas, una fila por agente, con su +PID, status, etime (tiempo de vida), KITTY_PID, sessionId y cwd. Pensada para el +modo orquestador: ver la flota de Claudes secundarios y su sessionId para retomar +(claude --resume <sessionId>) o decidir cual parar. + +Opciones: + --json Imprime un array JSON (pid, session_id, cwd, status, etime, + kitty_pid) en vez de la tabla. Util para parsear. + --exclude-current Omite la propia sesion (sube por la cadena de ancestros de + $$ hasta hallar un proceso con comm=claude). Sin esta opcion, + la sesion actual se marca con self=true / SELF en la tabla. + -h, --help Muestra esta ayuda. + +Ejemplos: + list_claude_agents + list_claude_agents --json + list_claude_agents --exclude-current +USAGE + return 0 + ;; + *) + echo "list_claude_agents: opcion desconocida: '$1' (usa -h)" >&2 + return 2 + ;; + esac + shift + done + + # ----------------------------------------------------------------------- + # Detectar el PID de la sesion actual subiendo por la cadena de ancestros + # hasta encontrar un proceso cuyo comm sea exactamente "claude". + # Se usa tanto para --exclude-current como para marcar la fila propia. + # ----------------------------------------------------------------------- + local current_claude_pid="" + local walk="$$" + local guard=0 + while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do + local comm="" + comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)" + if [[ "$comm" == "claude" ]]; then + current_claude_pid="$walk" + break + fi + # campo 4 de /proc/<pid>/stat es el PPID + walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)" + guard=$((guard + 1)) + [[ "$guard" -gt 64 ]] && break + done + + # ----------------------------------------------------------------------- + # Recolectar las sesiones vivas y validarlas. + # ----------------------------------------------------------------------- + local sessions_dir="$HOME/.claude/sessions" + local pids="" + pids="$(pgrep -x claude 2>/dev/null || true)" + + if [[ -z "$pids" ]]; then + if [[ "$output" == "json" ]]; then + echo "[]" + else + echo "list_claude_agents: no hay sesiones de Claude Code vivas (pgrep -x claude vacio)." + fi + return 0 + fi + + # Arrays paralelos con la flota validada. + local -a a_pid a_status a_etime a_kitty a_sid a_cwd a_self + + local pid + for pid in $pids; do + # Validacion 1: el proceso debe seguir vivo. + if ! kill -0 "$pid" 2>/dev/null; then + continue + fi + + # Validacion 2: debe existir su JSON de sesion. + local json="$sessions_dir/$pid.json" + if [[ ! -f "$json" ]]; then + continue + fi + + # Parsear el JSON con python3 (campos sessionId, cwd, status, procStart). + # Salida: lineas "clave=valor" en orden fijo. + local parsed="" + parsed="$(python3 - "$json" <<'PY' 2>/dev/null || true +import json, sys +try: + with open(sys.argv[1]) as fh: + d = json.load(fh) +except Exception: + sys.exit(0) +print("sessionId=" + str(d.get("sessionId", ""))) +print("cwd=" + str(d.get("cwd", ""))) +print("status=" + str(d.get("status", ""))) +print("procStart=" + str(d.get("procStart", ""))) +PY +)" + [[ -z "$parsed" ]] && continue + + local sid cwd status proc_start_json + sid="$(printf '%s\n' "$parsed" | sed -n 's/^sessionId=//p')" + cwd="$(printf '%s\n' "$parsed" | sed -n 's/^cwd=//p')" + status="$(printf '%s\n' "$parsed" | sed -n 's/^status=//p')" + proc_start_json="$(printf '%s\n' "$parsed" | sed -n 's/^procStart=//p')" + + [[ -z "$sid" ]] && continue + + # Validacion 3 (anti-PID-reciclado): procStart del JSON debe coincidir + # con el campo 22 de /proc/<PID>/stat. + local proc_start_real="" + proc_start_real="$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true)" + if [[ -n "$proc_start_json" && "$proc_start_json" != "$proc_start_real" ]]; then + # JSON huerfano de un PID reciclado: omitir. + continue + fi + + # Omitir la propia sesion si se pidio --exclude-current. + if [[ "$exclude_current" -eq 1 && -n "$current_claude_pid" && "$pid" == "$current_claude_pid" ]]; then + continue + fi + + # KITTY_PID de la ventana kitty (vacio si claude no corre en kitty). + local kitty_pid="" + kitty_pid="$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | sed -n 's/^KITTY_PID=//p' | head -n1)" + + # etime: tiempo transcurrido desde que arranco el proceso. + local etime="" + etime="$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ' || true)" + + # Marca de sesion propia (solo relevante cuando NO se excluye). + local self="false" + if [[ -n "$current_claude_pid" && "$pid" == "$current_claude_pid" ]]; then + self="true" + fi + + a_pid+=("$pid") + a_status+=("${status:-?}") + a_etime+=("${etime:-?}") + a_kitty+=("${kitty_pid:-}") + a_sid+=("$sid") + a_cwd+=("${cwd:-?}") + a_self+=("$self") + done + + local total="${#a_pid[@]}" + if [[ "$total" -eq 0 ]]; then + if [[ "$output" == "json" ]]; then + echo "[]" + else + echo "list_claude_agents: ninguna sesion valida encontrada (PIDs huerfanos, reciclados, o sin JSON)." + fi + return 0 + fi + + # ----------------------------------------------------------------------- + # Salida JSON. + # ----------------------------------------------------------------------- + if [[ "$output" == "json" ]]; then + # Delegar el escaping correcto de strings (cwd con espacios, etc.) a python3. + # El codigo python va en -c y los datos por stdin (TSV), para no colisionar + # el heredoc con el stdin del pipe. + local i + { + for ((i = 0; i < total; i++)); do + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${a_pid[$i]}" \ + "${a_sid[$i]}" \ + "${a_cwd[$i]}" \ + "${a_status[$i]}" \ + "${a_etime[$i]}" \ + "${a_kitty[$i]}" \ + "${a_self[$i]}" + done + } | python3 -c ' +import json, sys +out = [] +for line in sys.stdin: + line = line.rstrip("\n") + if not line: + continue + pid, sid, cwd, status, etime, kitty, self_ = line.split("\t") + out.append({ + "pid": int(pid) if pid.isdigit() else pid, + "session_id": sid, + "cwd": cwd, + "status": status, + "etime": etime, + "kitty_pid": (int(kitty) if kitty.isdigit() else (kitty or None)), + "self": (self_ == "true"), + }) +print(json.dumps(out, indent=2)) +' + return 0 + fi + + # ----------------------------------------------------------------------- + # Salida tabla legible. + # ----------------------------------------------------------------------- + echo "list_claude_agents — claudes vivos: ${total}" + echo + printf '%-8s %-7s %-12s %-9s %-6s %-38s %s\n' \ + "PID" "STATUS" "ETIME" "KITTY" "SELF" "SESSION_ID" "CWD" + printf '%-8s %-7s %-12s %-9s %-6s %-38s %s\n' \ + "--------" "-------" "------------" "---------" "------" \ + "--------------------------------------" "---" + + local i + for ((i = 0; i < total; i++)); do + local self_mark="" + [[ "${a_self[$i]}" == "true" ]] && self_mark="SELF" + printf '%-8s %-7s %-12s %-9s %-6s %-38s %s\n' \ + "${a_pid[$i]}" \ + "${a_status[$i]}" \ + "${a_etime[$i]}" \ + "${a_kitty[$i]:-(none)}" \ + "${self_mark:--}" \ + "${a_sid[$i]}" \ + "${a_cwd[$i]}" + done + return 0 +} + +# Permitir ejecutar el archivo directamente (no solo como funcion sourced). +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + list_claude_agents "$@" +fi From 9a9b8764003283e044f0df7ebfe712f026b2cf4f Mon Sep 17 00:00:00 2001 From: Egutierrez <egutierrez@dead.dd> Date: Sun, 7 Jun 2026 19:32:47 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(commands):=20/orquestador=20=E2=80=94?= =?UTF-8?q?=20modo=20de=20coordinacion=20de=20Claudes=20secundarios=20en?= =?UTF-8?q?=20kitty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevo slash command que codifica el modo orquestador: el Claude principal descompone una tarea grande y lanza Claudes secundarios interactivos, cada uno en su propia terminal kitty con un prompt autonomo inyectado y aislamiento git impuesto (worktree / sub-repo / scope disjunto). El humano habla solo con el orquestador, ve a los secundarios en sus terminales y puede saltar a cualquiera. El cuerpo cubre los 8 pasos del ciclo (descomponer, lanzar, aislar, prompt, seguir, no pkill, integrar, kitty vs Agent tool), la plantilla del comando de lanzamiento, la tabla de seguimiento de la flota, las reglas de aislamiento, los anti-patrones y un ejemplo end-to-end. Referencia las funciones del registry launch_claude_agent_kitty_bash_infra, list_claude_agents_bash_infra y reboot_all_claudes_bash_infra (grupo orchestration). Deja explicita la diferencia con fn-orquestador / autopilot (Agent tool en sandbox no-interactivo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .claude/commands/orquestador.md | 279 ++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 .claude/commands/orquestador.md diff --git a/.claude/commands/orquestador.md b/.claude/commands/orquestador.md new file mode 100644 index 00000000..a8d392c1 --- /dev/null +++ b/.claude/commands/orquestador.md @@ -0,0 +1,279 @@ +--- +name: orquestador +description: "Modo orquestador: el Claude principal NO hace el trabajo pesado — descompone la tarea y lanza Claudes SECUNDARIOS interactivos, cada uno en su propia terminal kitty con un prompt autonomo y aislamiento git impuesto. El humano habla solo con el orquestador, ve a los secundarios en sus kitties y puede saltar a cualquiera. El orquestador sigue la flota, lee sus reports e integra. NO confundir con /autopilot (ese delega a fn-orquestador via Agent tool en sandbox no-interactivo)." +--- + +# /orquestador — coordinar Claudes secundarios interactivos en kitty + +Activa un **modo de comportamiento** persistente. Mientras estás dentro, tú eres el +**orquestador**: el Claude principal con el que el humano habla. Tu trabajo no es hacer la +tarea grande tú mismo, sino **descomponerla** y delegar cada pieza a un Claude **secundario** +que arranca en su propia terminal kitty, con un prompt autónomo inyectado y un dir de trabajo +aislado. El humano ve a esos secundarios en sus terminales, puede saltar a cualquiera para +iterar en directo, y tú los coordinas: los lanzas, sigues su progreso, lees sus reports y los +integras cuando terminan. + +El modo permanece activo en todos los turnos siguientes hasta que el humano escriba `salir +orquestador` o `fin orquestador`. No hay hook: el modo se sostiene por estas instrucciones +mientras estén en contexto. Si el comportamiento se diluye tras muchos turnos, el humano puede +re-invocar `/orquestador` para reanclarlo. + +Al entrar, responde con una sola línea de confirmación y queda a la espera de la tarea grande: + +``` +MODO ORQUESTADOR activo. Dame la tarea grande; la descompongo y lanzo secundarios. 'fin orquestador' para terminar. +``` + +## Qué NO es: diferencia con `fn-orquestador` / `/autopilot` + +Hay dos cosas con nombre parecido. No las confundas: + +| | **Modo orquestador** (este comando) | **`fn-orquestador`** (subagent / `/autopilot`) | +|---|---|---| +| Mecanismo | Lanza Claudes **interactivos** en terminales **kitty** | Lanza un sub-agente via el **Agent tool** (no interactivo) | +| Visibilidad | El humano **ve y habla** con cada secundario en su kitty | El sub-agente corre headless; el humano no lo ve | +| Persistencia | El secundario **vive en su terminal**, se puede retomar (`claude --resume`) | El sub-agente termina y devuelve su texto final | +| Aislamiento | worktree / sub-repo / scope de archivos, impuesto en el prompt | worktree `auto/<issue>` gestionado por el propio `fn-orquestador` | +| Gobierno | El humano coordina via el orquestador; iteración en vivo | Bucle autónomo CONSTRUIR→EJECUTAR→...→MEJORAR hasta converger, PR draft | +| Regla de referencia | esta página | `.claude/rules/autonomous_loop.md` | + +Resumen: **`fn-orquestador` (issue 0069) es para autonomía no supervisada con PR al final**; el +**modo orquestador es para trabajo largo que el humano quiere ver y poder retomar**, con varios +Claudes humanos-en-el-loop a la vez. Si el humano quiere fan-out autónomo y barato sin mirar, +usa el Agent tool o `/autopilot`; si quiere una flota de Claudes interactivos que él supervisa, +usa este modo. + +## El ciclo del orquestador (8 pasos) + +### 1. Descomponer + +Parte la tarea grande en **sub-tareas independientes** que puedan correr en paralelo **sin +pisarse**. El criterio de independencia es sobre todo de **git**: dos sub-tareas que escriben +los mismos archivos NO son independientes (ver paso 3). Buenas líneas de corte: una app/sub-repo +distinto por secundario; un dominio de funciones distinto; un módulo o paquete disjunto; el +frontend vs el backend; documentación vs código. Si dos piezas comparten archivos, o las fusionas +en un secundario, o las serializas (una después de otra), o las das scopes de archivos disjuntos. + +### 2. Lanzar cada secundario + +Comando canónico de lanzamiento (memoria `lanzar-agentes-skip-permissions`), **siempre** con +`--dangerously-skip-permissions` porque los secundarios trabajan autónomos y desatendidos y los +prompts de permiso en cada Bash los atascarían: + +```bash +setsid nohup kitty --title "<PROYECTO> · <subtarea>" --directory <dir-aislado> \ + zsh -ic 'claude --dangerously-skip-permissions "$(cat /tmp/orq_<slug>.md)"; exec zsh' \ + >/tmp/orq_<slug>_kitty.log 2>&1 & disown +``` + +`setsid nohup ... & disown` hace que la kitty sobreviva al cierre de la terminal padre. El +`zsh -ic '...; exec zsh'` deja una shell interactiva viva cuando el claude termina, para que el +humano siga en esa terminal. El log de `/tmp/orq_<slug>_kitty.log` es donde se ve el arranque. + +**Prefiere la función del registry** en vez de teclear el one-liner a mano (registry-first, +queda en telemetría): + +```bash +./fn run launch_claude_agent_kitty "<PROYECTO> · <subtarea>" <dir-aislado> /tmp/orq_<slug>.md +``` + +- `launch_claude_agent_kitty_bash_infra(title, directory, prompt_file)` — lanza el secundario con + 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. + +### 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 +interleavean y los commits caen en la rama equivocada** (memoria `multi-agent-git-race-same-repo`, +caso real del 06/06/2026: los commits de un agente acabaron en la rama del otro y su propia rama +quedó vacía). Por eso **cada secundario trabaja en un espacio aislado**, y el orquestador elige +cuál y se lo **impone** en el prompt del secundario: + +| Opción | Cómo | Cuándo | +|---|---|---| +| **(a) Sub-repo Gitea propio** | El secundario trabaja dentro de `apps/<x>/`, `analysis/<x>/`, `projects/<p>/...` — cada uno tiene su `.git` independiente (regla `apps_subrepo.md`) | Cuando las sub-tareas caen en apps/analyses/projects distintos. Es el aislamiento natural del monorepo. | +| **(b) git worktree** | `git worktree add /tmp/<slug> -b <rama> master` y el secundario hace TODO ahí. Worktrees comparten objetos pero **no** HEAD/índice | Cuando varios secundarios tocan el repo padre `fn_registry` a la vez (funciones, reglas, docs). | +| **(c) Scope de archivos disjunto** | Mismo working tree pero cada secundario commitea **solo sus paths**: `git add <paths-específicos>`, **nunca** `git add -A` | Último recurso, solo si los scopes están garantizados disjuntos y no hay `git checkout` de rama de por medio. Frágil; prefiere (a) o (b). | + +Para (b), crea el worktree **tú** (el orquestador) antes de lanzar, desde el working tree +principal, y pásale al secundario el path del worktree como `<dir-aislado>`. + +### 4. El prompt de cada secundario + +Lo escribes tú en `/tmp/orq_<slug>.md` antes de lanzar. El secundario **no ve este historial**; +el prompt debe ser **autocontenido**. Incluye SIEMPRE: + +1. **Objetivo claro** — qué construir/arreglar, acotado y verificable. +2. **Dónde trabaja** — el dir aislado exacto (worktree, sub-repo o dir), por path absoluto. +3. **Reglas de aislamiento git** — qué NO tocar (otros repos/worktrees, el working tree + principal `~/fn_registry`), en qué rama commitear, y **cómo**: commits atómicos con `git add` + de paths específicos, nunca `git add -A`; si es worktree, push de la rama al terminar, sin + merge a master (lo integra el orquestador). +4. **Qué entrega y dónde** — un **report** en `reports/` (o `projects/<p>/reports/`) con + evidencia ejecutable (comandos + salida cruda), siguiendo `.claude/rules/reports.md` y + `.claude/rules/dod_quality.md`. Reports son artefacto local gitignored: se escriben, no se + commitean. +5. **Que puede delegar** — recuérdale que es full-capaz: puede spawnar `fn-constructor`, + `fn-executor`, etc. via el Agent tool, y debe seguir registry-first (`registry_calls.md`, + `delegation.md`). +6. **La coletilla**: *"reporta tu progreso en esta terminal"* — para que el humano que mire la + kitty vea el estado sin abrir el report. + +Mira `/tmp/unibus_agent_*.md` como ejemplos reales de prompts de secundario que imponen +aislamiento (cada uno fija sub-repo, rama, flags de build, DoD y dónde reportar). + +### 5. Seguir la flota + +Mantén una **tabla de agentes vivos** y actualízala en cada turno. La fuente de verdad del +mapeo PID→sessionId→cwd son los archivos `~/.claude/sessions/<PID>.json` (memoria +`claude-session-pid-mapping`). Usa la función del registry para listarla: + +```bash +./fn run list_claude_agents # tabla: PID, STATUS, ETIME, KITTY, SELF, SESSION_ID, CWD +./fn run list_claude_agents --json # para parsear y decidir +``` + +- `list_claude_agents_bash_infra([--json] [--exclude-current])` — cruza `pgrep -x claude` con los + `sessions/<PID>.json` (con validación anti-PID-reciclado), marca tu propia sesión como `SELF`, + y reporta cwd + sessionId de cada secundario (para retomar con `claude --resume <sessionId>`). + +Tu tabla de seguimiento, una fila por secundario: + +| slug | título kitty | PID | cwd / dir aislado | rama | log | report | estado | +|---|---|---|---|---|---|---|---| +| docs | fn_registry · docs | 3637133 | /tmp/orq_docs_wt | orq/docs | /tmp/orq_docs_kitty.log | reports/00NN-…-docs.md | en curso | + +Cuando un secundario parezca terminado, confirma: ¿pusheó la rama? ¿escribió el report? Lee el +report (`reports/`), revisa los commits de su rama (`git -C <dir> log --oneline`). + +### 6. NUNCA `pkill`/`killall` sobre claude + +Un `pkill claude` o `killall claude` **te mata a ti mismo** (el orquestador) junto con la flota. +Para parar un secundario: + +- **Kill por PID exacto** del secundario (lo tienes en la tabla / `list_claude_agents`): + `kill <PID>` (o `kill <KITTY_PID>` para cerrar su ventana). Verifica que NO es tu `SELF`. +- **`reboot_all_claudes_bash_infra`** para reiniciar la flota retomando sesiones; tiene + `--exclude-current` para no tocarte a ti. Es dry-run por defecto; `--go` para ejecutar. + +### 7. Integrar + +Cuando un secundario termina (rama pusheada + report verde): + +1. **Revisa** su diff y su report. Si el report no trae evidencia ejecutable o falla la DoD, + devuélvele trabajo (el humano puede saltar a su kitty, o tú le mandas otro prompt). +2. **Mergea si procede** desde el **working tree principal** (ahí suele estar `master` + checked-out): `git -C ~/fn_registry merge --no-ff <rama>` para apps con TBD, o el flujo que + corresponda al sub-repo. Para funciones nuevas del registry padre, sus archivos viajan en la + rama y el merge los lleva a master. +3. **Informa al humano** y **resume el estado de la flota** en cada turno: quién terminó, quién + sigue, qué se integró, qué falta. + +### 8. kitty vs Agent tool — cuándo cada uno + +- **kitty (este modo)**: trabajo **largo e interactivo** que el humano quiere **ver** y poder + **retomar** — implementar una feature de horas, depurar en vivo, una sesión que evoluciona. +- **Agent tool directo**: fan-out **acotado y no interactivo** — buscar en el codebase, crear + una función con `fn-constructor`, auditar N apps con `fn-recopilador`. Más barato, sin + terminal, sin supervisión humana. Para esto NO lances kitty: usa `Agent(...)` y ya. + +Regla práctica: si el humano va a querer hablar con ello o mirarlo trabajar → kitty. Si es una +sub-tarea que devuelve un resultado y se acabó → Agent tool. + +## Reglas duras del modo + +- **El orquestador no hace el trabajo pesado.** Descompone, lanza, sigue, integra. Si te + encuentras escribiendo tú la feature, párate: ¿no debería ser un secundario? +- **Cada secundario, su aislamiento.** Nunca lances dos secundarios sobre el mismo working tree + sin worktrees/sub-repos/scopes disjuntos. Es la causa nº1 de commits perdidos. +- **El prompt del secundario lleva SIEMPRE las reglas de aislamiento.** Un prompt sin "trabaja + aquí, no toques aquello, commitea así" es un secundario que contaminará otro repo. +- **Nunca `git add -A` en un secundario** salvo que su dir aislado sea exclusivamente suyo + (worktree/sub-repo). En scope compartido, paths específicos. +- **Nunca `pkill`/`killall claude`.** Kill por PID exacto o `reboot_all_claudes --exclude-current`. +- **El humano habla contigo.** Tú resumes la flota; no le hagas perseguir 5 terminales. + +## Anti-patrones + +| Anti-patrón | Por qué es malo | En su lugar | +|---|---|---| +| `pkill claude` para parar la flota | Te mata a ti (el orquestador) también | Kill por PID exacto / `reboot_all_claudes --exclude-current` | +| Dos secundarios en el mismo working tree | Comparten HEAD/índice → commits dispersos, ramas vacías | worktree / sub-repo / scope disjunto por secundario | +| Prompt de secundario sin reglas de aislamiento | El secundario contamina el repo padre u otro worktree | El prompt fija dir, qué NO tocar, rama y cómo commitear | +| `git add -A` en scope compartido | Arrastra cambios de otra sub-tarea al commit | `git add <paths-específicos>` | +| Lanzar kitty para un fan-out trivial | Caro y sin supervisión que aporte | Agent tool directo (`fn-constructor`, `Explore`, …) | +| Hacer tú la feature "porque es rápido" | Pierdes el sentido del modo; el humano no lo ve evolucionar | Descompón y lanza un secundario | +| Lanzar sin `--dangerously-skip-permissions` | El secundario se atasca pidiendo permiso en cada Bash | Siempre `--dangerously-skip-permissions` (riesgo asumido) | +| Mergear desde el dir del secundario | Master suele estar en el working tree principal; colisión de HEAD | Mergear desde `~/fn_registry` | + +## Funciones del registry que usa este modo (grupo `orchestration`) + +| Función | Para qué | +|---|---| +| `launch_claude_agent_kitty_bash_infra` | Lanzar un secundario en kitty con prompt autónomo + `--dangerously-skip-permissions` | +| `list_claude_agents_bash_infra` | Listar la flota de Claudes vivos (PID, sessionId, cwd, status, kitty) para seguirla | +| `reboot_all_claudes_bash_infra` | Reiniciar/parar la flota retomando sesiones; `--exclude-current` para no tocarte | + +## Ejemplo end-to-end + +Tarea grande: *"añade un endpoint `/api/health` al backend de la app `kanban` y, en paralelo, +documenta el grupo de capacidad `deploy` en `docs/capabilities/deploy.md`"*. Dos piezas +independientes: una toca el sub-repo `apps/kanban` (su propio `.git`), la otra toca el repo +padre `fn_registry` (docs). Aislamiento natural distinto para cada una. + +```bash +# 1. Descomponer → 2 secundarios independientes: +# A) health endpoint → sub-repo apps/kanban (aislamiento (a)) +# B) doc capability → worktree del padre (aislamiento (b)) + +# 2. Preparar aislamiento de B (worktree del padre; A ya está aislado por su sub-repo): +git -C ~/fn_registry worktree add /tmp/orq_capdoc -b orq/cap-deploy master + +# 3. Escribir los prompts autónomos (autocontenidos, con reglas de aislamiento): +# /tmp/orq_health.md → "trabaja en apps/kanban (sub-repo propio), rama issue/health, +# commits atómicos de tus paths, push al terminar, report en reports/. No toques el +# repo padre. Reporta tu progreso en esta terminal." +# /tmp/orq_capdoc.md → "trabaja SOLO en /tmp/orq_capdoc (worktree), rama orq/cap-deploy, +# toca solo docs/capabilities/deploy.md, git add de ese path, push al terminar, report +# en reports/. No toques ~/fn_registry. Reporta tu progreso en esta terminal." + +# 4. Lanzar ambos secundarios (cada uno su kitty, su dir aislado): +./fn run launch_claude_agent_kitty "kanban · health endpoint" \ + ~/fn_registry/apps/kanban /tmp/orq_health.md +./fn run launch_claude_agent_kitty "fn_registry · doc deploy" \ + /tmp/orq_capdoc /tmp/orq_capdoc.md + +# 5. Seguir la flota (cada turno): +./fn run list_claude_agents +# → tabla con los 2 secundarios vivos (PID, cwd, sessionId, status) + tu SELF. +# Lee /tmp/orq_*_kitty.log para el arranque; cuando terminen, lee sus reports/. + +# 7. Integrar (desde el working tree principal): +git -C ~/fn_registry/apps/kanban merge --no-ff issue/health # sub-repo de la app +git -C ~/fn_registry merge --no-ff orq/cap-deploy # repo padre (la doc) +git -C ~/fn_registry worktree remove /tmp/orq_capdoc # limpiar worktree + +# Resumen al humano: A integrado (endpoint + test verde), B integrado (doc), +# flota vacía. Tarea grande hecha. +``` + +## Salida del modo + +Cuando el humano escriba `salir orquestador` o `fin orquestador`, cierra con un resumen de la +flota: secundarios lanzados, cuáles terminaron e integraste, cuáles siguen vivos (con su kitty +para que el humano decida), y los reports generados. Si quedan secundarios vivos, recuérdale que +`list_claude_agents` los lista y que para pararlos es kill por PID exacto, nunca `pkill`. + +## Relación con otras reglas + +- `.claude/rules/autonomous_loop.md` — `fn-orquestador` (Agent tool, sandbox no-interactivo). Es + lo que este modo **no** es; tenlas claras separadas. +- `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*` + gitignored): el aislamiento natural (opción (a)) y el gotcha de `git init` antes de limpiar un + worktree con una app nueva dentro. +- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario: + report con evidencia ejecutable + gaps. +- `.claude/rules/delegation.md` + `.claude/rules/registry_calls.md` — los secundarios siguen + registry-first y delegan a `fn-constructor` igual que tú. +- Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`, + `claude-session-pid-mapping`, `prefiere-kitty-terminal`.