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/` 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 " · " --directory \ + zsh -ic 'claude --dangerously-skip-permissions "$(cat /tmp/orq_.md)"; exec zsh' \ + >/tmp/orq__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__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 " · " /tmp/orq_.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//`, `analysis//`, `projects/

/...` — 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/ -b 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 `, **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 ``. + +### 4. El prompt de cada secundario + +Lo escribes tú en `/tmp/orq_.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/

/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/.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/.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 `). + +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

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 ` (o `kill ` 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 ` 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 ` | +| 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`. 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