Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00cd5274bc | |||
| cd658cc703 | |||
| 415154d9a3 | |||
| d479a8e4e2 | |||
| 9286e3b6b1 | |||
| d412522db9 | |||
| c1a4a83717 | |||
| 81e8597d21 | |||
| 4de071f2f9 | |||
| fcf5a4c6a3 | |||
| 959648ec4f | |||
| a3f75d61ec | |||
| cb7a7fc1fd | |||
| 9cdde4a341 | |||
| 5501507588 | |||
| 88eabb0457 | |||
| ebb00d8a42 | |||
| e142ef026d | |||
| c4cff5ed5b | |||
| caf8c25d99 | |||
| 7ac69ab4fb | |||
| 02301aaed3 | |||
| 2729629f0a | |||
| 6cc90558d4 | |||
| 36a725ba10 | |||
| 1dd6c889e5 | |||
| 7aaac44a49 | |||
| ffcb69ce02 | |||
| c79f33265e | |||
| 31c2f6ac7f | |||
| 3bc97828e3 | |||
| ccdd529bdc | |||
| 741724f633 | |||
| 2be62f6ef6 | |||
| 8e9e1e6c8a | |||
| ec46aae04c | |||
| b173ac2703 | |||
| ec0a5e53ac | |||
| 5280499df5 | |||
| 346f859b86 | |||
| 604d3d4feb | |||
| 287abbd6ee | |||
| f8793f96ac | |||
| 643ebfb849 | |||
| 537516e32e | |||
| ca07b25297 | |||
| fbbff7d5e7 | |||
| bdd841d9af | |||
| 7d33b39859 | |||
| a1074d32e7 | |||
| fd16453691 | |||
| 5494507c39 | |||
| dfb3eda087 | |||
| 83738d4035 | |||
| b77d223f01 | |||
| e178ab8d2d | |||
| cda36408d0 | |||
| 10dbc510b7 | |||
| d3d846f748 | |||
| a5748cb147 | |||
| 0eefb7cfcd | |||
| 9f0d2e2338 | |||
| 2bab120d7c | |||
| d08667df9b | |||
| 9f1d643013 | |||
| 914def9e5c | |||
| 1012355998 | |||
| 1585e986c1 | |||
| e1f1be02ce | |||
| a27dcc028c | |||
| 8a4cc323a3 | |||
| 2a7c77cb56 | |||
| fa94f7a235 | |||
| 0ce1c31fb9 | |||
| 5a0818ee9c | |||
| 1a8093a7be | |||
| ba302dd793 | |||
| 0421bc6d4f | |||
| 5662a54fa7 | |||
| b45165dbc5 | |||
| dbb040aa12 | |||
| 91cf683289 | |||
| 696148d56b | |||
| 19ad2b3e5d | |||
| b88730b7cb | |||
| 6add50311b | |||
| ab27c253c5 | |||
| 8fb10fdf8a | |||
| 0c1d2aa4fc | |||
| 2ff111bae4 | |||
| d7387d9d2c | |||
| 03df14df97 | |||
| d0960bed70 | |||
| 0dd2718c95 | |||
| 4c4eec4b1d | |||
| f5387aa30e | |||
| 3980fbbffb | |||
| 4886305d49 | |||
| 404e2e4d0c | |||
| 3f465aceed | |||
| 3be8b28a8f | |||
| aeefd09f19 | |||
| e57da2f6d5 | |||
| 9508fff282 | |||
| 8121e4b04e | |||
| 4302212b34 | |||
| 394221f8c7 | |||
| 69d9aed46a | |||
| c36c80dda9 | |||
| 3887e59092 | |||
| d5660aa13f | |||
| a56b6e36ea | |||
| 5f0df32728 | |||
| 6d1b66167d | |||
| 04ecf9f394 | |||
| 46954d8584 | |||
| 6f4b440762 | |||
| bcf731275e | |||
| 974cc06bc7 | |||
| 70d541fca9 | |||
| e8a66f0dad | |||
| 898502a321 | |||
| 2fe36e314e | |||
| 11ef8ef6db | |||
| 3e75d1bf79 | |||
| 68f0ce0dae | |||
| c0b2dce3b0 | |||
| ff41f4f053 | |||
| f686b338d6 | |||
| 3823a28d1c | |||
| 337f75b527 | |||
| d3f05a19a5 | |||
| d7245efa59 | |||
| 1311c7e585 | |||
| db4f454f8a | |||
| f12272d002 | |||
| 495f545ec1 | |||
| f34badb500 | |||
| 3289c67986 | |||
| bcc1fe1738 | |||
| 7619347be8 | |||
| f55e41cf74 | |||
| e2e8669edf | |||
| 86d68dc9f0 | |||
| b18759823d | |||
| a59d50238d | |||
| f17d957a8f | |||
| c1f355ffa5 | |||
| 237f763c19 | |||
| bf67ff3180 | |||
| 03fc0461fa | |||
| a1105dc4c5 | |||
| 3c9e909eda | |||
| 3cf8b21fea | |||
| cbefc82c02 | |||
| fb76b53c17 | |||
| 8e16202935 | |||
| e4a36f1133 | |||
| 295f90afaf | |||
| f85c1a322a | |||
| 32c7336bf6 | |||
| c1071a82b3 |
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: Modo ausente — el orquestador itera solo (lanza agentes, verifica cierres, genera tareas del roadmap, push periódico) sin supervisión, hasta que el humano vuelva. Auto-continúa con ScheduleWakeup.
|
||||
---
|
||||
|
||||
# /ausente — orquestador autónomo desatendido
|
||||
|
||||
Activa un **loop autónomo del modo orquestador**: el humano se va y tú sigues trabajando solo
|
||||
—lanzando agentes, verificando sus cierres, cerrando los que cumplen su DoD, generando tareas
|
||||
nuevas cuando la flota se vacía, y sincronizando— **hasta que el humano vuelva**. Es el modo
|
||||
orquestador (`.claude/commands/orquestador.md` + `.claude/rules/orchestration.md`) corriendo sin
|
||||
prompts humanos, con un mecanismo de auto-continuación.
|
||||
|
||||
Requisito: estar ya en modo orquestador (`role=orchestrator`). `/ausente` NO sustituye al
|
||||
orquestador, lo deja en piloto automático.
|
||||
|
||||
## Configuración de esta sesión (elegida por el humano)
|
||||
- **Al vaciarse la flota**: seguir el **roadmap ComfyUI** — generar tareas nuevas sin parar.
|
||||
- **Git**: **push periódico** — `/full-git-push` tras cada bloque de tareas cerrado.
|
||||
- **Límite**: **hasta que el humano vuelva** — heartbeat ~25 min + el watcher; tope DURO de 6
|
||||
ejecutores a la vez; parar en cuanto el humano escriba.
|
||||
|
||||
(Si se reinvoca `/ausente` en otra sesión, re-confirmar estas 3 con el humano vía AskUserQuestion.)
|
||||
|
||||
## El bucle (cada vez que te re-invocan: por FLEET-DONE del watcher o por el heartbeat)
|
||||
|
||||
1. **Drena la flota**: `./fn run drain_fleet_events`. Para cada ejecutor `DICE_TERMINADO`:
|
||||
**verifica de primera mano** (lee su report + comprueba en disco/CDP que el golden existe — no
|
||||
te fíes del autodeclarado). Si cumple el DoD → `set_dod_contract <sid> "<c>" met` y **ciérralo
|
||||
con `kill <PID>` directo** (NUNCA `kill_fleet_agent`/`kill-window`: cierra windows ajenas y se
|
||||
llevó la console de fleetview — incidente real). Si falla → nudge con el gap concreto.
|
||||
2. **Nudge** a los `ESTANCADO` (idle > 10 min con DoD sin cerrar). NUNCA a `waiting`.
|
||||
3. **¿Flota con hueco?** (< 6 ejecutores y hay backlog) → **genera la siguiente tarea del roadmap**
|
||||
(lista abajo), escribe su prompt autocontenido con aislamiento + DoD-contrato, lánzala con
|
||||
`spawn_fleet_agent --parent <tu-sid>`, fíjale nombre (`fleet_set_name`) + DoD. Respeta el tope
|
||||
de 6 y la disjunción de recursos (server/venv/GPU vs functions+fn_index vs disco — ver
|
||||
`orchestration.md`): solo UN agente dueño del server/venv a la vez; solo UNO toca
|
||||
`functions/`+`fn index` a la vez; los descargadores de modelos van a carpetas distintas.
|
||||
4. **Push periódico**: cuando cierres un bloque (>=1 tarea met e integrada), corre
|
||||
`./fn run full_git_push_bash_pipelines ""` y verifica que el padre queda alineado con
|
||||
`origin/master`. Diagnostica y reintenta si falla (regla de `/full-git-push`).
|
||||
5. **Bitácora**: añade una línea al report de bitácora `reports/NNNN-ausente-bitacora.md` (créalo
|
||||
la primera vez): timestamp + qué cerraste + qué lanzaste + push. Es lo que el humano lee al
|
||||
volver.
|
||||
6. **Reprograma el heartbeat**: `ScheduleWakeup(delaySeconds≈1500, prompt="/ausente",
|
||||
reason="loop ausente: vigilar flota + roadmap ComfyUI")`. Si hay agentes en vuelo, el watcher
|
||||
te empujará sus FLEET-DONE antes (no hace falta wakeup corto); el heartbeat es el fallback para
|
||||
cuando la flota está vacía y hay que generar tareas nuevas.
|
||||
|
||||
## Supervivencia a la compactación de contexto
|
||||
El loop es de larga duración → el contexto se llenará. **Cuando te quedes sin contexto, deja que
|
||||
el harness compacte la conversación y CONTINÚA el modo ausente** — no lo trates como una parada.
|
||||
El modo sobrevive porque su estado es **durable fuera del contexto**:
|
||||
- El `ScheduleWakeup(prompt="/ausente")` re-inyecta el modo en cada heartbeat (y el FLEET-DONE del
|
||||
watcher también te re-entra).
|
||||
- La **bitácora** `reports/ausente-bitacora-2026-06-24.md` es la memoria persistente: qué se cerró,
|
||||
qué se lanzó, qué falta del backlog, último push. **Tras una compactación, lo PRIMERO es releer
|
||||
la bitácora** (y `fleet_list`) para reconstruir el estado y seguir donde lo dejaste.
|
||||
- Mantén la bitácora al día en CADA turno (no solo al cerrar bloques) para que la compactación
|
||||
nunca pierda progreso. El comando `/ausente` + `orchestration.md` reconstruyen la doctrina.
|
||||
Una compactación NO es el humano volviendo — sigue iterando con normalidad.
|
||||
|
||||
## Parada
|
||||
- **El humano vuelve** = recibes un prompt que NO es un FLEET-DONE ni el `/ausente` del heartbeat
|
||||
(es texto del humano). Entonces: **no reprogrames el wakeup**, resume todo lo hecho durante la
|
||||
ausencia (lee la bitácora) y vuelve al modo orquestador interactivo normal.
|
||||
- Si el backlog del roadmap se agota del todo (raro): haz un último push, deja la flota cerrada,
|
||||
escribe el resumen en la bitácora, programa un heartbeat largo y queda a la espera.
|
||||
|
||||
## Reglas duras (más estrictas sin supervisión)
|
||||
- **Nada destructivo ni irreversible sin el humano**: no borrar datos/modelos/repos, no `git push
|
||||
--force`, no tocar producción/VPS, no mandar nada hacia afuera (correos, mensajes, APIs con
|
||||
efecto), no pagar/descargar gated de pago. Ante la duda, NO lo hagas: déjalo anotado en la
|
||||
bitácora como "pendiente de revisión humana".
|
||||
- **Aislamiento git por agente** SIEMPRE (sub-repo / worktree / scope disjunto). Ningún agente
|
||||
commitea el padre salvo el push periódico que corres tú.
|
||||
- **Tope 6 ejecutores**. Encola el resto.
|
||||
- **Cierre por `kill <PID>`**, jamás `pkill`/`killall`/`kill_fleet_agent` (protege la TUI/console
|
||||
de fleetview y a ti mismo).
|
||||
- **Verificación adversarial**: el golden de cada cierre se comprueba en disco/CDP/ejecución, no
|
||||
por lo que el agente diga. Honestidad en la bitácora (gaps incluidos).
|
||||
- Cada agente full-capaz sigue registry-first y delega a `fn-constructor`; tú no escribes lógica
|
||||
reutilizable inline.
|
||||
|
||||
## Backlog del roadmap ComfyUI (fuente de tareas a generar; prioriza arriba→abajo)
|
||||
Base: `reports/0064-comfyui-roadmap-plan.md` + propuestas de los reports 0069/0073/0075/0079.
|
||||
1. **Funciones 3D propuestas pendientes**: `comfyui_build_view_3d_workflow`,
|
||||
`comfyui_generate_views_from_image` (Zero123/SV3D), `comfyui_text_to_3d_oneshot` (pipeline),
|
||||
`comfyui_build_multiview_textured_3d_workflow`. (Dueño de functions/+fn index, uno a la vez.)
|
||||
2. **`comfyui_download_workflow`** (detecta Drive/GitHub/Civitai/PNG → API format) — del catálogo
|
||||
de fuentes (report `comfyui-wf-sources`).
|
||||
3. **P2 del roadmap**: `comfyui_batch_generate`, `comfyui_interrupt_queue`,
|
||||
`comfyui_ensure_server` (systemd-user con --lowvram + health).
|
||||
4. **Vídeo end-to-end**: montar workflow LTX-Video y Wan2.1 (modelos ya en /mnt/2tb), generar un
|
||||
clip corto SFW de prueba, validar VRAM 8GB; capitalizar `comfyui_build_video_workflow`.
|
||||
5. **Calidad 3D**: decimación de mesh (`fast_simplification`, gap del 0069) + watertight
|
||||
(`VoxelToMesh`); función `comfyui_simplify_mesh`.
|
||||
6. **Librería de workflows**: bajar+validar los ejemplos recomendados por `comfyui-wf-sources`,
|
||||
dejarlos en una librería local validada contra nuestro server.
|
||||
7. **Higiene**: `fn doctor` sobre las funciones nuevas (uses-functions/unused), capability page
|
||||
`docs/capabilities/comfyui.md` al día, tests de las funciones sin cobertura.
|
||||
8. Cuando ideas concretas se agoten: un agente "completeness critic" que audite el grupo `comfyui`
|
||||
y proponga el siguiente lote.
|
||||
|
||||
Cada tarea generada respeta el patrón del orquestador: prompt autocontenido (objetivo, dir,
|
||||
aislamiento, qué entrega, DoD-contrato golden+edge+error), `--parent`, nombre + DoD fijados al
|
||||
lanzar, verificación de primera mano al cerrar.
|
||||
|
||||
## Relación
|
||||
- `.claude/commands/orquestador.md` — el modo base; `/ausente` es su versión desatendida.
|
||||
- `.claude/rules/orchestration.md` — maquinaria (drain, clasificación, verificador, nudge, tope).
|
||||
- `.claude/rules/autonomous_loop.md` — `fn-orquestador` (Agent tool, sandbox). `/ausente` NO es
|
||||
eso: aquí TÚ (el orquestador interactivo) sigues conduciendo la flota de Claudes interactivos.
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
description: Genera en un vault Obsidian un resumen capítulo a capítulo de uno o varios libros, siguiendo el formato de notas del vault captacion_clientes (MOC de libro + una nota por capítulo + MOC de categoría, todo enlazado con wikilinks).
|
||||
---
|
||||
|
||||
# /capitulos — resumen de libros capítulo a capítulo en Obsidian
|
||||
|
||||
Genera notas de estudio de un libro (o varios) en un vault Obsidian, replicando el formato
|
||||
canónico del vault `captacion_clientes`: una nota MOC por libro, una nota por capítulo, y una
|
||||
nota MOC de categoría que agrupa los libros. Todo enlazado con wikilinks `[[ ]]` para que
|
||||
Obsidian construya el grafo.
|
||||
|
||||
## Argumentos
|
||||
|
||||
`$ARGUMENTS` contiene, en lenguaje natural, los libros a procesar y opcionalmente el destino.
|
||||
Interpreta:
|
||||
|
||||
- **Libros** — uno o varios títulos. Pueden venir con autor ("Forecasting de Hyndman"). Si el
|
||||
usuario dice "los libros que me has dicho" o similar, usa los que se recomendaron en la
|
||||
conversación previa.
|
||||
- **Vault destino** — si no se especifica, **PREGUNTA** antes de escribir (ver Decisiones).
|
||||
Vault por defecto de ejemplo de formato: `/home/enmanuel/Obsidian/captacion_clientes`.
|
||||
- **Categoría** — la subcarpeta bajo `Libros/` que agrupa los libros (ej. "Marca y Mercado",
|
||||
"Datos e Inversión"). Si no se da, propón una coherente con el tema de los libros y confírmala.
|
||||
- **Profundidad** — `completo` (default, como The Mom Test: idea central + puntos clave +
|
||||
citas + aplicación por capítulo) o `breve` (idea central + 3 bullets por capítulo).
|
||||
|
||||
## Decisiones a confirmar antes de escribir (si faltan en los argumentos)
|
||||
|
||||
Usa `AskUserQuestion` para resolver lo que cambie el trabajo, NO inventes:
|
||||
|
||||
1. **Vault y categoría destino** — dónde se crean las notas.
|
||||
2. **Alcance** — qué libros exactamente y cuántos (si la lista es grande, confirma si son
|
||||
todos o un subconjunto; cada libro es trabajo no trivial).
|
||||
3. **Enfoque de "Aplicación"** — el ángulo desde el que se escribe la sección "Aplicación a mi
|
||||
negocio / a mi caso" de cada capítulo (ej. inversión cuantitativa, data-analyst, SaaS…).
|
||||
El vault de captación lo orienta al negocio del usuario; mantén ese espíritu pero ajustado
|
||||
al tema real de los libros.
|
||||
|
||||
## Estructura de archivos a crear
|
||||
|
||||
```
|
||||
<vault>/Libros/<Categoría>/
|
||||
<Categoría> - MOC.md # MOC de categoría (crear o ACTUALIZAR, no sobrescribir)
|
||||
<Libro>/
|
||||
<Libro> - MOC.md # MOC del libro
|
||||
01 - <Título capítulo>.md # una nota por capítulo, NN zero-padded a 2 dígitos
|
||||
02 - <Título capítulo>.md
|
||||
...
|
||||
```
|
||||
|
||||
- Carpeta por libro, archivo por capítulo. Nombre de capítulo: `NN - <Título>.md` con `NN`
|
||||
empezando en `01`. Si el capítulo tiene título original en otro idioma, puedes incluir la
|
||||
traducción entre paréntesis como en el vault (`01 - The Mom Test (El test de la madre).md`).
|
||||
- Nombres de archivo sin caracteres que rompan en Obsidian (evita `/`, `:`; los paréntesis y
|
||||
acentos son válidos).
|
||||
|
||||
## Determinar los capítulos de cada libro
|
||||
|
||||
Para listar los capítulos reales de un libro:
|
||||
|
||||
1. Usa tu conocimiento del libro si lo conoces con fiabilidad (índice real, no inventado).
|
||||
2. Si no estás seguro del índice exacto, **búscalo en la web** (`WebSearch` / `WebFetch` sobre
|
||||
la tabla de contenidos del libro) antes de escribir. No inventes capítulos.
|
||||
3. Indica en el MOC del libro si el índice procede de una edición concreta.
|
||||
|
||||
**Regla dura:** nunca te inventes el número o los títulos de los capítulos. Si no puedes
|
||||
verificarlos, dilo y pregunta al usuario en vez de fabricar un índice plausible.
|
||||
|
||||
## Plantilla — MOC del libro (`<Libro> - MOC.md`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: <Libro> - MOC
|
||||
book: <Libro>
|
||||
author: <Autor>
|
||||
year: <Año>
|
||||
type: book-moc
|
||||
tags:
|
||||
- <slug-libro>
|
||||
- <tema-1>
|
||||
- moc
|
||||
---
|
||||
|
||||
# <Libro> — Mapa de contenidos (MOC)
|
||||
|
||||
## Metadata
|
||||
- **Autor:** <Autor>
|
||||
- **Año:** <Año> (<edición si aplica>)
|
||||
- **Subtítulo:** *<subtítulo original>* (<traducción>)
|
||||
- **Tema:** <de qué va en una frase>
|
||||
- **Por qué importa:** <2-3 frases sobre qué problema resuelve y para quién>
|
||||
|
||||
## Resumen global
|
||||
<Un párrafo denso (8-15 líneas) que sintetiza la tesis del libro y recorre el hilo de los
|
||||
capítulos sin enumerarlos uno a uno: cuenta el argumento completo en prosa.>
|
||||
|
||||
## Capítulos
|
||||
1. [[01 - <Título capítulo>]]
|
||||
2. [[02 - <Título capítulo>]]
|
||||
...
|
||||
|
||||
## Aplicación a mi caso (visión transversal)
|
||||
<Párrafo que conecta el libro entero con el objetivo concreto del usuario (el enfoque
|
||||
confirmado en las Decisiones): qué capítulos son los más relevantes y por qué.>
|
||||
```
|
||||
|
||||
## Plantilla — nota de capítulo (`NN - <Título>.md`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: <Título capítulo>
|
||||
book: <Libro>
|
||||
author: <Autor>
|
||||
chapter: <N>
|
||||
type: chapter-summary
|
||||
tags:
|
||||
- <slug-libro>
|
||||
- <tema>
|
||||
---
|
||||
|
||||
# NN. <Título capítulo>
|
||||
|
||||
> Libro: [[<Libro> - MOC]]
|
||||
|
||||
## Idea central
|
||||
<1-3 frases con la tesis del capítulo.>
|
||||
|
||||
## Puntos clave
|
||||
- <bullet sustantivo, no genérico>
|
||||
- <…>
|
||||
- <…>
|
||||
|
||||
## Ejemplos / citas
|
||||
- <ejemplo concreto del capítulo o cita textual con su traducción si es en otro idioma>
|
||||
- <…>
|
||||
|
||||
## Aplicación a mi caso
|
||||
<Párrafo concreto: cómo aplicar la idea del capítulo al caso del usuario.>
|
||||
|
||||
---
|
||||
Anterior: [[NN-1 - <Título anterior>]] · Siguiente: [[NN+1 - <Título siguiente>]] · Índice: [[<Libro> - MOC]]
|
||||
```
|
||||
|
||||
Notas de la plantilla:
|
||||
- El primer capítulo: `Anterior: —`. El último: `Siguiente: —`. (Ver patrón en el vault.)
|
||||
- La sección "Aplicación" es obligatoria y debe ser específica del caso del usuario, no un
|
||||
consejo genérico. Es lo que da valor a estas notas frente a un resumen cualquiera.
|
||||
- En profundidad `breve`, omite "Ejemplos / citas" y deja "Puntos clave" en 3 bullets.
|
||||
|
||||
## Plantilla — MOC de categoría (`<Categoría> - MOC.md`)
|
||||
|
||||
Si ya existe, **ACTUALÍZALO** añadiendo los libros nuevos a la sección que corresponda (no lo
|
||||
reescribas perdiendo lo previo). Si no existe, créalo:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: <Categoría> — MOC
|
||||
type: moc
|
||||
tags:
|
||||
- libros
|
||||
- <tema-categoría>
|
||||
---
|
||||
|
||||
# <Categoría> — Mapa de contenidos
|
||||
|
||||
<Frase que describe el tema común de los libros de esta categoría.>
|
||||
|
||||
Cada libro tiene su propia nota MOC con el índice de capítulos enlazados.
|
||||
|
||||
## <Sub-tema 1>
|
||||
- [[<Libro A> - MOC]] — <Autor>. <una línea de qué aporta>.
|
||||
- [[<Libro B> - MOC]] — <Autor>. <…>.
|
||||
|
||||
## Orden de lectura recomendado
|
||||
1. **<Libro>** — <por qué primero>.
|
||||
2. ...
|
||||
```
|
||||
|
||||
## Flujo de ejecución
|
||||
|
||||
1. Parsear `$ARGUMENTS`: libros, vault, categoría, profundidad, enfoque.
|
||||
2. Resolver decisiones faltantes con `AskUserQuestion`.
|
||||
3. Para cada libro: verificar el índice real de capítulos (conocimiento fiable o WebSearch).
|
||||
4. Crear carpeta del libro. Escribir el MOC del libro y todas las notas de capítulo con
|
||||
wikilinks y navegación correctos.
|
||||
5. Crear o actualizar el MOC de categoría enlazando los libros nuevos.
|
||||
6. **Paralelización:** si son varios libros, cada libro es independiente (carpetas disjuntas).
|
||||
En modo orquestador, lanza un ejecutor por libro (o por lote de libros) escribiendo en
|
||||
carpetas distintas del mismo vault. Cada ejecutor escribe SOLO su carpeta de libro; el MOC
|
||||
de categoría lo actualiza UN único agente al final (o el orquestador) para evitar que dos
|
||||
ejecutores editen el mismo archivo a la vez.
|
||||
7. Reportar: lista de archivos creados (MOC + nº de capítulos por libro) y la ruta del vault
|
||||
para abrirlo en Obsidian.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **El vault es artefacto local** (gitignored en fn_registry, symlink a `~/Obsidian/<vault>`).
|
||||
Escribir notas NO toca el repo `fn_registry`. Si el vault es su propio repo git, NO commitees
|
||||
desde varios ejecutores a la vez (race): deja el commit/sync al usuario o a un único paso final.
|
||||
- **No sobrescribas** un MOC de categoría existente ni notas de capítulo ya escritas a mano sin
|
||||
confirmarlo. Ante colisión de nombre, pregunta.
|
||||
- **Índices inventados = bug.** Verifica los capítulos reales antes de escribir.
|
||||
- **Wikilinks deben resolver:** el texto dentro de `[[ ]]` debe coincidir exactamente con el
|
||||
nombre de archivo (sin extensión). Un typo rompe el enlace en Obsidian.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: EDA (exploratory data analysis) de una tabla o de una base entera con el grupo `eda` del registry. Perfila, escribe el report (JSON + Markdown + PDF móvil) y monta un analysis Jupyter lanzado en el navegador colaborativo y ejecutado en vivo por Claude.
|
||||
---
|
||||
|
||||
# /eda — Exploratory Data Analysis con el grupo `eda`
|
||||
|
||||
Cuando Enmanuel pide un EDA ("hazme un EDA de X", "analiza esta tabla", "qué hay en estos datos"), **no escribas análisis inline**: usa el grupo de capacidad `eda` del registry, escribe los reports y monta el analysis Jupyter en su navegador colaborativo, ejecutando las celdas tú mismo en vivo. Respeta la memoria `eda-workflow-registry` y la regla `.claude/rules/notebook_collaboration.md`.
|
||||
|
||||
Página madre del grupo: `docs/capabilities/eda.md` (léela primero para cargar el cluster entero).
|
||||
|
||||
## Uso
|
||||
|
||||
```
|
||||
/eda /ruta/datos.duckdb tabla # EDA de una tabla DuckDB
|
||||
/eda /ruta/datos.csv # CSV/Parquet → cargar a DuckDB y perfilar
|
||||
/eda postgresql://user:pass@host:5432/db tabla # EDA de una tabla PostgreSQL (backend="postgres")
|
||||
/eda /ruta/datos.duckdb --all # EDA de TODA la base (todas las tablas + FK + join graph)
|
||||
/eda /ruta/datos.duckdb ventas --series --pdf # con análisis de serie temporal + PDF móvil
|
||||
```
|
||||
|
||||
`$ARGUMENTS` lleva la fuente y, opcionalmente, la tabla y flags. Interpreta:
|
||||
- **Fuente**: ruta a `.duckdb`/`.csv`/`.parquet`, o un DSN PostgreSQL (`postgresql://...` o `postgres://...`).
|
||||
- **Tabla**: nombre de la tabla. Si no se da y la fuente es un único archivo CSV/Parquet, usa su nombre base. Si se pide "toda la base" / `--all`, usa `profile_database`.
|
||||
- **Flags** (actívalos según lo que pida el usuario; pregunta solo si es ambiguo y costoso):
|
||||
- `--models` → `run_models=True` (PCA/KMeans/IsolationForest/normalidad).
|
||||
- `--llm` → `run_llm=True` (1 call LLM sobre el perfil agregado).
|
||||
- `--series` → `run_series=True` (estacionariedad ADF+KPSS, ACF/PACF, STL, retornos por columna numérica).
|
||||
- `--pdf` → `emit_pdf=True` (PDF A5 vertical legible en móvil).
|
||||
|
||||
Por defecto, para un EDA "completo" cuando el usuario no especifica, activa `run_models`, `run_series` y `emit_pdf`; deja `run_llm` para cuando lo pida o cuando interese la interpretación semántica (es la única parte que gasta tokens del modelo).
|
||||
|
||||
## Reglas duras
|
||||
|
||||
1. **Registry-first**: invoca las funciones del grupo `eda`, no reescribas lógica de perfilado ni de gráficos inline (regla `registry_first.md`).
|
||||
2. **CSV/Parquet/Excel** entran cargándolos antes a DuckDB (`read_csv_auto`/`read_parquet`/`read_xlsx`) — DuckDB es el motor por defecto. No traigas la tabla entera a RAM.
|
||||
3. **Secretos**: si la fuente es un DSN PostgreSQL con credenciales, NO las imprimas en los reports ni en el notebook; resuélvelas vía `resolve_pg_dsn`/`pass` cuando aplique.
|
||||
4. **El report es un artefacto local**: vive en `reports/` (gitignored), no se sube a Gitea ni se versiona. Compartir = pasar la ruta (regla `reports.md`).
|
||||
5. **Entrega las 4 salidas**: JSON sidecar + Markdown + **PDF móvil** + **notebook Jupyter colaborativo ejecutado en vivo**.
|
||||
|
||||
## Paso 1 — Perfilar y escribir los reports
|
||||
|
||||
Una tabla (caso normal):
|
||||
|
||||
```bash
|
||||
PYTHONPATH=python/functions python/.venv/bin/python3 - <<'PYEOF'
|
||||
from pipelines.profile_table import profile_table
|
||||
r = profile_table(
|
||||
"/ruta/datos.duckdb", "ventas",
|
||||
run_models=True, run_series=True, emit_pdf=True, run_llm=False,
|
||||
)
|
||||
print("status:", r["status"])
|
||||
print("md: ", r["report_md_path"])
|
||||
print("json: ", r["report_json_path"])
|
||||
print("pdf: ", r["pdf_path"])
|
||||
PYEOF
|
||||
```
|
||||
|
||||
Una base entera (todas las tablas + relaciones FK):
|
||||
|
||||
```bash
|
||||
PYTHONPATH=python/functions python/.venv/bin/python3 - <<'PYEOF'
|
||||
from pipelines.profile_database import profile_database
|
||||
r = profile_database("/ruta/datos.duckdb")
|
||||
print(r["db_profile"]["join_graph"]["mermaid"])
|
||||
PYEOF
|
||||
```
|
||||
|
||||
Lee el Markdown resultante y resume a Enmanuel lo esencial: forma, calidad, correlaciones fuertes (ya corregidas por FDR), series no estacionarias, transformaciones sugeridas y avisos exploratorios.
|
||||
|
||||
## Paso 2 — Notebook Jupyter colaborativo, ejecutado en vivo por Claude
|
||||
|
||||
Sigue la memoria `eda-workflow-registry` y la regla `notebook_collaboration.md`:
|
||||
|
||||
1. Genera el notebook con `build_eda_notebook` (mismo perfil de la tabla):
|
||||
|
||||
```bash
|
||||
PYTHONPATH=python/functions python/.venv/bin/python3 - <<'PYEOF'
|
||||
from datascience import build_eda_notebook
|
||||
build_eda_notebook("/ruta/datos.duckdb", "ventas",
|
||||
"analysis/eda_ventas/notebooks/01_eda.ipynb", run_models=True)
|
||||
PYEOF
|
||||
```
|
||||
|
||||
(o crea un analysis dedicado con `fn run init_jupyter_analysis eda_ventas duckdb` y escribe el notebook dentro de `notebooks/`).
|
||||
|
||||
2. Confirma que hay Jupyter colaborativo activo con `jupyter_discover` (o lánzalo con el `run-jupyter-lab.sh` del analysis) y **ábrelo en el navegador colaborativo** para que Enmanuel lo vea en vivo.
|
||||
|
||||
3. **Ejecuta tú las celdas** (no se las dejes para que las corra él): usa las funciones del dominio `notebook` (`jupyter_exec` append+execute / `jupyter_read`) descritas en `notebook_collaboration.md`, o el MCP `jupyter` si está conectado en la sesión del analysis. Ejecuta de arriba a abajo, comenta cada bloque relevante y deja el notebook navegable.
|
||||
|
||||
## Notas
|
||||
|
||||
- El `TableProfile` lleva ahora, además del perfilado base y las correlaciones con FDR: `series` (por columna numérica, con `run_series`), `reexpression` por columna numérica (escalera de Tukey) y `caveats` (siempre, avisos exploratorios). El Markdown y el PDF renderizan estas secciones automáticamente cuando están presentes.
|
||||
- El PDF (`emit_pdf`) está pensado para leerse en el móvil (A5 vertical, tipografía grande, gráficos Tufte). Se escribe junto al Markdown en `reports/`.
|
||||
- `run_series` ordena por la primera columna datetime si existe; si no, por el orden físico de filas. Necesita ≥8 puntos válidos por columna.
|
||||
- Fuentes: DuckDB (CSV/Parquet/Excel cargados antes) y PostgreSQL (`backend="postgres"`). `profile_database` (multi-tabla + FK) es solo DuckDB por ahora.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
description: "Espejo de requisitos: Claude reformula con detalle la última tarea pedida (objetivo, alcance, entregables, supuestos, criterios de aceptación, fuera de alcance y dudas) para confirmar alineación antes de ejecutar. No ejecuta nada."
|
||||
argument-hint: "[opcional: matiz o foco a tener en cuenta al reformular]"
|
||||
---
|
||||
|
||||
# /equal — confirmar alineación reformulando la tarea pedida
|
||||
|
||||
Mecanismo de **espejo de requisitos**. Cuando el usuario invoca `/equal`, NO ejecutas la tarea: devuelves tu interpretación detallada y estructurada del encargo más reciente, para que el usuario confirme o corrija antes de que empieces a trabajar.
|
||||
|
||||
El objetivo es eliminar el malentendido silencioso: prefieres gastar un turno reflejando lo que crees que se te pide que arrancar en la dirección equivocada.
|
||||
|
||||
## Qué hacer al invocarse
|
||||
|
||||
1. **Identifica la tarea más reciente que el usuario te ha pedido** en la conversación actual: la última petición de trabajo real, no el `/equal` en sí ni un comando de utilidad anterior. Si hay `$ARGUMENTS`, úsalos como matiz o foco adicional al reformular (p. ej. "céntrate en el alcance" o "asume que es solo el backend"), no como la tarea nueva.
|
||||
|
||||
2. **Reformula esa tarea de forma detallada y estructurada**, con estas secciones (omite una sección solo si es genuinamente no aplicable, no para abreviar):
|
||||
|
||||
- **Objetivo** — qué se quiere conseguir, en una o dos frases claras. El "para qué", no solo el "qué".
|
||||
- **Alcance / qué incluye** — los trozos concretos de trabajo que entiendes incluidos. Lista, no párrafo.
|
||||
- **Entregables** — qué archivos, cambios, salidas o artefactos concretos vas a producir.
|
||||
- **Supuestos** — lo que estás asumiendo por defecto al no estar dicho explícitamente (stack, ubicación, convenciones, datos, alcance temporal). Hazlos visibles para que el usuario los pueda tumbar.
|
||||
- **Criterios de aceptación** — cómo sabremos que está bien hecho. Condiciones verificables, no deseos vagos. Cuando aplique, golden + edge + caso de error (alineado con `dod_quality.md`).
|
||||
- **Fuera de alcance** — lo que NO vas a hacer, para acotar expectativas y evitar scope creep.
|
||||
- **Dudas / ambigüedades a confirmar** — preguntas concretas sobre lo que no está claro. Numéralas para que el usuario pueda responder por número. Si no hay dudas reales, dilo explícitamente ("sin dudas bloqueantes").
|
||||
|
||||
3. **Cierra pidiendo validación**: una línea clara del tipo "¿Alineado? Corrige lo que no cuadre y arranco." No empieces a trabajar hasta que el usuario confirme.
|
||||
|
||||
## Caso sin tarea previa
|
||||
|
||||
Si **no hay** una petición de trabajo clara que reformular (la conversación está vacía de encargos, o lo último fueron solo comandos de utilidad / charla), NO inventes una tarea. Dilo directamente y pide al usuario que describa qué quiere hacer, ofreciéndole la misma estructura como guía de qué información es útil (objetivo, entregables, criterios).
|
||||
|
||||
## Reglas duras
|
||||
|
||||
- **No ejecutas la tarea.** `/equal` solo refleja y pregunta. Nada de editar archivos, lanzar comandos de trabajo ni delegar a subagentes para "ir avanzando".
|
||||
- **No inventes requisitos.** Lo que no se ha pedido va a *Supuestos* (marcado como asunción) o a *Dudas* (como pregunta), nunca colado como hecho.
|
||||
- **Refleja lo que se pidió, no lo que crees que debería pedirse.** Si detectas una mejora obvia, propónla como una duda al final, no la incorpores al objetivo.
|
||||
- **Sé concreto.** Evita reformulaciones genéricas que valdrían para cualquier tarea. Cita los detalles reales del encargo (nombres de archivos, rutas, tecnologías, cifras que el usuario mencionó).
|
||||
- Mantén el comando **simple**: es un reformulador de requisitos, no un planificador de implementación ni un generador de tareas.
|
||||
|
||||
## Ejemplo de salida
|
||||
|
||||
Para una petición previa como *"añade un endpoint `/health` al servicio de deploy que devuelva 200 y la versión"*:
|
||||
|
||||
```
|
||||
## Objetivo
|
||||
Añadir un health check HTTP al deploy_server para poder verificar de forma
|
||||
programática que el servicio está vivo y saber qué versión corre.
|
||||
|
||||
## Alcance / qué incluye
|
||||
- Nuevo handler GET `/health` en el servidor HTTP de deploy_server.
|
||||
- Respuesta 200 con un cuerpo JSON que incluya la versión del binario.
|
||||
- Registro de la ruta en el router existente.
|
||||
|
||||
## Entregables
|
||||
- Cambio en el código del servidor de `apps/deploy_server/` (handler + ruta).
|
||||
- Sin migración de BD (no toca schema).
|
||||
|
||||
## Supuestos
|
||||
- La versión a devolver es la que ya expone el binario (no hay que crear un
|
||||
sistema de versionado nuevo).
|
||||
- Formato de respuesta JSON `{"status":"ok","version":"x.y.z"}`.
|
||||
- Sin autenticación en `/health` (endpoint público de liveness).
|
||||
|
||||
## Criterios de aceptación
|
||||
- Golden: `GET /health` con el servicio arriba → 200 + JSON con `version`.
|
||||
- Edge: la versión se lee correctamente aunque el binario se compile sin tag.
|
||||
- Error: si un subsistema crítico no está listo, devuelve 503, no 200 falso.
|
||||
|
||||
## Fuera de alcance
|
||||
- Readiness check de dependencias remotas (VPS, SSH).
|
||||
- Métricas / observabilidad más allá del 200.
|
||||
|
||||
## Dudas / ambigüedades a confirmar
|
||||
1. ¿`/health` debe comprobar algo real (DB, disco) o basta con responder vivo?
|
||||
2. ¿La versión sale de un ldflag de build, de un fichero, o de constante?
|
||||
3. ¿Puerto y router son los que ya usa `deploy_server serve`?
|
||||
|
||||
¿Alineado? Corrige lo que no cuadre y arranco.
|
||||
```
|
||||
|
||||
El ejemplo es ilustrativo del **formato y el nivel de detalle**; el contenido real sale siempre de la tarea concreta que el usuario haya pedido en la conversación.
|
||||
@@ -31,12 +31,13 @@ Diferencia con `dev/flows/`:
|
||||
|
||||
**Fase 1 (manual via Claude):**
|
||||
|
||||
El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
|
||||
El agente lee `dev/issues/**/*.md` (recursivo: incluye subcarpetas por dominio como `dev/issues/kanban/`, `dev/issues/cpp/`, ... excluyendo `completed/`), parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
|
||||
|
||||
```python
|
||||
import yaml, pathlib, re
|
||||
issues = []
|
||||
for f in pathlib.Path("dev/issues").glob("*.md"):
|
||||
for f in pathlib.Path("dev/issues").glob("**/*.md"):
|
||||
if f.parent.name == "completed": continue
|
||||
if f.name in {"README.md", "template.md"}: continue
|
||||
txt = f.read_text()
|
||||
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
|
||||
|
||||
@@ -75,36 +75,59 @@ siendo grande para un agente, pásala por el **splitter** (ver `.claude/rules/or
|
||||
### 2. Lanzar cada secundario
|
||||
|
||||
**Regla dura: cada secundario se lanza SIEMPRE como terminal visible — window de la flota tmux si
|
||||
hay perfil fleet (`$FLEET_SOCKET`, lo normal), o kitty fuera de él. NUNCA como sub-agente del Agent
|
||||
tool (ver paso 8).** Empieza por el bloque de flota tmux cuando estás en un perfil fleet; kitty es
|
||||
el fallback para secundarios que deban vivir fuera de la flota.
|
||||
estás dentro de tmux/una flota, o kitty SOLO cuando de verdad no hay tmux. NUNCA como sub-agente del
|
||||
Agent tool (ver paso 8).** La detección de "estoy en una flota" se hace por **`$TMUX`** (señal
|
||||
fiable, vía `detect_fleet_context`), **NO por `$FLEET_SOCKET`** (a veces viene vacía en un claude
|
||||
resumido/relanzado pese a vivir en la flota → te haría caer a kitty por error). El hook
|
||||
`hook_fleet_state_inject.sh` te inyecta cada turno una línea `CONTEXTO FLEET: … socket=<X>` cuando
|
||||
estás dentro de la flota; úsala. Empieza por el bloque de flota tmux; kitty es el fallback solo fuera
|
||||
de tmux.
|
||||
|
||||
Siempre con `--dangerously-skip-permissions` (memoria `lanzar-agentes-skip-permissions`): los
|
||||
secundarios trabajan autónomos y desatendidos; los prompts de permiso en cada Bash los atascarían.
|
||||
|
||||
#### En la flota tmux (PREFERIDO en perfil fleet)
|
||||
**Nombra cada secundario para diferenciarlo de un vistazo (regla dura).** Cuando lances varios a la
|
||||
vez, el humano tiene que poder distinguirlos rápido en el sidebar de fleetview. Dos cosas:
|
||||
|
||||
Si estás dentro de un perfil FleetView (`$FLEET_SOCKET` seteada), **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`:
|
||||
1. **`--title` descriptivo y prefijado** en cada `spawn_fleet_agent`: un slug corto y único que diga
|
||||
QUÉ hace ese agente, idealmente con una letra/índice para ordenarlos (`A·mcp-rename`,
|
||||
`B·sql-navision`, `C·kanban`, `D·equal-skill`). Esto nombra la window tmux.
|
||||
2. **El nombre del sidebar fleetview = el campo `goal`** del `~/.claude/goals/<sid>.json`. En cuanto
|
||||
resuelvas el `sessionId` del secundario, fíjale un nombre claro con la tool
|
||||
`mcp__orchestrator__fleet_set_name` (o `./fn run set_fleet_name` cuando exista el fallback CLI) —
|
||||
mismo slug descriptivo que el `--title`. Si esa capacidad aún no está disponible en la sesión,
|
||||
apóyate solo en `--title` y en que el `goal` autogenerado del prompt sea descriptivo, pero el
|
||||
objetivo es que el sidebar liste nombres legibles, no objetivos genéricos repetidos.
|
||||
|
||||
#### En la flota tmux (PREFERIDO siempre que estés en tmux)
|
||||
|
||||
Si estás dentro de tmux/una flota (`$TMUX` seteada — compruébalo con `detect_fleet_context`, **no**
|
||||
con `$FLEET_SOCKET`), **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" \
|
||||
# spawn_fleet_agent auto-detecta el socket/session de $TMUX — NO hace falta pasar --socket/--session:
|
||||
./fn run spawn_fleet_agent \
|
||||
--cwd <dir-aislado> --prompt-file /tmp/orq_<slug>.md --title "<subtarea>" \
|
||||
--parent "$MI_SESSION_ID"
|
||||
# devuelve el window_id; despues escribe el DoD-contrato del ejecutor:
|
||||
./fn run set_dod_contract <sessionId-del-ejecutor> "<DoD golden+edge+error>" pending
|
||||
```
|
||||
|
||||
- `spawn_fleet_agent_bash_infra` crea la window tmux + arranca claude con el prompt autocontenido
|
||||
(o `--skill <name>`), y con `--role executor|orchestrator` marca su `goal.json`. El aislamiento
|
||||
git (sub-repo / worktree / scope) sigue imponiéndose en el prompt.
|
||||
- `spawn_fleet_agent_bash_infra` **auto-detecta** socket/session del contexto tmux (`$TMUX`) vía
|
||||
`detect_fleet_context`; pásalos explícitos solo si quieres otra flota (los explícitos priman).
|
||||
Crea la window tmux + arranca claude con el prompt autocontenido (o `--skill <name>`), y con
|
||||
`--role executor|orchestrator` marca su `goal.json`. El aislamiento git (sub-repo / worktree /
|
||||
scope) sigue imponiéndose en el prompt.
|
||||
- **`--parent <mi-sessionId>` (recomendado):** escribe `parent_orchestrator` en el `goal.json` del
|
||||
ejecutor atribuyéndotelo a ti. Es lo que habilita el **push activo** del watcher (te avisa en TU
|
||||
pane cuando ese ejecutor termina). Sin `--parent` el aviso no se rutea. Opcional y
|
||||
retro-compatible. Ver `.claude/rules/orchestration.md`.
|
||||
|
||||
#### Fuera de la flota (kitty fallback)
|
||||
#### Fuera de tmux (kitty fallback)
|
||||
|
||||
Solo cuando `detect_fleet_context` reporta `in_tmux=false` (de verdad no hay tmux):
|
||||
|
||||
```bash
|
||||
./fn run launch_claude_agent_kitty "<PROYECTO> · <subtarea>" <dir-aislado> /tmp/orq_<slug>.md
|
||||
@@ -113,7 +136,8 @@ la flota, se vea en la TUI `fleetview` y sea conmutable con `/fleet focus`:
|
||||
- `launch_claude_agent_kitty_bash_infra(title, directory, prompt_file)` lanza el secundario con el
|
||||
comando canónico (`setsid nohup kitty … zsh -ic 'claude --dangerously-skip-permissions … ; exec
|
||||
zsh'`) que sobrevive al cierre de la terminal padre y deja una shell viva al terminar el claude;
|
||||
devuelve el log de arranque (`/tmp/orq_<slug>_kitty.log`). Usa kitty solo fuera de un perfil fleet.
|
||||
devuelve el log de arranque (`/tmp/orq_<slug>_kitty.log`). Usa kitty solo cuando NO estás en tmux
|
||||
(`$TMUX` vacía); estando en una flota, kitty fragmenta la flota — usa `spawn_fleet_agent`.
|
||||
|
||||
### 3. Aislamiento git obligatorio por secundario (regla de oro)
|
||||
|
||||
@@ -168,6 +192,13 @@ políticas por clasificación, verificador, auto-kill, nudge, splitter, cadencia
|
||||
no el número de agentes vivos — el hook te empuja un bloque `FLEET-STATE` cada turno; tú drenas con
|
||||
`./fn run drain_fleet_events` y actúas por clasificación.
|
||||
|
||||
**Vía preferida — tools MCP `fleet_*`:** si la sesión tiene el MCP `orchestrator` conectado (lo
|
||||
normal: está en `.mcp.json`), usa sus 6 tools — `mcp__orchestrator__fleet_list` / `fleet_drain` /
|
||||
`fleet_classify` / `fleet_set_dod` / `fleet_kill` / `fleet_spawn` — en lugar de los `./fn run`
|
||||
equivalentes: permisos pre-aprobados y salida estructurada, y `fleet_list` expone `role`/`dod_*`
|
||||
directamente. El `./fn run` (y el binario `fleetview` para el listado) es el fallback CLI. Mapa
|
||||
completo op→tool en `.claude/rules/orchestration.md`.
|
||||
|
||||
### 6. Parar un ejecutor — NUNCA `pkill`/`killall claude` (canónica)
|
||||
|
||||
Un `pkill claude` o `killall claude` **te mata a ti mismo** (el orquestador) junto con la flota.
|
||||
@@ -197,8 +228,8 @@ Cuando un secundario termina (rama pusheada + report verde):
|
||||
|
||||
**Todo agente de trabajo va como terminal visible del fleet, NUNCA como sub-agente headless del Agent tool.** Un sub-agente headless corre invisible: no sale en `fleetview`, no es conmutable con `/fleet focus` ni se puede retomar. Jerarquía al lanzar un agente:
|
||||
|
||||
1. **En perfil fleet** (`$FLEET_SOCKET`, lo normal) → `spawn_fleet_agent` (window de la flota tmux).
|
||||
2. **Fuera de un perfil fleet** → kitty con `launch_claude_agent_kitty`.
|
||||
1. **Dentro de tmux/flota** (`$TMUX` seteada — comprueba con `detect_fleet_context`, NO con `$FLEET_SOCKET`) → `spawn_fleet_agent` (auto-detecta el socket; window de la flota tmux).
|
||||
2. **Fuera de tmux** (`in_tmux=false`) → kitty con `launch_claude_agent_kitty`.
|
||||
3. **Agent tool (sub-agente headless)** → **PROHIBIDO para lanzar un agente de trabajo.** SOLO para
|
||||
utilidades internas read-only tuyas que devuelven un resultado y mueren: el **verificador**
|
||||
adversarial de un cierre, el **splitter** (`Plan`), o una búsqueda puntual (`Explore`).
|
||||
@@ -261,10 +292,10 @@ git -C ~/fn_registry worktree add /tmp/orq_capdoc -b orq/cap-deploy master
|
||||
# /tmp/orq_health.md → trabaja en apps/kanban (sub-repo propio), rama issue/health, push, report.
|
||||
# /tmp/orq_capdoc.md → trabaja SOLO en /tmp/orq_capdoc (worktree), rama orq/cap-deploy, push, report.
|
||||
|
||||
# 4. Lanzar ambos (window de la flota si hay $FLEET_SOCKET; aquí kitty fallback). Tras conocer su
|
||||
# sessionId, escribe su DoD-contrato con set_dod_contract.
|
||||
./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
|
||||
# 4. Lanzar ambos como windows de la flota (estás en tmux → spawn_fleet_agent auto-detecta el socket
|
||||
# de $TMUX; kitty SOLO si in_tmux=false). Tras conocer su sessionId, escribe su DoD-contrato.
|
||||
./fn run spawn_fleet_agent --cwd ~/fn_registry/apps/kanban --prompt-file /tmp/orq_health.md --title "kanban · health endpoint" --parent "$MI_SESSION_ID"
|
||||
./fn run spawn_fleet_agent --cwd /tmp/orq_capdoc --prompt-file /tmp/orq_capdoc.md --title "fn_registry · doc deploy" --parent "$MI_SESSION_ID"
|
||||
|
||||
# 5. Seguir cada turno: drena FLEET-STATE, verifica DICE_TERMINADO, nudge a ESTANCADO, lee reports/ (maquinaria en orchestration.md).
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta
|
||||
|
||||
Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente.
|
||||
|
||||
`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, relaunch, start, stop, kill, restart, reboot, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate`
|
||||
`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, relaunch, start, stop, kill, restart, reboot, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate, save, bump, harvest, judge, critique`
|
||||
|
||||
### Excepciones
|
||||
|
||||
|
||||
@@ -27,15 +27,18 @@ La fuente de verdad del mapeo PID→sessionId→cwd son los archivos `~/.claude/
|
||||
`goal`, `phase`, `status`, `tmux_window` y `age`/`idle_seconds` la da el CLI de la app fleetview:
|
||||
|
||||
```bash
|
||||
apps/fleetview/fleetview list --json # flota tipada: session_id, goal, phase, status, tmux_window, age, idle_seconds
|
||||
apps/fleetview/fleetview list --json # flota tipada: session_id, goal, phase, status, pane_id ("%N", el id estable), tmux_window ("@N", interno para focus/send-keys), age, idle_seconds
|
||||
apps/fleetview/fleetview list # tabla legible (incluye columna AGE)
|
||||
```
|
||||
|
||||
Nota: **NO** uses `./fn run list_claude_fleet` — `list_claude_fleet_go_infra` es una función Go con
|
||||
tests, así que `fn run` la despacha como `go test` (corre la suite, no imprime la flota). La vía
|
||||
ejecutable es el binario `apps/fleetview/fleetview` (el atajo `/fleet` del humano envuelve este mismo
|
||||
CLI). Gotcha: el JSON de `fleetview list` **no** incluye todavía `role`/`dod_contract`/`dod_status`;
|
||||
para esos campos lee el sidecar `~/.claude/goals/<session_id>.json` (ver abajo).
|
||||
CLI). El JSON de `fleetview list` **ya incluye** `role`/`dod_contract`/`dod_status` (además de
|
||||
`tmux_window`): el binario los serializa directamente (`""` cuando el `goal.json` no los declara,
|
||||
ver `apps/fleetview/cli.go`). El tool MCP `fleet_list` (ver abajo) además rellena los que el binario
|
||||
deje vacíos leyéndolos del sidecar `~/.claude/goals/<session_id>.json`, así que con el MCP nunca te
|
||||
faltan. Ya no hace falta leer el sidecar a mano salvo que uses el binario crudo y el campo venga vacío.
|
||||
|
||||
**Tiempo — usa el de ACTIVIDAD, no el del proceso.** Para "cuánto lleva cada agente" usa la columna
|
||||
`AGE` de `fleetview list` (o `age`/`idle_seconds` en `--json`): es el tiempo desde su última
|
||||
@@ -43,6 +46,39 @@ actividad (proxy de cuánto lleva sin avanzar / en su estado), lo útil para det
|
||||
`etime` de `list_claude_agents` es la **vida del proceso** (cuánto lleva la terminal abierta, p.ej.
|
||||
8h) — NO es el tiempo de la tarea; nunca lo reportes como progreso.
|
||||
|
||||
### Vía preferida: tools MCP `fleet_*` (`orchestrator_mcp`)
|
||||
|
||||
El MCP `orchestrator` (registrado en `.mcp.json` como `orchestrator`, binario
|
||||
`apps/orchestrator_mcp/orchestrator_mcp`) expone la maquinaria de la flota como **6 tools** que
|
||||
envuelven las mismas funciones del registry. **En una sesión con `orchestrator_mcp` conectado,
|
||||
prefiere los tools `mcp__orchestrator__fleet_*` sobre `./fn run`**: tienen permisos pre-aprobados,
|
||||
devuelven salida estructurada y se registran en la telemetría como cualquier MCP (regla
|
||||
`registry_calls.md`). El `./fn run` (o el binario `fleetview` para el listado) sigue siendo el
|
||||
**fallback CLI** cuando el MCP no está conectado. Mapa de cada operación de la flota a su tool:
|
||||
|
||||
| Operación de la flota | Tool MCP (preferido) | Fallback `./fn run` / binario |
|
||||
|---|---|---|
|
||||
| Listar la flota tipada (session_id, goal, phase, status, **role, dod_contract, dod_status**, **pane_id** (el id estable), age, idle_seconds) | `mcp__orchestrator__fleet_list` | `apps/fleetview/fleetview list --json` (NO `./fn run list_claude_fleet`) |
|
||||
| Drenar la cola de transiciones del watcher (agrupada por clasificación + urgentes) | `mcp__orchestrator__fleet_drain` (`advance` true consume, false hace peek) | `./fn run drain_fleet_events` |
|
||||
| Clasificar el estado de terminación de UN agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO) | `mcp__orchestrator__fleet_classify` | (Go con tests; lo consume el watcher, no se invoca a mano) |
|
||||
| Escribir el DoD-contrato fijo (`dod_contract`/`dod_status`) en el `goal.json` de un agente | `mcp__orchestrator__fleet_set_dod` | `./fn run set_dod_contract` |
|
||||
| Cerrar dirigido UN ejecutor (auto-kill: SIGTERM + kill-window, con guards) | `mcp__orchestrator__fleet_kill` (`dry_run` para ver el plan) | `./fn run kill_fleet_agent` |
|
||||
| Lanzar un ejecutor como window de la flota tmux (con `parent` para el push) | `mcp__orchestrator__fleet_spawn` | `./fn run spawn_fleet_agent` |
|
||||
|
||||
Ventaja extra de `fleet_list`: expone `role`/`dod_contract`/`dod_status` directamente (y rellena los
|
||||
vacíos desde el sidecar `goal.json`), así que la regla "No te vigiles a ti mismo" se resuelve sin leer
|
||||
el sidecar a mano — filtra por el `role` que ya trae cada fila.
|
||||
|
||||
**Identifica a cada agente por su `pane_id` ("%N").** Es el id ESTABLE de por vida del pane: el
|
||||
`fleet_list` del MCP lo expone como el único identificador y **omite a propósito el `tmux_window`
|
||||
("@N")**, que migra cuando el focus-swap mueve el pane entre windows y por eso nunca debe usarse ni
|
||||
mostrarse como id (la persona no tiene referencia mental de "@4"). Las operaciones internas que sí
|
||||
necesitan la window/pane viva — `focus`, `send-keys`/nudge y `kill` — la resuelven BAJO DEMANDA contra
|
||||
tmux a partir del session_id/PID (`kill_fleet_agent` y `fleetview focus` la recalculan por llamada).
|
||||
Para el **nudge** NO leas ni caches el `@N`: usa `fleet_send_text` (grupo `orchestration`), que resuelve
|
||||
el `pane_id` (`%N`) ESTABLE fresco a partir del `sessionId`/PID en el momento del envío — el `@N` migra
|
||||
con el focus-swap y mandaría el texto al agente equivocado (ver sección Nudge).
|
||||
|
||||
Mantén una **tabla de seguimiento**, una fila por secundario, y actualízala en cada turno:
|
||||
|
||||
| slug | título kitty | PID | cwd / dir aislado | rama | log | report | estado |
|
||||
@@ -97,6 +133,21 @@ existe, degrada limpio sin romper el turno (la línea de rol se sigue emitiendo)
|
||||
clasificación sigues drenando (abajo). El resumen lo produce `summarize_fleet_transitions_py_infra`
|
||||
sobre el feed del watcher.
|
||||
|
||||
Además, el mismo hook inyecta una línea **`CONTEXTO FLEET`** cuando detecta (vía
|
||||
`detect_fleet_context_bash_infra`, leyendo **`$TMUX`**, no `$FLEET_SOCKET`) que el orquestador vive
|
||||
dentro de una flota tmux:
|
||||
|
||||
```
|
||||
CONTEXTO FLEET: estás dentro de la fleet tmux socket=<X> session=<Y>. Lanza ejecutores con spawn_fleet_agent (auto-detecta el socket) — NUNCA kitty/launch_claude_agent_kitty estando aquí.
|
||||
```
|
||||
|
||||
Es el recordatorio que evita el bug de caer a kitty cuando `$FLEET_SOCKET` viene vacía pese a estar
|
||||
en la flota: la detección de contexto se hace por `$TMUX` (señal fiable que todo proceso dentro de
|
||||
tmux tiene siempre), no por `$FLEET_SOCKET` (a veces ausente en un claude resumido/relanzado). Esta
|
||||
parte del hook no necesita venv ni python (solo bash + tmux) y se emite antes del bloque
|
||||
`FLEET-STATE`; si el detector falta o `$TMUX` está vacía, simplemente no se emite la línea (turno
|
||||
intacto).
|
||||
|
||||
Gotcha conocido: el bloque `FLEET-STATE` (peek pasivo) lista transiciones de TODA la flota, incluidas
|
||||
las de otros orquestadores y sus ejecutores. Si hay más de un orquestador activo, filtra por tu propia
|
||||
familia de agentes (los que tú lanzaste) — igual que en "No te vigiles a ti mismo" más abajo. El **push
|
||||
@@ -134,10 +185,14 @@ produce `classify_fleet_termination` (pura) desde su estado (status + phase + do
|
||||
dod_status + segundos ociosos).
|
||||
|
||||
**No te vigiles a ti mismo.** Al procesar la cola, **ignora** los eventos de tu propia sesión y de
|
||||
cualquier agente con `role=orchestrator`. Como `fleetview list --json` no expone `role`, resuélvelo
|
||||
leyendo el sidecar del goal de cada `session_id`:
|
||||
cualquier agente con `role=orchestrator`. El `role` ya viene en cada fila de `fleet_list` (y de
|
||||
`fleetview list --json`), así que filtras directamente por ese campo. Solo si usas el binario crudo y
|
||||
la fila trae `role` vacío, cae al sidecar del goal de cada `session_id`:
|
||||
|
||||
```bash
|
||||
# Preferido: filtrar por el role que ya trae fleet_list / fleetview list --json.
|
||||
apps/fleetview/fleetview list --json | jq -r '.[] | select((.role // "executor") != "orchestrator") | .session_id'
|
||||
# Fallback solo si el binario dejó role vacío en alguna fila:
|
||||
jq -r '.role // "executor"' ~/.claude/goals/<session_id>.json # "orchestrator" => ignóralo
|
||||
```
|
||||
|
||||
@@ -208,18 +263,24 @@ verificas → `kill_fleet_agent` libera el slot. No uses `pkill`/`killall` ni `k
|
||||
### Nudge — `ESTANCADO`
|
||||
|
||||
Agente idle con `dod_contract` sin cumplir y sin actividad > umbral (10 min). Empújalo a cerrar SU DoD
|
||||
inyectando en su pane tmux:
|
||||
inyectando texto en su pane con la función `fleet_send_text` (grupo `orchestration`):
|
||||
|
||||
```bash
|
||||
tmux -L "${FLEET_SOCKET:-fleet}" send-keys -t <window_id> \
|
||||
"Sigues idle con tu DoD-contrato sin cerrar. Falta: <gap>. Cierra el golden+edge+error con evidencia, o reporta el bloqueo concreto." Enter
|
||||
./fn run fleet_send_text <sessionId> \
|
||||
"Sigues idle con tu DoD-contrato sin cerrar. Falta: <gap>. Cierra el golden+edge+error con evidencia, o reporta el bloqueo concreto." \
|
||||
--socket "$FLEET_SOCKET"
|
||||
```
|
||||
|
||||
El `window_id` es el campo `tmux_window` (p.ej. `@20`) de `apps/fleetview/fleetview list --json`:
|
||||
`fleet_send_text` resuelve el **`pane_id` (`%N`) ESTABLE FRESCO** del agente justo antes de enviar (a
|
||||
partir del `sessionId` → PID → pane, leyendo `tmux list-panes -a` en el momento), y manda el texto
|
||||
literal y el `Enter` en invocaciones **separadas**, verificando con `capture-pane` que el texto llegó
|
||||
antes de hacer submit (reintenta si no). Acepta el target por `sessionId` (exacto o prefijo) o por PID.
|
||||
|
||||
```bash
|
||||
apps/fleetview/fleetview list --json | jq -r '.[] | select(.session_id|startswith("<sid>")) | .tmux_window'
|
||||
```
|
||||
**NO uses `tmux send-keys -t <window_id @N>` a mano para esto.** El `window_id` (`@N`, p.ej. `@20`) que
|
||||
expone `fleetview list --json` MIGRA cuando el focus-swap recrea windows (`break-pane`+`join-pane`):
|
||||
`@32` → `@34`. Enviar al `@N` viejo (cacheado por el bloque `FLEET-STATE` o leído un instante antes)
|
||||
manda el texto al window equivocado o a otro agente — esa era la causa de "el nudge a veces no llega al
|
||||
agente correcto". `fleet_send_text` nunca usa `@N`; usa el `pane_id` (`%N`), que no migra.
|
||||
|
||||
**Solo a idle/ESTANCADO. JAMÁS a un agente en `waiting`/`preguntando`** — esos te reclaman a TI, no un
|
||||
empujón del bot.
|
||||
@@ -271,16 +332,19 @@ en lote.
|
||||
| `drain_fleet_events_py_infra` | Consumir la cola de transiciones del watcher (`~/.claude/fleet/events.jsonl`), agrupada por clasificación + urgentes |
|
||||
| `summarize_fleet_transitions_py_infra` | Resumir las transiciones del feed en una línea (`terminados/reclaman/estancados`); alimenta el bloque `FLEET-STATE` que el hook `UserPromptSubmit` inyecta cada turno |
|
||||
| `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/`role` + `tmux_window` (alimenta `/fleet` y el watcher). **Invócala por el binario `apps/fleetview/fleetview list --json`**, NUNCA por `./fn run` (la despacha como `go test`). El JSON del CLI aún no expone `role`/`dod_contract`/`dod_status`; léelos de `~/.claude/goals/<session_id>.json` |
|
||||
| `spawn_fleet_agent_bash_infra` | Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty cuando hay perfil fleet. `--parent <tu-sessionId>` atribuye el ejecutor a ti y habilita el push activo del watcher |
|
||||
| `list_claude_fleet_go_infra` | Fleet tipado con goal/phase/`role` + `dod_contract`/`dod_status` + `tmux_window` (alimenta `/fleet`, el watcher y el tool `fleet_list`). **Invócala por el tool `mcp__orchestrator__fleet_list` (preferido) o el binario `apps/fleetview/fleetview list --json`**, NUNCA por `./fn run` (la despacha como `go test`). El JSON del CLI **ya expone** `role`/`dod_contract`/`dod_status` (`""` si el `goal.json` no los declara); el tool MCP además rellena los vacíos desde `~/.claude/goals/<session_id>.json` |
|
||||
| `detect_fleet_context_bash_infra` | Detectar si estás en una flota tmux derivando socket/session de `$TMUX` (señal fiable), con fallback a `$FLEET_SOCKET`. Devuelve JSON `{in_fleet,in_tmux,socket,session,source}`. Lo usan `spawn_fleet_agent` (auto-detección de socket) y el hook (línea `CONTEXTO FLEET`) para no caer a kitty estando en la flota |
|
||||
| `spawn_fleet_agent_bash_infra` | Lanzar un ejecutor (o el orquestador) como window de la flota tmux — preferido sobre kitty siempre que estés en tmux. **Auto-detecta socket/session de `$TMUX`** (vía `detect_fleet_context`) si no se pasan `--socket`/`--session` (los explícitos priman). `--parent <tu-sessionId>` atribuye el ejecutor a ti y habilita el push activo del watcher |
|
||||
| `mark_claude_role_py_infra` | Marcar `role` (orchestrator/executor) en el goal.json de un Claude resolviendo PID→sessionId |
|
||||
| `mark_claude_parent_py_infra` | Marcar `parent_orchestrator` (sessionId del orquestador que lo lanzó) en el goal.json de un ejecutor resolviendo PID→sessionId. Lo invoca `spawn_fleet_agent --parent`; habilita el routing del watcher al pane del orquestador padre |
|
||||
| `kill_fleet_agent_bash_infra` | Cierre dirigido de UN ejecutor: SIGTERM al claude + kill-window de su window tmux. Guards anti-orquestador y anti-self. Lo usa el orquestador para liberar el slot idle tras verificar `met` (auto-kill) |
|
||||
| `fleet_send_text_bash_infra` | Empujar texto al input de UN agente (nudge) resolviendo su `pane_id` (`%N`) ESTABLE FRESCO justo antes de enviar — NO el `window_id` (`@N`), que migra con el focus-swap y manda el texto al agente equivocado. Texto literal + `Enter` en invocaciones separadas, verificado con `capture-pane` + reintento. Guard anti-self. Reemplaza el `tmux send-keys -t <@N>` manual del nudge |
|
||||
| `notify_desktop_go_infra` | Notificación de escritorio del fleet (`notify-send --app-name=fleetview`, degradación silenciosa si no hay `notify-send`). La usa el orquestador/watcher para avisar a la persona de un `RECLAMA` u otro evento urgente cuando no está mirando la terminal |
|
||||
|
||||
**Cómo invocarlas.** Las Bash y Python del grupo se lanzan con `./fn run <id> [args]` (verificado:
|
||||
`list_claude_agents`, `drain_fleet_events`, `reboot_all_claudes`, `set_dod_contract`,
|
||||
`mark_claude_role`, `mark_claude_parent`, `kill_fleet_agent`, `launch_claude_agent_kitty`,
|
||||
`spawn_fleet_agent`). Las **Go con tests** NO: `./fn run` las despacha como `go test`. Por eso
|
||||
`spawn_fleet_agent`, `detect_fleet_context`). Las **Go con tests** NO: `./fn run` las despacha como `go test`. Por eso
|
||||
`list_claude_fleet_go_infra` se usa por el binario `apps/fleetview/fleetview list --json`, y
|
||||
`classify_fleet_termination_go_infra` la consume el watcher embebido en fleetview (no se invoca a
|
||||
mano).
|
||||
|
||||
@@ -46,6 +46,24 @@ ROLE=""
|
||||
printf '%s\n' "MODO ORQUESTADOR activo (role=orchestrator)."
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/fn_registry}"
|
||||
|
||||
# Contexto de flota: recordarle al orquestador en que socket/sesion tmux vive,
|
||||
# para que lance ejecutores con spawn_fleet_agent (auto-detecta el socket) y
|
||||
# NUNCA caiga a kitty estando dentro de la flota. La deteccion va por $TMUX
|
||||
# (senal fiable), no por $FLEET_SOCKET (a veces vacia en un claude resumido/
|
||||
# relanzado). No necesita venv ni python: solo bash + tmux. Degrada limpio: si
|
||||
# el detector falta o falla, simplemente no se emite la linea (turno intacto).
|
||||
DETECTOR="$PROJECT_DIR/bash/functions/infra/detect_fleet_context.sh"
|
||||
if [ -f "$DETECTOR" ]; then
|
||||
CTX=$(bash "$DETECTOR" 2>/dev/null || true)
|
||||
IN_FLEET=$(printf '%s' "$CTX" | sed -n 's/.*"in_fleet":\(true\|false\).*/\1/p')
|
||||
F_SOCKET=$(printf '%s' "$CTX" | sed -n 's/.*"socket":"\([^"]*\)".*/\1/p')
|
||||
F_SESSION=$(printf '%s' "$CTX" | sed -n 's/.*"session":"\([^"]*\)".*/\1/p')
|
||||
if [ "$IN_FLEET" = "true" ]; then
|
||||
printf 'CONTEXTO FLEET: estas dentro de la fleet tmux socket=%s session=%s. Lanza ejecutores con spawn_fleet_agent (auto-detecta el socket) — NUNCA kitty/launch_claude_agent_kitty estando aqui.\n' "$F_SOCKET" "$F_SESSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
PY="$PROJECT_DIR/python/.venv/bin/python3"
|
||||
{ [ -x "$PY" ] && [ -d "$PROJECT_DIR/python/functions" ]; } || exit 0
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"registry",
|
||||
"jupyter"
|
||||
"jupyter",
|
||||
"orchestrator",
|
||||
"godot",
|
||||
"ardour"
|
||||
],
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
@@ -56,6 +59,10 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_registry_first_reminder.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_fleet_state_inject.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ python/.venv/
|
||||
|
||||
# Externalized apps and analysis (each is its own Gitea repo)
|
||||
apps/*/
|
||||
cpp/apps/*/
|
||||
analysis/*/
|
||||
|
||||
# Projects (each is its own git repo, only project.md templates are versioned)
|
||||
|
||||
@@ -4,9 +4,21 @@
|
||||
"command": "./apps/registry_mcp/registry_mcp",
|
||||
"args": ["--enable-run", "--enable-write"]
|
||||
},
|
||||
"orchestrator": {
|
||||
"command": "./apps/orchestrator_mcp/orchestrator_mcp",
|
||||
"args": []
|
||||
},
|
||||
"jupyter": {
|
||||
"command": "bash",
|
||||
"args": ["-c", "exec bash \"$(git rev-parse --show-toplevel)/bash/functions/infra/jupyter_mcp_serve.sh\""]
|
||||
},
|
||||
"godot": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:8000/mcp"
|
||||
},
|
||||
"ardour": {
|
||||
"command": "/home/enmanuel/audio-tools/ardour-mcp/target/release/ardour_mcp_server",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "build_wasm_cpp_app(app_name: string, [--no-budget-check]) -> void"
|
||||
description: "Compila una app C++ del registry (cpp/apps/<name>) a WASM via emscripten. Sale build/wasm/<name>/<name>.{html,js,wasm,wasm.gz}. Falla si gzip > 2 MB."
|
||||
tags: [wasm, emscripten, cpp, build, gamedev, pendiente-usar]
|
||||
tags: [wasm, emscripten, cpp, build, gamedev-engine, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: check_service_health_via_ssh
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "check_service_health_via_ssh(ssh_host: string, local_url: string, [--token-from-env <remote_env_path> <ENV_VAR>], [--token <literal>], [--expect-status <code>], [--connect-timeout <s>], [--curl-timeout <s>]) -> json"
|
||||
description: "Comprueba la salud de un service HTTP que solo escucha en loopback (127.0.0.1) de un host remoto, entrando por SSH y haciendo curl con bearer token opcional. El token se resuelve dentro del host remoto (leyendo una variable de un .env remoto via grep, o pasado literal) y NUNCA se imprime ni se hardcodea. Emite JSON con http_code y healthy. Reemplaza el patron inline 'ssh host -> grep token .env -> curl -H Authorization: Bearer' repetido en monitorizacion."
|
||||
tags: [ssh, systemd, health, curl, remote, service, bearer, loopback, monitoring, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: ssh_host
|
||||
desc: "alias SSH del host remoto definido en ~/.ssh/config (ej: om, organic-machine). Resuelve user/puerto/identityfile del config."
|
||||
- name: local_url
|
||||
desc: "URL del endpoint que el service expone en loopback del host remoto (ej: http://127.0.0.1:8487/agent). No es accesible desde fuera del host."
|
||||
- name: --token-from-env
|
||||
desc: "dos valores: <remote_env_path> <ENV_VAR>. Lee el bearer del .env remoto con grep '^ENV_VAR=' (ej: /home/ubuntu/app/.env AGENTS_API_KEY). El token se resuelve dentro del host, no viaja en argv local."
|
||||
- name: --token
|
||||
desc: "bearer literal (alternativa a --token-from-env). Util para tokens ya en variables de entorno locales; preferir --token-from-env para secretos en disco remoto."
|
||||
- name: --expect-status
|
||||
desc: "codigo HTTP exacto que marca healthy (ej: 200). Si se omite, cualquier 2xx cuenta como healthy."
|
||||
- name: --connect-timeout
|
||||
desc: "timeout de conexion SSH en segundos (default 5)."
|
||||
- name: --curl-timeout
|
||||
desc: "timeout maximo del curl remoto en segundos (default 10)."
|
||||
output: "JSON a stdout: {\"status\":\"ok|error\",\"host\":\"...\",\"url\":\"...\",\"http_code\":NNN,\"healthy\":true|false}. status=error si el SSH fallo sin obtener codigo. healthy=true si http_code coincide con expect-status (o es 2xx por defecto). Exit 0 si healthy, 1 si no, 2 en error de uso."
|
||||
tested: true
|
||||
tests: ["service healthy con token desde env remoto", "service no healthy con http_code 503", "salida JSON nunca filtra el token", "sin token 2xx por defecto es healthy", "falta argumento obligatorio devuelve error de uso", "falta argumento sale con codigo distinto de 0"]
|
||||
test_file_path: "bash/functions/infra/check_service_health_via_ssh_test.sh"
|
||||
file_path: "bash/functions/infra/check_service_health_via_ssh.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/check_service_health_via_ssh.sh
|
||||
|
||||
# 1) Service en loopback del host 'om' con bearer leido de un .env remoto.
|
||||
# Reemplaza el patron inline de monitorizacion del agents_and_robots.
|
||||
result=$(check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env /home/ubuntu/CodeProyects/agents_and_robots/.env AGENTS_API_KEY \
|
||||
--expect-status 200)
|
||||
echo "$result"
|
||||
# {"status":"ok","host":"om","url":"http://127.0.0.1:8487/agent","http_code":200,"healthy":true}
|
||||
|
||||
# 2) Sin token (endpoint publico del host pero solo accesible por loopback).
|
||||
check_service_health_via_ssh organic-machine "http://127.0.0.1:8080/healthz"
|
||||
# {"status":"ok","host":"organic-machine","url":"http://127.0.0.1:8080/healthz","http_code":200,"healthy":true}
|
||||
|
||||
# 3) Uso como gate en un script de monitorizacion (exit code).
|
||||
if check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env /home/ubuntu/CodeProyects/agents_and_robots/.env AGENTS_API_KEY >/dev/null; then
|
||||
echo "service vivo"
|
||||
else
|
||||
echo "service caido — alertar"
|
||||
fi
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando necesites comprobar si un service HTTP de un host remoto esta sano y ese
|
||||
service **solo escucha en loopback** (127.0.0.1) del host, por lo que no puedes
|
||||
curl-earlo directamente desde tu maquina. Tipico de APIs internas detras de un reverse
|
||||
proxy, daemons con bearer auth, o services systemd que exponen un `/health` privado.
|
||||
Antes de reiniciar un service, en un cron de monitorizacion, o como `e2e_check` de un
|
||||
deploy.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Requiere **SSH por key auth** al host (usa `-o BatchMode=yes`): si el host pide
|
||||
password, falla en vez de colgarse. El alias debe estar en `~/.ssh/config`.
|
||||
- El service objetivo **debe escuchar en loopback del host remoto** — la URL se
|
||||
resuelve *dentro* del host. `http://127.0.0.1:PORT` apunta al host remoto, no a tu PC.
|
||||
- **No requiere sudo**: solo lee un `.env` (grep) y hace curl como el usuario SSH.
|
||||
El usuario SSH debe tener permiso de lectura sobre el `.env` remoto.
|
||||
- El **token nunca se imprime ni se hardcodea**: con `--token-from-env` se resuelve
|
||||
dentro del host y solo se usa en el header `Authorization`. Con `--token <literal>`
|
||||
el secreto queda en el argv del comando ssh local — preferir `--token-from-env`
|
||||
para secretos persistidos en disco.
|
||||
- `grep` del `.env` toma la **primera** linea que matchea `^<ENV_VAR>=` y recorta
|
||||
comillas/espacios. Si la var aparece varias veces o usa interpolacion, revisa el match.
|
||||
- `curl -sf` no sigue redirects: un 3xx cuenta como no-2xx (healthy=false salvo
|
||||
`--expect-status` explicito).
|
||||
- Requiere `curl` instalado en el **host remoto** (no en el local).
|
||||
- El JSON de salida se emite siempre (incluso en fallo); el caller decide por el
|
||||
`exit code` (0 healthy, 1 no healthy, 2 error de uso) o por el campo `healthy`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Testeable sin red: el runner SSH es inyectable via `CHECK_HEALTH_SSH_BIN` (un stub
|
||||
que emite el `http_code` deseado), por eso los tests no abren conexiones reales.
|
||||
- El snippet remoto normaliza la salida de curl a un unico `http_code` aunque
|
||||
`curl -sf` devuelva error (emite `<curl_rc>:<http_code>` y la funcion extrae el codigo).
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# check_service_health_via_ssh — Comprueba la salud de un service HTTP que solo
|
||||
# escucha en loopback de un host remoto, entrando por SSH y haciendo curl con
|
||||
# bearer token opcional (leido de un .env remoto o pasado literal).
|
||||
set -euo pipefail
|
||||
|
||||
check_service_health_via_ssh() {
|
||||
local ssh_host="" local_url=""
|
||||
local remote_env_path="" env_var=""
|
||||
local token_literal=""
|
||||
local expect_status="" # vacio = aceptar cualquier 2xx
|
||||
local connect_timeout=5
|
||||
local curl_timeout=10
|
||||
|
||||
# --- parseo de args (posicionales + flags) ---
|
||||
local positional=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--token-from-env)
|
||||
remote_env_path="${2:-}"
|
||||
env_var="${3:-}"
|
||||
if [[ -z "$remote_env_path" || -z "$env_var" ]]; then
|
||||
echo "check_service_health_via_ssh: --token-from-env requiere <remote_env_path> <ENV_VAR>" >&2
|
||||
return 2
|
||||
fi
|
||||
shift 3
|
||||
;;
|
||||
--token)
|
||||
token_literal="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--expect-status)
|
||||
expect_status="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--connect-timeout)
|
||||
connect_timeout="${2:-5}"
|
||||
shift 2
|
||||
;;
|
||||
--curl-timeout)
|
||||
curl_timeout="${2:-10}"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "check_service_health_via_ssh: flag desconocida '$1'" >&2
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
positional+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ssh_host="${positional[0]:-}"
|
||||
local_url="${positional[1]:-}"
|
||||
|
||||
if [[ -z "$ssh_host" || -z "$local_url" ]]; then
|
||||
echo "check_service_health_via_ssh: uso: check_service_health_via_ssh <ssh_host> <local_url> [--token-from-env <remote_env_path> <ENV_VAR>] [--token <literal>] [--expect-status 200]" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# --- construir el snippet remoto que se ejecuta dentro del host via SSH ---
|
||||
# El token NUNCA se imprime: se resuelve dentro del host remoto y se usa
|
||||
# directamente en el header Authorization. El snippet emite SOLO el http_code.
|
||||
#
|
||||
# Casos de token:
|
||||
# 1) --token-from-env: lee el valor de <ENV_VAR>= del .env remoto.
|
||||
# 2) --token <literal>: el literal se inyecta en el snippet (cuidado: queda
|
||||
# en argv del comando ssh local; preferir --token-from-env para secretos).
|
||||
# 3) sin token: curl sin header Authorization.
|
||||
local remote_script
|
||||
if [[ -n "$remote_env_path" ]]; then
|
||||
# grep el valor del .env remoto, recortando posibles comillas y espacios.
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
TOKEN=\$(grep -E '^[[:space:]]*${env_var}[[:space:]]*=' '${remote_env_path}' 2>/dev/null | head -n1 | cut -d= -f2- | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*\$//' -e 's/^["'\'']//' -e 's/["'\'']\$//')
|
||||
if [ -z "\$TOKEN" ]; then
|
||||
echo "000"
|
||||
exit 7
|
||||
fi
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
elif [[ -n "$token_literal" ]]; then
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
TOKEN='${token_literal}'
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
else
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
fi
|
||||
|
||||
# --- ejecutar via SSH (o via runner inyectado en tests) ---
|
||||
# CHECK_HEALTH_SSH_BIN permite a los tests sustituir el comando ssh por un
|
||||
# stub que devuelve un http_code fijo, sin tocar la red.
|
||||
local ssh_bin="${CHECK_HEALTH_SSH_BIN:-ssh}"
|
||||
local raw rc=0
|
||||
raw=$("$ssh_bin" -o BatchMode=yes -o ConnectTimeout="$connect_timeout" "$ssh_host" "$remote_script" 2>/dev/null) || rc=$?
|
||||
|
||||
# El snippet remoto, cuando curl -sf falla, emite "<curl_rc>:<http_code>".
|
||||
# Cuando curl tiene exito, emite solo "<http_code>". Normalizamos a http_code.
|
||||
local http_code
|
||||
if [[ "$raw" == *:* ]]; then
|
||||
http_code="${raw##*:}"
|
||||
else
|
||||
http_code="$raw"
|
||||
fi
|
||||
# sanitizar: solo digitos; cualquier otra cosa => 000
|
||||
if [[ ! "$http_code" =~ ^[0-9]+$ ]]; then
|
||||
http_code="000"
|
||||
fi
|
||||
|
||||
# Si el SSH en si fallo (conexion, host caido) y no hay codigo util.
|
||||
local status="ok"
|
||||
if [[ "$rc" -ne 0 && "$http_code" == "000" ]]; then
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# --- decidir healthy ---
|
||||
local healthy="false"
|
||||
if [[ -n "$expect_status" ]]; then
|
||||
[[ "$http_code" == "$expect_status" ]] && healthy="true"
|
||||
else
|
||||
# default: cualquier 2xx
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && healthy="true"
|
||||
fi
|
||||
|
||||
printf '{"status":"%s","host":"%s","url":"%s","http_code":%s,"healthy":%s}\n' \
|
||||
"$status" "$ssh_host" "$local_url" "$http_code" "$healthy"
|
||||
|
||||
[[ "$healthy" == "true" ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
check_service_health_via_ssh "$@"
|
||||
fi
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para check_service_health_via_ssh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/check_service_health_via_ssh.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if ! echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected NOT to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- stub SSH: en vez de conectarse, lee el .env remoto fake (si el snippet lo
|
||||
# referencia) y emite el http_code de la env var STUB_HTTP_CODE. Simula tanto el
|
||||
# caso "curl exito" (solo http_code) como "curl fallo" (<rc>:<http_code>). ---
|
||||
STUB=$(mktemp)
|
||||
chmod +x "$STUB"
|
||||
cat > "$STUB" <<'STUBEOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stub de ssh para tests. Ignora flags -o ... y el host; el ultimo arg es el
|
||||
# script remoto. Emite el codigo segun STUB_HTTP_CODE / STUB_CURL_RC.
|
||||
code="${STUB_HTTP_CODE:-200}"
|
||||
rc="${STUB_CURL_RC:-0}"
|
||||
# Si el script remoto referencia un .env y STUB_TOKEN_EMPTY=1, simular token vacio.
|
||||
if [[ "${STUB_TOKEN_EMPTY:-0}" == "1" ]]; then
|
||||
echo "000"
|
||||
exit 7
|
||||
fi
|
||||
if [[ "$rc" == "0" ]]; then
|
||||
echo "$code"
|
||||
else
|
||||
echo "${rc}:${code}"
|
||||
exit 0
|
||||
fi
|
||||
STUBEOF
|
||||
chmod +x "$STUB"
|
||||
|
||||
FAKE_ENV=$(mktemp)
|
||||
cat > "$FAKE_ENV" <<'ENVEOF'
|
||||
SOME_OTHER=foo
|
||||
AGENTS_API_KEY=supersecret-token-123
|
||||
ANOTHER=bar
|
||||
ENVEOF
|
||||
|
||||
trap 'rm -f "$STUB" "$FAKE_ENV"' EXIT
|
||||
|
||||
# --- Test: service healthy con token desde .env remoto (200 esperado) ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=200 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env "$FAKE_ENV" AGENTS_API_KEY --expect-status 200) || true
|
||||
assert_contains "service healthy con token desde env remoto" '"healthy":true' "$result"
|
||||
assert_contains "service healthy con token desde env remoto" '"http_code":200' "$result"
|
||||
assert_contains "service healthy con token desde env remoto" '"status":"ok"' "$result"
|
||||
assert_not_contains "service healthy con token desde env remoto" 'supersecret' "$result"
|
||||
|
||||
# --- Test: service no healthy cuando http_code no coincide con expect-status ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=503 STUB_CURL_RC=22 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env "$FAKE_ENV" AGENTS_API_KEY --expect-status 200) || true
|
||||
assert_contains "service no healthy con http_code 503" '"healthy":false' "$result"
|
||||
assert_contains "service no healthy con http_code 503" '"http_code":503' "$result"
|
||||
|
||||
# --- Test: salida JSON nunca filtra el token ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=200 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:9000/health" \
|
||||
--token literal-secret-xyz) || true
|
||||
assert_not_contains "salida JSON nunca filtra el token" 'literal-secret-xyz' "$result"
|
||||
assert_contains "salida JSON nunca filtra el token" '"healthy":true' "$result"
|
||||
|
||||
# --- Test: sin token y 2xx por defecto cuenta como healthy ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=204 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8080/ping") || true
|
||||
assert_contains "sin token 2xx por defecto es healthy" '"healthy":true' "$result"
|
||||
assert_contains "sin token 2xx por defecto es healthy" '"http_code":204' "$result"
|
||||
|
||||
# --- Test: falta argumento obligatorio devuelve error de uso ---
|
||||
set +e
|
||||
err=$(check_service_health_via_ssh om 2>&1)
|
||||
ec=$?
|
||||
set -e
|
||||
assert_contains "falta argumento obligatorio devuelve error de uso" 'uso:' "$err"
|
||||
if [[ "$ec" -ne 0 ]]; then
|
||||
echo "PASS: falta argumento sale con codigo distinto de 0"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: falta argumento deberia salir != 0 (got $ec)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: detect_fleet_context
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.0.0
|
||||
purity: impure
|
||||
signature: "detect_fleet_context() -> JSON {in_fleet,in_tmux,socket,session,source}"
|
||||
description: "Detecta de forma robusta si el proceso corre dentro de una flota tmux FleetView, derivando socket y sesion de $TMUX (senal fiable) en vez de $FLEET_SOCKET (fragil, a veces vacia en un claude resumido/relanzado). Salida JSON con in_fleet/in_tmux/socket/session/source."
|
||||
tags: [orchestration, fleet, tmux, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
tested: false
|
||||
file_path: "bash/functions/infra/detect_fleet_context.sh"
|
||||
params:
|
||||
- name: "(ninguno)"
|
||||
desc: "No recibe argumentos. Lee el entorno ($TMUX, con fallback a $FLEET_SOCKET/$FLEET_SESSION) y consulta el servidor tmux."
|
||||
output: "JSON en stdout: {\"in_fleet\":bool, \"in_tmux\":bool, \"socket\":str, \"session\":str, \"source\":\"tmux|fleet_socket|none\"}. in_tmux=true basta para lanzar una window; in_fleet es la senal semantica de 'estoy en una flota'."
|
||||
---
|
||||
|
||||
# detect_fleet_context
|
||||
|
||||
Detecta el contexto de flota del proceso actual sin depender de `$FLEET_SOCKET`.
|
||||
|
||||
## Por que existe
|
||||
|
||||
La deteccion de "estoy en una flota FleetView" dependia de la variable de
|
||||
entorno `$FLEET_SOCKET`, que `launch_fleetclaude` exporta con
|
||||
`tmux set-environment -g`. Esa variable solo llega a los procesos que tmux
|
||||
arranca **despues** de setearla: un `claude` relanzado o resumido a mano puede
|
||||
no heredarla y `$FLEET_SOCKET` queda vacia, aunque ese claude SI viva en una
|
||||
window de la flota. Cuando eso pasa, el modo orquestador cae al fallback kitty
|
||||
(`launch_claude_agent_kitty`) y lanza ejecutores en terminales sueltas en vez de
|
||||
como windows de la flota.
|
||||
|
||||
La senal **fiable** es `$TMUX`: todo proceso dentro de tmux la tiene SIEMPRE, con
|
||||
el formato `/tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>`. De ahi se extrae
|
||||
el socket (basename del path antes de la primera coma) y, con
|
||||
`tmux -L <socket> display-message -p '#{session_name}'`, la sesion actual.
|
||||
|
||||
## Salida
|
||||
|
||||
```json
|
||||
{"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"}
|
||||
```
|
||||
|
||||
| Campo | Significado |
|
||||
|---|---|
|
||||
| `in_fleet` | Heuristica de "estoy en una flota". `true` si en tmux Y (socket/sesion casan `fleet`, O hay window `fleetview`, O la sesion tiene >= 2 windows). |
|
||||
| `in_tmux` | `true` si el proceso esta dentro de tmux. Basta para lanzar una window (mejor que caer a kitty). |
|
||||
| `socket` | Socket tmux derivado de `$TMUX` (o de `$FLEET_SOCKET` en fallback). |
|
||||
| `session` | Sesion tmux actual resuelta con `display-message` (fallback a `$FLEET_SESSION` o al socket). |
|
||||
| `source` | `tmux` (derivado de `$TMUX`), `fleet_socket` (fallback), o `none`. |
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Dentro de una window de la flota fleet3:
|
||||
bash bash/functions/infra/detect_fleet_context.sh
|
||||
# {"in_fleet":true,"in_tmux":true,"socket":"fleet3","session":"fleet3","source":"tmux"}
|
||||
|
||||
# Fuera de tmux, sin FLEET_SOCKET:
|
||||
env -u TMUX -u FLEET_SOCKET bash bash/functions/infra/detect_fleet_context.sh
|
||||
# {"in_fleet":false,"in_tmux":false,"socket":"","session":"","source":"none"}
|
||||
|
||||
# Parsear el socket con jq para pasarlo a spawn_fleet_agent:
|
||||
ctx=$(bash bash/functions/infra/detect_fleet_context.sh)
|
||||
sock=$(printf '%s' "$ctx" | jq -r .socket)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de lanzar un ejecutor de la flota: llama a esta funcion para saber si
|
||||
estas dentro de una flota tmux. Si `in_tmux=true`, lanza con `spawn_fleet_agent`
|
||||
(que ya la usa para auto-detectar el socket); NUNCA caigas a kitty. Tambien la
|
||||
usa el hook `hook_fleet_state_inject.sh` para recordarle al orquestador el socket
|
||||
de su flota cada turno.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es **impura**: consulta el servidor tmux (`display-message`, `list-windows`).
|
||||
No modifica estado.
|
||||
- `in_fleet` es **heuristico** a proposito. Para LANZAR basta `in_tmux=true`
|
||||
(lanzar una window en cualquier tmux supera a una kitty suelta). `in_fleet` es
|
||||
solo la senal semantica que consume el hook y la doctrina.
|
||||
- Fallback `source=fleet_socket`: si `$TMUX` no esta pero `$FLEET_SOCKET` si,
|
||||
devuelve `socket`/`session` de esas vars con `in_tmux=false`. Un
|
||||
`tmux -L <socket> new-window` puede seguir funcionando si el servidor existe,
|
||||
aunque el caller no este attached.
|
||||
- No requiere `jq` ni python: emite el JSON con `printf`, para poder ser el
|
||||
detector base que invocan hooks y otras funciones bash.
|
||||
- Si `tmux` no esta instalado y `$TMUX` esta seteada (raro), `socket` se deriva
|
||||
igual de `$TMUX` pero `session` cae al fallback y `in_fleet` no se puede afinar
|
||||
por windows.
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# detect_fleet_context — detecta de forma robusta si el proceso actual corre
|
||||
# dentro de una sesion tmux de una flota FleetView, derivando el socket y la
|
||||
# sesion de la variable de entorno $TMUX (senal fiable) en vez de depender de
|
||||
# $FLEET_SOCKET (que a veces viene vacia en el entorno de un claude resumido o
|
||||
# relanzado, aunque ese claude SI viva en una window de la flota).
|
||||
#
|
||||
# Por que $TMUX y no $FLEET_SOCKET:
|
||||
# launch_fleetclaude exporta FLEET_SOCKET/FLEET_SESSION con `tmux
|
||||
# set-environment -g`. Esa variable solo llega a los procesos que tmux arranca
|
||||
# DESPUES de setearla; un claude relanzado o resumido a mano puede no heredarla
|
||||
# y entonces $FLEET_SOCKET queda vacia. En cambio, todo proceso que corre
|
||||
# dentro de tmux tiene SIEMPRE $TMUX seteada, con el formato:
|
||||
# /tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>
|
||||
# De ahi se extrae el socket (basename del path antes de la primera coma) y,
|
||||
# con `tmux -L <socket> display-message -p '#{session_name}'`, la sesion
|
||||
# actual. Eso identifica el contexto fleet sin depender de $FLEET_SOCKET.
|
||||
#
|
||||
# Salida: JSON en stdout con los campos:
|
||||
# in_fleet : true|false — heuristica de "estoy en una flota" (ver criterio).
|
||||
# in_tmux : true|false — estoy dentro de tmux (basta para lanzar una window).
|
||||
# socket : nombre del socket tmux derivado ("" si no hay).
|
||||
# session : nombre de la sesion tmux actual ("" si no se resuelve).
|
||||
# source : "tmux" | "fleet_socket" | "none" — de donde se derivo el contexto.
|
||||
#
|
||||
# Criterio de "flota reconocible" (in_fleet): estar en tmux (in_tmux) Y que se
|
||||
# cumpla al menos uno, de mas fiable a menos:
|
||||
# 1. el socket o la sesion casan el patron de flota (contienen "fleet"), o
|
||||
# 2. existe una window llamada "fleetview" (la TUI de la flota), o
|
||||
# 3. la sesion tiene >= 2 windows (una flota agrupa varios agentes en windows).
|
||||
# Es heuristico a proposito: para LANZAR un ejecutor basta con in_tmux (lanzar
|
||||
# una window en cualquier tmux es mejor que caer a una kitty suelta); in_fleet es
|
||||
# la senal semantica que consume el hook del orquestador y la doctrina.
|
||||
#
|
||||
# Funcion IMPURA: lee el entorno y consulta el servidor tmux (display-message,
|
||||
# list-windows). No modifica estado. Degrada limpio: si tmux no esta o falla
|
||||
# cualquier consulta, devuelve los campos que pueda y nunca aborta con error.
|
||||
set -euo pipefail
|
||||
IFS=$' \t\n'
|
||||
|
||||
detect_fleet_context() {
|
||||
local socket="" session="" source="none"
|
||||
local in_tmux="false" in_fleet="false"
|
||||
|
||||
if [[ -n "${TMUX:-}" ]]; then
|
||||
in_tmux="true"
|
||||
source="tmux"
|
||||
# $TMUX = /tmp/tmux-<uid>/<socket>,<server_pid>,<client_id>
|
||||
# Socket = basename del path antes de la primera coma.
|
||||
local tmux_path="${TMUX%%,*}"
|
||||
socket="$(basename "$tmux_path" 2>/dev/null || true)"
|
||||
# Sesion actual: tmux resuelve el cliente via $TMUX. -L fija el socket.
|
||||
if command -v tmux >/dev/null 2>&1 && [[ -n "$socket" ]]; then
|
||||
session="$(tmux -L "$socket" display-message -p '#{session_name}' 2>/dev/null || true)"
|
||||
fi
|
||||
# Fallback de sesion si display-message no resolvio nada.
|
||||
[[ -z "$session" ]] && session="${FLEET_SESSION:-$socket}"
|
||||
elif [[ -n "${FLEET_SOCKET:-}" ]]; then
|
||||
# No estamos en tmux pero hay FLEET_SOCKET exportada: usarla como ultimo
|
||||
# recurso (un claude que perdio $TMUX pero conserva la env del perfil).
|
||||
in_tmux="false"
|
||||
source="fleet_socket"
|
||||
socket="${FLEET_SOCKET}"
|
||||
session="${FLEET_SESSION:-$socket}"
|
||||
fi
|
||||
|
||||
# Heuristica in_fleet: solo tiene sentido si estamos en tmux.
|
||||
if [[ "$in_tmux" == "true" && -n "$socket" ]]; then
|
||||
local sl="${socket,,}" sesl="${session,,}"
|
||||
if [[ "$sl" == *fleet* || "$sesl" == *fleet* ]]; then
|
||||
in_fleet="true"
|
||||
elif command -v tmux >/dev/null 2>&1; then
|
||||
# Construir el target de sesion sin trucos de expansion fragiles.
|
||||
local -a tgt=()
|
||||
[[ -n "$session" ]] && tgt=(-t "$session")
|
||||
# window "fleetview" presente => flota.
|
||||
if tmux -L "$socket" list-windows "${tgt[@]}" \
|
||||
-F '#{window_name}' 2>/dev/null | grep -qx 'fleetview'; then
|
||||
in_fleet="true"
|
||||
else
|
||||
# >= 2 windows => agrupacion tipo flota.
|
||||
local nwin
|
||||
nwin="$(tmux -L "$socket" list-windows "${tgt[@]}" \
|
||||
-F x 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[[ "${nwin:-0}" -ge 2 ]] && in_fleet="true"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# JSON sin dependencias (jq/python no requeridos: este es el detector base).
|
||||
printf '{"in_fleet":%s,"in_tmux":%s,"socket":"%s","session":"%s","source":"%s"}\n' \
|
||||
"$in_fleet" "$in_tmux" "$socket" "$session" "$source"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
detect_fleet_context "$@"
|
||||
fi
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: fleet_send_text
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.0.0
|
||||
purity: impure
|
||||
signature: "fleet_send_text <sessionId|PID> \"<texto>\" [--socket <s>] [--no-enter] [--retries N] [--dry-run]"
|
||||
description: "Empuja texto a UN agente de la flota tmux de forma fiable, resolviendo su pane_id (%N) ESTABLE FRESCO justo antes de enviar. Es el reemplazo del nudge antiguo del orquestador, que apuntaba al window_id (@N) leido del JSON de la flota: ese @N MIGRA cuando el focus-swap de FleetView (break-pane + join-pane) recrea windows, asi que enviar al @N viejo (cacheado por el bloque FLEET-STATE o leido un instante antes) mandaba el texto al window equivocado o a otro agente. fleet_send_text resuelve sessionId -> PID (sessions/<PID>.json) -> el pane cuyo proceso (o un ancestro suyo en /proc) es pane_pid, leyendo tmux list-panes -a en el momento del envio, y usa el pane_id (%N) que NO migra. Ademas manda el texto literal (send-keys -l) y el Enter en invocaciones SEPARADAS, verificando con capture-pane que el texto aparecio en el input antes de pulsar Enter; reintenta si no aparece. Guards: NO envia a tu propio pane; error claro si el target no resuelve a un pane vivo. Por defecto EJECUTA; --dry-run imprime el plan sin enviar."
|
||||
tags: [fleet, claude-fleet, orchestration, tmux, nudge, send-keys, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
error_type: error_go_core
|
||||
file_path: "bash/functions/infra/fleet_send_text.sh"
|
||||
tested: true
|
||||
tests:
|
||||
- "golden: envio por PID resuelve el pane_id estable, inyecta el texto y se verifica via capture-pane"
|
||||
- "edge: tras break-pane (focus-swap) el pane_id NO migra y el reenvio sigue llegando"
|
||||
- "edge: resolucion por prefijo de sessionId (sessions/<pid>.json) entrega el texto"
|
||||
- "edge: --dry-run no inyecta nada y reporta status=dry-run"
|
||||
- "error: sessionId no resuelto rc=2; falta texto rc=2; PID sin pane vivo rc=4"
|
||||
- "guard: enviar a la sesion actual (self) rc=3"
|
||||
test_file_path: "bash/functions/infra/fleet_send_text_test.sh"
|
||||
params:
|
||||
- name: target
|
||||
desc: "Primer arg posicional: sessionId del agente (exacto o prefijo) o su PID (todo digitos). Por sessionId se busca en sessions/*.json el que case y su archivo (<pid>.json) da el PID; por PID se usa directo."
|
||||
- name: texto
|
||||
desc: "Segundo arg posicional: el texto a inyectar en el input del agente (entre comillas)."
|
||||
- name: --socket
|
||||
desc: "Socket tmux del perfil FleetView donde vive el pane. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada."
|
||||
- name: --no-enter
|
||||
desc: "Deja el texto en el input sin pulsar Enter (no hace submit). Por defecto envia el Enter en una invocacion separada tras el texto."
|
||||
- name: --retries
|
||||
desc: "Numero de reintentos si el texto no aparece en el pane tras el send (default 2). Cada reintento limpia el input con C-u antes de reenviar."
|
||||
- name: --dry-run
|
||||
desc: "Imprime el plan (PID, sessionId, pane, socket) y NO envia nada. Sin esto, ejecuta."
|
||||
output: "Imprime una linea de plan (target, PID, sessionId, socket, pane resuelto, modo de envio) y una linea final parseable 'pane=%N intento=N status=ok|dry-run'. Exit 0 ok/dry-run; 2 uso incorrecto o target no resuelto a PID; 3 guard (target es la sesion actual); 4 no se encontro pane vivo para el target; 5 enviado pero no verificado tras los reintentos."
|
||||
---
|
||||
|
||||
# fleet_send_text
|
||||
|
||||
Empuja texto al input de **un** agente de la flota tmux de forma fiable. Resuelve el `pane_id` (`%N`) **estable** del agente **fresco** justo antes de enviar (nunca cachea el `window_id` `@N`, que migra con el focus-swap), manda el texto literal y el `Enter` en invocaciones **separadas**, y verifica con `capture-pane` que el texto llegó antes de hacer submit. Es el reemplazo del patrón de nudge antiguo (`tmux send-keys -t <window_id @N>`), que fallaba "a veces" porque enviaba al window equivocado tras un focus-swap.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Nudge a un ejecutor estancado por sessionId (el orquestador lo llama tras detectar ESTANCADO):
|
||||
./fn run fleet_send_text 32945650-a4e1-472b-90c9-5b38ef60a463 \
|
||||
"Sigues idle con tu DoD-contrato sin cerrar. Falta: el error path con evidencia. Cierralo o reporta el bloqueo." \
|
||||
--socket "$FLEET_SOCKET"
|
||||
|
||||
# Por prefijo de sessionId, en el socket por defecto ($FLEET_SOCKET o "fleet"):
|
||||
./fn run fleet_send_text 32945650 "Recuerda pushear la rama antes de cerrar."
|
||||
|
||||
# Dejar texto en el input sin hacer submit (--no-enter), o solo ver el plan (--dry-run):
|
||||
./fn run fleet_send_text 48213 "borrador..." --no-enter
|
||||
./fn run fleet_send_text 48213 "texto" --dry-run
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Úsala desde el modo orquestador siempre que necesites **inyectar texto en el input de un agente** de la flota: el **nudge** a un `ESTANCADO`, el aviso de un gap concreto a un ejecutor cuyo cierre falló la verificación, o cualquier mensaje dirigido. Sustituye al `tmux send-keys -t <window_id>` manual. Resuelve el target por sessionId (exacto o prefijo) o por PID. **Solo a idle/ESTANCADO; jamás a un agente en `waiting`/`preguntando`** (esos te reclaman a ti, no un empujón del bot). Para *cerrar* un ejecutor verificado `met` no es esto: usa `kill_fleet_agent`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **El bug que arregla — el `window_id` (`@N`) MIGRA**: el focus-swap de FleetView (`tmux_swap_window_into_console.go`) trae el claude objetivo a la console con `break-pane` + `join-pane`, lo que **recrea windows** y cambia el `@N` del agente (`@32` → `@34`). El bloque `FLEET-STATE` y el JSON de la flota pueden traer un `@N` ya viejo. Enviar a ese `@N` manda el texto al window equivocado o a otro agente. Esta función NUNCA usa `@N`: resuelve el `pane_id` (`%N`), que se **preserva** durante toda la vida del pane aunque el pane se mueva de window. Verificado en test: tras `break-pane` el `window_id` pasa de `@0` a `@1` pero el `pane_id` sigue `%0` y el envío sigue llegando.
|
||||
- **Resolución fresca**: el mapa `pane_pid → pane_id` se lee con `tmux -L <socket> list-panes -a` **en el momento del envío**, no se cachea. La resolución sube por los ancestros de `/proc` desde el PID del agente hasta casar un `pane_pid`: cubre tanto `exec claude` (pane_pid == claude pid, match directo, como hace `spawn_fleet_agent`) como un claude lanzado bajo un shell (pane_pid == shell ancestro).
|
||||
- **Texto y Enter separados**: el texto va con `send-keys -l` (literal, sin interpretar nombres de tecla), luego `sleep 0.3`, y el `Enter` en una **invocación aparte**. Mandar texto+Enter juntos hace que el TUI de Claude Code a veces no interprete el Enter como submit. La verificación con `capture-pane` se hace **antes** del Enter (tras el submit el TUI vacía el input y no se podría comprobar). Si el texto no aparece, limpia el input con `C-u` y reintenta (`--retries`, default 2).
|
||||
- **Impura**: inyecta teclas en un pane ajeno. Por defecto EJECUTA; usa `--dry-run` para inspeccionar el plan antes.
|
||||
- **Guard anti-self**: resuelve el PID de `claude` de la sesión actual subiendo por los ancestros de `/proc`; si el target coincide, rehúsa con exit 3 ("No me autoenvio").
|
||||
- **Verificación por fragmento ancla**: comprueba que aparezcan los primeros 24 caracteres del texto (no el texto completo) para no dar falso negativo cuando el input del TUI wrapea un mensaje largo en varias líneas.
|
||||
- **Socket**: si no pasas `--socket`, usa `$FLEET_SOCKET` o `"fleet"`. Si el agente no está en ese socket, no se encontrará el pane (exit 4).
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
# fleet_send_text — empuja texto a UN agente de la flota tmux de forma fiable.
|
||||
#
|
||||
# El problema que resuelve: el orquestador "nudgea" a los ejecutores con
|
||||
# `tmux send-keys`. El patron antiguo apuntaba al `window_id` (`@N`) leido del
|
||||
# JSON de la flota. Pero el focus-swap de FleetView (`break-pane` + `join-pane`)
|
||||
# RECREA windows, asi que el `@N` de un agente MIGRA (p.ej. `@32` -> `@34`) cada
|
||||
# vez que se entra/sale de su window. Enviar al `@N` viejo (cacheado por el bloque
|
||||
# FLEET-STATE o leido un instante antes) manda el texto al window equivocado o a
|
||||
# otro agente -> "a veces no llega al agente correcto". Ademas, mandar el texto y
|
||||
# el `Enter` en la MISMA invocacion hace que el TUI de Claude Code a veces no
|
||||
# interprete el Enter como submit.
|
||||
#
|
||||
# Esta funcion arregla las dos cosas:
|
||||
# 1. Resuelve el `pane_id` ESTABLE (`%N`) FRESCO justo antes de enviar. El
|
||||
# `pane_id` se preserva durante toda la vida del pane aunque el pane se mueva
|
||||
# de window con break/join — NO migra como el `window_id`. La resolucion va
|
||||
# sessionId -> PID (sessions/<PID>.json) -> el pane cuyo proceso (o un
|
||||
# ancestro suyo en /proc) es `pane_pid`, leyendo `tmux list-panes -a` en el
|
||||
# momento del envio.
|
||||
# 2. Manda el texto literal (`send-keys -l`), espera un poco, y el `Enter` en
|
||||
# una invocacion SEPARADA. Verifica con `capture-pane` que el texto aparecio
|
||||
# en el pane antes de pulsar Enter; si no, reintenta.
|
||||
#
|
||||
# Guards: NO envia a tu propio pane (la sesion que invoca la funcion). Error claro
|
||||
# si el sessionId/PID no resuelve a un pane vivo.
|
||||
#
|
||||
# Funcion IMPURA: inyecta teclas en un pane tmux ajeno. Por defecto EJECUTA (es el
|
||||
# caso de uso del bot: nudgear a un ejecutor). Usa --dry-run para ver el plan sin
|
||||
# enviar nada.
|
||||
#
|
||||
# Overrides de entorno (testabilidad, no para uso normal):
|
||||
# FN_FLEET_SESSIONS_DIR directorio de los sessions JSON. Default ~/.claude/sessions
|
||||
# FN_FLEET_SELF_PID fuerza el PID propio (salta la deteccion por /proc)
|
||||
set -euo pipefail
|
||||
IFS=$' \t\n'
|
||||
|
||||
# Resuelve el pane_id (%N) ESTABLE de un PID dado, leyendo el mapa fresco de panes
|
||||
# del socket. Sube por la cadena de ancestros del PID en /proc hasta encontrar un
|
||||
# `pane_pid` del mapa: cubre tanto el caso `exec claude` (pane_pid == claude pid,
|
||||
# match directo) como el de un claude lanzado bajo un shell (pane_pid == shell
|
||||
# ancestro). Imprime el pane_id y devuelve 0 si lo encuentra; 1 si no.
|
||||
# $1 = PID objetivo
|
||||
# $2 = texto del mapa "pane_pid pane_id" (una linea por pane)
|
||||
_fleet_resolve_pane_for_pid() {
|
||||
local p="${1:-}" panes_map="${2:-}" guard=0 pane_id
|
||||
while [[ -n "$p" && "$p" != "0" && "$p" != "1" ]]; do
|
||||
pane_id="$(awk -v pp="$p" '$1==pp {print $2; exit}' <<<"$panes_map")"
|
||||
if [[ -n "$pane_id" ]]; then
|
||||
printf '%s\n' "$pane_id"
|
||||
return 0
|
||||
fi
|
||||
p="$(awk '{print $4}' "/proc/$p/stat" 2>/dev/null || true)"
|
||||
guard=$((guard + 1))
|
||||
[[ "$guard" -gt 64 ]] && break
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
fleet_send_text() {
|
||||
local target="" txt="" socket="" do_enter=1 dry=0 retries=2
|
||||
local got_target=0 got_text=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--socket) shift; socket="${1:-}" ;;
|
||||
--no-enter) do_enter=0 ;;
|
||||
--retries) shift; retries="${1:-2}" ;;
|
||||
--dry-run) dry=1 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Uso: fleet_send_text <sessionId|PID> "<texto>" [--socket <s>] [--no-enter] [--retries N] [--dry-run]
|
||||
|
||||
Empuja <texto> a UN agente de la flota tmux resolviendo su pane_id (%N) ESTABLE
|
||||
FRESCO justo antes de enviar (no cachea el window_id @N, que migra con el
|
||||
focus-swap). Manda el texto literal y el Enter en invocaciones separadas, y
|
||||
verifica con capture-pane que el texto aparecio antes de pulsar Enter;
|
||||
reintenta si no.
|
||||
|
||||
Argumentos:
|
||||
<sessionId|PID> Primer posicional: sessionId del agente (exacto o prefijo) o
|
||||
su PID (todo digitos). Por sessionId se busca en
|
||||
sessions/*.json el que case; su archivo (<pid>.json) da el PID.
|
||||
"<texto>" Segundo posicional: el texto a inyectar en el input del agente.
|
||||
|
||||
Opciones:
|
||||
--socket <s> Socket tmux del perfil FleetView. Default: $FLEET_SOCKET, o "fleet".
|
||||
--no-enter Deja el texto en el input sin pulsar Enter (no hace submit).
|
||||
--retries N Reintentos si el texto no aparece tras el send (default 2).
|
||||
--dry-run Imprime el plan (PID, sessionId, pane, socket) y NO envia nada.
|
||||
-h, --help Esta ayuda.
|
||||
|
||||
Salida: linea de resultado con `pane=%N` usado e `intento=N`. Exit 0 ok/dry-run;
|
||||
2 uso incorrecto o target no resuelto; 3 guard (target es la sesion actual);
|
||||
4 no se encontro pane vivo para el target; 5 enviado pero no verificado tras los
|
||||
reintentos.
|
||||
|
||||
Ejemplos:
|
||||
fleet_send_text 32945650-a4e1-472b-90c9-5b38ef60a463 "Cierra tu DoD o reporta el bloqueo." --socket "$FLEET_SOCKET"
|
||||
fleet_send_text 32945650 "Falta el error path con evidencia." # por prefijo de sessionId
|
||||
fleet_send_text 48213 "texto" --no-enter --dry-run # por PID, solo ver el plan
|
||||
USAGE
|
||||
return 0 ;;
|
||||
--*)
|
||||
echo "fleet_send_text: opcion desconocida '$1' (usa -h)" >&2
|
||||
return 2 ;;
|
||||
*)
|
||||
if [[ "$got_target" -eq 0 ]]; then
|
||||
target="$1"; got_target=1
|
||||
elif [[ "$got_text" -eq 0 ]]; then
|
||||
txt="$1"; got_text=1
|
||||
else
|
||||
echo "fleet_send_text: argumento extra '$1' (target y texto ya fijados)" >&2
|
||||
return 2
|
||||
fi ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[[ "$got_target" -eq 0 ]] && {
|
||||
echo "fleet_send_text: falta el target (sessionId o PID). Usa -h." >&2
|
||||
return 2
|
||||
}
|
||||
[[ "$got_text" -eq 0 ]] && {
|
||||
echo "fleet_send_text: falta el texto a enviar. Usa -h." >&2
|
||||
return 2
|
||||
}
|
||||
[[ "$retries" =~ ^[0-9]+$ ]] || {
|
||||
echo "fleet_send_text: --retries debe ser un entero (recibido '$retries')" >&2
|
||||
return 2
|
||||
}
|
||||
|
||||
local sessions_dir="${FN_FLEET_SESSIONS_DIR:-$HOME/.claude/sessions}"
|
||||
[[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"
|
||||
|
||||
command -v tmux >/dev/null 2>&1 || {
|
||||
echo "fleet_send_text: tmux no esta instalado" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Resolver (PID, sessionId) a partir del target. Mismo patron que
|
||||
# kill_fleet_agent: por PID directo, o por sessionId (exacto/prefijo)
|
||||
# buscando en sessions/*.json.
|
||||
# -----------------------------------------------------------------------
|
||||
local pid="" sid=""
|
||||
if [[ "$target" =~ ^[0-9]+$ ]]; then
|
||||
pid="$target"
|
||||
local sfile="$sessions_dir/$pid.json"
|
||||
if [[ -f "$sfile" ]] && command -v jq >/dev/null 2>&1; then
|
||||
sid="$(jq -r '.sessionId // ""' "$sfile" 2>/dev/null || true)"
|
||||
fi
|
||||
else
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "fleet_send_text: jq no esta instalado (necesario para resolver el sessionId)" >&2
|
||||
return 1
|
||||
}
|
||||
local f base candidate_sid
|
||||
for f in "$sessions_dir"/*.json; do
|
||||
[[ -f "$f" ]] || continue
|
||||
candidate_sid="$(jq -r '.sessionId // ""' "$f" 2>/dev/null || true)"
|
||||
[[ -z "$candidate_sid" ]] && continue
|
||||
if [[ "$candidate_sid" == "$target" || "$candidate_sid" == "$target"* ]]; then
|
||||
base="$(basename "$f" .json)"
|
||||
pid="$base"
|
||||
sid="$candidate_sid"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
[[ -z "$pid" ]] && {
|
||||
echo "fleet_send_text: no se pudo resolver el target '$target' a un PID (sessions en $sessions_dir)" >&2
|
||||
return 2
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Guard — anti-self: no enviar a la sesion que invoca la funcion.
|
||||
# -----------------------------------------------------------------------
|
||||
local self_pid="${FN_FLEET_SELF_PID:-}"
|
||||
if [[ -z "$self_pid" ]]; then
|
||||
local walk="$$" guard=0 comm
|
||||
while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do
|
||||
comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)"
|
||||
if [[ "$comm" == "claude" ]]; then
|
||||
self_pid="$walk"
|
||||
break
|
||||
fi
|
||||
walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)"
|
||||
guard=$((guard + 1))
|
||||
[[ "$guard" -gt 64 ]] && break
|
||||
done
|
||||
fi
|
||||
if [[ -n "$self_pid" && "$pid" == "$self_pid" ]]; then
|
||||
echo "fleet_send_text: REHUSADO — el target (PID $pid) es la sesion actual. No me autoenvio." >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Resolver el pane_id (%N) ESTABLE FRESCO. Mapa pane_pid->pane_id del socket
|
||||
# leido AHORA; subir por ancestros del PID hasta casar un pane_pid.
|
||||
# -----------------------------------------------------------------------
|
||||
local panes_map pane=""
|
||||
panes_map="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{pane_id}' 2>/dev/null || true)"
|
||||
if [[ -n "$panes_map" ]]; then
|
||||
pane="$(_fleet_resolve_pane_for_pid "$pid" "$panes_map" || true)"
|
||||
fi
|
||||
|
||||
[[ -z "$pane" ]] && {
|
||||
echo "fleet_send_text: no se encontro un pane vivo para el target '$target' (PID $pid) en el socket '$socket'." >&2
|
||||
return 4
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Plan.
|
||||
# -----------------------------------------------------------------------
|
||||
local enter_desc; [[ "$do_enter" -eq 1 ]] && enter_desc="texto + Enter separado" || enter_desc="solo texto (--no-enter)"
|
||||
echo "fleet_send_text — target: $target PID: $pid sessionId: ${sid:-?} socket: $socket pane: $pane envio: $enter_desc retries: $retries"
|
||||
|
||||
if [[ "$dry" -eq 1 ]]; then
|
||||
echo "DRY-RUN: no se ha enviado nada."
|
||||
echo "pane=$pane intento=0 status=dry-run"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Enviar + verificar. El texto se manda literal (-l); el Enter va en una
|
||||
# invocacion separada tras un sleep. Verificamos ANTES del Enter (el texto
|
||||
# esta en el input; tras Enter el TUI vacia el input y no se podria verificar).
|
||||
# Si el texto no aparece, limpiamos el input (C-u) y reintentamos.
|
||||
# -----------------------------------------------------------------------
|
||||
local anchor="${txt:0:24}" # fragmento ancla (evita falsos negativos por wrapping)
|
||||
local i cap ok=0 used_try=0
|
||||
for (( i=1; i<=retries+1; i++ )); do
|
||||
tmux -L "$socket" send-keys -t "$pane" -l -- "$txt" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
cap="$(tmux -L "$socket" capture-pane -p -t "$pane" 2>/dev/null || true)"
|
||||
if grep -qF -- "$anchor" <<<"$cap"; then
|
||||
ok=1; used_try="$i"
|
||||
break
|
||||
fi
|
||||
# No aparecio: limpiar el input antes de reintentar.
|
||||
tmux -L "$socket" send-keys -t "$pane" C-u 2>/dev/null || true
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
if [[ "$ok" -ne 1 ]]; then
|
||||
echo "fleet_send_text: texto enviado pero NO verificado en el pane $pane tras $((retries+1)) intentos." >&2
|
||||
echo "pane=$pane intento=$((retries+1)) status=unverified" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# Texto presente en el input. Ahora el Enter (separado) para hacer submit.
|
||||
if [[ "$do_enter" -eq 1 ]]; then
|
||||
tmux -L "$socket" send-keys -t "$pane" Enter 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "fleet_send_text: OK — texto inyectado en el pane $pane (intento $used_try)$([[ "$do_enter" -eq 1 ]] && echo " + Enter")."
|
||||
echo "pane=$pane intento=$used_try status=ok"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
fleet_send_text "$@"
|
||||
fi
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para fleet_send_text. Levanta un socket tmux PROPIO de test
|
||||
# (fleet_test_<pid>, nunca el socket "fleet" real) con un pane `cat` vivo, y
|
||||
# verifica: envio + verificacion via capture-pane (golden), supervivencia al
|
||||
# focus-swap (break-pane preserva el pane_id), resolucion por sessionId fake,
|
||||
# y los paths de error/guard. No toca la flota real ni ningun agente.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/fleet_send_text.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "FAIL: $test_name — should NOT contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_rc() {
|
||||
local test_name="$1" expected="$2" actual="$3"
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
echo "PASS: $test_name (rc=$actual)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected rc=$expected, got rc=$actual"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
command -v tmux >/dev/null 2>&1 || { echo "SKIP: tmux no instalado"; exit 0; }
|
||||
|
||||
# --- Socket de test PROPIO + pane `cat` vivo (con echo de tty) ---
|
||||
SOCK="fleet_test_$$"
|
||||
TMP="$(mktemp -d)"
|
||||
SESS="$TMP/sessions"
|
||||
mkdir -p "$SESS"
|
||||
|
||||
cleanup() {
|
||||
tmux -L "$SOCK" kill-server 2>/dev/null || true
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
tmux -L "$SOCK" new-session -d -s t -x 120 -y 30 'cat'
|
||||
sleep 0.4
|
||||
|
||||
PANE_PID="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid}' | head -n1)"
|
||||
PANE_ID0="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{pane_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
|
||||
WIN_ID0="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{window_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
|
||||
echo "INFO: socket=$SOCK pane_pid=$PANE_PID pane_id=$PANE_ID0 window_id=$WIN_ID0"
|
||||
|
||||
# self_pid forzado a un PID que nunca sera target en los tests golden.
|
||||
export FN_FLEET_SELF_PID=1
|
||||
export FN_FLEET_SESSIONS_DIR="$SESS"
|
||||
|
||||
# --- Test 1 (golden): enviar por PID, verificar via capture-pane ---
|
||||
set +e
|
||||
out=$(fleet_send_text "$PANE_PID" "HOLA_FLEET_123" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "golden: envio por PID sale 0" 0 "$rc"
|
||||
assert_contains "golden: reporta status=ok" "status=ok" "$out"
|
||||
assert_contains "golden: reporta el pane_id estable" "pane=$PANE_ID0" "$out"
|
||||
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID0")"
|
||||
assert_contains "golden: el texto llego al pane (capture-pane)" "HOLA_FLEET_123" "$cap"
|
||||
|
||||
# limpiar input del cat
|
||||
tmux -L "$SOCK" send-keys -t "$PANE_ID0" C-u; sleep 0.2
|
||||
tmux -L "$SOCK" send-keys -t "$PANE_ID0" C-l 2>/dev/null || true; sleep 0.2
|
||||
|
||||
# --- Test 2 (edge focus-swap): mover el pane a otra window, pane_id NO migra ---
|
||||
# Anadimos un segundo pane para poder break-pane el nuestro a una window nueva.
|
||||
tmux -L "$SOCK" split-window -t "$WIN_ID0" -d 'cat'; sleep 0.3
|
||||
tmux -L "$SOCK" break-pane -d -s "$PANE_ID0"; sleep 0.3
|
||||
WIN_ID1="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{window_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
|
||||
PANE_ID1="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{pane_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
|
||||
echo "INFO: tras break-pane: pane_id=$PANE_ID1 (era $PANE_ID0) window_id=$WIN_ID1 (era $WIN_ID0)"
|
||||
assert_contains "edge: pane_id NO cambia tras mover de window" "$PANE_ID0" "$PANE_ID1"
|
||||
|
||||
set +e
|
||||
out=$(fleet_send_text "$PANE_PID" "TRAS_MOVER_456" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "edge: reenvio tras focus-swap sale 0" 0 "$rc"
|
||||
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
|
||||
assert_contains "edge: el texto sigue llegando tras mover de window" "TRAS_MOVER_456" "$cap"
|
||||
tmux -L "$SOCK" send-keys -t "$PANE_ID1" C-u; sleep 0.2
|
||||
|
||||
# --- Test 3 (edge): resolver por sessionId (sessions/<pid>.json fake) ---
|
||||
echo "{\"sessionId\":\"test-sid-aaa-111\",\"cwd\":\"/tmp/x\"}" > "$SESS/$PANE_PID.json"
|
||||
set +e
|
||||
out=$(fleet_send_text "test-sid-aaa" "VIA_SID_789" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "edge: resolucion por prefijo de sessionId sale 0" 0 "$rc"
|
||||
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
|
||||
assert_contains "edge: texto llego resolviendo por sessionId" "VIA_SID_789" "$cap"
|
||||
tmux -L "$SOCK" send-keys -t "$PANE_ID1" C-u; sleep 0.2
|
||||
|
||||
# --- Test 4 (edge): --dry-run no envia nada ---
|
||||
set +e
|
||||
out=$(fleet_send_text "$PANE_PID" "NO_DEBE_APARECER_000" --socket "$SOCK" --no-enter --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "edge: dry-run sale 0" 0 "$rc"
|
||||
assert_contains "edge: dry-run reporta status=dry-run" "status=dry-run" "$out"
|
||||
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
|
||||
assert_not_contains "edge: dry-run NO inyecto texto" "NO_DEBE_APARECER_000" "$cap"
|
||||
|
||||
# --- Test 5 (error): sessionId que no resuelve a PID -> rc 2 ---
|
||||
set +e
|
||||
out=$(fleet_send_text "sid-inexistente-zzz" "x" --socket "$SOCK" 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "error: sessionId no resuelto sale 2" 2 "$rc"
|
||||
assert_contains "error: mensaje de target no resuelto" "no se pudo resolver" "$out"
|
||||
|
||||
# --- Test 6 (error): falta el texto -> rc 2 ---
|
||||
set +e
|
||||
out=$(fleet_send_text "$PANE_PID" --socket "$SOCK" 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "error: falta texto sale 2" 2 "$rc"
|
||||
|
||||
# --- Test 7 (guard anti-self): target == self_pid -> rc 3 ---
|
||||
set +e
|
||||
out=$(FN_FLEET_SELF_PID="$PANE_PID" fleet_send_text "$PANE_PID" "x" --socket "$SOCK" 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "guard: enviar a la sesion actual sale 3" 3 "$rc"
|
||||
assert_contains "guard: mensaje anti-self" "No me autoenvio" "$out"
|
||||
|
||||
# --- Test 8 (error): PID sin pane vivo -> rc 4 ---
|
||||
set +e
|
||||
out=$(fleet_send_text 999999 "x" --socket "$SOCK" 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "error: PID sin pane vivo sale 4" 4 "$rc"
|
||||
assert_contains "error: mensaje no pane vivo" "no se encontro un pane vivo" "$out"
|
||||
|
||||
# --- Resumen ---
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "PASS: $PASS FAIL: $FAIL"
|
||||
echo "================================"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
@@ -3,10 +3,10 @@ name: kill_fleet_agent
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.0.0
|
||||
version: 1.1.0
|
||||
purity: impure
|
||||
signature: "kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]"
|
||||
description: "Cierre limpio y dirigido de UN ejecutor de la flota tmux. Dado un sessionId (exacto o prefijo) o un PID, manda SIGTERM al proceso claude del ejecutor (cierre limpio, recuperable con claude --resume) y cierra su window tmux (kill-window) en el socket del perfil FleetView. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Guards de seguridad: NUNCA mata a un agente con role=orchestrator (leido de su goal.json) ni a la sesion que invoca la funcion (resuelve su propio PID de claude por los ancestros de /proc). Por defecto EJECUTA; --dry-run imprime el plan sin tocar nada. Es el cierre dirigido a UN agente, frente a reboot_all_claudes que opera sobre toda la flota."
|
||||
description: "Cierre limpio y dirigido de UN ejecutor de la flota tmux. Dado un sessionId (exacto o prefijo) o un PID, manda SIGTERM al proceso claude del ejecutor (cierre limpio, recuperable con claude --resume) y cierra su window tmux en el socket del perfil FleetView. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Tres guards de seguridad: NUNCA mata a un agente con role=orchestrator (leido de su goal.json); NUNCA a la sesion que invoca la funcion (resuelve su propio PID de claude por los ancestros de /proc); y NUNCA cierra la window que aloja la TUI fleetview o la window 'console' con kill-window (eso se llevaria el panel de control por delante) — en ese caso cierra SOLO el pane del target con kill-pane y preserva la TUI. Por defecto EJECUTA; --dry-run imprime el plan (incluida la accion kill-pane vs kill-window) sin tocar nada. Es el cierre dirigido a UN agente, frente a reboot_all_claudes que opera sobre toda la flota."
|
||||
tags: [fleet, claude-fleet, orchestration, tmux, kill, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -17,6 +17,7 @@ tests:
|
||||
- "golden: ejecutor por sessionId, PID y prefijo se resuelve y dry-run imprime el plan"
|
||||
- "guard: matar un role=orchestrator devuelve rc=3 y se niega"
|
||||
- "guard: matar la sesion actual (self) devuelve rc=3 y se niega"
|
||||
- "guard3: predicado _fleet_window_hosts_tui detecta window 'console' o pane fleetview"
|
||||
- "error: target no resuelto rc=2; sin target rc=2"
|
||||
test_file_path: "bash/functions/infra/kill_fleet_agent_test.sh"
|
||||
params:
|
||||
@@ -55,11 +56,13 @@ Cierra de forma dirigida UN ejecutor de la flota tmux: SIGTERM al proceso `claud
|
||||
- **Impura y destructiva**: manda SIGTERM y cierra una window tmux. Por defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado `met`); usa `--dry-run` para inspeccionar antes.
|
||||
- **Guard anti-orquestador**: si el goal.json del target tiene `role=orchestrator`, rehúsa con exit 3. Evita decapitar la flota por error. El `role` se lee de `~/.claude/goals/<sessionId>.json` (lo escribe `mark_claude_role`).
|
||||
- **Guard anti-self**: resuelve el PID de `claude` de la sesión actual subiendo por los ancestros de `/proc`; si el target coincide, rehúsa con exit 3 ("No me suicido"). Es el equivalente dirigido de la regla "nunca `pkill claude`".
|
||||
- **Resolución de la window**: usa `tmux -L <socket> list-panes -a` y casa `pane_pid == PID`. Funciona porque `spawn_fleet_agent` arranca el ejecutor con `exec claude`, así el `pane_pid` ES el PID de claude. Si no hay socket/tmux, la window queda "(no resuelta)" y solo se manda el SIGTERM (best-effort, no falla).
|
||||
- **Guard 3 — anti-TUI/console (no decapitar el panel)**: antes de cerrar nada, comprueba si la window del target **aloja la TUI fleetview** (algún pane corre el binario `fleetview`) o se llama **`console`**. El layout FleetView mete la TUI y un Claude en la misma window `console`, y los focus-swaps (`join-pane`) pueden meter al ejecutor target en esa window; un `kill-window` ahí se llevaría la TUI por delante (causa del fallo descrito en `fleetview` v0.4.3). En ese caso la función NO usa `kill-window`: manda el SIGTERM al claude y cierra **solo su pane** con `kill-pane`, preservando el pane de la TUI. El plan (y el `--dry-run`) lo refleja como `accion: kill-pane … (aloja la TUI/console)` vs `accion: kill-window …`. El predicado es la función interna `_fleet_window_hosts_tui` (testeada). Se mantiene inline (no función propia del registry) por estar acoplada a este flujo y para no dejar una capacidad huérfana (KISS).
|
||||
- **Resolución de la window y el pane**: usa `tmux -L <socket> list-panes -a` y casa `pane_pid == PID`, capturando `window_id`, `pane_id` y `window_name`. Funciona porque `spawn_fleet_agent` arranca el ejecutor con `exec claude`, así el `pane_pid` ES el PID de claude. Si no hay socket/tmux, la window queda "(no resuelta)" y solo se manda el SIGTERM (best-effort, no falla).
|
||||
- **SIGTERM, no SIGKILL**: cierre limpio para que Claude Code persista su sesión; el trabajo se puede retomar con `claude --resume <sessionId>`.
|
||||
- **Requiere `jq`** para leer los JSON de sessions/goals.
|
||||
- **Overrides de entorno solo para tests**: `FN_FLEET_SESSIONS_DIR`, `FN_FLEET_GOALS_DIR` y `FN_FLEET_SELF_PID` redirigen los directorios y fuerzan el PID propio; no usarlos en operación normal.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
(v1.0.0 — sin cambios todavía.)
|
||||
- v1.1.0 (2026-06-24) — **Guard 3 anti-TUI/console** (elimina un gotcha conocido). Antes, si un focus-swap metía al ejecutor target en la window `console` (la que aloja la TUI fleetview), `kill-window` cerraba la TUI por error. Ahora, cuando la window del target aloja la TUI (pane `fleetview`) o se llama `console`, se cierra solo el pane del target con `kill-pane` y la TUI sobrevive; el resto de windows siguen cerrándose con `kill-window`. Predicado interno `_fleet_window_hosts_tui` con tests. Es la causa raíz que complementa el auto-respawn de la TUI (`supervise_fleetview_tui`).
|
||||
- v1.0.0 — versión inicial.
|
||||
|
||||
@@ -26,6 +26,25 @@
|
||||
set -euo pipefail
|
||||
IFS=$' \t\n'
|
||||
|
||||
# Predicado (puro respecto a tmux): dada una window — su nombre y el texto de sus
|
||||
# panes en formato "<pane_pid> <pane_current_command>" (una linea por pane) —
|
||||
# decide si esa window ALOJA la TUI fleetview o es la window 'console' del perfil.
|
||||
# Si es asi, cerrar la window entera con kill-window se llevaria la TUI por
|
||||
# delante; el caller debe cerrar solo el pane del target con kill-pane.
|
||||
# - Nombre de window 'console' = la window del panel FleetView por convencion
|
||||
# del launcher (y a donde el focus-swap ancla la TUI, ver fleetview v0.4.3).
|
||||
# - Algun pane corre el binario 'fleetview' (pane_current_command) = la TUI
|
||||
# vive ahi aunque la window se haya renombrado.
|
||||
# Devuelve 0 si aloja la TUI/console, 1 si no.
|
||||
_fleet_window_hosts_tui() {
|
||||
local window_name="${1:-}" panes_text="${2:-}"
|
||||
[[ "$window_name" == "console" ]] && return 0
|
||||
if printf '%s\n' "$panes_text" | awk '{print $2}' | grep -qx 'fleetview'; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
kill_fleet_agent() {
|
||||
local target="" socket="" dry=0
|
||||
|
||||
@@ -155,27 +174,65 @@ USAGE
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Resolver la window tmux del PID en el socket (pane_pid == claude por el
|
||||
# `exec claude` de spawn_fleet_agent). Best-effort: vacio si no hay socket.
|
||||
# Resolver la window tmux Y el pane del PID en el socket (pane_pid == claude
|
||||
# por el `exec claude` de spawn_fleet_agent). Capturamos window_id, pane_id y
|
||||
# window_name juntos. Best-effort: vacio si no hay socket.
|
||||
# -----------------------------------------------------------------------
|
||||
local window=""
|
||||
local window="" pane="" wname=""
|
||||
if command -v tmux >/dev/null 2>&1; then
|
||||
window="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{window_id}' 2>/dev/null \
|
||||
| awk -v p="$pid" '$1==p {print $2; exit}' || true)"
|
||||
local line
|
||||
line="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{window_id} #{pane_id} #{window_name}' 2>/dev/null \
|
||||
| awk -v p="$pid" '$1==p {print $2, $3, $4; exit}' || true)"
|
||||
if [[ -n "$line" ]]; then
|
||||
window="$(awk '{print $1}' <<<"$line")"
|
||||
pane="$(awk '{print $2}' <<<"$line")"
|
||||
wname="$(awk '{print $3}' <<<"$line")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Guard 3 — anti-TUI/console: si la window del target aloja la TUI fleetview
|
||||
# o es la window 'console' del perfil, NO cerramos la window entera (eso se
|
||||
# llevaria la TUI), sino solo el pane del target con kill-pane. El layout
|
||||
# FleetView mete la TUI y un Claude en la misma window 'console', y los
|
||||
# focus-swaps (join-pane) pueden meter al ejecutor target en esa window.
|
||||
# -----------------------------------------------------------------------
|
||||
local hosts_tui=0
|
||||
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
|
||||
local panes_text
|
||||
panes_text="$(tmux -L "$socket" list-panes -t "$window" -F '#{pane_pid} #{pane_current_command}' 2>/dev/null || true)"
|
||||
if _fleet_window_hosts_tui "$wname" "$panes_text"; then
|
||||
hosts_tui=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Accion sobre la window/pane segun lo resuelto y el Guard 3.
|
||||
local action
|
||||
if [[ -z "$window" ]]; then
|
||||
action="solo SIGTERM (window no resuelta)"
|
||||
elif [[ "$hosts_tui" -eq 1 ]]; then
|
||||
if [[ -n "$pane" ]]; then
|
||||
action="kill-pane $pane (window '${wname:-$window}' aloja la TUI/console; se preserva la TUI)"
|
||||
else
|
||||
action="solo SIGTERM (window '${wname:-$window}' aloja la TUI y no se resolvio el pane; window preservada)"
|
||||
fi
|
||||
else
|
||||
action="kill-window $window"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Plan (se imprime siempre).
|
||||
# -----------------------------------------------------------------------
|
||||
echo "kill_fleet_agent — target: $target PID: $pid sessionId: ${sid:-?} role: ${role:-executor} socket: $socket window: ${window:-(no resuelta)}"
|
||||
echo "kill_fleet_agent — target: $target PID: $pid sessionId: ${sid:-?} role: ${role:-executor} socket: $socket window: ${window:-(no resuelta)} pane: ${pane:-?} accion: $action"
|
||||
|
||||
if [[ "$dry" -eq 1 ]]; then
|
||||
echo "DRY-RUN: no se ha matado el proceso ni cerrado la window."
|
||||
echo "DRY-RUN: no se ha matado el proceso ni cerrado nada."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Ejecutar: SIGTERM al claude (cierre limpio) + kill-window (idempotente).
|
||||
# Ejecutar: SIGTERM al claude (cierre limpio) + cierre de pane/window segun
|
||||
# el Guard 3 (idempotente).
|
||||
# -----------------------------------------------------------------------
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
@@ -185,8 +242,17 @@ USAGE
|
||||
fi
|
||||
|
||||
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
|
||||
tmux -L "$socket" kill-window -t "$window" 2>/dev/null || true
|
||||
echo "kill_fleet_agent: window $window cerrada en el socket $socket."
|
||||
if [[ "$hosts_tui" -eq 1 ]]; then
|
||||
if [[ -n "$pane" ]]; then
|
||||
tmux -L "$socket" kill-pane -t "$pane" 2>/dev/null || true
|
||||
echo "kill_fleet_agent: pane $pane cerrado (window '${wname:-$window}' aloja la TUI; window preservada)."
|
||||
else
|
||||
echo "kill_fleet_agent: window '${wname:-$window}' aloja la TUI pero no se resolvio el pane; solo SIGTERM (window preservada)."
|
||||
fi
|
||||
else
|
||||
tmux -L "$socket" kill-window -t "$window" 2>/dev/null || true
|
||||
echo "kill_fleet_agent: window $window cerrada en el socket $socket."
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
||||
@@ -104,6 +104,24 @@ set -e
|
||||
assert_rc "error: sin target devuelve rc=2" 2 "$rc"
|
||||
assert_contains "error: mensaje falta target" "falta el target" "$out"
|
||||
|
||||
# --- Test 7 (Guard 3 predicado): _fleet_window_hosts_tui ---
|
||||
# La window 'console' SIEMPRE se considera que aloja la TUI (no se cierra entera).
|
||||
assert_predicate() {
|
||||
local test_name="$1" expected="$2"; shift 2
|
||||
set +e
|
||||
_fleet_window_hosts_tui "$@"; local rc=$?
|
||||
set -e
|
||||
assert_rc "$test_name" "$expected" "$rc"
|
||||
}
|
||||
# Nombre de window 'console' -> aloja TUI (rc 0), aunque ningun pane sea fleetview.
|
||||
assert_predicate "guard3: window 'console' aloja la TUI" 0 "console" $'1234 claude\n5678 bash'
|
||||
# Algun pane corre 'fleetview' -> aloja TUI (rc 0), aunque la window no sea console.
|
||||
assert_predicate "guard3: pane fleetview aloja la TUI" 0 "claude" $'1111 bash\n2222 fleetview'
|
||||
# Ni console ni fleetview -> NO aloja la TUI (rc 1): kill-window normal.
|
||||
assert_predicate "guard3: window normal no aloja la TUI" 1 "claude" $'3333 claude\n4444 bash'
|
||||
# Substring que contiene 'fleetview' pero no es el comando exacto -> NO matchea (grep -qx).
|
||||
assert_predicate "guard3: comando 'fleetviewer' no falsea positivo" 1 "work" $'7777 fleetviewer'
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
|
||||
@@ -3,11 +3,11 @@ name: launch_fleetclaude
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.4.0"
|
||||
version: "1.6.0"
|
||||
purity: impure
|
||||
signature: "launch_fleetclaude [--cwd <dir>] [--bin <path>] [--session <name>] [--reuse] [--cols <n>]"
|
||||
description: "Entrypoint de FleetView: abre una ventana kitty con una sesion tmux (socket aislado por perfil) de dos panes (TUI fleetview a la izquierda, claude --dangerously-skip-permissions a la derecha) para centralizar la flota de Claudes. Soporta PERFILES multiples: sin --session/--reuse cada invocacion abre un perfil nuevo (fleet, fleet2, fleet3, ...) con su propia flota; inyecta FLEET_SOCKET/FLEET_SESSION a la TUI para que cada panel vea solo sus Claudes. Instala atajos alt+flechas/alt+enter/alt+n que controlan la TUI desde cualquier pane, y fija el ancho del sidebar con hooks."
|
||||
tags: [claude-fleet, infra, kitty, tmux, claude, fleetview, launcher]
|
||||
description: "Entrypoint de FleetView: abre una ventana de terminal con una sesion tmux (socket aislado por perfil) de dos panes (TUI fleetview a la izquierda, claude --dangerously-skip-permissions a la derecha) para centralizar la flota de Claudes. La terminal se AUTO-DETECTA sin config por PC: kitty si esta instalado y hay display ($DISPLAY/$WAYLAND_DISPLAY), si no Windows Terminal (wt.exe) en WSL adjuntando via wsl.exe. El pane de la TUI corre dentro del bucle supervisor supervise_fleetview_tui, que la relanza si muere (crash/panic/kill), asi el panel de control NUNCA se pierde. Soporta PERFILES multiples: sin --session/--reuse cada invocacion abre un perfil nuevo (fleet, fleet2, fleet3, ...) con su propia flota; inyecta FLEET_SOCKET/FLEET_SESSION a la TUI para que cada panel vea solo sus Claudes. Instala atajos alt+flechas/alt+enter/alt+n que controlan la TUI desde cualquier pane, y fija el ancho del sidebar con hooks."
|
||||
tags: [claude-fleet, infra, kitty, tmux, claude, fleetview, launcher, wsl, windows-terminal]
|
||||
params:
|
||||
- name: --cwd
|
||||
desc: "Directorio de trabajo de ambos panes tmux. Opcional. Default: raiz del repo fn_registry, derivada dinamicamente via git rev-parse desde la ubicacion del script (sin hardcodear paths de usuario)."
|
||||
@@ -19,8 +19,9 @@ params:
|
||||
desc: "Reattach al perfil principal 'fleet' en vez de abrir uno nuevo. Opcional. Recupera el comportamiento idempotente clasico (volver a invocar NO duplica la flota, reusa la existente)."
|
||||
- name: --cols
|
||||
desc: "Ancho en columnas del pane izquierdo (la TUI). Opcional. Default: 40."
|
||||
output: "Crea/reutiliza una sesion tmux detached con dos panes y lanza una ventana kitty 'FleetView' adjunta a ella, desacoplada del shell padre (setsid). Imprime el estado por stdout. Sin valor de retorno; exit 0 en exito."
|
||||
uses_functions: []
|
||||
output: "Crea/reutiliza una sesion tmux detached con dos panes y lanza una ventana de terminal 'FleetView' adjunta a ella (kitty o Windows Terminal segun auto-deteccion), desacoplada del shell padre. Imprime el estado por stdout. Sin valor de retorno; exit 0 en exito."
|
||||
uses_functions:
|
||||
- supervise_fleetview_tui_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
@@ -48,7 +49,7 @@ launch_fleetclaude --reuse
|
||||
launch_fleetclaude --session trabajo --cols 50
|
||||
```
|
||||
|
||||
Tras invocarlo aparece una ventana kitty titulada `FleetView (<perfil>)` con dos
|
||||
Tras invocarlo aparece una ventana de terminal titulada `FleetView (<perfil>)` con dos
|
||||
panes lado a lado: a la izquierda la TUI `fleetview`, a la derecha una sesion de
|
||||
`claude --dangerously-skip-permissions`. Cada perfil es un socket+sesion tmux
|
||||
aislados con su propia flota: puedes tener varias FleetView abiertas a la vez.
|
||||
@@ -77,16 +78,38 @@ al retomar el trabajo en el repo `fn_registry`.
|
||||
`respawn-pane` de alt+R y los Claude nuevos hereden el socket). `main.go` los
|
||||
lee con fallback a `fleet`. Por eso cada panel ve SOLO los Claude de su perfil
|
||||
(cruza la lista del sistema con los panes de su socket).
|
||||
- **Auto-deteccion de terminal (sin config por PC)**: en la ruta ventana-nueva el
|
||||
launcher elige terminal solo. (1) `kitty` instalado **y** display usable
|
||||
(`$DISPLAY`/`$WAYLAND_DISPLAY`) → kitty (escritorio Linux nativo o WSLg con
|
||||
kitty). (2) Si no, WSL con `wt.exe` en el PATH → Windows Terminal ejecutando
|
||||
`wsl.exe [-d $WSL_DISTRO_NAME] -- bash -lic 'tmux -L <perfil> attach ...'`.
|
||||
(3) Ninguna → error con las salidas posibles. Asi el MISMO `fleetclaude`
|
||||
funciona en un PC con kitty y en otro WSL sin kitty, cada uno elige su
|
||||
terminal. Causa raiz del sintoma "se lanza la flota pero no se ve": kitty no
|
||||
instalado en WSL hacia que la sesion tmux se creara sin ventana que la mostrara.
|
||||
- **Dentro de tmux abre ventana nueva**: si invocas `fleetclaude` desde dentro de
|
||||
una sesion tmux (`$TMUX` definido), NO hace `attach` anidado (rompe / avisa de
|
||||
nesting); cae a la ruta kitty y abre una ventana nueva. Fuera de tmux y con
|
||||
TTY, reutiliza la terminal actual con `exec tmux attach`.
|
||||
- **kitty detached (setsid)**: la ventana se lanza con `setsid ... &` para
|
||||
sobrevivir al cierre de la terminal que la invoco. No bloquea al shell padre.
|
||||
- **`exec` en los panes**: tanto la TUI como `claude` se lanzan con `exec`, asi
|
||||
que al terminar el proceso el pane se cierra en vez de dejar una shell zombie
|
||||
colgando. Excepcion: el fallback cuando `fleetview` no esta compilado deja una
|
||||
shell interactiva a proposito (para que veas el mensaje y puedas compilar).
|
||||
nesting); cae a la ruta ventana-nueva (auto-deteccion de terminal). Fuera de
|
||||
tmux y con TTY, reutiliza la terminal actual con `exec tmux attach`.
|
||||
- **kitty detached (setsid)**: la ventana kitty se lanza con `setsid ... &` para
|
||||
sobrevivir al cierre de la terminal que la invoco. La ventana de Windows
|
||||
Terminal (wt.exe) ya es un proceso Windows independiente del arbol Linux, asi
|
||||
que sobrevive sola (se lanza con `&`+`disown` desde un subshell con cwd `/mnt/c`
|
||||
para evitar el warning de wt.exe por cwd UNC `\\wsl.localhost\...`).
|
||||
- **TUI bajo supervisor (auto-respawn)**: el pane izquierdo NO corre un
|
||||
`exec fleetview` de una sola vida, sino `supervise_fleetview_tui` (bucle que
|
||||
relanza la TUI si muere por crash/panic/kill). Asi el panel de control nunca se
|
||||
pierde por un fallo puntual. El supervisor para limpio con su sentinel
|
||||
(`touch ~/.claude/fleet/tui_stop_<perfil>` y deja salir la TUI) o se rinde si la
|
||||
TUI entra en crash-loop; en ambos casos el pane cae a una shell viva (no se
|
||||
cierra solo) para inspeccionar. Es la mitad "auto-recuperacion" del par de
|
||||
fixes que blindan FleetView; la otra es el Guard 3 anti-TUI/console de
|
||||
`kill_fleet_agent` (la causa raiz del cierre accidental). Si el script del
|
||||
supervisor no estuviera en disco, cae al `exec fleetview` clasico.
|
||||
- **`exec` en los demas panes**: `claude` (orquestador e idle) se lanza con
|
||||
`exec`, asi que al terminar el proceso el pane se cierra en vez de dejar una
|
||||
shell zombie. Excepcion: el fallback cuando `fleetview` no esta compilado deja
|
||||
una shell interactiva a proposito (para que veas el mensaje y puedas compilar).
|
||||
- **Requiere fleetview compilado**: el default `--bin` apunta a
|
||||
`<repo>/apps/fleetview/fleetview`. Si ese binario no existe, el pane izquierdo
|
||||
muestra `cd apps/fleetview && go build -o fleetview .` en lugar de fallar en
|
||||
@@ -105,14 +128,30 @@ al retomar el trabajo en el repo `fn_registry`.
|
||||
- **Ancho del sidebar via hooks**: `client-resized` y `window-layout-changed`
|
||||
re-fijan el pane 0 (TUI) a `--cols` columnas, porque el `attach` de kitty y el
|
||||
conmutar de Claude redistribuyen el espacio.
|
||||
- **tmux siempre, kitty solo sin TTY**: `tmux` es obligatorio (aborta != 0 si
|
||||
falta). `kitty` solo se necesita en la ruta sin-TTY (atajo de escritorio, cron,
|
||||
script), donde abre una ventana nueva. Invocado desde una terminal interactiva
|
||||
(el caso normal del alias `fleetclaude`), reutiliza la terminal actual con
|
||||
`exec tmux attach` y NO necesita kitty — util en WSL u hosts sin kitty.
|
||||
- **tmux siempre; terminal (kitty/wt.exe) solo sin TTY**: `tmux` es obligatorio
|
||||
(aborta != 0 si falta). Una terminal nueva (kitty o Windows Terminal) solo se
|
||||
necesita en la ruta sin-TTY (dentro de tmux, atajo de escritorio, cron, script),
|
||||
donde abre una ventana nueva. Invocado desde una terminal interactiva fuera de
|
||||
tmux (el caso normal del alias `fleetclaude`), reutiliza la terminal actual con
|
||||
`exec tmux attach` y no necesita ni kitty ni wt.exe.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.6.0 (2026-06-29) — **auto-deteccion de terminal (kitty ↔ Windows Terminal)**.
|
||||
La ruta ventana-nueva ya no asume kitty: elige terminal segun el host. kitty si
|
||||
esta instalado y hay display (`$DISPLAY`/`$WAYLAND_DISPLAY`); si no, en WSL abre
|
||||
Windows Terminal (`wt.exe`) ejecutando `wsl.exe [-d $WSL_DISTRO_NAME] -- bash
|
||||
-lic 'tmux ... attach'`. Mismo `fleetclaude` en un PC con kitty y en otro WSL
|
||||
sin kitty. Arregla el sintoma "se lanza la flota pero no se ve": en WSL sin
|
||||
kitty la sesion tmux se creaba pero ninguna ventana la mostraba. wt.exe se
|
||||
lanza desde un subshell con cwd `/mnt/c` para evitar el warning por cwd UNC.
|
||||
- v1.5.0 (2026-06-24) — **auto-respawn de la TUI**. El pane izquierdo ya no corre
|
||||
`exec fleetview` (una sola vida), sino el bucle supervisor
|
||||
`supervise_fleetview_tui`, que relanza la TUI si muere (crash/panic/kill de su
|
||||
proceso o pane). Asi el panel de control NUNCA se pierde por un fallo puntual.
|
||||
Parada voluntaria via sentinel; crash-loop guard para no relanzar en bucle
|
||||
cerrado. Complementa el Guard 3 anti-TUI/console de `kill_fleet_agent` (causa
|
||||
raiz del cierre accidental). Nueva dependencia: `supervise_fleetview_tui_bash_infra`.
|
||||
- v1.4.0 (2026-06-18) — **perfiles multiples**. Socket+sesion tmux ya no son el
|
||||
fijo `fleet`: cada perfil tiene los suyos (mismo nombre). Sin `--session`/
|
||||
`--reuse`, cada invocacion abre el primer perfil libre (`fleet`, `fleet2`, ...),
|
||||
|
||||
@@ -170,7 +170,22 @@ USAGE
|
||||
envpfx="FLEET_SOCKET=$(printf '%q' "$session") FLEET_SESSION=$(printf '%q' "$session")"
|
||||
local left_cmd
|
||||
if [[ -x "$bin" ]]; then
|
||||
left_cmd="$envpfx exec $(printf '%q' "$bin")"
|
||||
# NO un `exec fleetview` de una sola vida: lo envolvemos en el bucle
|
||||
# supervisor supervise_fleetview_tui, que relanza la TUI si muere (crash,
|
||||
# panic, kill de su proceso o de su pane). Asi el panel de control de la
|
||||
# flota NUNCA se pierde por un fallo puntual. El supervisor para limpio
|
||||
# con su sentinel (touch ~/.claude/fleet/tui_stop_<perfil>) o se rinde si
|
||||
# la TUI entra en crash-loop; en ambos casos cae a una shell viva.
|
||||
local sup="$repo_root/bash/functions/infra/supervise_fleetview_tui.sh"
|
||||
if [[ -f "$sup" ]]; then
|
||||
# bash <sup> (no exec): al volver el supervisor (sentinel o crash-loop)
|
||||
# caemos a una shell viva para que el mensaje siga visible y se pueda
|
||||
# inspeccionar/relanzar. El env aplica al supervisor y a su hijo TUI.
|
||||
left_cmd="$envpfx bash $(printf '%q' "$sup") --bin $(printf '%q' "$bin") --socket $(printf '%q' "$session"); exec \"\$SHELL\""
|
||||
else
|
||||
# Fallback si falta el supervisor en disco: comportamiento clasico.
|
||||
left_cmd="$envpfx exec $(printf '%q' "$bin")"
|
||||
fi
|
||||
else
|
||||
# Fallback claro: instruye como compilar la TUI y deja una shell viva.
|
||||
left_cmd="echo 'fleetview no compilado: cd apps/fleetview && go build -o fleetview .'; exec \"\$SHELL\""
|
||||
@@ -279,31 +294,61 @@ USAGE
|
||||
$T set-hook -g window-layout-changed "resize-pane -t $left_pane -x $cols"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Lanzar kitty adjuntando la sesion, DESACOPLADA del shell padre con
|
||||
# setsid, para que no muera al cerrar la terminal invocadora.
|
||||
# (Mismo patron que reboot_all_claudes para relanzar terminales.)
|
||||
# Adjuntar la sesion en una terminal, DESACOPLADA del shell padre para que
|
||||
# no muera al cerrar la terminal invocadora.
|
||||
# -----------------------------------------------------------------------
|
||||
# Adjuntar la sesion:
|
||||
# - Terminal interactiva y FUERA de tmux: convertir ESA terminal en el
|
||||
# panel FleetView (exec reemplaza el proceso; al hacer detach vuelve la
|
||||
# shell). Asi `fleetclaude` no abre otra ventana: usa la actual.
|
||||
# - DENTRO de tmux (o sin TTY: atajo de escritorio, cron, script): abrir
|
||||
# una ventana kitty nueva desacoplada (setsid). No hacemos `attach`
|
||||
# una ventana de terminal NUEVA desacoplada. No hacemos `attach`
|
||||
# anidado dentro de otra sesion tmux (rompe / da el warning de nesting).
|
||||
if [ -t 0 ] && [ -t 1 ] && [ -z "${TMUX:-}" ]; then
|
||||
exec tmux -L "$session" attach -t "$session"
|
||||
fi
|
||||
# Ruta ventana-nueva: necesitamos kitty para abrirla.
|
||||
if ! command -v kitty >/dev/null 2>&1; then
|
||||
echo "launch_fleetclaude: kitty no esta instalado (necesario para abrir ventana nueva)." >&2
|
||||
echo "launch_fleetclaude: lanzalo desde una terminal interactiva fuera de tmux, o instala kitty." >&2
|
||||
return 1
|
||||
fi
|
||||
setsid kitty --title "FleetView ($session)" -e tmux -L "$session" attach -t "$session" </dev/null >/dev/null 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
|
||||
echo "launch_fleetclaude: ventana kitty 'FleetView ($session)' adjunta al perfil '$session'."
|
||||
return 0
|
||||
# -----------------------------------------------------------------------
|
||||
# Ruta ventana-nueva: AUTO-DETECTAR la terminal disponible (sin config por
|
||||
# PC). El mismo `fleetclaude` funciona en un escritorio Linux con kitty y en
|
||||
# un WSL sin kitty pero con Windows Terminal.
|
||||
# 1. kitty instalado + display usable ($DISPLAY/$WAYLAND_DISPLAY) -> kitty
|
||||
# (escritorio Linux nativo, o WSLg con kitty instalado).
|
||||
# 2. WSL con wt.exe alcanzable -> Windows Terminal ejecutando wsl.exe que
|
||||
# adjunta la sesion tmux (PCs WSL sin kitty: la ventana kitty nunca
|
||||
# aparece sin una terminal Linux real, por eso "se lanza pero no se ve").
|
||||
# 3. Ninguna -> error claro con las dos salidas posibles.
|
||||
# -----------------------------------------------------------------------
|
||||
if command -v kitty >/dev/null 2>&1 && [[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]]; then
|
||||
setsid kitty --title "FleetView ($session)" -e tmux -L "$session" attach -t "$session" </dev/null >/dev/null 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
echo "launch_fleetclaude: ventana kitty 'FleetView ($session)' adjunta al perfil '$session'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v wt.exe >/dev/null 2>&1; then
|
||||
# bash -lic <attach> dentro de wsl.exe: login+interactive para que tmux y
|
||||
# el PATH del perfil esten disponibles en la ventana de Windows Terminal.
|
||||
local attach_cmd
|
||||
attach_cmd="tmux -L $(printf '%q' "$session") attach -t $(printf '%q' "$session")"
|
||||
local distro="${WSL_DISTRO_NAME:-}"
|
||||
local wsl_args=(wsl.exe)
|
||||
[[ -n "$distro" ]] && wsl_args+=(-d "$distro")
|
||||
wsl_args+=(-- bash -lic "$attach_cmd")
|
||||
# cd a una ruta Windows (/mnt/c) evita el warning de wt.exe por cwd UNC
|
||||
# (\\wsl.localhost\...). El cwd real de los panes lo fija la sesion tmux.
|
||||
( cd /mnt/c 2>/dev/null || cd /
|
||||
wt.exe new-tab --title "FleetView ($session)" "${wsl_args[@]}" </dev/null >/dev/null 2>&1 &
|
||||
disown 2>/dev/null || true )
|
||||
echo "launch_fleetclaude: Windows Terminal 'FleetView ($session)' adjunta al perfil '$session' (WSL distro '${distro:-default}')."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "launch_fleetclaude: no hay terminal para abrir una ventana nueva." >&2
|
||||
echo "launch_fleetclaude: - escritorio Linux: instala kitty y exporta DISPLAY/WAYLAND_DISPLAY." >&2
|
||||
echo "launch_fleetclaude: - WSL: usa Windows Terminal (wt.exe debe estar en el PATH)." >&2
|
||||
echo "launch_fleetclaude: - o lanza fleetclaude desde una terminal interactiva fuera de tmux." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "reboot_all_claudes([--go|--yes] [--resume-mode resume|continue|none] [--exclude-current] [--only-idle] [-h|--help])"
|
||||
description: "Cierra todas las terminales kitty con una sesion de Claude Code corriendo y las relanza retomando la misma sesion (claude --resume <sessionId>). Mapea cada PID vivo a su ~/.claude/sessions/<PID>.json para sacar sessionId, cwd y la ventana kitty. DRY-RUN por defecto; --go ejecuta de verdad de forma desacoplada."
|
||||
tags: [claude, session, terminal, kitty, reboot, infra, terminal-capture]
|
||||
tags: [claude, session, terminal, kitty, reboot, infra, terminal-capture, orchestration]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -3,23 +3,24 @@ name: spawn_fleet_agent
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.1.0
|
||||
version: 1.2.0
|
||||
purity: impure
|
||||
signature: "spawn_fleet_agent --socket <s> --session <s> --cwd <dir> [--prompt-file <f> | --skill <name>] [--role orchestrator|executor] [--parent <sid>] [--title <t>]"
|
||||
description: "Lanza un Claude como window nueva dentro de la sesion tmux de un perfil FleetView (socket aislado), opcionalmente en modo orquestador (skill embebida como primer prompt), marcado con un role en su goal.json y atribuido a su orquestador padre. Es la forma de que un ejecutor o el propio orquestador VIVAN en la flota tmux (visibles en la TUI fleetview, conmutables con /fleet focus) en vez de en kitties sueltas. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de un perfil fleet ya montado. Con --parent <sid> escribe parent_orchestrator en el goal.json del nuevo Claude (via mark_claude_parent) para que el watcher de fleetview rutee sus avisos al orquestador que lo lanzo. Imprime el window_id creado."
|
||||
signature: "spawn_fleet_agent [--socket <s>] [--session <s>] [--cwd <dir>] [--prompt-file <f> | --skill <name>] [--role orchestrator|executor] [--parent <sid>] [--title <t>]"
|
||||
description: "Lanza un Claude como window nueva dentro de la sesion tmux de un perfil FleetView (socket aislado), opcionalmente en modo orquestador (skill embebida como primer prompt), marcado con un role en su goal.json y atribuido a su orquestador padre. --socket/--session son opcionales: si no se pasan se auto-detectan del contexto tmux ($TMUX) via detect_fleet_context (los explicitos tienen prioridad), evitando caer a kitty cuando $FLEET_SOCKET viene vacia. Es la forma de que un ejecutor o el propio orquestador VIVAN en la flota tmux (visibles en la TUI fleetview, conmutables con /fleet focus) en vez de en kitties sueltas. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de un perfil fleet ya montado. Con --parent <sid> escribe parent_orchestrator en el goal.json del nuevo Claude (via mark_claude_parent) para que el watcher de fleetview rutee sus avisos al orquestador que lo lanzo. Imprime el window_id creado."
|
||||
tags: [fleet, claude-fleet, orchestration, tmux, infra]
|
||||
uses_functions:
|
||||
- mark_claude_role_py_infra
|
||||
- mark_claude_parent_py_infra
|
||||
- detect_fleet_context_bash_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)."
|
||||
desc: "Socket tmux del perfil FleetView (ej. fleet, fleet2). Opcional: se auto-detecta de $TMUX via detect_fleet_context si no se pasa. El perfil debe estar ya montado (sesion viva)."
|
||||
- name: --session
|
||||
desc: "Nombre de la sesion tmux dentro del socket (normalmente igual al socket)."
|
||||
desc: "Nombre de la sesion tmux dentro del socket (normalmente igual al socket). Opcional: se auto-detecta de $TMUX si no se pasa."
|
||||
- name: --cwd
|
||||
desc: "Directorio de trabajo del nuevo Claude. Default: PWD."
|
||||
- name: --prompt-file
|
||||
@@ -54,6 +55,11 @@ Lanza un Claude dentro de un perfil FleetView (sesion tmux de un socket aislado)
|
||||
./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \
|
||||
--prompt-file /tmp/orq_health.md --title "kanban-health" \
|
||||
--parent 32945650-a4e1-472b-90c9-5b38ef60a463
|
||||
|
||||
# Sin --socket/--session: auto-detecta el socket de la flota actual ($TMUX).
|
||||
# Forma preferida desde dentro de la flota — no hace falta saber el socket:
|
||||
./fn run spawn_fleet_agent --cwd "$HOME/fn_registry" \
|
||||
--prompt-file /tmp/orq_health.md --title "kanban-health"
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
@@ -62,9 +68,14 @@ Cuando el orquestador (o el launcher) necesita arrancar un Claude que debe vivir
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Auto-deteccion de socket/session**: si no pasas `--socket`/`--session`, se derivan de `$TMUX` via `detect_fleet_context`. Los explicitos tienen prioridad. Solo aborta (exit 2) si tras auto-detectar siguen vacios (de verdad no hay tmux). No dependas de `$FLEET_SOCKET`: a veces viene vacia en un claude resumido/relanzado aunque viva en la flota — `$TMUX` es la senal fiable.
|
||||
- El perfil (socket+session) debe estar **ya montado** (`launch_fleetclaude` primero); si la sesion no existe, falla con exit 1.
|
||||
- El `--role` se aplica en **background**: el `sessionId` del nuevo Claude no existe hasta que Claude escribe `~/.claude/sessions/<PID>.json` (unos segundos). `mark_claude_role` espera ese archivo. Si el arranque es muy lento, el role puede tardar en aparecer; es no-fatal (el agente simplemente no se pinea/identifica hasta entonces).
|
||||
- El `--parent` se aplica igual en **background** via `mark_claude_parent` (misma espera del `sessions/<PID>.json`). Cuando se pasan `--role` y `--parent` juntos se encadenan **secuencialmente** en el mismo subshell (primero role, luego parent) para que la segunda escritura lea el goal ya con la primera clave puesta — sin carrera de lectura-modificacion-escritura. Es no-fatal: si el sessions JSON no aparece a tiempo, el `parent_orchestrator` simplemente no se escribe.
|
||||
- `--skill` envia `/<name>` como primer prompt: depende de que Claude Code interprete el primer argumento como invocacion de slash command (verificado con `/orquestador`).
|
||||
- El nuevo Claude hereda `FLEET_SOCKET`/`FLEET_SESSION` del entorno del server tmux (que `launch_fleetclaude` fija con `set-environment`), asi apunta al perfil correcto.
|
||||
- `--dangerously-skip-permissions` siempre (los agentes de la flota trabajan desatendidos); riesgo asumido como en el resto del modo orquestador.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.2.0 (2026-06-21) — `--socket`/`--session` ahora son opcionales: se auto-detectan del contexto tmux (`$TMUX`) via `detect_fleet_context` cuando no se pasan. Elimina el gotcha de caer a kitty cuando `$FLEET_SOCKET` viene vacia pese a estar en la flota. Los valores explicitos siguen primando.
|
||||
|
||||
@@ -29,11 +29,15 @@ spawn_fleet_agent() {
|
||||
--title) shift; title="${1:-claude}" ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Uso: spawn_fleet_agent --socket <s> --session <s> --cwd <dir> [opciones]
|
||||
Uso: spawn_fleet_agent [--socket <s>] [--session <s>] [--cwd <dir>] [opciones]
|
||||
|
||||
Lanza un Claude como window nueva en la sesion tmux <session> del socket <socket>
|
||||
(un perfil FleetView ya montado). Imprime el window_id creado.
|
||||
|
||||
--socket/--session son OPCIONALES: si no se pasan, se auto-detectan del contexto
|
||||
tmux actual ($TMUX) via detect_fleet_context. Los valores explicitos tienen
|
||||
prioridad. Aborta solo si tras auto-detectar siguen vacios (no hay tmux).
|
||||
|
||||
Opciones:
|
||||
--prompt-file <f> Primer prompt del Claude = contenido del archivo (prompt
|
||||
autocontenido del ejecutor). El cat lo hace el shell del
|
||||
@@ -66,8 +70,25 @@ USAGE
|
||||
shift
|
||||
done
|
||||
|
||||
# Auto-detectar socket/session del contexto tmux ($TMUX) cuando no se pasan
|
||||
# explicitos. Los --socket/--session explicitos SIEMPRE tienen prioridad.
|
||||
# Esto evita el bug de caer a kitty cuando $FLEET_SOCKET viene vacia pese a
|
||||
# estar dentro de una window de la flota (ver detect_fleet_context).
|
||||
if [[ -z "$socket" || -z "$session" ]]; then
|
||||
local _detector ctx det_socket="" det_session=""
|
||||
_detector="$(dirname "${BASH_SOURCE[0]}")/detect_fleet_context.sh"
|
||||
if [[ -f "$_detector" ]]; then
|
||||
ctx="$(bash "$_detector" 2>/dev/null || true)"
|
||||
# Parseo minimo sin depender de jq: extraer "socket":"..." / "session":"...".
|
||||
det_socket="$(printf '%s' "$ctx" | sed -n 's/.*"socket":"\([^"]*\)".*/\1/p')"
|
||||
det_session="$(printf '%s' "$ctx" | sed -n 's/.*"session":"\([^"]*\)".*/\1/p')"
|
||||
[[ -z "$socket" ]] && socket="$det_socket"
|
||||
[[ -z "$session" ]] && session="$det_session"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -z "$socket" || -z "$session" ]] && {
|
||||
echo "spawn_fleet_agent: --socket y --session son obligatorios" >&2
|
||||
echo "spawn_fleet_agent: no se detecto contexto tmux (\$TMUX vacia) y no se pasaron --socket/--session. Lanza desde dentro de la flota o pasa el socket/session explicito." >&2
|
||||
return 2
|
||||
}
|
||||
[[ -z "$cwd" ]] && cwd="$PWD"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: supervise_fleetview_tui
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "supervise_fleetview_tui --bin <path> [--socket <s>] [--sentinel <path>] [--backoff <s>] [--min-uptime <s>] [--max-fast-exits <n>]"
|
||||
description: "Bucle supervisor que mantiene viva la TUI fleetview: lanza el binario y, si sale (crash, panic, kill de su proceso o pane), lo relanza tras un backoff, para que el panel de control de la flota NUNCA se pierda por un fallo puntual. Es la pieza que hace resiliente al pane izquierdo de la sesion tmux FleetView (lo invoca launch_fleetclaude). Dos valvulas de escape evitan el respawn infinito: un fichero centinela (touch <sentinel> => parada voluntaria al siguiente ciclo) y un crash-loop guard (si la TUI sale demasiado rapido muchas veces seguidas, el supervisor se rinde con rc=3 en vez de quemar CPU relanzando un binario roto)."
|
||||
tags: [fleet, claude-fleet, orchestration, fleetview, tui, supervisor, resilience, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
error_type: error_go_core
|
||||
file_path: "bash/functions/infra/supervise_fleetview_tui.sh"
|
||||
tested: true
|
||||
tests:
|
||||
- "golden: tras salir el binario, el supervisor lo relanza (respawn observable)"
|
||||
- "sentinel: tocar el fichero centinela para el bucle limpio (rc=0) y lo consume"
|
||||
- "crash-loop: salidas rapidas seguidas >= max_fast_exits hacen que se rinda (rc=3)"
|
||||
- "error: sin --bin rc=1; binario no ejecutable rc=1"
|
||||
test_file_path: "bash/functions/infra/supervise_fleetview_tui_test.sh"
|
||||
params:
|
||||
- name: --bin
|
||||
desc: "Ruta al binario fleetview a supervisar. Obligatorio. Si no es ejecutable, sale con rc=1 con instruccion de compilado."
|
||||
- name: --socket
|
||||
desc: "Socket del perfil FleetView. Solo fija el nombre del sentinel por defecto. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada."
|
||||
- name: --sentinel
|
||||
desc: "Ruta del fichero centinela de parada voluntaria. Si existe tras una salida de la TUI, se borra y el bucle termina. Default: $HOME/.claude/fleet/tui_stop_<socket>."
|
||||
- name: --backoff
|
||||
desc: "Segundos de espera antes de relanzar la TUI tras una salida. Default: 1."
|
||||
- name: --min-uptime
|
||||
desc: "Umbral en segundos para considerar una salida 'rapida' (sospecha de crash-loop). Un arranque que dura >= este valor resetea el contador. Default: 2."
|
||||
- name: --max-fast-exits
|
||||
desc: "Numero de salidas rapidas seguidas tras las que el supervisor se rinde (crash-loop guard) en vez de seguir relanzando. Default: 5."
|
||||
output: "No retorna valor; corre indefinidamente relanzando la TUI. Sale 0 ante parada voluntaria (sentinel), 1 ante uso incorrecto / binario no ejecutable, 3 cuando el crash-loop guard se rinde. Imprime una linea por cada relanzamiento o parada."
|
||||
---
|
||||
|
||||
# supervise_fleetview_tui
|
||||
|
||||
Bucle supervisor de la TUI `fleetview`. Corre el binario y, cada vez que sale (crash, panic, `kill` de su proceso, cierre de su pane), lo **relanza** tras un pequeño backoff. Hace que el panel de control de la flota — el pane izquierdo de la sesión tmux FleetView — **nunca se pierda** por un fallo puntual. `launch_fleetclaude` lo usa como comando del pane izquierdo en vez de un `exec fleetview` de una sola vida.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Como lo invoca el launcher en el pane izquierdo (relanza la TUI si muere):
|
||||
FLEET_SOCKET=fleet bash bash/functions/infra/supervise_fleetview_tui.sh \
|
||||
--bin apps/fleetview/fleetview --socket fleet
|
||||
|
||||
# Pararlo voluntariamente desde otra terminal: tocar el sentinel y dejar salir la TUI.
|
||||
touch ~/.claude/fleet/tui_stop_fleet
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Úsala como wrapper del binario `fleetview` siempre que quieras que la TUI sobreviva a un crash o a un `kill` accidental de su proceso/pane (p. ej. un `kill_fleet_agent` que cierre la window que la aloja). Es la mitad "auto-recuperación" del par de fixes que blindan FleetView; la otra mitad es el Guard 3 anti-TUI/console de `kill_fleet_agent` (la causa raíz). No la uses para supervisar Claudes (esos se relanzan con `claude --resume`, no en bucle ciego).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura y de larga duración**: corre indefinidamente. Está pensada para vivir en un pane tmux con TTY, no como systemd service (la TUI necesita PTY; el watcher de fleetview sí es systemd `Restart=always`).
|
||||
- **Crash-loop guard**: si la TUI sale en menos de `--min-uptime` segundos, `--max-fast-exits` veces seguidas, el supervisor se **rinde** (rc=3) en vez de relanzar para siempre un binario roto. Ajusta los umbrales si tu arranque es legítimamente lento.
|
||||
- **Sentinel = única parada voluntaria limpia**: `touch <sentinel>` y deja que la TUI salga; al siguiente ciclo el supervisor ve el fichero, lo borra y termina. Sin sentinel, **relanza siempre** (es el objetivo: que no se pierda). Un sentinel huérfano de una sesión previa se limpia al arrancar para no parar de inmediato.
|
||||
- **El sentinel por defecto depende del socket**: `~/.claude/fleet/tui_stop_<socket>`. Dos perfiles (`fleet`, `fleet2`) tienen sentinels distintos, así parar uno no para el otro.
|
||||
- **No supervisa Claudes**: su contrato es solo la TUI. Relanzar un Claude en bucle ciego perdería su sesión; los Claudes se recuperan con `claude --resume`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
(v1.0.0 — sin cambios todavía.)
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# supervise_fleetview_tui — bucle supervisor que mantiene viva la TUI fleetview.
|
||||
#
|
||||
# Lanza el binario fleetview y, si sale (crash, panic, kill de su proceso o de su
|
||||
# pane), lo relanza tras un pequeno backoff. Asi el panel de control de la flota
|
||||
# NUNCA se pierde por un fallo puntual: es la pieza que hace resiliente al pane
|
||||
# izquierdo de la sesion tmux FleetView (lo invoca launch_fleetclaude).
|
||||
#
|
||||
# Dos valvulas de escape para no hacer respawn infinito:
|
||||
# - Sentinel file: si tras una salida existe el fichero centinela, se borra y
|
||||
# el bucle termina (parada voluntaria solicitada por el usuario). El default
|
||||
# es $HOME/.claude/fleet/tui_stop_<socket>; pararla a mano: `touch <sentinel>`
|
||||
# y dejar que la TUI salga (o matar su proceso).
|
||||
# - Crash-loop guard: si la TUI sale demasiado rapido (uptime < min_uptime
|
||||
# segundos) muchas veces seguidas (>= max_fast_exits), el supervisor se rinde
|
||||
# y devuelve != 0, para no quemar CPU relanzando un binario roto en caliente.
|
||||
# Un arranque que dura >= min_uptime resetea el contador.
|
||||
#
|
||||
# Funcion IMPURA: lanza un proceso en bucle y lee/escribe un fichero centinela.
|
||||
#
|
||||
# Overrides de entorno (testabilidad, no para uso normal):
|
||||
# FLEET_SOCKET socket del perfil; fija el nombre del sentinel por defecto.
|
||||
set -euo pipefail
|
||||
IFS=$' \t\n'
|
||||
|
||||
supervise_fleetview_tui() {
|
||||
local bin="" socket="" sentinel="" backoff=1 min_uptime=2 max_fast_exits=5
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--bin) shift; bin="${1:-}" ;;
|
||||
--socket) shift; socket="${1:-}" ;;
|
||||
--sentinel) shift; sentinel="${1:-}" ;;
|
||||
--backoff) shift; backoff="${1:-1}" ;;
|
||||
--min-uptime) shift; min_uptime="${1:-2}" ;;
|
||||
--max-fast-exits) shift; max_fast_exits="${1:-5}" ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Uso: supervise_fleetview_tui --bin <path> [opciones]
|
||||
|
||||
Bucle supervisor: corre el binario fleetview y lo relanza si sale, para que el
|
||||
panel de la flota nunca se pierda por un crash/kill puntual.
|
||||
|
||||
Opciones:
|
||||
--bin <path> Ruta al binario fleetview (obligatorio).
|
||||
--socket <s> Socket del perfil FleetView. Default: $FLEET_SOCKET o "fleet".
|
||||
--sentinel <path> Fichero centinela de parada voluntaria.
|
||||
Default: $HOME/.claude/fleet/tui_stop_<socket>.
|
||||
--backoff <s> Segundos de espera antes de relanzar. Default: 1.
|
||||
--min-uptime <s> Umbral (s) para considerar una salida "rapida". Default: 2.
|
||||
--max-fast-exits <n> Salidas rapidas seguidas tras las que el supervisor se
|
||||
rinde (crash-loop guard). Default: 5.
|
||||
-h, --help Esta ayuda.
|
||||
|
||||
Parar el bucle a mano: `touch <sentinel>` y dejar que la TUI salga (o matar su
|
||||
proceso); en el siguiente ciclo el supervisor ve el sentinel, lo borra y termina.
|
||||
|
||||
Salida: 0 parada voluntaria (sentinel); 1 binario no ejecutable / uso incorrecto;
|
||||
3 el supervisor se rindio por crash-loop (demasiadas salidas rapidas seguidas).
|
||||
USAGE
|
||||
return 0 ;;
|
||||
--*)
|
||||
echo "supervise_fleetview_tui: opcion desconocida '$1' (usa -h)" >&2
|
||||
return 1 ;;
|
||||
*)
|
||||
if [[ -z "$bin" ]]; then
|
||||
bin="$1"
|
||||
else
|
||||
echo "supervise_fleetview_tui: argumento extra '$1' (bin ya es '$bin')" >&2
|
||||
return 1
|
||||
fi ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[[ -z "$bin" ]] && {
|
||||
echo "supervise_fleetview_tui: falta --bin <path> al binario fleetview. Usa -h." >&2
|
||||
return 1
|
||||
}
|
||||
[[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"
|
||||
[[ -z "$sentinel" ]] && sentinel="$HOME/.claude/fleet/tui_stop_${socket}"
|
||||
mkdir -p "$(dirname "$sentinel")" 2>/dev/null || true
|
||||
|
||||
if [[ ! -x "$bin" ]]; then
|
||||
echo "supervise_fleetview_tui: binario '$bin' no es ejecutable. Compila la TUI: cd apps/fleetview && go build -o fleetview ." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Limpiar un sentinel huerfano de una sesion anterior, para no parar al arrancar.
|
||||
[[ -f "$sentinel" ]] && rm -f "$sentinel" 2>/dev/null || true
|
||||
|
||||
local fast_exits=0
|
||||
while true; do
|
||||
local start end uptime code
|
||||
start=$(date +%s)
|
||||
set +e
|
||||
"$bin"
|
||||
code=$?
|
||||
set -e
|
||||
end=$(date +%s)
|
||||
uptime=$(( end - start ))
|
||||
|
||||
# Valvula 1 — parada voluntaria por sentinel.
|
||||
if [[ -f "$sentinel" ]]; then
|
||||
rm -f "$sentinel" 2>/dev/null || true
|
||||
echo "[fleetview: parada solicitada via sentinel ($sentinel) — fin del supervisor]"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Valvula 2 — crash-loop guard.
|
||||
if [[ "$uptime" -lt "$min_uptime" ]]; then
|
||||
fast_exits=$(( fast_exits + 1 ))
|
||||
else
|
||||
fast_exits=0
|
||||
fi
|
||||
if [[ "$fast_exits" -ge "$max_fast_exits" ]]; then
|
||||
echo "[fleetview: $fast_exits salidas rapidas seguidas (ultimo code=$code) — el supervisor se rinde para no hacer respawn infinito. Inspecciona el binario y relanza.]" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
echo "[fleetview salio (code=$code, uptime=${uptime}s) — relanzando en ${backoff}s. Para parar: touch $sentinel, o Ctrl-C.]"
|
||||
sleep "$backoff"
|
||||
done
|
||||
}
|
||||
|
||||
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
supervise_fleetview_tui "$@"
|
||||
fi
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para supervise_fleetview_tui. Usa un binario falso (un script) que cuenta
|
||||
# sus invocaciones, para verificar respawn, crash-loop guard y sentinel sin correr
|
||||
# la TUI real.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/supervise_fleetview_tui.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local test_name="$1" expected="$2" actual="$3"
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
echo "PASS: $test_name ($actual)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected '$expected', got '$actual'"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
COUNTER="$TMP/runs"
|
||||
SENTINEL="$TMP/sentinel"
|
||||
|
||||
# --- Test 1 (crash-loop guard): binario que sale rapido siempre se rinde a las N ---
|
||||
# Fake bin: registra una linea por invocacion y sale 1 inmediato.
|
||||
FAKE_FAST="$TMP/fake_fast.sh"
|
||||
cat > "$FAKE_FAST" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
echo run >> "$COUNTER"
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$FAKE_FAST"
|
||||
|
||||
: > "$COUNTER"
|
||||
set +e
|
||||
out=$(supervise_fleetview_tui --bin "$FAKE_FAST" --backoff 0 --min-uptime 100 \
|
||||
--max-fast-exits 3 --sentinel "$SENTINEL" 2>&1); rc=$?
|
||||
set -e
|
||||
runs=$(wc -l < "$COUNTER" | tr -d ' ')
|
||||
assert_eq "crash-loop: se rinde con rc=3" 3 "$rc"
|
||||
assert_eq "crash-loop: corrio exactamente 3 veces" 3 "$runs"
|
||||
assert_contains "crash-loop: mensaje de rendicion" "el supervisor se rinde" "$out"
|
||||
|
||||
# --- Test 2 (golden respawn + sentinel): relanza tras salir, para via sentinel ---
|
||||
# Fake bin: en la 2a invocacion crea el sentinel, luego sale. Prueba que:
|
||||
# (a) tras la 1a salida RELANZA (respawn) -> hay 2a invocacion (golden).
|
||||
# (b) al ver el sentinel, PARA (no hay 3a invocacion).
|
||||
FAKE_SENT="$TMP/fake_sent.sh"
|
||||
cat > "$FAKE_SENT" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
echo run >> "$COUNTER"
|
||||
n=\$(wc -l < "$COUNTER" | tr -d ' ')
|
||||
if [[ "\$n" -ge 2 ]]; then
|
||||
touch "$SENTINEL"
|
||||
fi
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$FAKE_SENT"
|
||||
|
||||
: > "$COUNTER"
|
||||
rm -f "$SENTINEL"
|
||||
set +e
|
||||
out=$(supervise_fleetview_tui --bin "$FAKE_SENT" --backoff 0 --min-uptime 0 \
|
||||
--max-fast-exits 99 --sentinel "$SENTINEL" 2>&1); rc=$?
|
||||
set -e
|
||||
runs=$(wc -l < "$COUNTER" | tr -d ' ')
|
||||
assert_eq "golden: relanzo tras morir (2 invocaciones)" 2 "$runs"
|
||||
assert_eq "sentinel: para limpio con rc=0" 0 "$rc"
|
||||
assert_contains "sentinel: mensaje de parada voluntaria" "parada solicitada via sentinel" "$out"
|
||||
assert_eq "sentinel: el fichero se consume (borrado)" "no" "$([[ -f "$SENTINEL" ]] && echo si || echo no)"
|
||||
assert_contains "golden: registra el respawn" "relanzando" "$out"
|
||||
|
||||
# --- Test 3 (error): falta --bin ---
|
||||
set +e
|
||||
out=$(supervise_fleetview_tui --backoff 0 2>&1); rc=$?
|
||||
set -e
|
||||
assert_eq "error: sin --bin devuelve rc=1" 1 "$rc"
|
||||
assert_contains "error: mensaje falta --bin" "falta --bin" "$out"
|
||||
|
||||
# --- Test 4 (error): binario no ejecutable ---
|
||||
set +e
|
||||
out=$(supervise_fleetview_tui --bin "$TMP/no_existe" 2>&1); rc=$?
|
||||
set -e
|
||||
assert_eq "error: binario no ejecutable rc=1" 1 "$rc"
|
||||
assert_contains "error: mensaje no ejecutable" "no es ejecutable" "$out"
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
+50
-34
@@ -18,6 +18,7 @@ type pyParam struct {
|
||||
Default string // empty if required
|
||||
IsKwargs bool // **kwargs
|
||||
IsRegistry bool // type is a registry type (needs factory)
|
||||
KwOnly bool // declared after a bare "*" or "*args" — must be passed by keyword
|
||||
}
|
||||
|
||||
// pyFactory links a registry type to the function that creates it.
|
||||
@@ -45,12 +46,21 @@ func parsePySignature(sig string) []pyParam {
|
||||
// Split by comma, respecting nested brackets
|
||||
parts := splitParams(raw)
|
||||
var params []pyParam
|
||||
kwOnly := false
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" || part == "self" || part == "cls" {
|
||||
continue
|
||||
}
|
||||
// A bare "*" (PEP 3102) or "*args" var-positional marks the start of
|
||||
// keyword-only params. Neither maps cleanly to positional CLI args, so
|
||||
// skip the marker itself and flag every following param as keyword-only.
|
||||
if part == "*" || (strings.HasPrefix(part, "*") && !strings.HasPrefix(part, "**")) {
|
||||
kwOnly = true
|
||||
continue
|
||||
}
|
||||
p := parseSingleParam(part)
|
||||
p.KwOnly = kwOnly
|
||||
params = append(params, p)
|
||||
}
|
||||
return params
|
||||
@@ -189,11 +199,19 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
|
||||
// Classify params
|
||||
var factoryImports []string // import lines for factories
|
||||
var factorySetup []string // code to create factory objects
|
||||
var argLines []string // code to parse CLI args
|
||||
var callArgs []string // arguments to pass to the function
|
||||
var bodyLines []string // code that fills _call_args / _call_kwargs
|
||||
|
||||
cliArgIdx := 0
|
||||
|
||||
// emitCall appends one param to _call_args (positional) or _call_kwargs
|
||||
// (keyword-only). indent prefixes the line (for params read inside an `if`).
|
||||
emitCall := func(p pyParam, indent string) string {
|
||||
if p.KwOnly {
|
||||
return fmt.Sprintf("%s_call_kwargs[%q] = %s", indent, p.Name, p.Name)
|
||||
}
|
||||
return fmt.Sprintf("%s_call_args.append(%s)", indent, p.Name)
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
if p.IsKwargs {
|
||||
// Skip **kwargs for now — can't auto-resolve from CLI
|
||||
@@ -235,27 +253,35 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
|
||||
fmt.Sprintf("%s = %s(%s)", p.Name, factory.FuncName,
|
||||
strings.Join(factoryArgs, ", ")))
|
||||
|
||||
callArgs = append(callArgs, p.Name)
|
||||
// Factory objects are always present (required).
|
||||
bodyLines = append(bodyLines, emitCall(p, ""))
|
||||
} else {
|
||||
// Primitive type — from CLI args
|
||||
// Primitive type — from CLI args.
|
||||
if p.Default != "" {
|
||||
// Optional param with default
|
||||
argLines = append(argLines,
|
||||
fmt.Sprintf("%s = _args[%d] if len(_args) > %d else %s",
|
||||
p.Name, cliArgIdx, cliArgIdx, convertDefault(p.Type, p.Default)))
|
||||
argLines = append(argLines,
|
||||
convertArg(p.Name, p.Type, true))
|
||||
// Optional: only pass when the CLI arg is present. When absent we
|
||||
// DON'T replicate the signature default (it may reference a module
|
||||
// constant that doesn't exist in this runner) — we simply omit the
|
||||
// argument so the function applies its own native default.
|
||||
bodyLines = append(bodyLines,
|
||||
fmt.Sprintf("if len(_args) > %d:", cliArgIdx))
|
||||
bodyLines = append(bodyLines,
|
||||
fmt.Sprintf(" %s = _args[%d]", p.Name, cliArgIdx))
|
||||
if conv := convertArg(p.Name, p.Type, true); conv != "" {
|
||||
bodyLines = append(bodyLines, " "+conv)
|
||||
}
|
||||
bodyLines = append(bodyLines, emitCall(p, " "))
|
||||
} else {
|
||||
// Required param
|
||||
argLines = append(argLines,
|
||||
// Required param.
|
||||
bodyLines = append(bodyLines,
|
||||
fmt.Sprintf("if len(_args) <= %d: sys.exit('error: missing required arg: %s (%s)')",
|
||||
cliArgIdx, p.Name, p.Type))
|
||||
argLines = append(argLines,
|
||||
bodyLines = append(bodyLines,
|
||||
fmt.Sprintf("%s = _args[%d]", p.Name, cliArgIdx))
|
||||
argLines = append(argLines,
|
||||
convertArg(p.Name, p.Type, false))
|
||||
if conv := convertArg(p.Name, p.Type, false); conv != "" {
|
||||
bodyLines = append(bodyLines, conv)
|
||||
}
|
||||
bodyLines = append(bodyLines, emitCall(p, ""))
|
||||
}
|
||||
callArgs = append(callArgs, p.Name)
|
||||
cliArgIdx++
|
||||
}
|
||||
}
|
||||
@@ -289,18 +315,18 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Arg parsing
|
||||
if len(argLines) > 0 {
|
||||
sb.WriteString("# --- parse CLI args ---\n")
|
||||
for _, line := range argLines {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
// Arg parsing — build the positional/keyword argument collections.
|
||||
sb.WriteString("# --- parse CLI args ---\n")
|
||||
sb.WriteString("_call_args = []\n")
|
||||
sb.WriteString("_call_kwargs = {}\n")
|
||||
for _, line := range bodyLines {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Call
|
||||
sb.WriteString("# --- execute ---\n")
|
||||
sb.WriteString(fmt.Sprintf("_result = %s(%s)\n", fn.Name, strings.Join(callArgs, ", ")))
|
||||
sb.WriteString(fmt.Sprintf("_result = %s(*_call_args, **_call_kwargs)\n", fn.Name))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Output
|
||||
@@ -365,16 +391,6 @@ func convertArg(name, typ string, _ bool) string {
|
||||
}
|
||||
}
|
||||
|
||||
// convertDefault ensures the default value is valid Python for the given type.
|
||||
func convertDefault(_, def string) string {
|
||||
// Most defaults from the signature are already valid Python
|
||||
// Just handle the None case for Optional types
|
||||
if def == "None" || def == "" {
|
||||
return "None"
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// pythonList creates a Python list literal from strings: ["a", "b", "c"]
|
||||
func pythonList(items []string) string {
|
||||
quoted := make([]string, len(items))
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"fn-registry/registry"
|
||||
)
|
||||
|
||||
// Signature with a bare "*" (PEP 3102) separating positional from keyword-only
|
||||
// params. This is the shape that used to make fn run emit "* = _args[3]".
|
||||
const kwOnlySig = "def add_event_dav(summary: str, start: str, end: str = '', *, location: str = '', all_day: bool = False) -> dict"
|
||||
|
||||
func TestParsePySignatureBareStarKeywordOnly(t *testing.T) {
|
||||
params := parsePySignature(kwOnlySig)
|
||||
|
||||
// The bare "*" marker must never surface as a real parameter.
|
||||
for _, p := range params {
|
||||
if p.Name == "*" {
|
||||
t.Fatalf("bare '*' leaked as a param: %+v", params)
|
||||
}
|
||||
}
|
||||
|
||||
want := map[string]bool{ // name -> expected KwOnly
|
||||
"summary": false,
|
||||
"start": false,
|
||||
"end": false,
|
||||
"location": true,
|
||||
"all_day": true,
|
||||
}
|
||||
if len(params) != len(want) {
|
||||
t.Fatalf("got %d params, want %d: %+v", len(params), len(want), params)
|
||||
}
|
||||
for _, p := range params {
|
||||
kw, ok := want[p.Name]
|
||||
if !ok {
|
||||
t.Errorf("unexpected param %q", p.Name)
|
||||
continue
|
||||
}
|
||||
if p.KwOnly != kw {
|
||||
t.Errorf("param %q KwOnly=%v, want %v", p.Name, p.KwOnly, kw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePyRunnerKeywordOnlyValid(t *testing.T) {
|
||||
fn := ®istry.Function{
|
||||
Name: "add_event_dav",
|
||||
Lang: "py",
|
||||
FilePath: "python/functions/pipelines/add_event_dav.py",
|
||||
Signature: kwOnlySig,
|
||||
}
|
||||
|
||||
// All params are primitive, so no factory lookup happens and db is unused.
|
||||
script, err := generatePyRunner(fn, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("generatePyRunner: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(script, "* = _args") {
|
||||
t.Fatalf("runner emitted invalid syntax '* = _args':\n%s", script)
|
||||
}
|
||||
|
||||
// The signature default DEFAULT_BASE_URL (a module constant) must NOT be
|
||||
// replicated into the runner — that NameErrors at runtime.
|
||||
if strings.Contains(script, "DEFAULT_BASE_URL") {
|
||||
t.Errorf("runner replicated non-literal default DEFAULT_BASE_URL:\n%s", script)
|
||||
}
|
||||
|
||||
// Required positionals are appended; keyword-only optionals go to kwargs.
|
||||
for _, want := range []string{
|
||||
"_call_args.append(summary)",
|
||||
"_call_args.append(start)",
|
||||
`_call_kwargs["location"] = location`,
|
||||
`_call_kwargs["all_day"] = all_day`,
|
||||
"_result = add_event_dav(*_call_args, **_call_kwargs)",
|
||||
} {
|
||||
if !strings.Contains(script, want) {
|
||||
t.Errorf("missing %q in generated runner:\n%s", want, script)
|
||||
}
|
||||
}
|
||||
|
||||
// The generated runner must itself be valid Python (compile, don't run).
|
||||
mustCompilePython(t, script)
|
||||
}
|
||||
|
||||
// mustCompilePython checks the script parses as valid Python via py_compile.
|
||||
func mustCompilePython(t *testing.T, script string) {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(t.TempDir(), "runner_*.py")
|
||||
if err != nil {
|
||||
t.Fatalf("temp file: %v", err)
|
||||
}
|
||||
if _, err := f.WriteString(script); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
py := pythonBinForTest()
|
||||
out, err := exec.Command(py, "-m", "py_compile", f.Name()).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("generated runner is not valid Python (%s): %v\n%s", py, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// pythonBinForTest prefers the project venv, falling back to python3 on PATH.
|
||||
func pythonBinForTest() string {
|
||||
for _, c := range []string{"../../python/.venv/bin/python3", "python3"} {
|
||||
if c == "python3" {
|
||||
return c
|
||||
}
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return "python3"
|
||||
}
|
||||
|
||||
// A "*args" var-positional marker must behave like the bare "*": skipped, and
|
||||
// everything after it treated as keyword-only.
|
||||
func TestParsePySignatureVarargsKeywordOnly(t *testing.T) {
|
||||
sig := "def f(a: str, *args, b: int = 0) -> dict"
|
||||
params := parsePySignature(sig)
|
||||
|
||||
for _, p := range params {
|
||||
if strings.HasPrefix(p.Name, "*") {
|
||||
t.Fatalf("'*args' marker leaked as a param: %+v", params)
|
||||
}
|
||||
}
|
||||
if len(params) != 2 {
|
||||
t.Fatalf("got %d params, want 2: %+v", len(params), params)
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, p := range params {
|
||||
got[p.Name] = p.KwOnly
|
||||
}
|
||||
if got["a"] != false || got["b"] != true {
|
||||
t.Errorf("KwOnly mismatch: a=%v (want false), b=%v (want true)", got["a"], got["b"])
|
||||
}
|
||||
}
|
||||
Submodule cpp/apps/chart_demo deleted from 026f514bb7
Submodule cpp/apps/shaders_lab deleted from ab38127ac0
@@ -114,6 +114,24 @@ static LRESULT CALLBACK fn_subclass_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPAR
|
||||
case WM_EXITSIZEMOVE:
|
||||
g_in_sizemove.store(false, std::memory_order_release);
|
||||
break;
|
||||
case WM_SYSKEYDOWN:
|
||||
// Alt+Enter would otherwise toggle a borderless-fullscreen mode
|
||||
// (driven by some GPU drivers' OpenGL/Vulkan hotkey, or by
|
||||
// DefWindowProc on certain window styles). We never want that:
|
||||
// these are docked tool windows, not games. Consume the keystroke
|
||||
// so the window stays in its normal decorated state. Every other
|
||||
// Alt+key combo chains through to GLFW/DefWindowProc untouched.
|
||||
if (wp == VK_RETURN) {
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_SYSCHAR:
|
||||
// Swallow the system "ding" beep that the suppressed Alt+Enter
|
||||
// above would otherwise trigger via the default char handler.
|
||||
if (wp == VK_RETURN) {
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_LBUTTONDOWN:
|
||||
// Alt + LMB anywhere on the window initiates a native modal MOVE
|
||||
// via WM_SYSCOMMAND, SC_MOVE | HTCAPTION. Same pattern as our
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "engine_init() -> Engine; engine_shutdown(Engine&); engine_set_volume(Engine&, float)"
|
||||
description: "Lifecycle del engine de audio basado en miniaudio (single-header, public domain). Inicializa device default, expone master volume, y libera recursos. Cross-platform: Windows/Linux/macOS via WASAPI/ALSA/CoreAudio y WebAudio bajo emscripten. Issue 0072b — runtime gamedev nucleo. Esta TU es la unica del proyecto que define MINIAUDIO_IMPLEMENTATION."
|
||||
tags: [gamedev, audio, miniaudio]
|
||||
tags: [gamedev-engine, audio, miniaudio]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "sound_load(Engine&, const char*) -> Sound; sound_play/stop/set_volume/destroy(Sound&); play_sound_oneshot(Engine&, const char*, float)"
|
||||
description: "Reproduccion de audio sobre fn::audio::Engine: carga sonidos con streaming desde disco (wav/mp3/flac/ogg), play/stop/volumen por sonido, y helper fire-and-forget para one-shots sin handle. Cross-platform via miniaudio. Issue 0072b — runtime gamedev nucleo."
|
||||
tags: [gamedev, audio, miniaudio]
|
||||
tags: [gamedev-engine, audio, miniaudio]
|
||||
uses_functions: ["audio_engine_cpp_gamedev"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: pure
|
||||
signature: "world_to_screen(Camera2D, Vec2) -> Vec2; screen_to_world(Camera2D, Vec2) -> Vec2; visible_world_rect(Camera2D) -> Rect; view_proj_matrix(Camera2D, float[16])"
|
||||
description: "Camara ortografica 2D pura: pos (centro), zoom, rotacion (rad) y viewport en pixeles. Conversiones world<->screen, AABB visible y matriz view-projection 4x4 column-major lista para cualquier renderer (sokol_gfx, OpenGL, WebGPU). Fast-path sin trig si rotation==0. Issue 0072b."
|
||||
tags: [gamedev, camera, 2d, math, pure]
|
||||
tags: [gamedev-engine, camera, 2d, math, pure]
|
||||
uses_functions: []
|
||||
uses_types: ["Vec2_cpp_core", "Rect_cpp_core"]
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "loop_run(SDL_Window*, const LoopCfg&) -> void"
|
||||
description: "Game loop fixed-timestep estilo Glenn Fiedler ('Fix Your Timestep'). Desacopla simulacion (on_fixed_update con dt fijo) de renderizado (on_render con factor de interpolacion). Acumulador con cap anti spiral-of-death. Branch automatico desktop (while loop bloqueante) vs __EMSCRIPTEN__ (emscripten_set_main_loop). Issue 0072b."
|
||||
tags: [gamedev, game-loop, sdl3, wasm, fixed-timestep]
|
||||
tags: [gamedev-engine, game-loop, sdl3, wasm, fixed-timestep]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "input_begin_frame(InputState&); input_process_event(InputState&, const SDL_Event*)"
|
||||
description: "Snapshot unificado de input por frame para SDL3. Mapea keyboard (WASD+arrows), mouse, gamepad (SDL_Gamepad) y touch a botones logicos (left/right/up/down/action_a..y/start/back) y ejes analogicos. Expone flags *_pressed con rising edge limpio cada frame. Issue 0072b — runtime gamedev PC + WASM."
|
||||
tags: [gamedev, input, sdl3, touch, gamepad]
|
||||
tags: [gamedev-engine, input, sdl3, touch, gamepad]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: pure
|
||||
signature: "make_environment() -> sg_environment; make_swapchain(int w, int h) -> sg_swapchain"
|
||||
description: "Builders puros para inicializar sokol_gfx encima de un GL context creado por SDL3 (no por sokol_app). Construye sg_environment con defaults RGBA8 + depth/stencil y sg_swapchain con el default framebuffer del contexto activo. Issue 0072b — base del runtime gamedev en PC + WASM."
|
||||
tags: [gamedev, sokol, gfx, sdl3, wasm]
|
||||
tags: [gamedev-engine, sokol, gfx, sdl3, wasm]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "sprite_batch_create(int cap=4096) -> SpriteBatch; sprite_batch_begin/draw/end"
|
||||
description: "Batched textured quad renderer sobre sokol_gfx. Begin/draw/end con auto-flush por atlas change o capacity full. Vertex layout pos+uv+color, alpha blending estandar, GLSL 330 / GLES 300. Issue 0072b runtime gamedev — base de plataformeros, top-down, UI sprites."
|
||||
tags: [gamedev, gfx, sokol, sprite, batch, 2d]
|
||||
tags: [gamedev-engine, gfx, sokol, sprite, batch, 2d]
|
||||
uses_functions:
|
||||
- sokol_setup_cpp_gfx
|
||||
uses_types:
|
||||
|
||||
@@ -11,7 +11,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0051 — Funciones pendientes del pipeline de extraccion (NER+RE+OpenIE)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0054 — deploy_server: refactor registry-first (SSH/systemd/rsync/health/docker-compose)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0055 — docker_tui: refactor para usar funciones docker_* del registry
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0056 — audit_uses_functions: detectar imports Python anidados (`from pkg.subpkg import X`)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0057 — audit_uses_functions: mejorar deteccion de simbolos Go con abreviaturas
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0060 — `fn doctor secrets`: scan de secrets en TODOS los repos
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0061 — Integrar `notify_telegram` en deploy_server + bucle reactivo
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: alta
|
||||
depends:
|
||||
- "0071f"
|
||||
depends: ["0071f"]
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
|
||||
@@ -7,8 +7,7 @@ domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: media
|
||||
depends:
|
||||
- "0071f"
|
||||
depends: ["0071f"]
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-10
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
@@ -12,7 +12,7 @@ blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0100 — Migrar frontmatter inline a YAML canonico en dev/issues/
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ related:
|
||||
- "0103"
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [slash-command, dispatch, type-aware]
|
||||
tags: [slash-command, dispatch, type-aware, ausente-ready]
|
||||
---
|
||||
|
||||
# 0104 — `/fix-issue` type-aware dispatch
|
||||
|
||||
@@ -16,7 +16,7 @@ related:
|
||||
- "0107"
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [modules, versioning, codegen, fail-loud]
|
||||
tags: [modules, versioning, codegen, fail-loud, ausente-ready]
|
||||
---
|
||||
|
||||
# 0107e — Version pinning + codegen fail-loud
|
||||
|
||||
@@ -15,12 +15,7 @@ related:
|
||||
- "0109"
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags:
|
||||
- skill-tree
|
||||
- cpp
|
||||
- imgui
|
||||
- dashboard
|
||||
- gamification
|
||||
tags: [ausente-ready, skill-tree, cpp, imgui, dashboard, gamification]
|
||||
---
|
||||
|
||||
# 0109k — Dashboard panel
|
||||
|
||||
@@ -16,13 +16,7 @@ related:
|
||||
- "0106"
|
||||
created: 2026-05-18
|
||||
updated: 2026-05-18
|
||||
tags:
|
||||
- service
|
||||
- go
|
||||
- http
|
||||
- issues
|
||||
- flows
|
||||
- api
|
||||
tags: [ausente-ready, service, go, http, issues, flows, api]
|
||||
---
|
||||
|
||||
# 0109m — issues_api service
|
||||
|
||||
@@ -16,7 +16,7 @@ related:
|
||||
- "0068"
|
||||
created: 2026-05-18
|
||||
updated: 2026-05-19
|
||||
tags: [e2e_checks, recopilador, batch, coverage, epic]
|
||||
tags: [e2e_checks, recopilador, batch, coverage, epic, ausente-ready]
|
||||
---
|
||||
|
||||
# Sub-issues
|
||||
|
||||
@@ -16,7 +16,7 @@ related:
|
||||
- "0068"
|
||||
created: 2026-05-19
|
||||
updated: 2026-05-19
|
||||
tags: [e2e_checks, recopilador, batch, design]
|
||||
tags: [e2e_checks, recopilador, batch, design, ausente-ready]
|
||||
---
|
||||
|
||||
# 0121a — Design-e2e batch
|
||||
|
||||
@@ -7,9 +7,7 @@ domain:
|
||||
- registry-quality
|
||||
scope: registry
|
||||
priority: media
|
||||
depends:
|
||||
- "0121a"
|
||||
- "0121b"
|
||||
depends: ["0121a"]
|
||||
blocks:
|
||||
- "0122"
|
||||
related:
|
||||
|
||||
@@ -17,7 +17,7 @@ related:
|
||||
- "0086"
|
||||
created: 2026-05-18
|
||||
updated: 2026-05-18
|
||||
tags: [revisor, mejorador, proposals, auto-apply, autonomous]
|
||||
tags: [revisor, mejorador, proposals, auto-apply, autonomous, ausente-ready]
|
||||
---
|
||||
|
||||
# 0122 — fn-revisor + ampliar filtro auto-aplicable del orquestador
|
||||
|
||||
@@ -13,7 +13,7 @@ related:
|
||||
- "0121a"
|
||||
created: 2026-05-19
|
||||
updated: 2026-05-19
|
||||
tags: [dag_engine, cleanup, technical-debt]
|
||||
tags: [dag_engine, cleanup, technical-debt, ausente-ready]
|
||||
---
|
||||
|
||||
# 0124 — dag_engine cleanup
|
||||
|
||||
@@ -13,7 +13,7 @@ related:
|
||||
- "0121a"
|
||||
created: 2026-05-19
|
||||
updated: 2026-05-19
|
||||
tags: [deploy_server, cli, idempotency]
|
||||
tags: [deploy_server, cli, idempotency, ausente-ready]
|
||||
---
|
||||
|
||||
# 0125 — deploy_server `--db` flag
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "0128"
|
||||
title: "kanban: adjuntar archivos (drag&drop desc/chat + tab Archivos)"
|
||||
status: in_progress
|
||||
status: in-progress
|
||||
type: feature
|
||||
domain:
|
||||
- apps-tools
|
||||
|
||||
@@ -13,12 +13,7 @@ blocks:
|
||||
- 0130b
|
||||
related:
|
||||
- "0130"
|
||||
tags:
|
||||
- registry
|
||||
- go
|
||||
- parser
|
||||
- frontmatter
|
||||
- fsnotify
|
||||
tags: [registry, go, parser, frontmatter, fsnotify, ausente-ready]
|
||||
flow: "0130"
|
||||
created: "2026-05-22"
|
||||
updated: "2026-05-22"
|
||||
|
||||
@@ -8,8 +8,7 @@ domain:
|
||||
- dev-ux
|
||||
scope: app-scoped
|
||||
priority: alta
|
||||
depends:
|
||||
- "0130a"
|
||||
depends: ["0130a"]
|
||||
blocks:
|
||||
- "0130c"
|
||||
related:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
id: "0134"
|
||||
title: "Mesh protocol spec: capability manifests, ed25519 envelopes, enrollment, audit chain"
|
||||
status: pending
|
||||
status: pendiente
|
||||
type: spec
|
||||
domain:
|
||||
- infra
|
||||
- cybersecurity
|
||||
- protocols
|
||||
scope: cross-app
|
||||
priority: high
|
||||
priority: alta
|
||||
depends: []
|
||||
blocks:
|
||||
- "0135"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "0144"
|
||||
title: "Agent LLM per machine (user + sudo) con tool registry y mesh dispatch"
|
||||
status: pending
|
||||
status: pendiente
|
||||
type: spec
|
||||
domain:
|
||||
- agents
|
||||
@@ -9,7 +9,7 @@ domain:
|
||||
- infra
|
||||
- cybersecurity
|
||||
scope: multi-app
|
||||
priority: high
|
||||
priority: alta
|
||||
depends:
|
||||
- "0134"
|
||||
- "0140"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0146"
|
||||
title: "add-pc one-shot: añade PC al mesh + agente LLM en <2min desde movil"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0009"]
|
||||
related_issues: ["0134", "0144", "0145"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0147"
|
||||
title: "matrix-client-pc scaffold: Wails + React+Mantine + login MAS"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0148", "0162"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0148"
|
||||
title: "matrix-client-pc rooms list + timeline con sync incremental"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0147", "0149"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0149"
|
||||
title: "matrix-client-pc composer: markdown, reply, edit, reactions, media"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0148", "0150"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0150"
|
||||
title: "matrix-client-pc E2EE: cross-signing, SAS verification, recovery"
|
||||
status: pending
|
||||
priority: critical
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0149", "0151"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0151"
|
||||
title: "matrix-client-pc calls LiveKit: 1:1 + grupales, mic/cam/screen"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0150", "0152"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0152"
|
||||
title: "matrix-client-pc mini-webapps embebidas: Matrix Widget API v2"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010"]
|
||||
related_issues: ["0151", "0153"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0154"
|
||||
title: "matrix-client-android scaffold: Kotlin + Compose + login MAS"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0155", "0162"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0155"
|
||||
title: "matrix-client-android rooms list + timeline Compose"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0154", "0156"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0156"
|
||||
title: "matrix-client-android composer: markdown, replies, edits, reactions, media"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0155", "0157"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0157"
|
||||
title: "matrix-client-android E2EE rust-sdk: cross-signing, SAS, recovery"
|
||||
status: pending
|
||||
priority: critical
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0156", "0158"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0158"
|
||||
title: "matrix-client-android calls LiveKit nativo: mic/cam/screen + PiP"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0157", "0159", "0161"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0159"
|
||||
title: "matrix-client-android push FCM via sygnal + Firebase setup"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0158", "0160"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0160"
|
||||
title: "matrix-client-android mini-webapps: WebView + Widget API v2 bridge"
|
||||
status: pending
|
||||
priority: medium
|
||||
status: pendiente
|
||||
priority: media
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0159", "0161"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0161"
|
||||
title: "matrix-client-android foreground service: calls + lifecycle + lockscreen"
|
||||
status: pending
|
||||
priority: high
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0011"]
|
||||
related_issues: ["0158", "0160"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0162"
|
||||
title: "Matrix: migrar Synapse a MAS como unico auth provider (MSC3861)"
|
||||
status: pending
|
||||
priority: critical
|
||||
status: pendiente
|
||||
priority: alta
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010", "0011"]
|
||||
related_issues: ["0147", "0154", "0163"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "0163"
|
||||
title: "Matrix admin panel propio: users, rooms, devices, sessions (sustituye synapse-admin)"
|
||||
status: pending
|
||||
priority: medium
|
||||
status: pendiente
|
||||
priority: media
|
||||
created: 2026-05-24
|
||||
related_flows: ["0010", "0011"]
|
||||
related_issues: ["0162", "0147"]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: "0179"
|
||||
title: "dev_console: escaneo recursivo de dev/issues/ (subcarpetas por dominio)"
|
||||
status: in-progress
|
||||
type: bugfix
|
||||
domain:
|
||||
- meta
|
||||
scope: app-scoped
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-06-30
|
||||
updated: 2026-06-30
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0179 — dev_console: escaneo recursivo de dev/issues/
|
||||
|
||||
## Contexto
|
||||
|
||||
Los issues activos se reorganizaron en subcarpetas por dominio dentro de `dev/issues/` (`kanban/`, `trading/`, `gamedev/`, `cpp/`, `matrix/`, `imagegen/`) para descongestionar el listado plano. El skill `/issue` ya se actualizó a glob recursivo (`dev/issues/**/*.md`, excluyendo `completed/`). Falta alinear el binario `dev_console`, que carga los issues con `LoadAllIssues(root)` / `LoadOpenIssues(root)` en `apps/dev_console/` y hoy no recorre subcarpetas — por lo que no ve los 49 issues movidos.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Que `dev_console issue list/board/work` y los flujos que dependen de `LoadAllIssues`/`LoadOpenIssues` recorran `dev/issues/` de forma recursiva, excluyendo `dev/issues/completed/`, manteniendo el resto del comportamiento idéntico.
|
||||
|
||||
## Tareas
|
||||
|
||||
- [ ] Localizar la implementación de `LoadAllIssues` / `LoadOpenIssues` en `apps/dev_console/` (probable `parser.go` o equivalente).
|
||||
- [ ] Cambiar el escaneo a `filepath.WalkDir` (o glob recursivo) bajo `dev/issues/`, saltando el directorio `completed/`.
|
||||
- [ ] Mantener el orden de salida estable (ordenar por `id`).
|
||||
- [ ] Recompilar el binario en el sub-repo de `dev_console` siguiendo TBD (`issue/0179-...`).
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: lista incluye subcarpetas | e2e | `./apps/dev_console/dev_console issue list` | Aparecen issues de `cpp/`, `kanban/`, `trading/`, etc. (>= 49 que antes faltaban) |
|
||||
| Edge: excluye completed/ | e2e | `dev_console issue list` | Ningún issue con `status: completado` de `completed/` aparece en el listado activo |
|
||||
| Edge: conteo total coincide con /issue | e2e | comparar conteo con el glob recursivo de `/issue` | Mismo total de activos |
|
||||
| Error: dev/issues vacío o ausente | unit | run en dir sin `dev/issues/` | Error claro, no panic |
|
||||
|
||||
## Notas
|
||||
|
||||
Hermano del cambio ya hecho en `.claude/commands/issue.md` (glob `**/*.md`). Hasta cerrar este issue, usar `/issue` (no `dev_console`) para vistas completas del backlog.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: "0180"
|
||||
title: "Modo ausente sobre la cola de issues: parametrizar /ausente + DAG dag_engine + validación"
|
||||
status: pendiente
|
||||
type: infra
|
||||
domain:
|
||||
- meta
|
||||
scope: multi-app
|
||||
priority: alta
|
||||
depends: ["0179"]
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-06-30
|
||||
updated: 2026-06-30
|
||||
tags: []
|
||||
---
|
||||
# 0180 — Modo ausente sobre la cola de issues (parametrizar /ausente + DAG + validación)
|
||||
|
||||
## Contexto
|
||||
|
||||
Modelo de colaboración acordado (ver memoria `modelo-colaboracion-ausente`): durante la jornada de oficina (L–J 10–14 / 15–19, V 10–16) y la noche (01–09), Claude trabaja en `/ausente` la cola de issues `ausente-ready` (39 issues hoy), sin supervisión. La curación del backlog ya está hecha (triage, taxonomía, deps de series formalizadas, tag `ausente-ready`).
|
||||
|
||||
Faltan 3 piezas para automatizarlo de forma segura.
|
||||
|
||||
## Problemas a resolver
|
||||
|
||||
1. **`/ausente` está acoplado al roadmap ComfyUI.** El skill (`.claude/commands/ausente.md`) hardcodea su backlog a funciones ComfyUI (secciones "Configuración" y "Backlog del roadmap ComfyUI"). Hay que **parametrizar la fuente de tareas** para que pueda tomar la cola de issues: la siguiente tarea = primer issue de `/issue list -t ausente-ready` cuyas `depends` estén todas en `completed/`, re-cruzando deps en cada ciclo (un issue se libera cuando su dep se cierra).
|
||||
2. **Lanzamiento headless desde dag_engine.** `dag_engine` ejecuta steps (command/script/function), no abre una sesión Claude interactiva. Hay que resolver cómo un step arranca una sesión `role=orchestrator` en modo `/ausente` (candidatos: `launch_claude_agent_kitty_bash_infra` con DISPLAY, o `spawn_fleet_agent_bash_infra` si hay sesión tmux fleet) con el prompt autónomo + presupuesto.
|
||||
3. **Presupuesto conservador aplicado.** Tope: 1–2 ejecutores concurrentes, solo issues S/M, ~1M tokens por franja, parada al llegar. Materializar el tope de tokens (hoy `orchestration.md` solo fija fan-out=6).
|
||||
|
||||
## Schedule objetivo (cuando se active)
|
||||
|
||||
- Inicio de franjas de oficina: `0 10 * * 1-5` (10:00 L–V) y `0 15 * * 1-4` (15:00 L–J, tras comida).
|
||||
- Nocturno: `0 1 * * *` (01:00 diario).
|
||||
- El modo, una vez lanzado, itera con `ScheduleWakeup` hasta que el humano vuelve (para al recibir prompt humano).
|
||||
|
||||
Borrador del DAG: `apps/dag_engine/dags/ausente-issues-queue.yaml` (creado como DRAFT sin schedule activo).
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: corrida manual | e2e | lanzar `/ausente` con backlog=issues sobre 1 issue S de la cola | Coge el issue, lo implementa en worktree/sub-repo aislado, cierra DoD verde (golden+edge+error), push, bitácora actualizada |
|
||||
| Edge: dep no satisfecha | e2e | cola con un issue cuya `depends` sigue activa | NO lo coge; pasa al siguiente arrancable |
|
||||
| Edge: flota llena | e2e | 2 ejecutores activos (tope conservador) | Encola el resto, no lanza el 3.º |
|
||||
| Error: presupuesto agotado | e2e | tope de tokens alcanzado | Para limpio, deja bitácora con lo pendiente, no deja agentes huérfanos |
|
||||
| Vida útil | observabilidad | tras activar cron, 1 semana | Issues cerrados/semana > 0, 0 merges rotos a master, bitácora legible |
|
||||
|
||||
## Plan
|
||||
|
||||
1. Cerrar `0179` (dev_console recursivo) — dependencia.
|
||||
2. Parametrizar `/ausente` (fuente de backlog = issues ausente-ready | roadmap; pasar la fuente al invocar).
|
||||
3. Resolver el step de lanzamiento headless + presupuesto de tokens.
|
||||
4. **Validación manual** (golden + edges) antes de activar el cron.
|
||||
5. Activar schedule en el DAG + `systemctl --user restart dag_engine.service` con `--scheduler`.
|
||||
|
||||
## Notas
|
||||
|
||||
Este issue NO es `ausente-ready` a propósito: requiere decisiones de diseño humanas (mecanismo de lanzamiento, forma del presupuesto) y toca el propio sistema que orquesta el modo ausente. Se hace JUNTOS, no desatendido.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "0059"
|
||||
title: "Resolver doble tracking de `apps/*/app.md` (fn_registry + sub-repo)"
|
||||
status: pendiente
|
||||
status: completado
|
||||
type: infra
|
||||
domain:
|
||||
- registry-quality
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "55"
|
||||
title: "Roadmap de prereqs — issues de osint_graph que odr_console necesita antes/durante MVP"
|
||||
status: pendiente
|
||||
status: deferred
|
||||
type: epic
|
||||
domain:
|
||||
- osint
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "0087"
|
||||
title: "Capability Discovery Acceleration"
|
||||
status: pendiente
|
||||
status: completado
|
||||
type: feature
|
||||
domain:
|
||||
- meta
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user