Compare commits

...

266 Commits

Author SHA1 Message Date
egutierrez 9c1b7dd0f3 feat(papers): render_paper_pdf (Markdown IMRaD → PDF) + agente paper-reviewer
Subsistema papers/: pieza de entrega + control de calidad.

- render_paper_pdf_py_datascience (Python, impure, dominio datascience, grupo
  `papers`): convierte papers/<slug>/paper.md (frontmatter YAML + cuerpo IMRaD)
  en papers/<slug>/out/paper.pdf. Reutiliza el motor de paginación de flujo del
  paquete automatic_eda (matplotlib PdfPages, el mismo PDF móvil A5 de los
  informes EDA) — no reimplementa paginación ni toca matplotlib, y no añade
  dependencias. Cada sección IMRaD (# H1) → un Chapter en página nueva; portada
  desde el frontmatter (title/authors/date europea/abstract); detecta las
  imágenes Markdown ![alt](src) que el motor no entiende y las parte en bloques
  Image resueltos contra base_dir y base_dir/figures/. dict-no-throw estricto.
  5 tests verdes (golden + edges: sin frontmatter, path inexistente, figura
  inexistente, ruta directa al .md).

- .claude/agents/paper-reviewer: revisor académico adversarial read-only (gate
  anti paper-mill). Puntúa novedad/rigor/reproducibilidad/validez (0-5), intenta
  refutar cada claim contra la evidencia citada, detecta HARKing contra el
  preregistration.md, exige limitaciones declaradas y claims ≤ evidencia, y
  emite veredicto estructurado JSON (accept|major_revision|reject) con default
  conservador. Tools: Read, Grep, Glob, Bash (sin Edit/Write: solo juzga).

Diseño completo: reports/0001-2026-06-30-papers-system-design.md (agente C).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:39:59 +02:00
egutierrez a1e2e3567c merge: 4c cat_distr una hoja por columna (PDF+PPTX 1:1) + sin descripcion entropia redundante + page_break motor (verificado met) 2026-06-30 19:53:57 +02:00
egutierrez 833597c831 fix(eda): cat_distr PPTX — columnas de alta cardinalidad caben en UN slide con su gráfico
La verificación adversarial detectó que, en PPTX (slide 16:9, corto), las columnas
categóricas de ALTA cardinalidad NO id-like (Ticket, Cabin) ocupaban 3 slides cada
una con el donut SEPARADO de su tabla: el top-k de 8 filas largas no cabía junto al
donut y el keep-together partía la columna. (El PDF, en A5, ya estaba 1:1 correcto.)

Arreglo SOLO en render_pptx_impl.py:

- `_fit_group_blocks` (nuevo): para un Group con figura + DataTable que no cabe en el
  slide, reserva un alto mínimo para el donut (`_GROUP_MIN_FIG_H`) y recorta las filas
  de la DataTable a lo que queda, de modo que el gráfico se queda en el MISMO slide,
  junto a su tabla. No-op cuando ya cabe o no hay par figura+tabla (p.ej. columnas
  id-like, que ya omiten la top-k).
- `_trim_data_table_to_budget` (nuevo): devuelve una COPIA de la DataTable con las
  filas que caben (al menos una) + nota honesta "top N de M categorías mostradas
  (recortado para caber en el slide; el PDF muestra más)". NUNCA muta el bloque
  original, que es compartido con el renderer PDF (el PDF sigue mostrando la tabla
  completa en A5).
- `_place_group`: aplica `_fit_group_blocks` antes de `_shrink_group_figures`.

Refuerzo de cat_distr_test.py:

- `test_golden_pptx_una_slide_por_columna_con_su_grafico`: perfil con una columna
  categórica de alta cardinalidad no-id-like (40 valores largos sobre 5000 filas,
  0.8% distinto) que reproduce el caso Ticket/Cabin. Asierta que CADA columna
  categórica aparece en EXACTAMENTE UN slide del capítulo y que ese mismo slide lleva
  su tabla (Cardinalidad/distintos) Y su donut (caption + shape Picture) — el gráfico
  nunca se separa de su tabla. Sustituye al laxo `n_slides >= 2`.

Verificado con titanic_train.csv (render_automatic_eda run_models=True): 5 columnas
categóricas (Name, Sex, Ticket, Cabin, Embarked); PDF 6 páginas y PPTX 6 slides del
capítulo (intro + 1 por columna), cada columna con su donut junto a su tabla en una
sola página/slide. Ticket y Cabin pasaron de 3 slides a 1. Suite verde (122 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:45:09 +02:00
egutierrez 7158be8142 feat(eda): cat_distr una hoja por columna (gráfico incluido) + sin descripción redundante con glosario
Cada columna categórica del capítulo CAT DISTR ocupa ahora su propia página
(PDF) / slide (PPTX) con su gráfico junto a su tabla, y se elimina la
explicación larga de la entropía que duplicaba el capítulo GLOSARIO.

Cambios:

- model.Group: nuevo campo aditivo `page_break_before` (default False). Cuando
  es True el renderer fuerza al grupo a empezar en página/slide nueva (salvo que
  la actual esté vacía). Comportamiento de todos los capítulos existentes
  intacto. Soportado también en el normalizador dict-defensivo `as_block`.

- render_pdf_impl / render_pptx_impl `_place_group`: respetan `page_break_before`.

- render_pdf_impl / render_pptx_impl `_measure_block`: medición fiel de KVTable y
  DataTable (replica `_place_*`: título-heading, wrap del valor/celdas por
  columna, nota). La estimación previa asumía una línea por fila e ignoraba el
  título, así que el keep-together infra-presupuestaba la figura y el gráfico se
  desbordaba a la página siguiente. Helpers `_measure_kv_table`/`_measure_data_table`.

- render_pptx_impl `_shrink_group_figures`: umbrales más bajos (budget>0.6,
  per>0.35) para que en el slide corto 16:9 la figura se encoja y conviva con la
  tabla en lugar de partir la columna (misma filosofía keep-together del PDF).

- cat_distr.py:
  - build envuelve cada columna en un `Group(page_break_before=idx>0)`: una
    columna por página/slide, con su tabla de cardinalidad, su top-k y su donut
    juntos. La primera comparte página con la intro para no dejar una casi vacía.
  - intro recortada: se elimina el párrafo que explicaba qué es la entropía
    (vive en el capítulo GLOSARIO, donde el término `[[term:entropia]]` enlaza);
    se conserva el término clicable y el total de filas de referencia.
  - `_cardinality_block`: métricas relacionadas agrupadas por fila (distintos·%·
    únicos; entropía bits·máx·norm; desbalance·longitud) sin perder ningún dato,
    para que tabla + gráfico quepan en el slide 16:9.
  - columnas id-like (≈100% distintas): se omite la top-k (sería una lista de
    valores únicos; la nota lo explica) y el donut ocupa ese hueco.
  - CHAPTER_VERSION 1.1.0 -> 1.2.0.

Verificado con titanic (render_automatic_eda run_models=True): PDF 5 páginas y
PPTX 5 slides del capítulo (intro + 1 por columna: Name, Sex, Ticket, Embarked),
cada columna con su gráfico junto a su tabla, sin cortes. Suite verde
(121 passed): pytest automatic_eda/ + render_automatic_eda_test.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:26:33 +02:00
egutierrez 9be84a48ea merge: 4c quitar definiciones redundantes con glosario en calidad/correlacion/modelos/agregacion/relaciones (links intactos, verificado met) 2026-06-30 19:24:22 +02:00
egutierrez fd63261444 refactor(eda): quitar definiciones inline redundantes con el glosario en 5 capítulos
Ahora que el AutomaticEDA tiene un capítulo GLOSARIO con las definiciones de los
términos técnicos (enganchados como links clicables desde el cuerpo), los
capítulos calidad/correlacion/modelos/agregacion/relaciones ya no repiten inline
esas explicaciones largas: se deja el TÉRMINO marcado (clicable, sigue saltando
al glosario) y se elimina el párrafo/oración de definición redundante. Los
HALLAZGOS y datos concretos del análisis se mantienen intactos; solo se quitan
las definiciones generales que el glosario ya cubre.

- calidad: _criteria_intro pasa de un bullet-list con las definiciones de
  completitud/validez/unicidad/calidad + fórmula renormalizada + párrafo de
  outliers a una frase que nombra las dimensiones, sus pesos (60/40) y el
  principio de outliers; los 4 términos siguen marcados.
- modelos: la nota de normalización deja de explicar la fórmula del z-score; la
  intro de PCA ya no define "componentes ortogonales ordenados por varianza"; la
  de KMeans quita "rango −1 a 1: cuanto más alto..." (silhouette); la sección de
  Isolation Forest quita la descripción de árboles/cortes/umbral. Términos
  marcados intactos.
- correlacion: la intro deja de describir cada método y consolida la duplicación
  signo/dirección; los 4 métodos + FDR siguen marcados.
- agregacion: la intro quita la definición de pivot ("cruzan dos categóricas
  sobre una medida") y abrevia la selección de claves; groupby y pivot marcados.
- relaciones: la intro y la sección de candidatas/inter-tabla quitan las
  definiciones de PK ("identifica cada fila"), FK ("referencian a otra tabla") y
  containment ("valores contenidos en la clave de otra"); pk/fk/cardinalidad/
  containment siguen marcados.

Verificado sobre el EDA de titanic (run_models + run_llm, 48 págs): los 23 link
annotations término→glosario se conservan (PyMuPDF), el glosario mantiene las 20
definiciones, y el texto visible de los 5 capítulos baja un 34.7% en conjunto
(calidad −67%, modelos −33%, relaciones −19%, agregacion −15%, correlacion −8%).
Tests actualizados (calidad_test asertaba el texto viejo). Suite EDA + pipeline
verde (118 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:15:24 +02:00
egutierrez 4099d88eaf merge: 4b salida markdown del AutomaticEDA (render_md, render_automatic_eda emite aeda_md_path, verificado met) 2026-06-30 18:59:33 +02:00
egutierrez 48de3ce3da feat(eda): salida Markdown del AutomaticEDA para pegar a un LLM
Añade un tercer formato de salida al AutomaticEDA, junto al PDF y el PPTX:
un Markdown autocontenido del MISMO documento por capítulos
(chapters_registry.build_document), optimizado para incorporar a un LLM
(texto plano + tablas markdown reales, sin binarios incrustados).

- render_md_impl.render_md(chapters, out_path, meta): serializa los bloques
  del modelo (Heading/Markdown/KVTable/DataTable/Figure/Image/Caption/Note/
  Group/GlossaryEntry) a Markdown. Cabecera con metadatos + índice navegable
  con anclas GitHub; tablas volcadas enteras (el MD no pagina); marcadores de
  glosario eliminados conservando la negrita; glosario al final.
- Figuras: un LLM no ve la imagen, así que se prioriza texto + datos. Se emite
  el caption y, cuando la figura tiene barras (histograma), se extrae la tabla
  de bins (Desde/Hasta/Frecuencia) de los artistas matplotlib. La banda ±1σ
  (axvspan) se descarta por ancho para que no aparezca como un falso bin.
  PNG opcional vía meta['embed_figures'] (off por defecto → sin binarios).
- render_automatic_eda_markdown: función pública del registry (tag eda),
  espejo de render_automatic_eda_pdf/pptx, acepta lista de capítulos o un
  TableProfile (build_document). dict-no-throw.
- render_automatic_eda (pipeline): emite también el .md (emit_md=True por
  defecto, clave de retorno aeda_md_path). Cambio aditivo: PDF/PPTX/manifest
  siguen saliendo igual.

Tests: golden de todos los kinds + regresión del filtro de la banda ±1σ +
edge documento vacío + profile path. Suite del paquete y del pipeline verde
(122 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:52:08 +02:00
egutierrez ab21e5d90b merge: 4b flag profile_level lite/standard/full en render_automatic_eda (lite 4.5s vs full 39.3s, verificado met) 2026-06-30 18:29:44 +02:00
egutierrez da60211826 merge: 4b relaciones — capitulo PK/FK + candidatos intra/inter-tabla (reusa infer_fk_containment_duckdb+build_join_graph, verificado met) 2026-06-30 18:22:29 +02:00
egutierrez 3be188a921 feat(eda): profile_level (lite/standard/full) en render_automatic_eda
Añade el parámetro profile_level a render_automatic_eda como preset de
consumo CPU/LLM que mapea a los flags existentes (run_models, run_series,
run_llm, sample). Tres niveles:

- lite (bajo consumo): run_llm=False, run_series=False, sample=2000 y modelos
  limitados a PCA + normalidad, SIN KMeans ni IsolationForest (lo caro en CPU).
  Para un vistazo rápido y barato.
- standard (default): comportamiento histórico — modelos completos, serie,
  sin LLM.
- full: standard + narrativa LLM por capítulo.

Precedencia: un flag explícito del caller (run_llm=..., run_models=..., etc.)
siempre prima sobre el default que fija el preset; el preset solo aplica al
parámetro que se deja en None.

Cableado del modo lite sin tocar profile_table (lo tocan otros agentes en
paralelo): profile_table NO corre los modelos (evita pagar KMeans +
IsolationForest); este pipeline los corre con run_eda_models(run_kmeans=False,
run_isolation=False) reusando ctx['raw_numeric'], y quita raw_numeric del ctx
para que el capítulo modelos no reproyecte clusters KMeans en vivo
(project_clusters_2d). geo_points ya queda derivado, así que geospatial no se
afecta.

Cambio aditivo y retro-compatible: sin profile_level el comportamiento es
idéntico al de v1.0.0 (standard). Tests nuevos cubren lite/standard, la
precedencia flag-sobre-preset, y la equivalencia del default con el histórico.
Bump 1.0.0 -> 1.1.0 + growth log en el .md. Skill /eda documenta --lite/--full.

Verificación: golden lite/standard/full sobre titanic — lite 4.8s (PCA+norm,
sin KMeans/iso/LLM/serie), standard 7.8s (modelos completos), full 38.3s
(+LLM). Suite render_automatic_eda + automatic_eda: 96 passed. fn index sin
error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:20:17 +02:00
egutierrez aa5aa67d50 merge: 4b calidad — nueva formula (completeness 0.6+validity 0.4, dataset row_uniqueness, outliers fuera a Observaciones, sin doble conteo) report 2046 (verificado met) 2026-06-30 18:17:23 +02:00
egutierrez 68f4ddabce feat(eda): capítulo RELACIONES para AutomaticEDA
Añade el capítulo `relaciones` al motor AutomaticEDA: analiza las
relaciones de clave de la tabla/base y se coloca tras `correlacion`,
antes de `modelos`, en CHAPTER_ORDER.

Capas que renderiza (solo las que aplican; None si no hay nada que decir):
- Claves declaradas: PK/FK/UNIQUE reales del esquema DuckDB, vía la nueva
  función `detect_declared_keys_duckdb` (lee `duckdb_constraints()`).
- Candidatos a clave primaria: los `key_candidates` del TableProfile.
- FK candidatas inter-tabla: reusa `infer_fk_containment_duckdb`
  (containment + señal de nombre) y `build_join_graph` (roles de nodos +
  diagrama Mermaid pegable). Solo si la fuente DuckDB tiene varias tablas.
- FK candidatas intra-tabla: heurística nombre + cardinalidad, vía la nueva
  función pura `suggest_intratable_fk_candidates`, marcada como sugerencia.

Engancha al glosario clicable los términos PK, FK, containment/inclusión y
cardinalidad (contrato §11.1) y usa Group (keep-together) para el grafo.

Funciones nuevas del registry (grupo `eda`):
- detect_declared_keys_duckdb (impure, datascience) + test.
- suggest_intratable_fk_candidates (pure, datascience) + test.

Tests: relaciones_test.py (golden intra + inter, edges, no-cut render) +
los tests de ambas funciones. Suite automatic_eda + render_automatic_eda
verde (89 passed). Golden end-to-end con el pipeline render_automatic_eda
verificado sobre titanic (intra) y una BD customers/orders (inter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:15:15 +02:00
egutierrez 43821ab11d merge: 4b analisis_llm — dedup Diccionario de datos + Datos personales (verificado met) 2026-06-30 18:14:17 +02:00
egutierrez 32054ad781 merge: 4b portada — tamano grande junto al nombre + descripcion y granularidad funcionando (verificado met) 2026-06-30 18:12:22 +02:00
egutierrez a2074a0167 feat(eda): nueva fórmula de calidad de datos (report 2046) + capítulo calidad
Implementa el modelo de calidad del report 2046 en el grupo eda.

Score de columna: 0.6·completeness + 0.4·validity con renormalización por
aplicabilidad (si la validez no es medible —texto libre o columna 100% nula— el
score se basa solo en completeness). Validez = conformidad real al tipo: nativo
numérico/fecha/bool = 1.0; texto promovido a número/fecha = parse rate
(validity_rate); texto con semantic_type = match_rate; texto libre = no aplica.

Outliers, columnas constantes e identificadores salen del score a un bloque de
observaciones analíticas (no son defectos de calidad). Se elimina el doble
conteo de la falta de datos (mostly_null ya no castiga validez) y el bug de
escala de outliers (que además ya no entran en el score).

Score de dataset: 100·(0.85·cell_quality + 0.15·row_uniqueness) en vez de la
media simple. Se pobla duplicate_rows/duplicate_pct push-down en
summarize_table_duckdb (COUNT sobre DISTINCT *, sin RAM) para habilitar la
unicidad de registro; renormaliza a solo cell_quality si no se puede calcular.

Capítulo calidad (v2.0.0): intro de dos dimensiones (60/40) que declara que los
outliers no bajan el score; tabla de scores Columna|Calidad|Completitud|Validez
(sin Consistencia, n/a cuando no aplica); DOS tablas separadas (Problemas de
calidad vs Observaciones analíticas); resumen con Unicidad de registro; glosario
clicable de completitud, validez, unicidad de registro y calidad de datos.

Verificado: 123 tests verdes (automatic_eda + render_automatic_eda +
column_quality_score + summarize_table_duckdb + profile_table). Golden EDA de
titanic (run_models+run_llm) con score recomputado a mano, outliers separados en
observaciones y glosario clicable (5 links GOTO en el PDF).

column_quality_score v2.0.0, summarize_table_duckdb v1.1.0, profile_table v1.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:10:23 +02:00
egutierrez d001d90306 merge: 4b glosario_hooks — terminos clicables en correlacion/modelos/agregacion (12/12 PDF+PPT, verificado met) 2026-06-30 18:09:37 +02:00
egutierrez 7045f37554 fix(eda): quita rótulos duplicados en capítulo ANÁLISIS LLM
El capítulo etiquetaba dos secciones por partida doble: un Heading de nivel 2
más el 'title' del propio DataTable, imprimiendo 'Diccionario de datos' y
'Datos personales (PII / RGPD)' dos veces seguidas en PDF y PPTX.

Se elimina el 'title' de ambos DataTable y se conserva el Heading único (el
patrón canónico OVERVIEW del contrato §8: el rótulo lo da el Heading, la tabla
solo repite su cabecera de columnas al paginar). El DataTable de PII mantiene su
'note' orientativa. La columna del diccionario ya lee 'Significado de negocio'.

CHAPTER_VERSION 1.0.0 -> 1.1.0. Test nuevo
test_sin_rotulos_duplicados_y_significado_de_negocio fija: tablas sin title,
cabecera exacta 'Significado de negocio', y cada rótulo una sola vez en el PDF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:07:12 +02:00
egutierrez fa8db01059 merge: 4b num_distr — desv std (sigma) en leyenda del histograma (verificado met) 2026-06-30 18:06:46 +02:00
egutierrez f2ac734ef7 merge: 4b head_rows — overview muestra df.head (build_eda_render_ctx pobla head_rows, verificado met) 2026-06-30 18:04:51 +02:00
egutierrez 048781df3f feat(eda): portada — tamaño grande + descripción/granularidad reales
El capítulo PORTADA ahora muestra SIEMPRE el tamaño del dataset (N filas ×
M columnas) en grande, como heading junto al nombre y agrupado con él
(Group keep-together), en lugar de enterrarlo en la tabla de metadatos.

La Descripción y la Granularidad ya no salen vacías ni con placeholders:
se resuelven por cascada — ctx explícito > bloque LLM (profile['llm'].summary
/ row_meaning de eda_llm_insights) > derivación del propio perfil (forma,
mezcla de tipos y score de calidad para la descripción; columnas
key_candidates o la forma de la tabla para una frase 'Cada fila es…').
Las derivaciones son honestas (declaran que vienen del perfil) y nunca
inventan significado de negocio.

Añade chapters/portada_test.py: golden (tamaño grande + textos del LLM,
sin fila 'Tamaño' duplicada), fallbacks sin LLM (keys / forma), prioridad
de ctx, edge de perfil vacío sin lanzar, y render a PDF + PPTX.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:04:05 +02:00
egutierrez a421f13d2e feat(eda): engancha glosario clicable en correlacion/modelos/agregacion
Fase 4b — extiende el glosario clicable de AutomaticEDA (mecanismo ya probado
end-to-end con `entropia` en cat_distr) a tres capítulos más, siguiendo el
contrato sección 11 (glossary.add(key,label,def) + span [[term:KEY]]texto[[/term]]):

- correlacion: Pearson, Spearman, Cramér's V, razón de correlación (η) y la
  corrección por comparaciones múltiples (FDR). Los métodos se marcan en el
  intro (siempre presente); FDR se registra y marca solo cuando se emite su
  resumen, para no dejar entradas de glosario sin aparición que las referencie.
- modelos: PCA, KMeans, coeficiente de silueta (silhouette), Isolation Forest y
  la estandarización z-score. Cada término se registra dentro de la sección que
  lo usa (tras su early-return), de modo que un término solo entra al glosario
  cuando su sección realmente se renderiza.
- agregacion: agrupación (split-apply-combine / groupby) y tabla dinámica
  (pivot), ambos en el intro siempre presente.

Solo se añaden los enganches de glosario: ningún cambio en la lógica de datos.
El texto visible es idéntico con o sin marcador (los renderers lo eliminan),
así que el layout de línea no cambia. Sin colector en ctx (render suelto) los
capítulos degradan y no marcan nada.

Tests: un test de glosario por capítulo verifica registro + marcado y la
degradación sin colector. Suite AutomaticEDA + render pipeline: 87 passed.
Golden titanic (run_models+series+llm): los 12 términos aparecen como entradas
del glosario en PDF (16 link annotations GOTO) y PPTX (15 saltos hlinksldjump).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:02:31 +02:00
egutierrez 13c82be780 feat(eda): NUM DISTR muestra el valor de σ (std) en la leyenda del histograma
La leyenda de cada histograma del capítulo de distribuciones numéricas ya
reporta el valor de la media y la mediana; ahora también reporta el valor de
la desviación estándar σ. La entrada de leyenda de la banda ±1σ pasa a incluir
el número (±1σ (σ = X)) y, cuando la banda no puede dibujarse (sin media o
std<=0) pero σ es conocido, se añade una entrada de leyenda mediante un handle
proxy sin trazo, de modo que el valor de σ se reporta siempre.

No se altera el boxplot de Tukey ni el keep-together (Group) por columna.
Se añaden tests de la leyenda: golden (σ con valor junto a media y mediana),
edge sin banda (proxy) y edge sin std (no revienta). Bump 1.1.0 -> 1.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:01:12 +02:00
egutierrez 7fb00defdf fix(fleetclaude): reusar contexto dentro de la flota tmux en vez de abrir kitty nueva
Lanzar `fleetclaude` estando ya dentro de una flota tmux viva abría una ventana
kitty nueva (y creaba un perfil/socket nuevo fleetN+1) en vez de mostrar la flota
en el pane actual. Causa: con $TMUX definido el launcher saltaba el `exec tmux
attach` y caía a la rama `setsid kitty`.

Cambio: cuando se invoca sin --new desde dentro de una flota fleetview viva (el
socket actual, derivado de $TMUX, tiene una sesión homónima con window 'console'),
se trae la TUI al contexto/pane actual (`fleetview show`, o `tmux select-window`
de la window console como fallback sin binario) y se retorna 0 antes de las ramas
kitty/wt.exe. Nuevo flag --new para forzar el comportamiento clásico (flota+ventana
nueva) aun dentro de tmux; pasar --session con un nombre distinto al perfil actual
equivale a --new implícito. Fuera de tmux el comportamiento es intacto (exec tmux
attach reutiliza la terminal).

Fix incidental: `local left_pane="" right_pane=""` (antes `local left_pane
right_pane` reventaba con "unbound variable" bajo `set -u` al reutilizar una sesión
existente, p. ej. con --reuse/--session sobre una flota viva).

Verificación e2e con sockets aislados fctest* (sin tocar la flota del humano):
golden (reuse, exit 0, kitty invariante), --new y --session-distinto (no reuse,
ruta ventana-nueva), fuera de tmux (salta reuse, ruta attach). bash -n limpio.

Docs: launch_fleetclaude.md (signature, params --new, ejemplo, cuando usarla,
gotchas, growth log v1.7.0) + /fleet show en .claude/commands/fleet.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:56:41 +02:00
egutierrez b1d205203a feat(eda): poblar head_rows real en el capitulo OVERVIEW (df.head)
El capitulo OVERVIEW del motor AutomaticEDA mostraba "df.head no disponible"
porque ninguna fase de calculo poblaba las primeras filas crudas de la tabla.

- build_eda_render_ctx: nuevo bloque que muestrea SELECT * LIMIT head_n
  (param nuevo head_n=10) y lo expone en ctx["head_rows"] como lista de
  dicts fila. Estilo dict-no-throw: si la query falla, se omite la clave.
- profile_table: puebla prof["head_rows"] reusando _sample_rows (SELECT de
  las columnas LIMIT 10) tras recalcular el type_breakdown. Asi el report
  JSON sidecar tambien lo lleva y el capitulo lo recoge via profile aunque
  no se construya el ctx.
- overview.py: la nota del DataTable de df.head ahora indica el total de
  filas del dataset cuando se conoce ("primeras 10 filas de 891"). Bump
  CHAPTER_VERSION 1.0.0 -> 1.1.0.
- overview_test.py (nuevo): golden (head via profile y via ctx, render PDF
  + PPTX muestran las filas reales, placeholder ausente), edge (sin
  head_rows degrada a nota honesta sin romper, None/vacio devuelven None).

Verificado end-to-end con titanic: render_automatic_eda emite PDF + PPTX con
df.head visible (Braund/Cumings/Heikkinen + columnas) y sin el placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:56:24 +02:00
egutierrez c6d9bc26da merge: Fase 4a AutomaticEDA motor+glosario (verificado met)
- fix negrita-pisa PDF, zebra striping (PDF+PPT), keep-together (Group: heading+figura+texto misma pagina/slide), imagenes con caption en PPT
- portada construida-al-final mostrada en posicion 1 (con resumen agregado del cuerpo)
- capitulo glosario al final + terminos clicables REALES: PDF link annotation (add_pdf_internal_links, PyMuPDF) + PPT hyperlink nativo (pptx_link_run_to_slide); entropia enganchado en cat_distr como ejemplo E2E
- contrato docs/automatic_eda_contract.md §11 (glosario + keep-together + zebra)
- pymupdf>=1.28.0
2026-06-30 17:45:30 +02:00
egutierrez d1a3d58a6b feat(eda): motor AutomaticEDA fase 4a — render fixes + keep-together + glosario clicable
Mejoras transversales del motor de render (no del contenido de capítulos):

1. Fix negrita pisa texto (PDF): _place_rich_lines mide el ancho REAL de cada
   span con las métricas de fuente del renderer (peso correcto) en vez del
   grid de ancho medio; negrita y normal en la misma línea ya no se solapan.
2. Zebra striping: filas pares sombreadas (#f6f8fa) en DataTable (PDF + PPTX),
   coherente al partir tablas largas (índice de fila lógico, no por página).
3. Keep-together: bloque Group nuevo; el renderer mide el grupo entero y lo
   mueve completo a la página/slide siguiente si no cabe, y encoge la figura
   (height_in) para dejar sitio a su título y texto. num_distr lo usa.
4. Caption siempre visible en toda figura PPTX (fallback al heading); la figura
   reserva el alto de su caption para que ambos quepan en el mismo slide.
5. Portada construida al final (con resumen agregado del análisis vía
   ctx['document_summary']) pero colocada primera por build_document.
6. Glosario: capítulo nuevo (último) + GlossaryCollector en ctx; los capítulos
   registran términos y marcan apariciones con [[term:key]]...[[/term]]. Links
   clicables reales: PDF (PyMuPDF, link GOTO) y PPTX (slide-jump nativo).
   Enganchado "entropía" en cat_distr como ejemplo end-to-end.

Funciones reutilizables delegadas a fn-constructor (tag eda):
- add_pdf_internal_links_py_datascience (PyMuPDF)
- pptx_link_run_to_slide_py_datascience (slide-jump)

Contrato docs/automatic_eda_contract.md actualizado (§1/§3/§5 + §11 nueva) con
la API de glosario, keep-together y zebra para la siguiente fase. PyMuPDF
declarado en pyproject. Suite verde (90 tests); golden titanic verificado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:35:19 +02:00
egutierrez b5334a2e97 merge: Fase 3 AutomaticEDA wiring (verificado met)
- build_eda_render_ctx: arma ctx (raw_numeric, timeseries_raw, geo_points, db_path+table) desde tabla DuckDB
- pipeline render_automatic_eda: perfila + ctx + build_document -> PDF + PPTX (11 capitulos poblados)
- profile_table: flag emit_automatic emite el report AutomaticEDA (PDF+PPT) sin romper render_eda_pdf
- text_layout: render real de **negrita** en PDF y PPTX
- .claude/commands/eda.md actualizado

Los 4 capitulos que degradaban (modelos/timeseries/geospatial/agregacion) ahora salen POBLADOS end-to-end.
2026-06-30 16:19:52 +02:00
egutierrez 437409641c docs(eda): el skill /eda emite SIEMPRE PDF + PPTX con AutomaticEDA
Actualiza el flujo del comando para que un EDA completo emita el informe
AutomaticEDA en sus dos formatos (PDF A5 móvil + PPTX 16:9) con los 11 capítulos
poblados, vía render_automatic_eda (o profile_table(emit_automatic=True)). El PDF
legacy (emit_pdf/render_eda_pdf) queda como salida independiente opcional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:08:50 +02:00
egutierrez f3d427d9e4 feat(eda): wiring AutomaticEDA — build_eda_render_ctx + pipeline render_automatic_eda + profile_table(emit_automatic)
Conecta el motor AutomaticEDA con los datos crudos para que los 4 capítulos
dependientes de ctx (modelos, timeseries, geospatial, agregacion) salgan
POBLADOS en vez de degradar a una nota.

- build_eda_render_ctx (datascience, impure, dict-no-throw): dado db_path+table
  y el TableProfile agregado, construye el ctx con los datos crudos que el
  perfil no incluye: raw_numeric {col:[float|None]} alineado por fila (modelos /
  geospatial), timeseries_raw {time_col,t,series} vía extract_timeseries_raw,
  geo_points {lats,lons} desde el par lat/lon detectado, y db_path/table para el
  groupby/pivot push-down de agregacion. Muestrea con LIMIT (no trae la tabla
  entera a RAM). Compone detect_time_column / extract_timeseries_raw /
  detect_latlon_columns / duckdb_query_readonly (imports lazy para evitar ciclo).
- render_automatic_eda (pipeline): one-shot perfil -> ctx -> PDF + PPTX con los
  11 capítulos poblados; devuelve rutas + manifest de versiones por capítulo.
- profile_table: flag aditivo emit_automatic=True emite el AutomaticEDA PDF+PPTX
  además del flujo legacy (emit_pdf/render_eda_pdf intacto). Nuevas claves de
  retorno aeda_pdf_path / aeda_pptx_path / aeda_manifest_path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:08:41 +02:00
egutierrez f5b30b23dc feat(eda): negrita inline real (**bold**) en renderers AutomaticEDA
El render de Markdown del motor AutomaticEDA quitaba los marcadores **negrita**
sin aplicar estilo. Ahora los spans **bold**/__bold__ se renderizan en negrita
real, de forma aditiva y sin romper el anti-corte:

- text_layout.py: parse_inline_bold() tokeniza spans preservando el texto
  visible (== strip_inline_md) y wrap_rich() envuelve por palabras a max_chars
  conservando el flag de negrita por segmento (la anchura visible no cambia, así
  que la paginación es idéntica).
- render_pdf_impl.py: _place_rich_lines() dibuja cada segmento con su fontweight
  avanzando x por el mismo grid de caracteres que usa el wrap (párrafos+bullets).
- render_pptx_impl.py: _add_rich_text() usa runs nativos de python-pptx con
  font.bold por segmento (negrita real de PowerPoint).
- bold_render_test.py: helpers puros (no-overflow, bold preservado, marcadores
  desbalanceados) + e2e que abre el .pptx y confirma un run con font.bold True.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:08:16 +02:00
egutierrez 5eaf3f662e merge: capitulo AutomaticEDA agregacion (verificado met) + funciones delegadas eda 2026-06-30 15:45:37 +02:00
egutierrez 05fe76bce0 merge: capitulo AutomaticEDA timeseries (verificado met) + funciones delegadas eda 2026-06-30 15:45:37 +02:00
egutierrez 864430e988 merge: capitulo AutomaticEDA geospatial (verificado met) + detect_latlon_columns/analyze_geo_extent/build_geo_scatter 2026-06-30 15:36:22 +02:00
egutierrez a69d14d38e feat(eda): capítulo TIMESERIES del AutomaticEDA (evolución + análisis de serie)
Capítulo nuevo build_timeseries(profile, ctx) -> Chapter|None del motor
AutomaticEDA. Cuando la tabla tiene columna de fecha/datetime, grafica la
evolución de cada columna numérica por periodo (valor agregado + conteo de filas)
y los paneles de descomposición STL y autocorrelación (ACF), con el análisis de
la serie: estacionariedad (ADF+KPSS), autocorrelación (Ljung-Box), fuerzas de
tendencia/estacionalidad (Hyndman) y la transformación sugerida (retornos o
diferencias) para evitar correlaciones espurias. Sin columna temporal devuelve
None. Consolida series OHLC casi idénticas en un único gráfico conservando el
análisis de cada columna.

La serie cruda llega por ctx['timeseries_raw'] (mismo patrón que modelos con
raw_numeric); las figuras son perezosas (Figure.make) y el paginador del núcleo
garantiza no-corte en PDF y PPTX. CHAPTER_VERSION 1.0.0.

Cubre los MUST del diseño (report 2043): MUST-9.1 (línea valor-vs-tiempo + conteo
por periodo), MUST-9.2 (paneles STL + ACF), MUST-9.3 (perfil datetime +
consolidación OHLC).

Funciones nuevas del registry (grupo eda), delegadas a fn-constructor, no inline:
- detect_time_column (pure): detecta la columna temporal y las numéricas
- profile_datetime (pure): rango/frecuencia/regularidad/huecos de la fecha
- resample_timeseries (pure): agrega la serie por periodo + conteo
- extract_timeseries_raw (impure): lee la serie cruda ordenada de DuckDB/PG

Verificación: 69 tests verdes (capítulo 9 + funciones 28 + núcleo/renderers);
golden real sobre seattle-weather (estacional) y aapl (OHLC) con PDF+PPTX sin
cortar nada (cols_cortadas=[]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:35:42 +02:00
egutierrez fd59530751 feat(eda): capítulo AGREGACION del AutomaticEDA (groupby + pivot + barras)
Capítulo nuevo (siempre presente cuando hay categóricas agrupables) que analiza la
tabla por grupos: stats de numéricas por grupo, tablas dinámicas (pivot) y gráficos
de barras desde cero. Obtiene los datos por ctx['aggregations'] precomputado o en
vivo vía push-down (ctx['db_path']+table), siguiendo el patrón de chapters/modelos.py.
Degrada a None cuando no hay categóricas; emite los bloques del modelo (DataTable,
Markdown, Figure) para que el paginador del núcleo no corte nada en PDF ni PPTX.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:33:55 +02:00
egutierrez 96da9e3015 feat(eda): funciones de agregación/OLAP para AutomaticEDA (groupby/pivot push-down + selección LLM)
Cuatro funciones nuevas del grupo eda que nutren el capítulo AGREGACION:
- select_groupby_keys (pure): elige categóricas agrupables + numéricas medida desde el TableProfile.
- groupby_stats_duckdb (impure): GROUP BY push-down en DuckDB (count/mean/median/std/min/max por grupo).
- pivot_table_duckdb (impure): pivot A×B push-down, limitado a top filas/cols para no cortar.
- suggest_aggregations_llm (impure): el LLM elige las agregaciones interesantes con fallback determinista.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:33:55 +02:00
egutierrez 00cd5274bc feat(eda): capítulo GEOSPATIAL del AutomaticEDA (scatter geográfico + zona/país)
Capítulo nuevo chapters/geospatial.py (CHAPTER_VERSION 1.0.0). Cuando el dataset
tiene un par de coordenadas, dibuja un scatter geográfico en proyección
equirectangular (la escala respeta la latitud para no estirar la longitud) y
analiza la extensión: bounding box, centroide, span, conteo por zona/país,
hemisferios y una interpretación. Cuando NO hay coordenadas, build_geospatial
devuelve None y el capítulo se omite.

Sigue el contrato de capítulos (firma build_<id>(profile, ctx) -> Chapter|None,
lectura defensiva, nunca lanza) y el patrón de modelos/num_distr: delega el
cálculo a las primitivas puras del registry (detect_latlon_columns,
analyze_geo_extent, build_geo_scatter) y solo dibuja la figura matplotlib de
forma perezosa. Las coordenadas crudas llegan por ctx['geo_points'] o
ctx['raw_numeric'] (como modelos lee raw_numeric); sin ellas, degrada con un
bounding box aproximado de numeric.min/max y una nota honesta.

Anti-cortes: usa DataTable/KVTable/Figure/Markdown del modelo, que el paginador
parte sin cortar. Test self-contained con golden + 6 edges + anti-cut (nombres
largos + 2100 puntos en varias regiones renderizan a PDF y PPTX sin truncar).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:29:33 +02:00
egutierrez cd658cc703 feat(eda): primitivas geoespaciales del grupo eda (detección lat/lon + extensión + scatter)
Tres funciones puras nuevas del dominio datascience (tags eda + geospatial) que
sostienen el capítulo GEOSPATIAL del AutomaticEDA, delegadas a fn-constructor:

- detect_latlon_columns: identifica el par (lat, lon) por nombre de columna +
  rango de valores ([-90,90] / [-180,180]) desde profile['columns']. Devuelve
  {lat_col, lon_col, confidence, reason}. 9 tests.
- analyze_geo_extent: bbox, centroide, span haversine, conteo por zona/país
  (lookup offline con bounding boxes embebidos, KISS sin geopandas) y
  hemisferios. 7 tests.
- build_geo_scatter: prepara los puntos del scatter en orden [lon, lat] con
  downsampling determinista por paso fijo + aspect equirectangular 1/cos(lat)
  clampado. 6 tests.

Registradas en datascience/__init__.py. Todas pure, params_schema completo,
.md autosuficiente (Ejemplo + Cuando usarla + Gotchas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:29:33 +02:00
egutierrez 81b57f9acd merge: capitulo AutomaticEDA analisis_llm (verificado met) 2026-06-30 15:15:39 +02:00
egutierrez 02ee222dde merge: capitulo AutomaticEDA cat_distr (verificado met) 2026-06-30 15:15:39 +02:00
egutierrez ba162ab301 merge: capitulo AutomaticEDA correlacion (verificado met) 2026-06-30 15:15:39 +02:00
egutierrez 415154d9a3 merge: capitulo AutomaticEDA modelos (verificado met) 2026-06-30 15:10:23 +02:00
egutierrez d479a8e4e2 merge: capitulo AutomaticEDA calidad (verificado met) 2026-06-30 15:10:22 +02:00
egutierrez 9286e3b6b1 merge: capitulo AutomaticEDA num_distr (verificado met) 2026-06-30 15:10:22 +02:00
egutierrez 649de07d6b feat(eda): capítulo AutomaticEDA CAT DISTR + funciones cardinalidad/pie
Capítulo cat_distr del motor AutomaticEDA: distribuciones categóricas con
explicación de entropía de Shannon, métricas de cardinalidad por columna
(valores distintos, % distintos, total de filas, valores únicos, entropía y
su máximo log2(k) + normalizada), tabla top-k y un donut de las categorías
más comunes (top-k + «Otros»). Marca columnas id-like y dominadas.

Delegadas a fn-constructor (grupo eda):
- categorical_cardinality_block: deriva métricas de cardinalidad/entropía.
- categorical_top_pie_figure: figura donut top-k + «Otros», leyenda lateral.

Defensivo (dict-no-throw): None si no hay columnas categóricas; normaliza
mode_pct a escala 0-100 (summarize_categorical lo emite como fracción).
Tablas vía DataTable y figura perezosa: el paginador del núcleo garantiza
no-corte en PDF y PPTX. Tests: golden + edge (sin categóricas) + anti-corte
(label largo / muchas columnas) en ambos renderers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:04:10 +02:00
egutierrez af1dd9bcc2 test(eda): tests del capítulo ANÁLISIS LLM (golden + edges + anti-cortes)
Suite self-contained (perfil sintético + un golden, sin DuckDB):
- golden: build_analisis_llm devuelve el Chapter y el documento entero renderiza
  a PDF y PPTX con resumen, análisis sugeridos, limpieza y una columna del
  diccionario presentes.
- orden: el capítulo queda inmediatamente después de `overview`.
- edges: profile sin bloque `llm` (o None/{}/malformado/llm vacío) -> None sin
  lanzar; fallback a ctx['llm'].
- anti-cortes: diccionario de 40 filas + sugerencia de limpieza de ~150 chars se
  reparten en varias páginas/slides sin perder ninguna fila ni palabra.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:01:26 +02:00
egutierrez fc5bc334c8 feat(eda): capítulo ANÁLISIS LLM para AutomaticEDA, junto al overview
Nuevo capítulo `analisis_llm` del motor AutomaticEDA. Consume el bloque `llm`
que `eda_llm_insights` (grupo eda) ya deja en el TableProfile —no llama al LLM
ni recalcula— y lo convierte en bloques del modelo de documento para que se
renderice sin cortarse en PDF ni PPTX:

- Resumen de la tabla y significado de una fila -> bloques Markdown (el
  renderer los envuelve a líneas completas, nunca pierde texto).
- Diccionario de datos y PII -> DataTable (el paginador parte por filas
  repitiendo cabecera y envuelve celdas largas dentro de su columna).
- Análisis sugeridos y limpieza sugerida -> listas de viñetas Markdown; cada
  entrada es una línea completa que el renderer envuelve, nunca trunca.

Lectura defensiva (.get) en todo; devuelve None si el profile no trae bloque
`llm` (p.ej. profile_table sin run_llm) para omitir el capítulo.

MUST-3.2 (report 2043): se mueve `analisis_llm` en CHAPTER_ORDER a la posición
inmediatamente posterior a `overview`, como pidió el usuario ("va junto al
overview").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:01:26 +02:00
egutierrez 03f3dca823 feat(eda): capítulo CORRELACION de AutomaticEDA (matriz + top pares ±)
Implementa chapters/correlacion.py siguiendo el contrato de capítulos:
build_correlacion(profile, ctx) -> Chapter|None, CHAPTER_VERSION="1.0.0".

Consume profile['correlations'] (salida de association_matrix del grupo eda,
sin recalcular estadística) y emite, como bloques del modelo:

- Matriz de asociación (Figure/heatmap perezoso, RdBu_r, con signo en num-num
  y magnitud en métricas mixtas; etiquetas ordenadas por conectividad y
  recortadas a las 16 más conectadas para legibilidad).
- TOP de pares POSITIVOS y TOP de pares NEGATIVOS en dos DataTable separadas
  (los negativos son por construcción num-num, único método con signo), con
  método, valor, p-valor corregido (FDR) y significancia.
- Resumen FDR (multiple_testing) + leyenda de métodos.
- Aviso de espuriedad por niveles no estacionarios (Granger-Newbold) cuando el
  profile lo marca.

Lectura defensiva en todo (None si no hay pares; nunca lanza). Anti-cortes:
sólo bloques del modelo, el paginador parte tablas repitiendo cabecera y escala
la figura entera.

Test self-contained (5 casos): golden a nivel de bloques + golden render
PDF/PPTX, edge sin pares -> None, edge sólo positivos -> nota honesta, y
anti-corte con matriz ancha + etiquetas largas (dato íntegro a nivel de bloque,
ambos renderers sin reventar).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:59:50 +02:00
egutierrez d412522db9 feat(eda): capítulo CALIDAD del AutomaticEDA (criterios + scores + problemas ES)
Añade el capítulo de calidad de datos al motor AutomaticEDA, siguiendo el
contrato de capítulos (build_calidad(profile, ctx) -> Chapter | None,
CHAPTER_VERSION). El capítulo responde lo que pidió el usuario, en español y
en formato de tabla:

- Intro "Cómo se calcula la calidad": explica los tres criterios y sus pesos
  (completitud 50%, validez 30%, consistencia 20%) antes de cualquier número,
  más una KVTable de resumen a nivel tabla (calidad global y agregados).
- Tabla "Scores por columna": score total más su desglose en completitud /
  validez / consistencia, ordenada de peor a mejor.
- Tabla "Problemas detectados": los issues en español por columna, separados de
  los flags de tipo. Cuando no hay problemas, una nota honesta.

Registry-first: el desglose y los issues NO se recalculan aquí; se consumen de
la función pura del registry column_quality_score (grupo eda), que ya deriva
{score, completeness, validity, consistency, issues} del ColumnProfile. El
capítulo es render-only y compone bloques del modelo; los renderers paginan las
tablas (parten por filas repitiendo cabecera) y envuelven celdas largas, de modo
que nada se corta en PDF ni en PPTX. La lista de issues por celda se acota a
160 caracteres con "(+N más)" para que una fila nunca crezca más que una página.

Test self-contained (sin DuckDB): golden con desglose + issues ES, edges
(None/{}/sin columnas -> None; perfil limpio -> nota), y anti-cortes (perfil de
22 columnas con nombres largos renderizado a PDF y PPTX: el nombre completo
sobrevive al envolverse, sin marcador de truncado).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:59:10 +02:00
Egutierrez c1a4a83717 feat(eda): capítulo num_distr — histograma con media/mediana/±σ + boxplot Tukey
Capítulo NUM DISTR del motor AutomaticEDA. Por cada columna numérica emite,
como una sola Figure indivisible de dos ejes compartiendo X, un histograma con
la media (línea roja discontinua), la mediana (línea verde continua) y la banda
±1σ dibujadas como referencias, y un boxplot de Tukey debajo (caja P25–P75,
bigotes a 1,5·IQR, marca de valores fuera de las vallas). Una nota por columna
traduce el distribution_type a lenguaje llano (MUST-4.1/4.2/4.3 del report 2043).

Consume el profile del grupo eda sin recalcular: el histograma usa los bins
{lo,hi,count} de describe_numeric y las vallas del boxplot las deriva la función
pura build_boxplot_stats_py_datascience. Lectura defensiva: sin columna numérica
devuelve None; profile None/{} no lanza. Test self-contained: golden + edges +
anti-corte (8 columnas no cortan en PDF ni PPTX).
2026-06-30 14:58:03 +02:00
egutierrez 81e8597d21 feat(eda): capitulo MODELOS de AutomaticEDA (markdown, scatter PCA+clusters, micro-LLM)
Implementa chapters/modelos.py (build_modelos / CHAPTER_VERSION) consumiendo
profile['models'] {pca,kmeans,outliers,normality} de run_eda_models. Render
markdown estructurado con bloques anti-corte:

- Intro de normalizacion z-score: por que se estandariza antes de PCA/KMeans (MUST-8.3).
- PCA: scree plot (varianza explicada + acumulada, un solo eje Y) + tablas de
  varianza y cargas principales (SHOULD-8.4).
- Segmentacion KMeans: scatter PCA coloreado por cluster con centroides, en su
  propia pagina/slide (MUST-8.1); tabla de tamaños; micro-analisis LLM por
  cluster con titulo, cada entrada indivisible (MUST-8.2).
- Isolation Forest: explicacion de la deteccion multivariante de outliers y del
  umbral + conteos (MUST-8.3).
- Normalidad: tabla por columna (Jarque-Bera / D'Agostino / Shapiro), pagina sola.

El scatter coloreado y los titulos LLM no estan en el TableProfile, asi que el
capitulo los toma de ctx (cluster_projection precomputado, o raw_numeric para
calcular project_clusters_2d en vivo, o cluster_titles/run_cluster_llm para el
micro-analisis), igual que overview lee head_rows; degrada honesto con una Note
cuando faltan. Devuelve None si el profile no trae bloque models renderizable.

Tests self-contained (sin DuckDB/sklearn/LLM/red): golden PDF+PPTX, edges
(profile None/vacio/insuficiente, kmeans sin proyeccion), anti-corte (tabla de
normalidad de 40 columnas parte repitiendo cabecera sin perder ninguna). 8/8.
Suite del nucleo render_automatic_eda_pdf/pptx sigue verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:57:43 +02:00
egutierrez 4de071f2f9 feat(eda): project_clusters_2d + describe_clusters_llm para el capitulo MODELOS
project_clusters_2d (pura): PCA(2)+KMeans sobre el MISMO subset estandarizado,
devolviendo proyeccion 2D y labels alineados por fila + centroides en espacio PCA
+ perfiles de cluster desestandarizados. Es la pieza que garantiza la alineacion
points<->labels que pca_explained y kmeans_segments no cubren (estandarizan por
separado y kmeans descarta los labels). Habilita el scatter PCA coloreado por
cluster (MUST-8.1).

describe_clusters_llm (impura): micro-analisis LLM de los clusters en una sola
llamada a ask_llm (grupo claude-direct), devuelve titulo + descripcion por cluster
con degradacion dict-no-throw a titulos genericos si el LLM no responde (MUST-8.2).

Ambas re-exportadas en datascience/__init__.py. Tests: 6/6 y 9/9 (sin red).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:57:27 +02:00
egutierrez fcf5a4c6a3 feat(eda): build_boxplot_stats — estadísticas de boxplot Tukey desde sub-bloque numeric
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:54:49 +02:00
egutierrez 959648ec4f Merge remote-tracking branch 'origin/master' 2026-06-30 14:43:51 +02:00
egutierrez a3f75d61ec chore: avance acumulado de sesiones previas (reorg dev/issues + ajustes)
Reorganizacion de dev/issues en subcarpetas (completed/, cpp/, gamedev/,
kanban/, trading/, imagegen/, matrix/) y cambios acumulados en cmd/fn/pyrunner,
.claude/commands y settings. Trabajo de otro LLM/sesion, commiteado a peticion
del usuario para desbloquear el working tree. Excluido logs/ardour_mcp_server.log (ruido).
2026-06-30 14:43:51 +02:00
egutierrez cb7a7fc1fd docs(eda): contrato de capítulos AutomaticEDA + capability page
Añade docs/automatic_eda_contract.md: documento autoritativo y autosuficiente
para que otros agentes escriban capítulos en paralelo (NUM DISTR, CAT DISTR,
CALIDAD, CORRELACIÓN, MODELOS, ANÁLISIS LLM, TIMESERIES, GEOSPATIAL,
AGREGACIÓN). Cubre el modelo de bloques/capítulo exacto, la firma
build_<chapter>(profile, ctx) -> Chapter|None, la declaración de
CHAPTER_VERSION, dónde colocar el módulo, cómo se registra el orden del
documento, qué claves del profile consume cada capítulo, las claves nuevas que
la fase de cálculo debe añadir (head_rows, columns[].examples) y un ejemplo
completo del capítulo de referencia OVERVIEW.

Enlaza las dos funciones nuevas y el contrato desde docs/capabilities/eda.md y
actualiza el recuento del grupo eda en el índice de capabilities.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:30:31 +02:00
egutierrez 9cdde4a341 feat(eda): núcleo AutomaticEDA — documento por capítulos + renderers PDF/PPTX anti-corte
Introduce la capa intermedia entre el contenido de un EDA y su formato de
salida. Un documento es una lista de capítulos versionados; cada capítulo es
un conjunto ordenado de bloques (heading, markdown, kv_table, data_table,
figure, image, caption, note) independientes del formato.

Núcleo (paquete de soporte python/functions/datascience/automatic_eda/):
- model.py: dataclasses de bloques + Chapter, normalizadores defensivos
  (aceptan dataclass o dict, nunca lanzan), ENGINE_VERSION y el manifiesto
  por capítulo (automatic_eda_manifest.json).
- text_layout.py: medición/wrapping por rejilla de caracteres compartida.
- chapters_registry.py: CHAPTER_ORDER pre-declarado + build_document con
  auto-discovery de capítulos por convención (permite añadir capítulos en
  paralelo sin editar el registro).
- render_pdf_impl.py: paginador A5 retrato móvil que MIDE cada bloque y nunca
  corta: texto a líneas completas, tablas largas partidas por filas repitiendo
  cabecera, figuras/imágenes escaladas para caber enteras. Pie versionado por
  capítulo.
- render_pptx_impl.py: mismo principio sobre slides 16:9 (continúa en slide
  "(cont.)"; tablas repiten cabecera; figuras exportadas a PNG escaladas).
- chapters/portada.py y chapters/overview.py: capítulos de referencia. Portada
  con nombre, rótulo Automatic-EDA, fuente, almacenamiento (inferido de
  source), fecha europea, filas×cols, descripción, granularidad y calidad con
  criterios. Overview con df.head (placeholder honesto si falta head_rows),
  diccionario de columnas (tipo/nulos/ejemplos) y describe numérico.

Funciones públicas del registry (grupo eda, dict-no-throw):
- render_automatic_eda_pdf / render_automatic_eda_pptx: aceptan capítulos o un
  TableProfile (construyen los capítulos con build_document) y escriben el
  manifiesto. Aditivas — no reemplazan render_eda_pdf.

Tests self-contained (sin DuckDB) para ambos renderers: golden (portada +
overview), partición de tablas largas repitiendo cabecera, no-corte de celdas
y markdown largos, profile None/{} válido de 1 página/slide, y error path en
directorio no escribible. 23 tests verdes (incluye los previos de
render_eda_pdf, intactos).

Dependencia nueva python-pptx>=1.0.2 declarada en python/pyproject.toml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:30:31 +02:00
egutierrez 5501507588 feat(infra): launch_fleetclaude auto-detecta terminal (kitty ↔ Windows Terminal)
La ruta ventana-nueva ya no asume kitty. Elige terminal según el host, sin
config por PC: kitty si está 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'`.

Arregla el síntoma "se lanza la flota pero no se ve": en WSL sin kitty la sesión
tmux se creaba pero ninguna ventana la mostraba. Mismo `fleetclaude` funciona en
un PC con kitty y en otro WSL sin kitty.

wt.exe se lanza desde un subshell con cwd /mnt/c para evitar el warning por cwd
UNC (\\wsl.localhost\...). El path de attach interactivo (terminal real fuera de
tmux) queda intacto. Bump 1.5.0 -> 1.6.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:50:20 +02:00
egutierrez 88eabb0457 chore: auto-commit (1 archivos)
- logs/ardour_mcp_server.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-29 11:05:00 +02:00
Egutierrez ebb00d8a42 chore(issues): cierra 0173-0177 (hallazgos del benchmark EDA resueltos en rondas 2-4)
Los 14 hallazgos H1-H14 del benchmark estan corregidos y verificados con re-corrida.
Commits: caf8c25d (S), c4cff5ed (render H4/H9), e142ef02 (comportamiento H2/H3/H6/H7/H8/H10/H11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:38:51 +02:00
Egutierrez e142ef026d fix(eda): hallazgos de comportamiento del benchmark (H2,H3,H6,H7,H8,H10,H11)
Ronda 4 (verificada con re-corrida sobre los datasets afectados):
- H2: stl_decompose deriva periodo de la frecuencia del indice (seattle period=365
  seasonal_strength=0.84; fin del period=2 espurio)
- H3+H10: infer_fk por senal de nombre (<X>Id->X.<X>Id) + excluir no-clave -> chinook
  111->9 FK, todas reales, cero absurdas, 16-27x mas rapido; base intacta (flag off->111)
- H6: association no computa eta2 si cardinalidad~=n (Ticket-Fare espurio fuera)
- H7: id secuencial monotono excluido de correlacion y PCA/KMeans (PassengerId fuera)
- H8: correlacion de series no estacionarias marcada espuria / sobre retornos
- H11: distribution_type usa modos/cardinalidad/normalidad (quality->discrete)
- 66 tests verdes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:37:47 +02:00
Egutierrez c4cff5ed5b feat(eda): render de models en markdown + PDF DB-level para profile_database (H4,H9)
- H4: render_eda_markdown anade seccion Modelos (PCA/KMeans/normalidad/outliers);
  render_eda_pdf formatea models/series/caveats como tablas (no str(dict) crudo)
- H9: profile_database gana flag emit_pdf -> PDF movil DB-level (resumen tablas +
  join graph) via render_eda_pdf_relational; clave report_pdf_path
- aditivos y retrocompatibles (flags default False). 38 tests verdes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 04:05:38 +02:00
Egutierrez caf8c25d99 fix(eda): bugs de bajo riesgo del benchmark (H1,H5,H12,H13,H14) + tests faltantes
- H1: render_eda_markdown ya no aplica doble x100 a outlier_pct (336% -> real)
- H5: profile_database filtra base_tables_only (excluye VIEWs; sakila 21->16)
- H12: suggest_reexpression salta columnas no-continuas
- H13: to_returns/profile_table elige retornos (financiera) vs diferencias (fisica)
- H14: test de regresion ATTACH sqlite via information_schema
- +8 tests de las funciones eda nuevas (acf_pacf, adf_kpss, ...). 77 tests verdes
- L/M (H2,H3,H4,H6,H7,H8,H9,H10,H11) quedan en issues 0174-0177 para revision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:51:11 +02:00
Egutierrez 7ac69ab4fb feat(eda): series temporales + rigor anti-data-mining + PDF movil + /eda + benchmark issues
Bloque del grupo eda (sesion ausente EDA-benchmark):
- 8 funciones nuevas: adf_kpss_stationarity, acf_pacf, stl_decompose, to_returns,
  fdr_correction, suggest_reexpression, exploratory_caveats, render_eda_pdf
- integracion: profile_table (run_series, emit_pdf), association_matrix (FDR Benjamini-Hochberg),
  render_eda_markdown (secciones series/reexpresion/caveats)
- slash commands /eda y /capitulos
- issues 0173-0177: mejoras del /eda derivadas del benchmark sobre 12 datasets reales
  (outlier_pct x100, periodo estacional, FK inference, render models, tipos id-like)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:34:01 +02:00
egutierrez 02301aaed3 feat(datascience): auto-commit con 5 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 18:16:24 +02:00
egutierrez 2729629f0a merge(gamedev-2d): comfyui_walk_cycle_oneshot — walk cycle pose-driven + pixelart animado (5 funciones) 2026-06-28 18:16:18 +02:00
egutierrez 6cc90558d4 feat(gamedev-2d): pipeline walk_cycle_oneshot — personaje andando en pixel-art animado
Promueve el caso 1 del report 0217 (animacion de sprites de personaje) a un
pipeline one-shot: de un prompt de personaje a un sprite sheet + GIF/WEBP en loop,
frame-by-frame dirigido por pose (ControlNet OpenPose + seed fija + Rembg) con cada
frame pixelizado a NxN RGBA.

Nuevas funciones reutilizables (issue 0087, crecimiento por composicion):
- comfyui_walk_cycle_oneshot (pipeline): orquesta poses -> generacion -> pixelizado
  -> ensamblado. No-throw, salta frames que fallan. Modo openpose (esqueletos reales)
  con fallback prompt-pose.
- render_openpose_walk_skeletons: dibuja N esqueletos OpenPose COCO-18 del walk cycle
  (el insumo que el report 0217 marco como faltante).
- comfyui_pixelize_sprite_png: PNG existente -> NxN RGBA pixel-art real (compone
  crop_to_content + pixeloe_downscale + comfyui_pixelize_image).
- assemble_animated_sprite: frames RGBA -> sprite sheet horizontal + WEBP/GIF loop.
- comfyui_build_walk_cycle_workflow (pura): grafo API del workflow animado para la UI
  (ControlNet OpenPose -> KSampler xN seed fija -> ImageBatch -> Rembg -> SaveAnimatedWEBP).

Verificado en GPU: GIF/WEBP de caballero andando, 4 frames 32x32 (y 64x64) RGBA con
fondo transparente y 16 colores, identidad de silueta consistente, piernas que cambian.
Metodo de poses usado: OpenPose real (sin fallback). Evidencia en report 0221.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:14:46 +02:00
egutierrez 36a725ba10 feat(ml): auto-commit con 4 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 16:02:08 +02:00
egutierrez 1dd6c889e5 tune(comfyui): crop_to_content pad_ratio 0.06->0.02 — sprite llena más el frame 2026-06-28 16:02:02 +02:00
egutierrez 7aaac44a49 test(comfyui): reubicar tests del sprite-fix a tests/ 2026-06-28 16:00:20 +02:00
egutierrez ffcb69ce02 merge(comfyui): pixelart sprite fix — llena el frame (crop_to_content) + fondo transparente (alpha-aware) 2026-06-28 16:00:20 +02:00
egutierrez c79f33265e fix(comfyui): pixelart_real_oneshot — sprite llena el frame + fondo transparente
Arregla los dos defectos reportados del pipeline comfyui_pixelart_real_oneshot:
el sujeto salía diminuto respecto al frame y siempre traía fondo (sin opción de
transparencia).

Causa raíz: comfyui_pixelize_image hacía convert("RGB") y descartaba el alpha;
comfyui_build_pixelart_workflow no inyectaba rembg (a diferencia de sus hermanos
item_icon/enemy_creature); y no había ningún paso de auto-crop al contenido.

Orden correcto del pipeline ahora:
generar (rembg) -> autocrop al bbox + cuadrar -> downscale (alpha aparte por
PixelOE) -> cuantización alpha-aware -> PNG RGBA transparente.

Piezas:
- comfyui_pixelize_image (1.1.0): keep_alpha/alpha_threshold. Con RGBA cuantiza
  solo el RGB (fondo transparente relleno con la moda del sujeto, fuera de la
  paleta) y preserva/binariza el alpha aparte. RGB sin alpha intacto.
- crop_to_content (NUEVA, pura PIL): bbox del contenido (alpha o diff-fondo) ->
  recorta -> margen -> cuadra centrando. No-throw; imagen vacía -> copia intacta.
- comfyui_build_pixelart_workflow (1.1.0): transparent=True + rembg_model.
  Inyecta nodo Image Rembg tras VAEDecode (patrón de item_icon).
- comfyui_pixelart_real_oneshot (1.1.0): transparent + autocrop + crop_pad_ratio
  + rembg_model. Recombina el alpha aparte tras PixelOE (trabaja en RGB). Campos
  nuevos: has_alpha, autocrop_applied.

Verificado en GPU (knight 64px): RGBA con 4 esquinas alpha==0, contenido cubre
88% del frame (antes 48%), 16 colores, 64x64. 32 tests offline en verde.
Report: reports/0218-2026-06-28-pixelart-sprite-fix.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:59:26 +02:00
egutierrez 31c2f6ac7f test(comfyui): reubicar test de pixeloe_downscale a tests/ 2026-06-28 15:27:13 +02:00
egutierrez 3bc97828e3 merge(comfyui): comfyui_pixelart_real_oneshot + pixeloe_downscale (pixelart real: PixelOE + cuantización dura) 2026-06-28 15:27:08 +02:00
egutierrez ccdd529bdc feat(comfyui): pipeline comfyui_pixelart_real_oneshot — pixelart REAL (PixelOE + cuantizacion dura)
Materializa el metodo ganador del report 0215: generar a alta-res con SDXL +
LoRA SDXL_pixel-art, downscale contrast-aware con PixelOE (engine=pixeloe para
sprites/personajes) o nearest (tiles), y cuantizacion dura con
comfyui_pixelize_image (16 colores libres o paleta fija pico-8/nes/game-boy).

- pixeloe_downscale_py_ml: downscale contrast-aware via lib pixeloe con bridge
  de interprete (la lib vive en el venv de ComfyUI, no en el del registry).
  No-throw, fallback limpio si pixeloe no disponible.
- comfyui_pixelart_real_oneshot_py_pipelines: one-shot que compone build_pixelart
  + submit + wait + fetch + pixeloe_downscale + pixelize_image. Fallback
  automatico pixeloe->nearest. Sweet-spot 64px personajes, 32px iconos.

Verificado por PIL: personaje 64x64=16 colores, icono 32x32=16 colores (vs ~33k
de la imagen de difusion cruda). 100% grid duro + outline nitido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:24:15 +02:00
egutierrez 741724f633 chore: auto-commit (1 archivos)
- logs/ardour_mcp_server.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 15:03:24 +02:00
egutierrez 2be62f6ef6 merge(comfyui): comfyui_generate_until_quality — loop generar/juzgar/refinar (best-of-N + escalate + refine_prompt) 2026-06-28 15:02:45 +02:00
Egutierrez 8e9e1e6c8a feat(comfyui): pipeline comfyui_generate_until_quality (loop evaluator-optimizer)
Loop tipo GAN sin entrenar: genera con un builder del registry, juzga con el
panel multi-juez (comfyui_judge_image) y, si no alcanza el umbral, refina (nueva
seed, mas steps/cfg, prompt corregido con el feedback del juez via ask_llm) y
regenera hasta converger (verdict 'good') o agotar max_iters. Devuelve siempre
la mejor candidata por score (best-of-N), nunca lanza excepcion cruda.

Compone comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image
+ comfyui_judge_image + ask_llm. Filtra kwargs por inspect.signature para ser
robusto entre builders. Caso HUD verificado: itera iter0 bad -> iter1 good.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:01:37 +02:00
egutierrez ec46aae04c chore: auto-commit (1 archivos)
- logs/ardour_mcp_server.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 07:34:26 +02:00
egutierrez b173ac2703 merge(comfyui): higiene capability pages (drift conteos + styles + audio/templates + build_flux + parallax) 2026-06-28 07:34:02 +02:00
egutierrez ec0a5e53ac docs(comfyui): remata drift de conteo en comfyui-skill (11→17) y gamedev-2d (36→47)
- gamedev-2d.md: el header decía '31 builders + 5 de apoyo' (=36); inventario real = 47
  funciones (36 builders: 31 de generación + 5 de transformación; 11 de apoyo: post-proceso,
  puente a Godot, style presets, pipelines one-shot).
- comfyui-skill.md: añade bloque de tamaño del grupo (17 funciones tag comfyui-skill); la
  página no tenía conteo interno (el 11 obsoleto vivía solo en INDEX.md).
- INDEX.md: gamedev-2d 36→47 y comfyui-skill 11→17, con descripciones actualizadas.

Cierra el drift residual señalado en el report 0210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:33:08 +02:00
egutierrez 5280499df5 merge(comfyui): tests offline para 16 builders puros (376 tests verdes) + tested:true 2026-06-28 07:32:15 +02:00
egutierrez 346f859b86 test(comfyui): tests offline para 15 builders/funciones puras sin test
Cubre 15 funciones del grupo comfyui (+ las 4 de comfyui-judge) que no tenian
test, con tests offline (sin red, sin GPU, sin servidor ComfyUI):

- 5 builders puros gamedev-2d: build_asset_variant, build_directional_sprite,
  build_inpaint_asset, build_outpaint_asset, build_sprite_from_sketch (estructura
  del workflow en API format + cableado + determinismo + error paths).
- 3 impuras offline via PIL/stdlib: build_grid, flatten_alpha_on_color,
  read_png_metadata (PNGs reales en tmp, error paths).
- 4 de comfyui-judge: score_aesthetic y score_clip_alignment por sus guards
  previos al subproceso torch; judge_image (panel) y critique_image_llm con la
  dependencia pesada monkeypatcheada.
- 3 que componen otras funciones: resolve_workflow_deps, import_workflow_json,
  extract_recipe_from_png (dependencia de red monkeypatcheada o fallback offline).

Cada .md actualizado con tested: true + test_file_path + tests.
Cobertura del grupo comfyui (tag plano): 79 -> 90 con test (47 -> 36 sin).
comfyui-judge: 0/4 -> 4/4. pytest: 101 passed; carpeta ml/tests: 376 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:30:59 +02:00
egutierrez 604d3d4feb docs(comfyui): higiene de capability pages — drift 29→126 + styles + build_flux + parallax
- comfyui.md: bloque de tamaño real del grupo (126 funciones tag comfyui: 63 puras,
  50 impuras, 13 pipelines) con punteros a los sub-grupos (comfyui-skill, comfyui-styles,
  comfyui-judge, gamedev-2d). Corrige la firma corta de build_flux (variant/steps=None/
  weight_dtype='default' + camino custom-advanced) que arrastraba drift del report 0205.
  Añade sección Styles con las 5 funciones del sub-grupo.
- comfyui-styles.md (NUEVA): página madre del sub-grupo de estilo (catálogo WAS +
  style presets gamedev), tabla de las 5 funciones, ejemplos canónicos alineados con
  los retornos reales y fronteras.
- comfyui-overview.md: añade audio (05b) y styles (04b) al mapa cross-grupo y a la tabla
  resumen; referencia las nuevas páginas madre comfyui-styles y gamedev-2d.
- INDEX.md: comfyui 29→126 con descripción actualizada; nueva fila comfyui-styles.
- comfyui_build_parallax_background_workflow.md: añade sección ## Ejemplo lanzable
  (el indexer extrae example del cuerpo, no del frontmatter) — cobertura del grupo
  pasa a 126/126 con ejemplo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:27:32 +02:00
egutierrez 287abbd6ee merge(comfyui): fix firmas keyword-only para que fn run despache (5 funciones de skills) 2026-06-28 07:26:02 +02:00
egutierrez f8793f96ac fix(comfyui): firmas sin keyword-only para que fn run las despache
El generador de runner de fn run (cmd/fn/pyrunner.go::generatePyRunner)
parsea la signature de la funcion desde el frontmatter del .md y emite
`<param> = _args[i]` por cada parametro posicional. Cuando la firma es
keyword-only (`def f(*, ...)`), el `*` se trata como un nombre de parametro
y genera la linea invalida `* = _args[0]`, que rompe el runner con
`SyntaxError: invalid syntax` antes de ejecutar la funcion.

Se quita el separador keyword-only (`*,`) de la firma — tanto en la `def`
del .py como en el campo `signature:` del .md (la fuente que lee el
indexer y el runner) — convirtiendo los parametros keyword-only en
parametros normales con su mismo default. No cambia nombres, defaults ni
comportamiento: las llamadas con keyword siguen siendo validas.

Afecta a 5 funciones detectadas en el report 0208 §3.3, todas con
SyntaxError reproducido via `fn run <id>`:
- comfyui_fetch_civitai_image_meta
- comfyui_load_skill
- comfyui_save_skill
- comfyui_import_workflow_png
- comfyui_list_skills

Se completa ademas el fix de comfyui_interrupt_queue: el commit 643ebfb8
quito el `*,` del .py pero dejo el `*,` en el campo `signature:` del .md,
que es justo lo que lee el runner — por eso `fn run comfyui_interrupt_queue`
seguia fallando. Aqui se corrige el .md.

Verificado: tras el cambio las 6 despachan sin SyntaxError (las 4 con
primer arg requerido devuelven el `missing required arg` esperado del
runner; list_skills e interrupt_queue ejecutan `ok:true`). Tests
existentes verdes (comfyui_fetch_civitai_image_meta_test.py +
tests/test_comfyui_interrupt_queue.py: 8 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:23:59 +02:00
egutierrez 643ebfb849 fix(comfyui): comfyui_interrupt_queue firma sin keyword-only para que fn run la despache 2026-06-28 04:55:39 +02:00
egutierrez 537516e32e merge(comfyui): comfyui_interrupt_queue — control de cola (interrupt + clear_pending) 2026-06-28 04:54:46 +02:00
egutierrez ca07b25297 feat(comfyui): comfyui_interrupt_queue v1.1.0 — clear_pending + cleared/queue_remaining + tests
Alinea la funcion al contrato de control de cola (punto 3 del roadmap ComfyUI):
- firma keyword-only: clear_pending (vacia pendientes con POST /queue {clear:true}) + timeout
- output {ok, interrupted, cleared, queue_remaining, error}; GET /queue al final
- no lanza en fallo de red: degrada a {ok:False, error}
- test con mock HTTP local (golden + clear + cola vacia + error path), 4/4 verde
- .md autosuficiente con gotchas + capability growth log

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 04:54:14 +02:00
egutierrez fbbff7d5e7 chore: auto-commit (1 archivos)
- logs/ardour_mcp_server.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 04:48:32 +02:00
egutierrez bdd841d9af merge(comfyui): higiene — 5 funciones de la sesión en capability page + tests list_templates/extract_template 2026-06-28 04:47:48 +02:00
egutierrez 7d33b39859 docs(comfyui): consolidar las 5 funciones nuevas del grupo (tests + capability page)
Higiene del grupo comfyui sobre las 5 funciones de la sesión:
comfyui_build_audio_workflow, comfyui_fetch_output_audio,
comfyui_build_flux_workflow, comfyui_list_templates, comfyui_extract_template.

- Tests nuevos para list_templates y extract_template (lógica pura: localización
  del intérprete, error-path sin el paquete instalado, contrato del dict; golden
  condicional con skip si no hay ComfyUI con comfyui-workflow-templates). 10 tests,
  todos verdes.
- comfyui_list_templates.md / comfyui_extract_template.md: tested true + tests +
  test_file_path.
- Fix drift de test_file_path en comfyui_fetch_output_audio.md (apuntaba a un
  *_test.py inexistente; corregido a tests/test_*.py). Elimina el WARN de fn index.
- docs/capabilities/comfyui.md: subsecciones Audio (ACE-Step) y Templates oficiales.
- docs/capabilities/comfyui-overview.md: sección 05b audio, fetch_output_audio en
  Outputs, Templates oficiales en Workflows I/O. (flux ya estaba documentada.)

fn index limpio (las 5 sin WARN); sin drift nuevo en fn doctor uses-functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 04:46:47 +02:00
egutierrez a1074d32e7 fix(test): corregir sys.path del test de comfyui_fetch_output_audio 2026-06-27 20:51:09 +02:00
egutierrez fd16453691 feat(ml): generación de audio en ComfyUI (ACE-Step) — comfyui_build_audio_workflow + comfyui_fetch_output_audio 2026-06-27 20:50:34 +02:00
egutierrez 5494507c39 chore: auto-commit (2 archivos)
- .mcp.json
- logs/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-27 20:43:03 +02:00
egutierrez dfb3eda087 merge(ml): comfyui_build_flux_workflow — builder Flux schnell+dev (custom-advanced) 2026-06-27 20:39:04 +02:00
egutierrez 83738d4035 merge(ml): comfyui_list_templates + comfyui_extract_template (extraer grafos de templates oficiales) 2026-06-27 20:37:18 +02:00
Egutierrez b77d223f01 feat(ml): comfyui_build_flux_workflow — builder Flux schnell+dev (camino custom-advanced)
Builder puro que arma el workflow ComfyUI de Flux en API format con el camino
canonico custom-advanced (UNETLoader + DualCLIPLoader[flux] + VAELoader ->
RandomNoise + KSamplerSelect + BasicScheduler -> BasicGuider ->
SamplerCustomAdvanced -> VAEDecode -> SaveImage).

- variant 'schnell' (~4 pasos, sin FluxGuidance) o 'dev' (~20 pasos, con
  FluxGuidance), con unet y steps por defecto por variante.
- Parametro 'available' opcional valida los modelos contra /object_info y lanza
  FileNotFoundError claro (que falta + carpeta) sin romper la pureza.
- width/height/seed/guidance/prefijo parametrizables.
- 11 tests unitarios (class_types schnell vs dev, defaults por variante, error
  path, determinismo). Verificado con generaciones reales (schnell 1024 y 768,
  dev 768x1024) que producen PNG en disco.
2026-06-27 20:36:55 +02:00
egutierrez e178ab8d2d feat(ml): comfyui_list_templates + comfyui_extract_template — extraer grafos de los templates oficiales de ComfyUI
Capitaliza el descubrimiento y extraccion de los workflow templates oficiales que
trae el paquete pip comfyui-workflow-templates 0.10.3 (los del menu Browse
Templates del frontend de ComfyUI). Hasta ahora no habia forma programatica de
listarlos ni extraer su grafo de nodos.

- comfyui_list_templates: lista los 451 templates reales (nombre, bundle/categoria,
  path, n_nodes, node_types). Filtra las ~16 entradas index* no-workflow.
- comfyui_extract_template: extrae el grafo + class_types de un template por nombre;
  to_api convierte a API format reusando comfyui_import_workflow_json.

Desde la 0.10.x el paquete es multi-bundle y ya no expone una carpeta templates/
unica; ambas funciones usan la API oficial comfyui_workflow_templates_core via el
interprete de ComfyUI. node_types aplana subgrafos y descarta los UUID de instancia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 20:35:46 +02:00
egutierrez cda36408d0 feat(ml): modelos con prefijo de categoría (IMG_/VIDEO_/3D_) + refs actualizadas
Renombra los 13 checkpoints/diffusion models de ComfyUI prefijando la
categoría al inicio del nombre, para que en el dropdown de carga el usuario
distinga de inmediato imagen/vídeo/3D y no cargue un modelo en el nodo
equivocado. Misma operación que se hizo con los LoRAs (report 0197) pero
sobre los modelos.

Clasificación:
- IMG_: dreamshaper_8, juggernaut_xl_v11, v1-5-pruned-emaonly-fp16,
  flux1-dev-fp8-e4m3fn, flux1-schnell-fp8-e4m3fn
- VIDEO_: svd, ltx-video-2b-v0.9.5, wan2.1_t2v_1.3B_fp16
- 3D_: stable_zero123, sv3d_p, hunyuan3d-dit-v2-mini, hunyuan3d-dit-v2-mv,
  hy3dgen/hunyuan3d-dit-v2-0-fp16 (mantiene subcarpeta)

A diferencia de los LoRAs aquí solo se PREFIJA la categoría conservando el
nombre completo (versión/arquitectura). Archivos físicos renombrados en
~/ComfyUI/models/checkpoints, /mnt/2tb/comfyui_models/{checkpoints,
diffusion_models} y la subcarpeta hy3dgen/. Mapa de reversión en
~/ComfyUI/models/checkpoints/_ckpt_rename_map.json.

Actualiza todas las refs (ckpt_name/unet_name + defaults + prosa) en los
builders gamedev/vídeo/3D, style presets, pipelines, tests y los workflows
de ComfyUI. Arregla de paso el default roto de comfyui_text_to_3d_oneshot
(apuntaba a v1-5-pruned-emaonly.safetensors inexistente; ahora al real
IMG_v1-5-pruned-emaonly-fp16.safetensors).

No tocados (justificado): repo-paths de HuggingFace en comfyui_install_3d_model
(<repo>/model.fp16.safetensors son rutas de descarga, no nombres de dropdown)
y el mock de stable-diffusion.cpp en test_genconfig_to_sdcpp_args.

Verificado: dropdowns CheckpointLoaderSimple + UNETLoader listan los nombres
con prefijo; 1 generación real con IMG_juggernaut_xl_v11 (node_errors vacío,
pixelart_00003_.png); 327 tests comfyui verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:24:52 +02:00
egutierrez 10dbc510b7 feat(ml): LoRAs con prefijo de arquitectura (SD15_/SDXL_/FLUX_) + refs actualizadas
Mueve el indicador de arquitectura del SUFIJO al PREFIJO del nombre de cada
LoRA para que el dropdown del LoraLoader muestre de inmediato que LoRA casa con
que checkpoint (evita el shape mismatch SD1.5 vs SDXL que crashea ComfyUI).

- 20 LoRAs renombradas en disco (15 SD15/SDXL en /mnt/2tb, 5 FLUX en ~/ComfyUI),
  mapa de reversion en ~/ComfyUI/models/loras/_rename_map.json.
- Refs actualizadas en builders gamedev-2d, style presets, pipelines, tests y
  docs/capabilities. Defaults hardcodeados (pixel-art, lcm-lora, etc.) apuntan a
  los nombres con prefijo.
- Ejemplos genericos en docstrings normalizados a la convencion de prefijo.
- comfyui_replicate_civitai_oneshot::_norm ignora el token de arquitectura al
  comparar, robusto al reordenado (sufijo civitai vs prefijo instalado).

Refs a repos HuggingFace (nerijs/pixel-art-xl) y checkpoints (juggernaut_xl_v11)
preservados. Verificado: dropdown LoraLoader con prefijos + generacion real
pixel-art OK + tests comfyui verdes (481 ml + 26 pipelines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:33:03 +02:00
agent d3d846f748 feat(ml): grupo comfyui-styles — catálogo curado + merge/dedup + generador LLM de estilos WAS
Tres funciones para gestionar y ampliar el repositorio de estilos del selector
WAS de ComfyUI (Prompt Styles Selector / Prompt Multiple Styles Selector):

- comfyui_curated_styles_catalog (pure): catálogo curado de 190 estilos en 13
  categorías (photography, render3d, painting, anime, pixel, illustration,
  comic, lighting, camera, material, scifi, fantasy, mood), formato WAS exacto.
- comfyui_append_styles (impure): merge+dedup no destructivo sobre el styles.json
  real, con backup atómico, validación de entradas y preservación de existentes.
- comfyui_generate_styles_llm (impure): genera estilos de una categoría vía
  ask_llm (grupo claude-direct); robusta (devuelve {} ante 429/JSON corrupto).

Aplicado en vivo: styles.json 269 -> 503 estilos (+190 curados +44 LLM),
backup hecho, selector verifica 503 en /object_info. Tests offline verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:50:25 +02:00
egutierrez a5748cb147 feat(gamedev): comfyui_generate_styled_asset_oneshot — aplica estilo a un asset con auto-post + amplía catálogo a 6 estilos
Pipeline one-shot que aplica un style preset curado a un asset en una llamada
(kind, subject, style_preset) y auto-ejecuta el post-proceso que el estilo declara:
los estilos pixelart (gameboy, pixel-art-retro) salen ya pixelizados del pipeline,
cerrando el hueco #1 del sistema de style presets (report 0190) donde el caller
tenía que llamar comfyui_pixelize_image a mano.

Reutiliza el dispatch _SUPPORTED (kind->builder) de comfyui_generate_asset_pack_oneshot
en vez de redefinir el mapa. Parte pura aislada en styled_asset_build_only para validar
kind/estilo desconocido sin tocar la GPU. Export a Godot consciente del post (pixelart
si hubo pixelize, para fijar el filtro Nearest).

Catálogo de estilos ampliado de 3 a 6: cyberpunk-neon (prompt puro SD1.5),
low-poly-flat (prompt puro SD1.5), cartoon-cel-shaded (LoRA anime_style_box_sd15 0.7).

Verificación: 11 tests offline del pipeline + suite de presets verde (27 passed).
Prueba real en GPU: mismo "treasure chest" en cyberpunk-neon, low-poly-flat y gameboy
one-shot; gameboy pasa de 17374 colores (crudo) a 4 (paleta Game Boy) auto-pixelizado
directo del pipeline. Detalle en reports/0191.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:50:30 +02:00
egutierrez 0eefb7cfcd feat(gamedev): sistema de style presets reutilizable (gameboy/ghibli/pixel-art-retro)
Calidad por ESTILO en vez de por tipo: un dev fija el look del juego una vez
y todos los assets salen coherentes. Diseño (A) datos puros + helper, no
pipeline monolítico (issue 0087, crecer por composición).

- comfyui_get_gamedev_style_preset(name=None): recetas curadas o catálogo.
  gameboy (sin LoRA, post pixelize paleta game-boy 4 tonos), ghibli (degrada
  a watercolor_style_sd15 gratis instalado, sin LoRA Ghibli gated), pixel-art-retro
  (reutiliza pixel-art-xl SDXL + juggernaut + post pixelize 16 colores). Extensible.
- comfyui_apply_style_preset(preset, subject): traduce a kwargs **spread-ables
  para cualquier builder de sujeto + size/transparent/post. Pura, no muta.
- 16 tests offline verdes. Validado e2e GPU: mismo 'knight character' en 3
  estilos visiblemente distintos (4 vs 78552 vs 16 colores).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:36:18 +02:00
egutierrez 9f0d2e2338 feat(gamedev): comfyui_generate_character_set_oneshot — set completo de un personaje coherente (2D + direccional 8-way + 3D)
Promueve a un pipeline one-shot la secuencia que hoy exige 4 llamadas a mano:
generar el set COMPLETO de un personaje de juego (imagen base 2D recortada,
sprite direccional N-way SV3D/Zero123 y malla 3D Hunyuan3D .glb), todos del
MISMO personaje. La coherencia cross-frontera se garantiza por construccion: el
direccional y el 3D parten de la MISMA base 2D aplanada (base_flat), no de tres
generaciones independientes. Es la culminacion de las 5 fronteras del grupo
gamedev-2d (issue 0087).

Compone builders del registry (enemy_creature/portrait_avatar/topdown_sprite
por introspeccion) + comfyui_flatten_alpha_on_color (nueva, aplana el sprite
recortado sobre fondo solido que SV3D/Hunyuan exigen) + comfyui_image_to_3d_oneshot
+ comfyui_build_directional_sprite_workflow + submit/wait/fetch + export Godot.
Secuencial liberando VRAM entre pasos pesados (3D antes que SV3D) para caber en
8 GB; fallo aislado deja set PARCIAL sin abortar.

Probado e2e en GPU (RTX 3070 8 GB) con 'armored paladin': base 2D RGBA 512
recortada + malla glTF 395600 triangulos + 8 vistas direccionales SV3D 576,
todos del mismo personaje. 9 tests offline verdes (incluye coherencia mockeada).
Ver reports/0189.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:02:24 +02:00
egutierrez 2bab120d7c feat(gamedev): comfyui_build_directional_sprite_workflow — sprite multi-direccional 2.5D (SV3D turntable / Zero123)
Builder puro (dict API format) que a partir del sprite frontal de un personaje
construye el workflow ComfyUI de N vistas direccionales consistentes (8-way
N/NE/E/SE/S/SW/W/NW o 4-way) rotando la figura en 3D. SV3D (orbit turntable) por
defecto, Stable Zero123 (batch por azimuth) como fallback de menor VRAM. Es el
puente 2.5D del catalogo gamedev-2d: consistencia rotacional real (el mismo
modelo rotado) frente a sprite_sheet (OpenPose 2D re-poza, identidad inconsistente).

Helper directional_sprite_view_order(directions) mapea frame i -> direccion i.
Funcion pura: solo construye el grafo; coste GPU al enviar con comfyui_submit_workflow.

Probado e2e en GPU: goblin enemy_creature_00001_ -> SV3D 8 direcciones elevation 15,
8 frames 576x576 en 75s, pico 7145/8192 MiB (prompt_id 8b9f75de). Consistencia
rotacional medida: MAE adyacentes 27 < frente-espalda 29.6, spread de paleta 3.83.

Report: reports/0187.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:36:38 +02:00
egutierrez d08667df9b docs(gamedev-2d): documentar animación de assets (vídeo) validada e2e
Añade la sub-sección "Animación de assets (vídeo) — caminos validados e2e" a la
capability page gamedev-2d con las tres vías para animar un asset 2D, todas
cabiendo en 8GB con la GPU vacía:

- txt2video LTX (comfyui_build_video_workflow model='ltx'): loop de elemento
  desde texto. Validado e2e: portal mágico 512×320, 25 frames, VRAM pico
  7717/8192 MiB (prompt_id 54eda033).
- img2vid SVD (comfyui_build_img2vid_workflow): animar un sprite/fondo ya
  generado. Validado e2e: enemy_creature del pack → 512×512 RGBA 14 frames
  animado, VRAM pico 7463/8192 MiB (prompt_id 5b501d03).
- txt2video Wan (enlazado + visible en /object_info, clip no generado aún).
- spritesheet AnimateDiff (ya validado en rondas previas).

Documenta el gate VRAM (el vídeo no convive con un juego AAA abierto), el gotcha
de comfyui_wait_result (lanza TimeoutError) y que SVD completa en GPU aunque el
script de orquestación expire (recuperar output sondeando /history). Los modelos
de vídeo de /mnt/2tb/comfyui_models ya estaban enlazados vía extra_model_paths.yaml
(verificado en /object_info, sin copiar, reversible). Evidencia: reports/0186.

No se creó builder nuevo: los builders del registry ya cubren el caso gamedev
(registry-first/KISS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:23:14 +02:00
egutierrez 9f1d643013 feat(gamedev): comfyui_build_outpaint_asset_workflow — extender el lienzo de un asset (outpaint)
Quinto vertice del eje transform de gamedev-2d. Funcion pura (dict API format)
que extiende el lienzo de un asset ya pintado por uno o varios lados y genera
contenido coherente mas alla de sus bordes via el nodo nativo ImagePadForOutpaint,
que ademas de ampliar el canvas EMITE la mascara feathered de la franja nueva (la
genera el grafo, no la recibe el caller — esa es la diferencia con inpaint_asset).

Compone comfyui_build_inpaint_workflow (base; su LoadImageMask se elimina y
VAEEncodeForInpaint se reconecta a las dos salidas del pad) + comfyui_inject_lora.

Probado e2e en GPU con SD1.5: seamless_00004 512x512 extendido right=256 -> 768x512
(prompt_id aa33de05), original conservado (diff 7.2) + franja nueva coherente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 04:59:50 +02:00
egutierrez 914def9e5c feat(gamedev): comfyui_build_inpaint_asset_workflow — editar solo una región de un asset (inpaint)
Cuarto vértice del eje transform de gamedev-2d: editar SOLO una región de un
asset 2D ya pintado vía inpaint, conservando el resto del sprite. Completa el
eje junto a txt2img (crear de cero), asset_variant (img2img: reescribe todo) y
sprite_from_sketch (ControlNet: sprite nuevo desde boceto).

Función pura (API format dict) que compone comfyui_build_inpaint_workflow (base)
+ comfyui_inject_lora (estilo opcional). Recibe asset + máscara (blanco=editar,
negro=conservar) + prompt de qué poner; VAEEncodeForInpaint codifica respetando
la máscara y dilata el borde grow_mask px para difuminar la costura; el KSampler
regenera solo esa zona. mode="noise_mask" degrada a VAEEncode+SetLatentNoiseMask
para servidores sin VAEEncodeForInpaint (error path). size escala imagen Y máscara
de forma consistente. class_types verificados contra /object_info (8GB lowvram).

Probado e2e en GPU con SD1.5: máscara circular sobre la mano del goblin
enemy_creature_00001_.png, prompt "a glowing blue magic orb" (prompt_id 88b52c66).
Solo la región enmascarada cambió: diff medio dentro 40.3 vs fuera 1.97 (ratio
20.4x), 44.6% px cambiados dentro vs 1.7% fuera. Confirmación visual: orbe azul
en la región, resto del goblin idéntico.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 04:45:50 +02:00
egutierrez 1012355998 feat(gamedev): comfyui_build_sprite_from_sketch_workflow — boceto→sprite vía ControlNet
Tercer eje del catálogo gamedev-2d: partir del DIBUJO del dev. Recibe un
boceto/lineart + un prompt de qué es y construye un workflow txt2img guiado por
ControlNet (lineart/scribble/canny) que pinta el sprite conservando la forma
dibujada. Distinto de los builders txt2img (inventan la forma desde texto) y de
asset_variant img2img (reescribe una imagen ya pintada conservando forma+color):
aquí el dev marca la silueta y la IA pone material/color/acabado, conservando
solo la forma.

Función pura (API format). Compone comfyui_build_txt2img_workflow +
comfyui_inject_controlnet + comfyui_inject_lora; el único código propio es el
helper que interpone el preprocesador (LineArt/Scribble/Canny) entre el boceto y
el ControlNet, análogo a _inject_image_scale del hermano asset_variant.

control_type selecciona preprocesador y modelo CN emparejado; controlnet_name y
preprocess dan override para degradar al modelo disponible. Gotcha documentado:
el server 8GB solo tiene modelos CN SD1.5 canny/depth/openpose — para
lineart/scribble usar override a canny o control_type=canny (pendiente humano
descargar los modelos lineart/scribble dedicados).

Verificación: tests offline verdes (cableado txt2img guiado, 3 control_types,
clamps, errores). E2E real GPU SD1.5: boceto del goblin → CannyEdgePreprocessor →
ControlNet canny → sprite que respeta pose/orejas/hombrera/lanza/espada del
dibujo (prompt_id ea6fc372, edge corr 0.545, luminance corr -0.19 confirmando
repintado). Report en reports/0182.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 04:31:41 +02:00
egutierrez 1585e986c1 feat(gamedev): comfyui_build_asset_variant_workflow — variantes img2img de un asset existente
Primer builder gamedev-2d de transformacion (img2img) en vez de generacion
(txt2img): parte de un asset ya generado y produce una variante coherente
(ice/fire/damaged/golden tier) cambiando material/paleta/estado y conservando
silueta, pose y composicion via denoise medio (~0.5). Compone
comfyui_build_img2img_workflow + comfyui_inject_lora + ImageScale opcional.

Probado e2e en GPU SD1.5: variante ice del goblin del demo pack
(prompt_id 5e4a5d3d) — silueta conservada (luminance corr 0.63) + paleta a
frio (blueness B-R -1.6 -> +1.9). Subseccion nueva en docs/capabilities y
report 0181.
2026-06-27 04:20:49 +02:00
egutierrez e1f1be02ce feat(gamedev): comfyui_generate_asset_pack_oneshot — set 2D coherente one-shot
Pipeline que genera un set de assets 2D de un mismo juego en una sola llamada,
compartiendo checkpoint, LoRA de estilo, estilo comun (inyectado al subject) y
seed derivada (base_seed + indice). Despacha 26 kinds gamedev-2d a sus builders
atomicos, encola/espera/descarga cada PNG y exporta opcionalmente a Godot.

Promocion de composicion a pipeline (issue 0087): el registry no crece inflando
builders, crece promoviendo la secuencia repetida 'N builders con el mismo estilo'
a un one-shot.

- Dispatch declarativo por kind con inyeccion de coherencia via inspect.signature
  (no hardcodea nombres de param; respeta LoRAs funcionales propios).
- Fail-fast si kind desconocido (sin tocar GPU); un OOM aislado no aborta el pack.
- 9 tests offline verdes (golden + edge + error).
- Probado e2e en GPU SD1.5 512: magic sword + goblin warrior, style dark fantasy
  hand-painted, seeds 42/43 -> 2/2 PNG 512x512 RGBA coherentes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 03:00:10 +02:00
egutierrez a27dcc028c docs(capabilities): unifica tag gamedev en gamedev-2d + separa gamedev-engine
El doctor reportaba el dominio gamedev en doble FAIL: el tag plano `gamedev`
(44 funciones) como `ungrouped_candidate` y la pagina `gamedev-2d.md` como
`doc_orphan`. Causa raiz: el INDEX declaraba `[gamedev](gamedev-2d.md)` y el
auditor solo registra el slug cuando label==target, asi que ni casaba la
pagina ni declaraba el tag.

Al revisar las 44 funciones habia dos clusters reales bajo el mismo tag, asi
que se separan en dos grupos honestos:

- gamedev-2d (tag canonico): 31 builders de workflow ComfyUI + 5 de apoyo
  (post-proceso + puente a Godot) = 36. Se elimina el tag plano `gamedev` de
  los builders (ya tenian `gamedev-2d`) y se reemplaza por `gamedev-2d` en las
  de apoyo.
- gamedev-engine (grupo nuevo, pagina madre nueva): runtime de juego C++
  multiplataforma (SDL3 + sokol_gfx + miniaudio, Issue 0072b) = 8. Game loop,
  camara 2D, input unificado, sprite batch, setup render/audio, build wasm.

El tag plano `gamedev` queda eliminado (count 0). INDEX corregido: fila
gamedev-2d con label==target y conteo 36 + fila nueva gamedev-engine (8).

Verificacion: `fn index` + `fn doctor capabilities` -> ambos grupos OK
(declared_in_index=yes, doc_exists=yes, sin issues); `gamedev` plano = 0.
Solo se modifico el campo `tags` de los .md, ningun archivo de codigo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:40:50 +02:00
egutierrez 8a4cc323a3 feat(gamedev): comfyui_build_weather_overlay_workflow — overlays de clima/atmósfera
Builder puro (dict API format) de capas de clima a pantalla completa: lluvia,
niebla, nieve, god rays, polvo, viñeta de tormenta. Capa de cobertura total
apaisada (16:9, 1024x576) que se superpone sobre la escena con blend del motor.

Dos modos via on_black: True (defecto) genera el clima brillante sobre negro
puro como insumo de comfyui_matting_luma_to_alpha (blend aditivo/screen);
False genera una pelicula translucida semi-transparente (multiply/overlay).
NO inyecta Rembg: el matting de una capa es luma->alpha de disco. Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora.

Diferenciado de decal_overlay (mancha localizada) y vfx_spritesheet (secuencia
animada de un efecto puntual): aqui es una pelicula estatica de cobertura
full-screen que el motor anima por scroll/loop/shader.

10 tests offline verdes. Probado e2e en GPU SD1.5 8GB lowvram: heavy rain
on_black seed 11 1024x576 (16:9 exacto), estrias brillantes sobre negro plano
(esquinas luma 0.00, dark 89.6%) apto luma->alpha (prompt_id 5d2300d1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:26:39 +02:00
egutierrez 2a7c77cb56 feat(gamedev): comfyui_build_achievement_badge_workflow — insignias/medallas/logros
Nuevo builder del grupo gamedev-2d para insignias de logro / medallas / trofeos de
sistemas de achievements/recompensas, con tier metálico (bronce/plata/oro/platino/
diamante) y fondo recortable a alpha. Función pura (dict API format) que compone
comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg.

Hermano de item_icon (objeto de inventario suelto) y skill_tree_node (nodo enmarcado
de la rejilla de talentos): aquí el asset es la insignia de logro/recompensa = medalla
con cinta + tier. El tier metálico y la forma de medalla/trofeo son la firma.

12 tests offline verdes. Probado e2e en GPU con SD1.5: badge="dragon slayer" tier gold
seed 77 256x256 RGBA, medalla circular dorada con emblema centrado y fondo recortado a
alpha (esquina α=0, centro α=254; prompt_id 8b8b7ede).
2026-06-27 02:18:12 +02:00
egutierrez fa94f7a235 feat(gamedev): comfyui_build_trap_hazard_workflow — trampas/peligros de escenario
Builder ComfyUI puro (dict API format) para UNA trampa/peligro JUGABLE de nivel
(pinchos, sierra giratoria, foso de lava, placa de presion, llamas, trampa de
flechas, charco acido, descarga electrica): objeto de peligro aislado y centrado
a perspectiva de juego (side/top-down/iso via view), fondo limpio recortable a
alpha, estilo consistente para poblar niveles.

Hermano de comfyui_build_prop_object/structure/foliage_set_workflow: mismo patron
que compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional)
+ Image Rembg (alpha si transparent). Diferenciado de prop_object (peligro con
hitbox de dano vs decoracion inerte) y enemy_creature (trampa vs enemigo vivo);
el negativo rechaza character/person/creature/multiple objects.

Gotcha documentado: para hazards puramente etereos (llamas/electricidad/gas) usar
transparent=False + comfyui_matting_luma_to_alpha (conserva el falloff), no Rembg.

12 tests offline en verde. Probado e2e en GPU con SD1.5 — spiked floor trap side
512x512 RGBA, mecanismo de peligro centrado recortado a alpha real (alpha extrema
0-255, prompt_id ab1b1560).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:10:26 +02:00
egutierrez 0ce1c31fb9 feat(gamedev): comfyui_build_skill_tree_node_workflow — nodos de arbol de habilidades/talentos
Builder puro del grupo gamedev-2d: nodo de skill tree (icono de habilidad dentro de
un marco circular/hexagonal/rombo/escudo) con variante de estado visual (unlocked
brillante / locked gris), centrado, recortable a alpha. Diferenciado de item_icon
(objeto suelto sin marco), status_effect_icon (simbolo superpuesto sin marco) y
ui_hud (chrome grande): el marco y el estado son la firma del asset. Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora + Image Rembg, sin reescribir el
grafo. 11 tests offline en verde. Probado e2e SD1.5 8GB lowvram: fireball hexagonal
unlocked 256x256 RGBA, prompt_id cf36b2ea, nodo enmarcado brillante centrado
(reports/0173).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:03:07 +02:00
egutierrez 5a0818ee9c feat(gamedev): comfyui_build_rune_glyph_workflow — runas/glifos/sigilos mágicos (símbolo arcano aislado sobre negro; glow=True -> luma->alpha conserva resplandor para blend aditivo, sin Rembg; glow=False runa mate; hermano de status_effect_icon/decal_overlay; probado e2e SD1.5 prompt_id 701d149a)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:55:27 +02:00
egutierrez 1a8093a7be feat(gamedev): comfyui_build_dialogue_box_workflow — caja de diálogo/bocadillo/panel de texto
Builder del contenedor de diálogo de juego (RPG, visual novel, aventura): marco
apaisado (768x256) con borde decorativo e interior plano reservado para el texto
del motor. Distinto de ui_hud (elementos sueltos): es el panel-contenedor completo.
Compone txt2img + inject_lora + Image Rembg (alpha). Pura, dict API format.
7 tests offline verdes; 1 generación real en GPU (medieval wood+gold, 768x256 RGBA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:48:00 +02:00
egutierrez ba302dd793 feat(gamedev): comfyui_build_foliage_set_workflow — vegetación/foliage de escenario (árbol, arbusto, hierba, flores, helecho, hongo, cactus, juncos; elemento de naturaleza aislado vista side/top, fondo limpio recortable a alpha; diferenciado de prop_object/structure)
Builder puro hermano de prop_object/structure: compone txt2img + inject_lora (estilo opcional) + Image Rembg (alpha). Scaffold '{plant}, {view} view, {style}, single plant element, centered, plain background, game nature asset'. Negativo rechaza manufacturado/edificio/persona/bosque/maceta para mantener UN elemento vegetal orgánico aislado.

13 tests offline verdes + generación real e2e (golden 'a glowing mushroom' seed 7, prompt_id 8fb65a51, RGBA recortable centrado). Dos gotchas reales SD1.5+Rembg documentados (planta grande->paisaje; follaje claro->Rembg come hojas) con evidencia en reports/0170.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:41:34 +02:00
egutierrez 0421bc6d4f feat(gamedev): comfyui_build_vehicle_mount_workflow — vehículos/monturas que el personaje usa o conduce (caballo, dragón-montura, nave, coche, barco, carro, grifo; vehículo completo vista side/iso, SIN jinete, alpha recortable)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:32:42 +02:00
egutierrez 5662a54fa7 feat(gamedev): comfyui_build_world_map_workflow — mapa de mundo/nivel ilustrado (lámina cartográfica cenital fantasy, regiones rotuladas, borde ornamental, cuadrado por defecto, hires opcional)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:19:17 +02:00
Egutierrez b45165dbc5 feat(gamedev): comfyui_build_title_lettering_workflow — texto/logo de título de juego (lettering estilizado, apaisado, alpha)
Builder puro (dict API format) hermano de ui_hud/splash_art: compone
comfyui_build_txt2img_workflow + comfyui_inject_lora + Image Rembg. Renderiza
el nombre del juego/una palabra con un tratamiento de lettering (metálico,
tallado, neón, fuego...), formato apaisado 1024x512, fondo recortable a alpha.

El negativo NO rechaza texto (el lettering es el sujeto). Documentada la
limitación clave: la difusión no garantiza la ortografía exacta del texto
(letras de más/deformadas; una palabra-objeto como DRAGON se ilustra en vez de
escribirse). Mitigaciones: palabras cortas en mayúscula, re-roll de seeds,
SDXL > SD1.5, o pintar el texto real en el motor.

Tests 9/9 verde (offline). Verificado e2e en GPU (8GB lowvram): DRAGON/fire
engraved (SD1.5, prompt_id 6f3920b7) y AETHER/epic fantasy metallic (SDXL,
prompt_id 2a7fe8ba, logo metálico dorado + alpha). Fila en
docs/capabilities/gamedev-2d.md. Report en reports/0165.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:10:55 +02:00
egutierrez dbb040aa12 feat(gamedev): comfyui_build_structure_workflow — edificios/estructuras de escenario (building completo, view iso/lateral, alpha)
Builder gamedev-2d nuevo: edificacion grande y completa (casa, torre, castillo, tienda,
posada, ruina, muralla, puente, templo, faro) para poblar mapas/escenarios. Diferenciado
de comfyui_build_prop_object (edificio completo vs objeto pequeno suelto): el negativo
rechaza small object/single item/prop/furniture y el scaffold empuja full building/
complete structure/single building. view (iso por defecto) fija la perspectiva del mapa.

Pura (dict API format): compone comfyui_build_txt2img_workflow + comfyui_inject_lora
(estilo/iso opcional) + Image Rembg (alpha si transparent). 12 tests offline verdes.
Probado e2e en GPU (8GB lowvram): medieval blacksmith shop iso 512x512 RGBA, edificio
centrado (centroide 0.54/0.53). Fila en docs/capabilities/gamedev-2d.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:01:15 +02:00
egutierrez 91cf683289 feat(gamedev): comfyui_build_particle_texture_workflow — texturas de partícula individuales (chispa/humo/polvo/destello, sobre negro, luma→alpha, size 256)
Builder PURO (dict API format) del grupo gamedev-2d/gamedev-vfx: UNA textura de
partícula reutilizable que el sistema de partículas del motor (Godot GPUParticles2D,
Unity VFX Graph) instancia a miles. Aislada sobre fondo negro puro, pensada para
luma→alpha (comfyui_matting_luma_to_alpha, additive blend); soft controla el borde
(glow difuso vs nítido); NO inyecta Rembg (rompería el falloff); size 256 por defecto.
Diferenciada de vfx_spritesheet (secuencia animada) y decal_overlay (mancha estática).
Compone comfyui_build_txt2img_workflow + comfyui_inject_lora. 9 tests offline verdes.
Generación real verificada e2e en GPU (spark sobre negro plano + luma→alpha RGBA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:54:58 +02:00
egutierrez 696148d56b feat(gamedev): comfyui_build_status_effect_icon_workflow — iconos de estado/buff-debuff (símbolo compacto legible a tamaño reducido, size 256, Rembg alpha)
Builder hermano de item_icon/ui_hud, diferenciado por rol: símbolo de estado
compacto (veneno/escudo/velocidad...) optimizado para 16-32 px, no objeto de
inventario ni chrome de interfaz. Pura (dict API format), 8 tests offline,
1 generación real verificada (poison 256x256 RGBA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:48:24 +02:00
egutierrez 19ad2b3e5d feat(gamedev): comfyui_build_projectile_workflow — proyectiles/balas/hechizos orientados (glow→luma-alpha, sólido→rembg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:41:52 +02:00
egutierrez b88730b7cb feat(gamedev): comfyui_build_decal_overlay_workflow — decals/overlays con alpha (luma→alpha sobre negro)
Builder puro (dict API format) para texturas de superposicion (sangre, grietas,
suciedad, grunge, oxido, quemaduras, salpicaduras): genera el decal aislado sobre
fondo plano (negro por defecto), pensado para extraer alpha con
comfyui_matting_luma_to_alpha (luminancia=alpha, conserva el falloff de translucidos).
NO inyecta Rembg (el matting es luma->alpha de disco, no un nodo). Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora. 9 tests offline verdes;
generacion real verificada e2e en GPU (8GB lowvram, SD1.5, prompt_id 109907a4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:34:53 +02:00
egutierrez 6add50311b feat(gamedev): comfyui_build_splash_art_workflow — splash/loading screen key art (apaisado 16:9, cinematográfico, espacio para título)
Builder PURO (dict API format) del grupo gamedev/gamedev-2d, hermano de
comfyui_build_card_art_workflow y comfyui_build_parallax_background_workflow.

Genera la ilustración grande de una pantalla de portada / loading screen / key
art en formato pantalla apaisado 16:9 (~1024x576), composición cinematográfica
(wide shot) con aire para superponer el título del juego. Compone
comfyui_build_hires_fix_workflow (si hires) o comfyui_build_txt2img_workflow +
comfyui_inject_lora (estilo opcional). Genera SOLO la ilustración: el negativo
por defecto rechaza text/title/logo/UI/frame para que el motor componga el
título encima.

- 9 tests offline verde (golden hires, apaisado width>height, batch_size, sin
  hires, dims/mood/lora reflejados, error scene vacío, determinismo).
- .md autosuficiente (Ejemplo + Cuando usarla + Gotchas) + fila en
  docs/capabilities/gamedev-2d.md.
- Probado e2e en GPU 8GB lowvram: 1 splash real (héroe ante castillo oscuro en
  tormenta), 1024x576 -> 1536x864 (16:9 exacto) tras hires, 54s, SD1.5
  dreamshaper_8. Evidencia en reports/0159.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:23:16 +02:00
Egutierrez ab27c253c5 fix(gamedev): card_art hires roto (UltimateSDUpscale exige batch_size) + INDEX conteo gamedev 10->20
El nodo UltimateSDUpscale declara batch_size como input requerido en /object_info;
comfyui_build_hires_fix_workflow y comfyui_inject_hires_fix no lo proveian, por lo
que card_art con hires=True fallaba en runtime. Se anade batch_size: 1 a ambos
constructores + guards de regresion en los tests (card_art golden hires, builder e
inject). Verificado con una generacion real en ComfyUI (carta 768x1152, sin
node_errors, prompt_id 4033fb0b). Bump de version 1.0.0->1.0.1 en ambos .md con
growth log y gotcha.

INDEX.md: la fila gamedev decia count=10; el cluster de assets 2D documentado en
gamedev-2d.md tiene 20 funciones (15 builders tag gamedev-2d + 5 de apoyo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:00:38 +02:00
egutierrez 8fb10fdf8a feat(gamedev): comfyui_build_topdown_sprite_workflow — sprite vista cenital (top-down RPG, direccion, alpha)
Builder puro (dict API format) del grupo gamedev: sprite de personaje/objeto en
vista cenital (top-down) estilo RPG clasico/roguelike, visto desde arriba,
centrado, fondo limpio recortable a alpha. Argumento direction (south/north/east/
west) para el set de sprites de movimiento. Compone comfyui_build_txt2img_workflow
+ comfyui_inject_lora (estilo opcional) + Image Rembg (alpha). Diferenciado de
comfyui_build_sprite_sheet_workflow (vista lateral/frontal): el negativo por
defecto rechaza side/front/isometric/perspective para forzar la cenital.

Probado e2e en GPU con SD1.5 (8GB lowvram): caballero cenital, fondo transparente
(reports/0157). 10 tests offline verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:49:24 +02:00
egutierrez 0c1d2aa4fc feat(gamedev): comfyui_build_prop_object_workflow — props/objetos de escenario (objeto de mundo, alpha, perspectiva de juego)
Builder PURO del grupo gamedev: dict API format de un prop/objeto de escenario
(barril, cofre, antorcha, planta, mueble, roca, fuente, estatua). Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora opcional + Image Rembg.
Diferenciado de item_icon: objeto de MUNDO (escala de escena, perspectiva
iso/lateral) vs icono plano de inventario. 10 tests offline verdes; 1 generacion
real en GPU (cofre del tesoro, RGBA 512x512, fondo recortado). reports/0155.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:36:21 +02:00
egutierrez 2ff111bae4 feat(browser): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-26 23:30:35 +02:00
egutierrez d7387d9d2c feat(gamedev): comfyui_build_enemy_creature_workflow — enemigos/criaturas (cuerpo entero, alpha, variantes)
Nuevo builder del grupo gamedev/gamedev-2d para enemigos/criaturas de juego
(goblin, esqueleto, slime, dragon, boss, elemental): figura de cuerpo entero,
centrada, fondo limpio recortable a alpha, estilo consistente entre criaturas
del bestiario. Variantes por nivel/elemento (ice, fire, elite, corrupted) via
el argumento variant, que se antepone a la criatura base.

Funcion pura (dict API format) que compone funciones existentes del registry:
comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) +
Image Rembg (fondo transparente). Hermano de comfyui_build_item_icon /
ui_hud / sprite_sheet_workflow.

- 8 tests offline verdes (golden/edge/error/determinismo)
- .md autosuficiente (Ejemplo + Cuando usarla + Gotchas)
- fila en docs/capabilities/gamedev-2d.md
- probado e2e en GPU (8GB lowvram, SD1.5): goblin warrior ice cuerpo entero,
  512x512 RGBA con alpha, prompt_id e770d050 (report 0154)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:29:54 +02:00
egutierrez 03df14df97 feat(gamedev): comfyui_build_card_art_workflow — arte de carta TCG (ilustración vertical)
Nuevo builder PURO (dict API format) del grupo gamedev-2d para la ilustración
central de una carta coleccionable (criatura/personaje/hechizo) en formato
vertical de carta (~512x768), composición centrada y dramática. Compone
comfyui_build_txt2img_workflow / comfyui_build_hires_fix_workflow (hires opcional)
+ comfyui_inject_lora (estilo opcional). Genera solo la ilustración; marco/título/
stats son composición del motor/post (el negativo por defecto los rechaza).

- 8 tests offline verdes (golden hires + edges dims/style/lora + error + determinismo).
- Generación real en GPU verificada: dragón de fuego 512x768 vertical, SD1.5, 5s
  (prompt_id 010dcdae-..., reports/0153).
- Fila en docs/capabilities/gamedev-2d.md.

Gap: el path hires=True falla hoy por bug del builder hermano
comfyui_build_hires_fix_workflow (nodo UltimateSDUpscale exige batch_size que el
builder no emite); abierta proposal improve_function. Usar hires=False hasta el fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:22:36 +02:00
egutierrez d0960bed70 feat(browser): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-26 23:15:30 +02:00
egutierrez 0dd2718c95 feat(gamedev): comfyui_build_ui_hud_workflow — elementos de UI/HUD (botones/marcos/barras)
Builder puro hermano de comfyui_build_item_icon_workflow: construye el dict (API
format) del workflow de UN elemento de interfaz/HUD de juego (botón, marco/panel,
barra de vida/maná/XP, icono de UI, cursor, viñeta de menú). Pieza única centrada,
fondo limpio recortable a alpha. Compone comfyui_build_txt2img_workflow +
comfyui_inject_lora + Image Rembg.

Tests offline 7/7 verdes (golden + 4 edge + error + determinismo). Generación real
verificada en GPU (8GB lowvram): ornate health bar frame -> PNG 512x512 RGBA con
alpha recortado (reports/0152). Fila añadida en docs/capabilities/gamedev-2d.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:14:48 +02:00
egutierrez 4c4eec4b1d feat(browser): auto-commit con 6 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-26 23:09:32 +02:00
egutierrez f5387aa30e feat(gamedev): comfyui_build_emote_workflow — emotes/expresiones de personaje
Builder puro (dict API format) del grupo gamedev: genera el workflow de UN
emote/expresion facial del mismo personaje (alegre, triste, enfadado,
sorprendido, neutral) para sistema de dialogo, retratos reactivos o emotes de
chat. La clave es la consistencia del personaje entre expresiones: ref_face
encadena IPAdapter-FaceID para que varie solo la expresion y el rostro sea el
mismo; facedetailer regenera la cara conservando la expresion.

Compone comfyui_build_ipadapter_workflow / comfyui_build_txt2img_workflow +
comfyui_inject_lora + comfyui_build_facedetailer_workflow. Hermano de
comfyui_build_portrait_avatar/sprite_sheet_workflow.

12 tests offline verdes (golden + edge + error) y 1 generacion real verificada
en GPU (8GB lowvram): la expresion happy/smiling se lee claramente. Fila en
docs/capabilities/gamedev-2d.md. Evidencia en reports/0151.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:08:49 +02:00
egutierrez 3980fbbffb feat(gamedev): comfyui_build_normal_map_workflow — normal/depth map de sprite para 2.5D
Builder puro (dict API format) que genera el normal map (o depth/height) de un
sprite existente para iluminacion dinamica 2.5D (Godot normal_map, Unity sprite
normal). Pipeline LoadImage -> preprocesador controlnet_aux -> SaveImage, ~0 VRAM.

method selecciona el nodo (verificados contra /object_info):
- normal (default): BAE-NormalMapPreprocessor, normal canonico azul/violeta usable
  directo en motor.
- normal_midas: MiDaS, unico con control de intensidad (strength -> a).
- normal_dsine: DSINE. depth: DepthAnythingV2 (height en gris).

Nodos de normal NATIVOS, sin necesidad de depth->Sobel post (gap innecesario).
11 tests offline verdes. Probado e2e en GPU (8GB): normal map de un icono de
pocion, prompt_id d47f9943, tono azul/violeta verificado. Report 0150.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:01:08 +02:00
egutierrez 4886305d49 feat(gamedev): comfyui_build_parallax_background_workflow — fondo en capas para parallax 2.5D
Builder puro (dict API format) del grupo gamedev-2d: genera el fondo apaisado
(txt2img) y su mapa de profundidad (DepthAnythingV2Preprocessor sobre el VAEDecode),
guardando ambos como PNG. El corte en N bandas por rango de profundidad queda como
post-proceso documentado (gap split_parallax_layers). Compone
comfyui_build_txt2img_workflow. 8 tests offline verdes; probado e2e en GPU
(RTX 3070 8GB lowvram): fondo de bosque + depth map, prompt_id
11763613-33cf-4f63-8405-34f75c1c89be. class_types verificados contra /object_info.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:50:07 +02:00
egutierrez 404e2e4d0c feat(gamedev): comfyui_build_portrait_avatar_workflow — retratos/avatares de personaje
Builder puro (dict API format) hermano de sprite_sheet/item_icon: retrato/avatar
de personaje (busto centrado, cara al espectador, fondo simple con vinheta) para
dialogo, perfil o seleccion de personaje. Con ref_face encadena IPAdapter-FaceID
(rostro consistente entre retratos); con facedetailer regenera la cara con detalle
(FaceDetailer de Impact-Pack). Compone txt2img/ipadapter + inject_lora + facedetailer.

9 tests offline en verde + 1 generacion real verificada (mujer caballero pelirroja,
cara nitida). Fila en docs/capabilities/gamedev-2d.md. Report 0148.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:40:30 +02:00
egutierrez 3f465aceed feat(gamedev): comfyui_build_item_icon_workflow — iconos de inventario
Builder puro hermano de pixelart/sprite_sheet/seamless_tile: arma el dict
(API format) para iconos de items (espada/pocion/anillo/libro/escudo).
txt2img cuadrado + prompt scaffold de icono + LoRA estilo opcional + Rembg
(alpha). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora.

Test offline 7/7 verde. Generacion real verificada (icono de pocion de
salud centrado, RGBA fondo recortado, prompt_id 70b7a52a, 512x512 SD1.5).
Fila en docs/capabilities/gamedev-2d.md. Detalle en report 0147.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:31:59 +02:00
egutierrez 3be8b28a8f feat(orchestration): fleet_send_text — nudge fiable por pane_id estable
El nudge del orquestador apuntaba al window_id (@N) de tmux, que migra cuando
el focus-swap de FleetView recrea windows (break-pane/join-pane): el texto
acababa en el window equivocado o en otro agente (a veces no llega). Ademas,
texto y Enter en la misma invocacion hacian que el TUI no interpretara el submit.

Nueva funcion fleet_send_text_bash_infra (grupo orchestration) que:
- resuelve el pane_id (%N) estable fresco justo antes de enviar (sessionId/PID
  a pane via tmux list-panes -a + walk de ancestros /proc), no el @N volatil;
- manda texto literal y Enter en invocaciones separadas;
- verifica con capture-pane que el texto llego antes del submit, con reintento;
- guards anti-self y error claro si el target no resuelve a un pane vivo.

Test (19/19) sobre socket tmux propio: confirma que tras break-pane el pane_id
no migra y el reenvio sigue llegando. orchestration.md (seccion Nudge + catalogo)
actualizado para usar la funcion en lugar del send-keys -t <@N> manual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:08:47 +02:00
egutierrez aeefd09f19 feat(gamedev): ronda 2b — 5 builders de workflow 2D (pixelart/seamless/iso/sprite/vfx)
Cinco builders puros que devuelven dict API format, cada uno componiendo funciones
existentes del registry (comfyui_build_txt2img_workflow, comfyui_inject_*,
comfyui_build_ipadapter_workflow). class_types verificados contra /object_info.
Probados e2e en GPU (8GB lowvram): pixelart (pixel-perfect), seamless (sin costura),
vfx (AnimateDiff loop -> luma-alpha -> spritesheet RGBA). 30 tests offline verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:16:16 +02:00
egutierrez e57da2f6d5 feat(gamedev): ronda 1 — pixelize + luma→alpha + export-godot (grupo gamedev)
Tres funciones CPU-only del lote gamedev 2D + 2 helpers puros + grupo de capacidad:

- comfyui_pixelize_image_py_ml (impure): Fase 2 pixelart — downscale nearest +
  cuantizacion a N colores / paleta fija (game-boy/pico-8/nes) + re-upscale nearest.
- comfyui_matting_luma_to_alpha_py_ml (impure): frame VFX sobre negro -> RGBA por
  luminancia ponderada (translucidos con additive blend).
- comfyui_export_asset_to_godot_py_pipelines (impure): puente ComfyUI -> Godot 4 —
  copia a res://assets/<dir> por kind + .import por tipo + filtro Nearest si pixelart
  + reimport headless best-effort. Compone los 2 helpers puros.
- godot_map_asset_dir_py_core, godot_clean_asset_name_py_core (pure): nucleos
  reutilizables del pipeline.
- docs/capabilities/gamedev-2d.md + INDEX: grupo nuevo gamedev.

Tests 33/33 verdes (offline PIL/numpy). Golden real verificado: asset de
~/ComfyUI/output -> /tmp/godot_test_proj con .import correcto y reimport headless
real de Godot 4.7. Sin GPU, sin red, sin tocar proyectos del usuario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:43:47 +02:00
egutierrez 9508fff282 docs: añade diseño de integración ComfyUI → Godot (puente de assets)
Mapa de ambas estructuras (output/ de ComfyUI + res://assets/ de Godot 4),
tabla tipo-de-asset → carpeta destino → import settings clave, y propuesta
de pipeline export_asset_to_godot que compone helpers atómicos + reimport
headless (gap confirmado: 0 funciones godot en el registry).

Documenta el gotcha de Godot 4: el filtro Nearest del pixelart se setea
global (default_texture_filter=0) o por override, no por .import por defecto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:25:29 +02:00
egutierrez 8121e4b04e chore: auto-commit (1 archivos)
- .mcp.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-25 00:26:34 +02:00
egutierrez 4302212b34 feat(ml): implementa camino sv3d en comfyui_generate_views_from_image
Completa la rama method='sv3d' (antes NotImplementedError) componiendo el
workflow SV3D nativo de ComfyUI (SV3D_Conditioning + VideoLinearCFGGuidance +
KSampler + VAEDecode + SaveImage): una imagen produce un orbit de N frames
equiespaciados en 360 grados en una pasada.

- _METHOD_CKPT['sv3d'] acepta sv3d_p (preferido) o sv3d_u; nuevo helper
  _resolve_ckpt sustituye a _method_ckpt_key.
- nuevos params keyword-only video_frames=21, sv3d_width=576, sv3d_height=576
  (configurables para densidad de orbit y control de VRAM).
- salida sv3d extendida con frames (orbit completo) + frame_count; views mapea
  cada azimuth al frame del orbit mas cercano (cardinales para multi-vista).
- _collect_views_sv3d + helpers compartidos _history_images/_fetch_or_name;
  _collect_views (zero123) refactorizado para reusarlos.

Probado en GPU (8 GB lowvram): sv3d_p.safetensors descargado a checkpoints/,
21 frames 576x576 en ~75 s, peak ~5.7 GB, sin OOM
(prompt_id 0caeedf4-baa0-4c8f-844a-867490ac4f85). Detalle en report 0128.

Bumpa version 1.0.0 -> 1.1.0 + Capability growth log. Pagina madre comfyui.md
marca ambos caminos (zero123/sv3d) operativos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:57:10 +02:00
egutierrez 394221f8c7 feat(ml): pipeline replicar imagen desde link de Civitai
Nueva capacidad del grupo comfyui: dado el id/URL de una imagen de Civitai,
extrae cómo se generó (prompt, modelo, sampler, LoRAs) vía los endpoints tRPC
image.getGenerationData + image.get (la API v1 da meta=null), reconstruye el
workflow y lo replica en nuestro ComfyUI, sustituyendo el checkpoint ausente por
el más parecido instalado y reportando lo que falta en missing_models sin bajar
nada a ciegas. Respeta SFW.

Funciones nuevas (registry-first, componen 8 funciones existentes):
- comfyui_fetch_civitai_image_meta_py_ml (impura): observa la receta por id/URL.
- comfyui_map_a1111_params_py_ml (pura): traduce meta A1111 -> params ComfyUI,
  familia del modelo y LoRAs.
- comfyui_replicate_civitai_oneshot_py_pipelines: orquesta fetch_meta ->
  map_a1111_params -> build/embebido -> run_foreign_workflow_oneshot -> judge.

Probado en vivo (imagen SFW 23526611): receta extraída + réplica 1024x1024
generada + panel de jueces. 12 tests unitarios verdes. Capability page comfyui.md
actualizada. Report 0127.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:25:31 +02:00
egutierrez 69d9aed46a feat(ml): mixer de capacidades comfyui (compose + generate_mixed_oneshot + inject controlnet/ipadapter)
Mezclador del grupo comfyui-skill que promueve a una sola llamada la secuencia
base -> compose -> submit -> wait -> fetch -> judge (issue 0087):

- comfyui_compose_capabilities_py_ml (PURA): aplica en orden las capacidades
  activadas (loras, controlnet, ipadapter, facedetailer, hires) sobre un
  workflow base, sin mutar la entrada.
- comfyui_generate_mixed_oneshot_py_pipelines: one-shot que resuelve el base
  (skill/txt2img/dict), compone, encola, espera, descarga el PNG y lo puntua
  con el panel comfyui-judge.
- comfyui_inject_controlnet_py_ml, comfyui_inject_ipadapter_py_ml: inyectores
  encadenables que consume el compose.
- Tests (24 passed) + pagina madre docs/capabilities/comfyui-skill.md.

Prueba real en GPU: txt2img dreamshaper_8 + 2 LoRAs (3d_render_redmond +
detail_tweaker) + FaceDetailer -> imagen 512x512 en ~24s, juez verdict 'good'
(score 4.69, votos aesthetic+clip good; voto llm degradado por rate-limit 429).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:02:10 +02:00
egutierrez c36c80dda9 docs(comfyui-skill): añade comfyui_inject_multi_lora + comfyui_build_ipadapter_workflow a la página madre 2026-06-24 17:51:03 +02:00
egutierrez 3887e59092 feat(ml): auto-commit con 6 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 17:47:28 +02:00
agent d5660aa13f docs(comfyui): añade capacidad 10 ipadapter/referencia (en construcción) al overview
El flujo de funciones+server está creando comfyui_build_ipadapter_workflow e
comfyui_inject_multi_lora (vistos sin indexar en python/functions/ml/ el
24/06/2026). Se documentan como capacidad emergente para que el mapa esté
completo; sus IDs reales se rellenarán cuando se ejecute fn index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:46:45 +02:00
agent a56b6e36ea docs(comfyui): comfyui-overview — mapa cross-grupo de capacidades de generación
Indexa las 58 funciones del stack ComfyUI (grupos comfyui + comfyui-skill +
comfyui-judge) por capacidad: txt2img, img2img/inpaint, controlnet,
skills/multiestilo-LoRA, video, upscale/detail, 3D, juez/calidad y
operación/infra. Cada capacidad mapea a sus builders/pipelines del registry,
grafos UI y skills. Añade fila en docs/capabilities/INDEX.md.

El catálogo navegable con los grafos en disco (reorganizados en subcarpetas
por capacidad bajo ~/ComfyUI/user/default/workflows/) vive fuera del repo en
~/ComfyUI/CAPABILITIES.md (no versionado).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:45:59 +02:00
egutierrez 5f0df32728 fix(browser): limpiar previews/outputs residuales al cargar workflow ComfyUI
app.loadApiJson (lo que usa comfyui_load_workflow_ui) reconstruye el grafo pero
no llama a app.clean(), por lo que no resetea el store app.nodeOutputs ni los
previews de los nodos. Cuando un workflow nuevo reusa un node_id existente en el
store, el preview cacheado del workflow anterior se re-pinta sobre el nodo nuevo
(visto: imagen 3D pegada bajo un CheckpointLoaderSimple/SaveGLB).

- Nueva funcion comfyui_clear_node_outputs_ui: limpieza no destructiva del store
  app.nodeOutputs + node.imgs/images, sin tocar la topologia del grafo.
- comfyui_load_workflow_ui v1.1.0: anade clear_outputs=True (default) que invoca
  la limpieza antes de loadApiJson, replicando la garantia de loadGraphData.

Reproducido y verificado en la UI real (CDP 9222) con evidencia antes/despues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:37:07 +02:00
egutierrez 6d1b66167d docs(comfyui): cerrar doc_orphan comfyui-skill + cobertura Civitai + allowlist naming
Aplica los 3 arreglos de orden de la auditoria 0120:
- INDEX.md: anade la fila del grupo comfyui-skill (11 fns), cerrando el unico FAIL
  doc_orphan de fn doctor capabilities.
- comfyui-skill.md: documenta las 2 funciones de cosecha Civitai
  (comfyui_extract_recipe_from_png, comfyui_harvest_civitai_skill_oneshot) en la
  tabla + seccion 'Cosecha Civitai -> skill candidata'. Cobertura 9/11 -> 11/11.
- ids_naming.md: anade save/bump/harvest/judge/critique a la allowlist documentada
  (espejo del cambio en apps/registry_mcp/naming.go, en su sub-repo).

No fn index (solo docs + rule). No renames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:10:47 +02:00
egutierrez 04ecf9f394 feat(ml): comfyui_export_skill_template — skills (recetas) como grafos cargables en el navegador
Cierra el gap receta->grafo del grupo comfyui-skill. La función impura
comfyui_export_skill_template compila una skill a template API format
(exports/<slug>.template.json) y, con ui_graph=True, genera el UI graph
posicionado vía CDP (load_workflow_ui + export_workflow_ui) en la carpeta
nativa de la UI (~/ComfyUI/user/default/workflows/<slug>.json), de modo que la
skill aparece en el menú Workflows del navegador y se abre como grafo visual.
Sin navegador, deja el template API y reporta el fallback (no falla).

- 4 tests offline (golden + edge + 2 error paths).
- Página madre comfyui-skill.md: fila en la tabla del grupo + sección
  "Skills como grafos en el navegador".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:58:11 +02:00
egutierrez 46954d8584 feat(infra): auto-commit con 8 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 15:35:59 +02:00
egutierrez 6f4b440762 feat(ml): cosecha Civitai → skills candidatas (search/fetch/extract + harvest oneshot)
Cierra la 3ª pieza del sistema comfyui-skill: cosechar de Civitai imágenes con su
workflow+receta embebidos para clonar su calidad y alimentar la librería de skills.

- comfyui_search_civitai_images: GET /api/v1/images; resuelve query->versión de
  modelo (el endpoint no admite query textual, da HTTP 500); token de pass; reintenta 503.
- comfyui_fetch_civitai_image: descarga el PNG original (conserva workflow embebido),
  SEGREGA NSFW a <dest>/nsfw/, validación no-HTML, nombre único por UUID.
- comfyui_extract_recipe_from_png: import_workflow_png + read_png_metadata + fallback
  flux (CLIPTextEncode/UNETLoader) -> receta candidata (source='civitai', score_n=0).
- comfyui_harvest_civitai_skill_oneshot (pipeline): search->fetch->extract->save_skill;
  itera items, 2º pase al feed global, NO baja modelos a ciegas (missing_models).

Hallazgo: la API de Civitai ya no expone meta (null); la receta sale del workflow
ComfyUI embebido en el PNG. Política: NSFW permitido pero SIEMPRE segregado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:35:12 +02:00
egutierrez bcf731275e feat(ml): cierre del bucle de mejora comfyui-skill (genera→juzga→bump)
Tres funciones nuevas que cierran el lazo skill→generación→juicio→promoción
del grupo comfyui-skill (issue 0087):

- comfyui_bump_skill_version (impura): promueve una versión nueva SOLO si el
  score del panel-juez sube (gate objetivo). Snapshot versions/vN.json
  pre-mutación, deep-merge de recipe_patch, semver↑, línea en growth_log.jsonl.
  force=True salta el gate. No usa datetime.now().
- comfyui_update_skill_score (impura): media incremental de score_mean/score_n
  reescribiendo recipe.json in-place (sin snapshot ni growth_log).
- comfyui_generate_with_skill_oneshot (pipeline): one-shot load→build→submit→
  wait→fetch→judge→score_mean. recipe_patch prueba variantes sin guardar score.
  Compone 7 funciones del registry.

Tests offline: 11 passed (gate, semver, deep-merge, media incremental, errores).
Página madre docs/capabilities/comfyui-skill.md: +3 funciones, sección "Bucle de
mejora" con diagrama, fronteras de scoring actualizadas.

Demo real verificada: skill seed portrait_cinematic_sd15 (SD1.5) generó imagen
SFW real, el panel la juzgó, una variante puntuó más alto (4.787 > 4.7276) y el
gate promovió v1.0.0→v1.1.0 con el judge_run_id como evidencia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:09:33 +02:00
egutierrez 974cc06bc7 feat(ml): panel multi-juez comfyui-judge (estetico + CLIP + LLM-vision)
Cuatro funciones impuras + pagina madre del grupo comfyui-judge, el gate
objetivo de calidad de imagen para tests/DoD y el bucle de mejora de skills:

- comfyui_score_aesthetic: estetico LAION-V2 (head MLP sobre CLIP ViT-L/14),
  subproceso al venv ComfyUI (torch+open_clip).
- comfyui_score_clip_alignment: fidelidad prompt-imagen via similitud coseno CLIP.
- comfyui_critique_image_llm: critica LLM-vision (compone ask_llm_vision), JSON
  verdict+score+reasons.
- comfyui_judge_image: agregadora, vota mayoria good/bad; degrada si un juez cae.

QuickGELU (ViT-L-14-quickgelu/openai) obligatorio: sin el, los embeddings se
degradan y el ranking de fidelidad se invierte en silencio.

Validado e2e sobre imagenes reales: golden 3 votos coherentes, asserts relativos
(nitida>ruido, alineado>desalineado), split 2-1 respeta mayoria en ambos sentidos,
degradacion ante 429/model invalido/path invalido sin crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:54:32 +02:00
agent 70d541fca9 feat(ml): núcleo subsistema comfyui-skill + ask_llm_vision
Grupo nuevo comfyui-skill: recetas versionadas de generación ComfyUI que
compilan a un workflow cambiando solo el subject.

- comfyui_build_skill_workflow (pura): receta -> workflow API format,
  despacha base (txt2img/flux/sdxl_refiner), sustituye {subject}+triggers,
  encadena loras e inject blocks (facedetailer, hires_fix). SkillWorkflowError tipada.
- comfyui_inject_hires_fix (pura): inyecta 2ª pasada UltimateSDUpscale sobre dict.
- comfyui_save/load/list_skill (impuras): CRUD de la librería en disco con
  versionado por snapshots, round-trip idéntico, filtro NSFW.
- ask_llm_vision (core, claude-direct): pregunta multimodal imagen+texto via
  API directa Anthropic, para puntuar generaciones.
- Página madre docs/capabilities/comfyui-skill.md con schema canónico de recipe.json.

Tests offline: 11 verdes (6 builder + 5 inject_hires_fix). Sin GPU.
2026-06-24 14:35:46 +02:00
egutierrez e8a66f0dad feat(ml): comfyui_run_foreign_workflow_oneshot + helper fetch_output_video
Pipeline one-shot para ejecutar workflows ComfyUI ajenos end-to-end
(import desde cualquier fuente -> resolve deps -> validate -> submit ->
wait -> fetch del output imagen/video/malla) componiendo 9 funciones
existentes del grupo comfyui. Gate de seguridad: si faltan nodos/modelos
NO encola y los reporta en `missing`; nunca descarga modelos a ciegas y
solo instala nodos custom confiables opt-in (install_nodes + node_repos).

Helper comfyui_fetch_output_video: hermana de fetch_output_image y
fetch_output_mesh para los nodos de video/animacion (SaveAnimatedWEBP,
SaveVideo nativo, VHS_VideoCombine). Localiza el output bajo images/gifs/
videos en /history y lo baja via /view a disco; acepta outputs= de
wait_result para evitar re-consultar /history.

Cierra la pieza marcada por el completeness critic (report 0107) del
roadmap 0064/0087. 13 tests unitarios de las partes puras en verde;
validacion de integracion contra server vivo sin generacion pesada
(report 0110).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:53:40 +02:00
egutierrez 898502a321 fix(ml): comfyui_wait_result no sale prematuro en jobs de video/3D
Exige outputs no vacios (no solo status terminal) para dar por completado
un prompt: en jobs pesados ComfyUI marca la entry de /history como
terminada antes de poblar outputs, lo que devolvia un dict vacio mientras
el job seguia en GPU. Ahora sigue sondeando hasta que los outputs aparecen
o hasta agotar el timeout. Timeout default 180s -> 600s (cubre video/3D) y
timeout HTTP por-request acotado a 30s. Firma y contrato de retorno intactos.

Tests nuevos (mock urllib CI-safe + live opcional contra /history real):
golden, regresion del bug, edge imagen corta, timeout y error. v1.0.0 -> 1.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:39:58 +02:00
egutierrez 2fe36e314e docs(ml): fix doc gap controlnet (.pth → _fp16.safetensors) + capability page comfyui completa
- comfyui_build_controlnet_workflow.md: el ejemplo usaba cn_name=control_v11p_sd15_canny.pth
  pero el modelo instalado es control_v11p_sd15_canny_fp16.safetensors. Corregido para que
  copia+pega funcione. Firma intacta.
- docs/capabilities/comfyui.md: añadida subsección "Lifecycle del server — dominio infra"
  con comfyui_ensure_server_py_infra (faltaba: página 48 vs registry 49). Ahora 49 == 49.

Higiene del grupo comfyui (report local 0104): tests de los builders puros flux/img2vid
verificados (10/10 pasan, suite del grupo 65/65), fn doctor uses-functions sin drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:09:04 +02:00
egutierrez 11ef8ef6db feat(ml): comfyui_build_img2vid_workflow builder img2vid SVD (API format)
Builder puro que construye el dict de un workflow ComfyUI img2vid (Stable Video
Diffusion) en API format a partir de una imagen estatica. Cadena de 7 nodos:
ImageOnlyCheckpointLoader(svd.safetensors, todo-en-uno) + LoadImage ->
SVD_img2vid_Conditioning -> VideoLinearCFGGuidance -> KSampler(denoise 1.0) ->
VAEDecode -> SaveAnimatedWEBP. SVD condiciona por CLIP_VISION de la imagen (sin
prompt de texto); movimiento via motion_bucket_id.

class_type/inputs verificados contra /object_info del servidor vivo. Validacion
estructural con comfyui_validate_workflow: 0 errores. 4 tests verdes. Sin submit
de generacion (GPU en uso por otro agente).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:02:04 +02:00
egutierrez 3e75d1bf79 feat(ml): comfyui_build_flux_workflow builder txt2img Flux (API format)
Builder puro hermano de comfyui_build_txt2img_workflow para modelos Flux
(schnell/dev): UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) +
VAELoader -> CLIPTextEncode -> FluxGuidance + EmptySD3LatentImage ->
KSampler (cfg fijo 1.0) -> VAEDecode -> SaveImage. La guia va por FluxGuidance,
no por el cfg del sampler. fp8 + ~4 pasos para GPU de 8GB.

class_type/inputs verificados contra /object_info del server vivo. Validado
end-to-end: genera imagen real (prompt_id 909b8876, flux_builder_test_00001_.png,
status success). 6 tests unitarios verde. Pagina madre docs/capabilities/comfyui.md
actualizada con la fila del builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:55:09 +02:00
egutierrez 68f0ce0dae feat(infra): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 11:45:31 +02:00
egutierrez c0b2dce3b0 feat(ml): auto-commit con 26 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 04:02:54 +02:00
egutierrez ff41f4f053 feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 02:52:51 +02:00
egutierrez f686b338d6 chore: auto-commit (14 archivos)
- docs/capabilities/comfyui.md
- python/functions/ml/comfyui_build_image_to_3d_workflow.md
- python/functions/ml/comfyui_build_image_to_3d_workflow.py
- python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py
- python/functions/ml/comfyui_build_facedetailer_workflow.md
- python/functions/ml/comfyui_build_facedetailer_workflow.py
- python/functions/ml/comfyui_build_hires_fix_workflow.md
- python/functions/ml/comfyui_build_hires_fix_workflow.py
- python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py
- python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 02:34:10 +02:00
egutierrez 3823a28d1c feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 02:05:43 +02:00
egutierrez 337f75b527 chore: auto-commit (5 archivos)
- docs/capabilities/comfyui.md
- python/functions/ml/comfyui_import_workflow_json.md
- python/functions/ml/comfyui_import_workflow_json.py
- python/functions/pipelines/comfyui_text_to_3d_oneshot.md
- python/functions/pipelines/comfyui_text_to_3d_oneshot.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 01:52:46 +02:00
egutierrez d3f05a19a5 feat(ml): auto-commit con 11 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 01:39:30 +02:00
egutierrez d7245efa59 feat(ml): auto-commit con 20 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 01:26:38 +02:00
egutierrez 1311c7e585 feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 01:16:37 +02:00
egutierrez db4f454f8a chore: auto-commit (1 archivos)
- .claude/commands/ausente.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 00:59:54 +02:00
egutierrez f12272d002 chore: auto-commit (61 archivos)
- docs/capabilities/INDEX.md
- docs/capabilities/comfyui.md
- python/functions/browser/comfyui_export_workflow_ui.md
- python/functions/browser/comfyui_export_workflow_ui.py
- python/functions/browser/comfyui_load_workflow_ui.md
- python/functions/browser/comfyui_load_workflow_ui.py
- python/functions/browser/comfyui_queue_prompt_ui.md
- python/functions/browser/comfyui_queue_prompt_ui.py
- python/functions/browser/comfyui_refresh_nodes_ui.md
- python/functions/browser/comfyui_refresh_nodes_ui.py
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-24 00:30:30 +02:00
egutierrez 495f545ec1 chore: untrack gitlinks fantasma cpp/apps/{chart_demo,shaders_lab}
Eran gitlinks (160000) en HEAD del padre sin entrada en .gitmodules,
restos del layout legacy cpp/apps/ (deprecado tras issue 0096, las apps
C++ viven ahora en apps/). Hacian fallar 'git submodule update' en cada
/full-git-pull. El sub-repo real shaders_lab vive sano en apps/shaders_lab;
chart_demo no existe en disco. Anadido cpp/apps/*/ al .gitignore para que
no recurra (regla apps_subrepo.md: el padre nunca trackea contenido de
artefactos hijos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:24:17 +02:00
egutierrez f34badb500 Merge remote-tracking branch 'origin/master' 2026-06-23 17:49:49 +02:00
egutierrez 3289c67986 chore: auto-commit (2 archivos)
- .claude/settings.local.json
- cpp/framework/app_base.cpp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-23 17:49:47 +02:00
egutierrez bcc1fe1738 feat(captacion_clientes): scraping freelance en perfil headless dedicado, no chromium-personal
El monitor de captación scrapeaba Workana sobre el navegador personal del
usuario (chromium-personal, CDP 9222), interfiriendo con su navegación. El
scraping CDP debe correr siempre en un perfil headless dedicado.

- Nuevo pipeline monitor_freelance_projects_headless: levanta un Chromium
  headless aislado con perfil dedicado (~/.config/fn_scrape_chrome, CDP 9334)
  vía systemd-run, ejecuta monitor_freelance_projects contra ese puerto y
  cierra la instancia al terminar (finally). Reutiliza el patrón de lifecycle
  de ingest_market_trends_headless. Reutiliza un CDP vivo si el puerto ya
  responde (no cierra lo ajeno).
- scrape_workana_projects y monitor_freelance_projects: default de `port`
  cambiado de 9222 (chromium-personal) a 9334 (perfil dedicado). Default seguro:
  correr a pelo sin Chrome en 9334 falla limpio, no contamina el 9222 personal.

Verificado: el wrapper arranca headless en 9334, scrapea 8 proyectos reales de
Workana, cierra la instancia (9334 muerto, sin proceso colgado) y deja el 9222
personal intacto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:11:26 +02:00
egutierrez 7619347be8 Merge orq/mcp-crud-ids: doc orchestration fleet_list identifica por pane_id (report 0008) 2026-06-22 12:07:54 +02:00
egutierrez f55e41cf74 docs(orchestration): fleet_list identifies agents by pane_id, not tmux_window
Reflects the orchestrator_mcp change: the MCP fleet_list payload now surfaces
pane_id ("%N", the stable per-pane id) and omits tmux_window ("@N"), which
migrates with the focus swap. Documents that focus/send-keys(nudge)/kill
resolve the live window on demand against tmux, and that the nudge reads
tmux_window from the fleetview binary (which keeps it as an internal field),
never from the MCP payload. The binary's list --json field list now mentions
pane_id as the identifier alongside the internal tmux_window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:03:50 +02:00
egutierrez e2e8669edf Merge orq/sql-connect: mssql_connect + mssql_query + run_mssql_query pipeline, grupo sql-connect (report 0007) 2026-06-22 11:32:47 +02:00
egutierrez 86d68dc9f0 feat(infra): conexion y consulta directa a SQL Server (Navision) via pymssql
Grupo de capacidad nuevo 'sql-connect' (3 funciones) para conectar a un
Microsoft SQL Server (donde corre Navision) y consultar directamente, en
lugar del ida y vuelta manual de pegar CSVs.

- mssql_connect_py_infra: abre conexion pymssql (login_timeout acotado,
  credenciales por argumento, RuntimeError claro si falla).
- mssql_query_py_infra: SELECT parametrizada con binding seguro (sin
  inyeccion) sobre conexion abierta; devuelve {columns, rows, row_count};
  0 filas -> lista vacia; max_rows con fetchmany; read-only.
- run_mssql_query_py_pipelines: one-shot que compone connect+query y cierra
  siempre; CLI imprime JSON o CSV; contrasena desde env var (pass).

Pagina madre docs/capabilities/sql-connect.md + fila en INDEX.md.
Dependencia pymssql>=2.3.13 anadida a python/pyproject.toml + uv.lock.
Tests mock-based (11) verdes; error path verificado end-to-end contra el
driver real (host inalcanzable -> RuntimeError, acotado por login_timeout).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:29:49 +02:00
egutierrez b18759823d Merge orq/equal-skill: /equal espejo de requisitos (report 0005) 2026-06-22 11:21:31 +02:00
egutierrez a59d50238d feat(commands): añadir /equal — espejo de requisitos para confirmar alineación
Reformula la última tarea pedida de forma detallada y estructurada
(objetivo, alcance, entregables, supuestos, criterios de aceptación,
fuera de alcance, dudas) para que usuario y Claude confirmen alineación
antes de ejecutar. No ejecuta la tarea: solo refleja y pregunta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:19:30 +02:00
egutierrez f17d957a8f docs(orquestador): nombrar cada secundario (--title + goal del sidebar) para distinguirlos
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:19:21 +02:00
egutierrez c1f355ffa5 Merge orq/fleet-detect: detect_fleet_context ($TMUX) + spawn auto-detecta socket + hook CONTEXTO FLEET + doctrina (report 0041) 2026-06-21 21:55:11 +02:00
egutierrez 237f763c19 Merge orq/img3d-registry-funcs: promover remove_background al registry + mask en depth_to_relief_glb (grupo img-to-3d, report 0040) 2026-06-21 21:51:13 +02:00
agent bf67ff3180 docs(orquestador): deteccion de flota por $TMUX, kitty solo fuera de tmux
orquestador.md + orchestration.md: la deteccion de 'estoy en una flota' se hace
por $TMUX (via detect_fleet_context), NO por $FLEET_SOCKET (fragil). kitty es
fallback SOLO cuando in_tmux=false. spawn_fleet_agent auto-detecta el socket
(ya no hace falta pasar --socket/--session). Documenta la linea CONTEXTO FLEET
del hook y anade detect_fleet_context al catalogo del grupo orchestration.
2026-06-21 21:50:32 +02:00
agent 03fc0461fa feat(hook): inyectar CONTEXTO FLEET con socket/session al orquestador
hook_fleet_state_inject.sh ahora, ademas de MODO ORQUESTADOR, llama a
detect_fleet_context (por $TMUX) e inyecta una linea CONTEXTO FLEET con
socket/session + recordatorio de usar spawn_fleet_agent (nunca kitty) cuando
in_fleet=true. No depende del venv (solo bash+tmux) y se emite antes del bloque
FLEET-STATE. Degrada limpio: si el detector falta o $TMUX esta vacia, no emite
la linea y el turno sigue intacto.
2026-06-21 21:50:32 +02:00
agent a1105dc4c5 feat(infra): spawn_fleet_agent auto-detecta socket/session de $TMUX
--socket/--session ahora opcionales: si no se pasan, se auto-detectan del
contexto tmux ($TMUX) via detect_fleet_context. Los explicitos siguen
primando. Aborta (exit 2) solo si tras auto-detectar siguen vacios (no hay
tmux). Elimina el bug de caer a kitty cuando $FLEET_SOCKET viene vacia pese a
estar en la flota. Bump v1.2.0 + growth log.
2026-06-21 21:50:32 +02:00
agent 3c9e909eda feat(infra): detect_fleet_context — contexto de flota por $TMUX (no $FLEET_SOCKET)
Funcion nueva detect_fleet_context_bash_infra (tag orchestration). Deriva
socket/session de $TMUX (senal fiable que todo proceso dentro de tmux tiene
siempre), con fallback a $FLEET_SOCKET/$FLEET_SESSION. Devuelve JSON
{in_fleet,in_tmux,socket,session,source}. Causa raiz del bug: $FLEET_SOCKET
(exportada con tmux set-environment -g por launch_fleetclaude) a veces viene
vacia en un claude resumido/relanzado pese a vivir en la flota, y el modo
orquestador caia al fallback kitty. .md self-doc (Ejemplo + Cuando usarla +
Gotchas).
2026-06-21 21:50:32 +02:00
egutierrez 3cf8b21fea feat(datascience): promover remove_background al registry + mask en depth_to_relief_glb (grupo img-to-3d)
Completa la promoción del flujo imagen->3D al registry (grupo de capacidad
img-to-3d), extraído de la app img_to_3d_webapp.

- remove_background_py_datascience (nueva): elimina el fondo con cascada
  rembg/U2Net -> OpenCV GrabCut -> umbral NumPy, compone el objeto sobre gris
  neutro y devuelve image + mask + engine. Impura, nunca lanza. Adaptada de
  backend/bg_removal.py con firma de ruta (image_path) y salida dict, demo CLI
  JSON-serializable.
- depth_to_relief_glb_py_datascience (v1.1.0): añade el parámetro opcional mask
  para recortar la malla de relieve al objeto (descarta las caras del fondo),
  cerrando la cadena con remove_background. Aditivo (mask=None = comportamiento
  previo), fiel al original de backend/depth.py.
- docs/capabilities/img-to-3d.md: incorpora remove_background como paso 0
  (pre-proceso), actualiza el flujo a 3 pasos encadenados, la tabla de funciones
  (4), el ejemplo end-to-end con mask y las deps (rembg/opencv).
- docs/capabilities/INDEX.md: conteo del grupo 3 -> 4.

Las dos funciones ya presentes (estimate_image_depth, depth_to_relief_glb) y el
pipeline build_relief_glb_from_image fueron promovidas en una ronda previa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:43:08 +02:00
egutierrez cbefc82c02 Merge orq/pane-id-json: campo ClaudeFleet.PaneID + resolve_pane_ids + poblar en list_claude_fleet (report 0039) 2026-06-21 21:30:40 +02:00
egutierrez fb76b53c17 feat(infra): exponer pane_id (%N) estable en el JSON de la flota
El orquestador identificaba cada agente por el campo tmux_window (@N), pero
el window_id de tmux cambia cuando un pane entra/sale de windows (el focus de
la flota usa break-pane + join-pane, que recrean windows). El pane_id (%N) en
cambio es estable durante toda la vida del pane: es el identificador correcto.

- claude_fleet.go: nuevo campo ClaudeFleet.PaneID `json:"pane_id"`. Se mantiene
  TmuxWindow (lo necesita el focus internamente); esto AÑADE pane_id, no lo
  reemplaza.
- resolve_pane_ids.go (+ .md, .go test): nueva función del registry
  ResolvePaneIDs(socket, pids) -> map[pid]pane_id. Lista los panes del socket
  (tmux -L <socket> list-panes -a) y para cada PID sube por el árbol de procesos
  (PPID en /proc) hasta dar con un pane_pid. Reutiliza runTmux y procPPID del
  paquete infra. Best-effort: tmux/socket caído o PID sin pane -> "" sin crash.
  Núcleo testeable con inyección de la salida tmux y del resolvedor de PPID.
- list_claude_fleet.go: ListClaudeFleet() puebla PaneID resolviendo cada PID
  vivo contra $FLEET_SOCKET (default "fleet"). Solo la entrada pública lo hace;
  ListClaudeFleetFrom() queda intacta (cero coste tmux en tests y en el bucle
  de render de fleetview).

Tag de grupo: orchestration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:19:55 +02:00
egutierrez 8e16202935 Merge orq/doc-tags: documentar tools MCP fleet_* + corregir drift role/dod + tag orchestration a 6 funciones 2026-06-21 18:05:48 +02:00
egutierrez e4a36f1133 chore(tags): anadir tag 'orchestration' a las 6 funciones del grupo que faltaban
capability_groups.md exige que toda funcion de un grupo lleve su tag plano para
ser descubrible via fn_search tag='orchestration'. 6 de las funciones del grupo
(reboot_all_claudes, classify_fleet_termination, list_claude_fleet,
drain_fleet_events, mark_claude_role, set_dod_contract) no lo llevaban. Se anade
sin borrar los tags existentes.

notify_desktop_go_infra ya llevaba el tag pero no figuraba en la tabla del grupo:
se decide que SI pertenece (la usa el orquestador/watcher para avisar de un
RECLAMA u otro evento urgente) y se anade a la tabla en orchestration.md (commit
anterior), en lugar de quitarle el tag. Resultado: 13 funciones con tag
orchestration, identicas a las 13 filas de la tabla del grupo (sin drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:03:14 +02:00
egutierrez 295f90afaf docs(orquestador): documentar tools MCP fleet_* + corregir drift role/dod en list --json
orchestration.md: nueva subseccion 'Via preferida: tools MCP fleet_*' con mapa
operacion->tool (fleet_list/drain/classify/set_dod/kill/spawn) marcando el MCP
orchestrator como via preferida sobre ./fn run (permisos pre-aprobados, salida
estructurada, telemetria) y el ./fn run / binario fleetview como fallback CLI.
Corrige la afirmacion obsoleta de que 'fleetview list --json no incluye todavia
role/dod_contract/dod_status': el CLI ya los expone directamente y el MCP rellena
los vacios desde el sidecar goal.json. Anade notify_desktop_go_infra a la tabla
del grupo. orquestador.md: linea en el flujo senalando el MCP como via preferida.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:03:04 +02:00
integrador f85c1a322a feat(mcp): registrar orchestrator_mcp en .mcp.json
Expone el grupo de capacidad de orquestación de flota (fleet_list/drain/classify/
kill/set_dod/spawn) como tools MCP tipadas para el Claude orquestador. Binario en
apps/orchestrator_mcp (sub-repo). Command relativo igual que registry_mcp; stdio
por defecto, sin flags. Listo para /mcp reconnect.
2026-06-21 15:00:22 +02:00
egutierrez 32c7336bf6 feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-21 14:22:55 +02:00
egutierrez c1071a82b3 Merge orq/orquestador-doc: dieta /orquestador + rules/orchestration.md + fan-out=6 + hook reancla rol
Fixes B (fan-out duro=6), C (hook_fleet_state_inject reancla role=orchestrator),
D (command 555->299 lineas, maquinaria extraida a .claude/rules/orchestration.md).
Verificado adversarial: met (todas las clausulas re-ejecutadas independientes).
2026-06-21 14:11:50 +02:00
egutierrez fac2cceea3 docs(orquestador): tope de fan-out duro = 6 ejecutores por orquestador
El doc hablaba de "tope de fan-out para no explotar la flota" sin numero. Se
fija un maximo DURO: 6 ejecutores role=executor activos simultaneos por
orquestador. Al alcanzarlo, el orquestador no lanza mas: encola las sub-tareas
restantes hasta que un slot se libere (ejecutor verificado met + kill_fleet_agent).

Justificacion: ya hubo el caso de 30 agentes que no cerraban nada y, al competir
todos por el mismo rate-limit compartido, hubo que desactivar goal_refine. Mas
ejecutores no es mas throughput; el cuello de botella es el rate-limit compartido
y los DoD que nadie cierra.

Escrito en el splitter + regla dura de orchestration.md (detalle + justificacion)
y en las reglas duras del command (numero + encolado, puntero al detalle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:55:03 +02:00
egutierrez f852993412 docs(orquestador): dieta del command + extraccion de maquinaria a rules/orchestration.md
El command /orquestador (555 lineas) se cargaba entero en contexto cada turno y
predicaba "responde conciso" siendo el mismo prolijo. Se parte en dos:

- Nueva regla .claude/rules/orchestration.md con la maquinaria estable: seguir
  la flota (fleetview, tiempo de actividad), cola del watcher (events.jsonl,
  push activo, FLEET-STATE), clasificacion (classify_fleet_termination),
  politicas por clasificacion, verificador adversarial, auto-kill
  (kill_fleet_agent), nudge, splitter, cadencia y el catalogo de funciones del
  grupo orchestration. Todo el contenido sustantivo se mueve integro.
- El command queda con la doctrina y el flujo: arranque (marcar role +
  confirmacion), diferencia con fn-orquestador/autopilot, ciclo de 8 pasos
  (resumido donde la maquinaria se fue a la regla, con punteros), reglas duras,
  anti-patrones, ejemplo end-to-end y salida del modo. Baja de 555 a 299 lineas.

Redundancias colapsadas a una canonica + punteros: "NUNCA Agent tool para
lanzar trabajo" vive en el paso 8; "nunca pkill/killall claude" en el paso 6.
El resto referencia esos pasos.

Fila 38 anadida a .claude/rules/INDEX.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:53:40 +02:00
egutierrez 8328637935 feat(orquestador): reanclar role=orchestrator en el hook de inyeccion de flota
El modo /orquestador dependia de que su prompt siguiera en contexto. Ahora el
hook UserPromptSubmit que ya filtra por role=orchestrator reinyecta tambien una
linea recordatorio del rol cada turno, reanclando el modo independientemente del
prompt. Se emite antes de la guarda del venv para sobrevivir a un watcher caido.
El path limpio (sin goal.json o role != orchestrator) sigue saliendo con exit 0
y stdout vacio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:47:46 +02:00
egutierrez 687c72805d docs(orquestador): paternidad spawn --parent, auto-kill, push activo, statusline sin LLM
Integra las 4 mejoras de la tanda: (1) --parent en los ejemplos de spawn y la
explicacion del routing del watcher por parent_orchestrator; (2) regla de
auto-kill — cerrar cada ejecutor con kill_fleet_agent tras verificar met, con sus
guards; (3) push activo del watcher (tmux send-keys al pane del orquestador padre)
+ indicador 'idle nuevo sin ver' de la TUI fleetview (los implementa otro agente,
aqui solo se describen); (4) el dod movil del statusline ya no se regenera con LLM
por turno (objetivo/DoD fijo, ajustable con dod:). Anade mark_claude_parent y
kill_fleet_agent a la tabla del grupo orchestration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:28:07 +02:00
egutierrez f415dd56f5 feat(infra): kill_fleet_agent — cierre dirigido de un ejecutor de la flota (auto-kill)
Cierra UN ejecutor por sessionId (exacto/prefijo) o PID: SIGTERM al proceso claude
(cierre limpio, recuperable con --resume) + kill-window de su window tmux. Lo usa
el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que
su DoD-contrato esta met. Guards: NO mata a un role=orchestrator (leido del
goal.json) ni a la sesion que invoca (PID propio por ancestros /proc). --dry-run
para inspeccionar sin tocar nada. Overrides FN_FLEET_* para tests. Tag grupo
orchestration. Tests: 17 asserts (golden por sessionId/PID/prefijo, guards
orchestrator/self rc=3, errores rc=2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:28:07 +02:00
egutierrez b6ad1a3a15 feat(infra): spawn_fleet_agent --parent <sid> atribuye el ejecutor a su orquestador
Nuevo flag opcional --parent: persiste parent_orchestrator en el goal.json del
Claude recien lanzado (via mark_claude_parent, en background). Habilita el routing
del watcher de fleetview, que asi sabe a que pane de orquestador empujar el aviso
de cierre del ejecutor. El bloque background ahora cubre --role y/o --parent
encadenados secuencialmente (primero role, luego parent) para evitar la carrera de
lectura-modificacion-escritura sobre el goal.json. Retro-compatible: sin --parent,
el spawn se comporta igual que antes. Bump 1.0.0 -> 1.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:28:07 +02:00
egutierrez 753e16b84c feat(infra): mark_claude_parent — escribe parent_orchestrator en goal.json (PID->sessionId)
Helper py analogo a mark_claude_role: resuelve el sessionId de un Claude recien
arrancado por su PID (sondeando sessions/<pid>.json) y escribe SOLO la clave
parent_orchestrator en su goal.json, preservando el resto. Lo consume
spawn_fleet_agent --parent para que el watcher de fleetview rutee los avisos del
ejecutor a su orquestador padre. Tests: escribe+preserva, goal inexistente,
parent vacio (ValueError), timeout sin crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:27:47 +02:00
Egutierrez b6f4b4eb03 docs(orquestador): documentar push automatico FLEET-STATE + fn summarize_fleet_transitions 2026-06-21 12:28:20 +02:00
agent 118d5d36d3 feat(orquestador): feed reactivo FLEET-STATE + fix peek de drain_fleet_events
El orquestador no se enteraba de los cambios de estado de su flota: el drenado
era manual y el peek documentado `./fn run drain_fleet_events --advance false`
devolvia un falso `{total_new:0, cursor:0}` porque `fn run` mapea los argumentos
posicionalmente y no parsea flags `--nombre valor` (events_path acababa valiendo
"--advance", una ruta inexistente).

- drain_fleet_events: nuevo helper _normalize_fn_run_flags que renormaliza el
  patron `--advance <bool>` aplanado por `fn run`, de modo que el peek funciona
  directo desde la CLI sin tocar el runner de Go. Bump 1.1.0 + growth log + tests
  del normalizador (unit y end-to-end por HOME).
- summarize_fleet_transitions (nueva, pure, grupo claude-fleet): resume el dict
  by_classification de drain en un bloque de una linea con las tres categorias
  accionables (terminados / reclaman / estancados), dedup por session_id y
  truncado de objetivo.
- hook_fleet_state_inject.sh (UserPromptSubmit): si la sesion es role=orchestrator
  (leido de ~/.claude/goals/<session_id>.json), hace peek de la cola sin mover el
  cursor y emite el bloque FLEET-STATE cada turno. Degrada limpio si el watcher
  esta caido, la cola no existe o la sesion no es orquestador.

El registro del hook va en .claude/settings.local.json (gitignored, fuera de este
commit). Pendiente, lo integra otro agente: documentar el bloque FLEET-STATE en
.claude/commands/orquestador.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 00:06:01 +02:00
agent b410328cec fix(orquestador): invocaciones exactas que funcionan en el system prompt 2026-06-20 23:27:25 +02:00
agent 2f184d9dd9 docs(orquestador): usar edad de actividad (fleetview list AGE), no etime de proceso
El orquestador reporta el tiempo de actividad (cuanto lleva sin avanzar)
via fleetview list AGE/idle_seconds, no el etime (vida del proceso = 8h de
sesion), que no es progreso.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:19:56 +02:00
agent b823271eb6 docs(orquestador): regla de concision + aclarar pin en sidebar
El orquestador responde conciso (velocidad de iteracion sobre detalle) y
va pinneado arriba en el sidebar de fleetview via role=orchestrator. Se
corrige una regla previa que afirmaba erroneamente un comportamiento de
no-conmutacion del pane (no era lo pedido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:05:54 +02:00
agent 2a279abb15 docs(flows): 0012 gap 1 cerrado (spawn en flota tmux validado en vivo)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:56:30 +02:00
agent 4b732ca4d3 feat: spawn de ejecutores en la flota tmux (flow 0012, gap 1 cerrado)
- spawn_fleet_agent (bash/functions/infra): lanza un Claude como window de
  la sesion tmux de un perfil fleet (no kitty suelta), con --skill para
  arrancar en un modo (ej. /orquestador), --prompt-file para ejecutores
  autocontenidos, y --role para marcar el goal.json via mark_claude_role.
  Asi ejecutores y orquestador viven en la flota, visibles en fleetview y
  conmutables con /fleet focus.
- skill /orquestador: paso 2 ahora prefiere spawn_fleet_agent sobre kitty
  cuando se opera dentro de un perfil fleet ($FLEET_SOCKET seteado); tabla
  de funciones del grupo actualizada.

Validado en vivo: el orquestador arranca en la flota fleet2 en modo
(MODO ORQUESTADOR activo), role=orchestrator marcado, pinneado arriba en
la TUI; los 9 ejecutores existentes intactos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:56:12 +02:00
agent 05d0b71d5d docs(flows): 0012 fase 3b validada (launcher+pin) + gap del spawn en flota
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:34:23 +02:00
agent 334a71eed1 feat: launcher arranca orquestador + idle, pin por role (flow 0012, fase 3b)
- mark_claude_role (python/functions/infra): resuelve PID->sessionId
  esperando sessions/<PID>.json y escribe role en el goal.json sin pisar
  el resto. 4 tests.
- launch_fleetclaude: el pane derecho arranca el ORQUESTADOR con el skill
  /orquestador embebido como primer prompt; tras arrancar, mark_claude_role
  le pone role=orchestrator (en background, no-fatal) para que la TUI lo
  pinee arriba; ademas siembra 1 ejecutor idle inicial en su propia window.
- skill /orquestador: regla 'no te vigiles a ti mismo' (ignora en la cola
  su propia sesion y cualquier role=orchestrator).

Validado en vivo (perfil aislado): claude /orquestador entra en modo,
role marcado, idle sembrado, pin correcto, fleet2 intacto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:33:37 +02:00
agent c55bb17d09 docs(flows): 0012 fase 2 construida (cerebro reactivo en /orquestador)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:19:51 +02:00
agent 9365def3dd feat: cerebro reactivo del meta-orquestador (flow 0012, fase 2)
Primitivas (python/functions/infra):
- drain_fleet_events: consume la cola del watcher (~/.claude/fleet/
  events.jsonl) desde un cursor, agrupa por clasificacion, marca
  urgentes. 7 tests.
- set_dod_contract: escribe el DoD-contrato fijo (dod_contract/dod_status)
  en el goal.json de un agente sin pisar el resto (escritura atomica).
  5 tests.

Skill /orquestador evolucionado (sin romper lo existente): vigila la
flota por su DoD (no por 'esta vivo'). Nueva seccion 'Consumo de la cola
de la flota': DoD-contrato obligatorio al lanzar, drenar la cola,
politicas por clasificacion (RECLAMA escala / DICE_TERMINADO verifica /
ESTANCADO nudge / MAL_LANZADO re-DoD), verificador independiente del
ejecutor (lee el report vs dod_contract), splitter con tope de fan-out,
y cadencia (drain al actuar + heartbeat).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:19:26 +02:00
agent 251db2bfc5 docs(flows): 0012 fase 1 validada en vivo (3 capas DoD cumplidas)
Transicion real MAL_LANZADO->DICE_TERMINADO escrita a la cola JSONL con
la TUI viva. Hallazgo: la flota actual clasifica MAL_LANZADO (sin
dod_contract), comportamiento correcto que Fase 3 corrige.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:10:36 +02:00
agent 0e93258974 docs(flows): 0012 fase 1 hecha — watcher dentro de fleetview + cola JSONL
Refleja la decisión real: el watcher no es un daemon aparte sino que se
embebe en fleetview (reutiliza su polling); la cola es JSONL en
~/.claude/fleet/events.jsonl (sin SQLite/CGO).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:02:01 +02:00
agent 28a53ee357 feat(infra): modelo de datos del meta-orquestador de flota (flow 0012)
Fase 1, piezas 1+2:
- ClaudeFleet + list_claude_fleet ganan DodContract/DodStatus/Role,
  leidos de goals/<sessionId>.json (.dod_contract/.dod_status/.role).
  Aditivo: fleetview sigue compilando.
- classify_fleet_termination (pura): clasifica el estado de terminacion
  de un agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO)
  con precedencia fija, para que un watcher sin LLM decida. 34 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:51:11 +02:00
agent b569561115 docs(flows): 0012 meta-orquestador de flota con DoD-contrato
Diseño del sistema para manejar 20-30 agentes Claude hablando solo con
uno: 4 roles (orquestador/splitter/ejecutores/verificador), DoD-contrato
fijo en goal.json como criterio estable de terminación, máquina de
terminación clasificable sin LLM, y 3 fases (watcher edge-triggered,
orquestador reactivo + verificador independiente, spawn en flota +
splitter). Métrica de salud = throughput de DoD cumplidos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:38:35 +02:00
agent 224f714d4a feat(commands): /fleet — listar y saltar con foco a la flota de Claudes
Slash command que envuelve el modo CLI de fleetview (list/focus) para
preguntar por la flota de Claudes vivos o saltar con foco a una
conversación dentro de la sesión tmux fleet, desde cualquier sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:38:12 +02:00
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00
egutierrez 7d100e7f3e feat(infra): auto-commit con 2 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 11:11:35 +02:00
egutierrez e7a8edfed8 fix(infra): tmux_swap_window_into_console base-index-agnostico
La funcion direccionaba panes por indice literal ("console.0",
"windowID.0", filtro pane_index != "0"). El socket aislado de fleetview
(tmux -L fleet) hereda ~/.tmux.conf, asi que con `pane-base-index 1`
(config muy comun) el primer pane es el indice 1 y no existe el 0:
join-pane fallaba con "can't find pane: 0" tras haber hecho ya el
break-pane, dejando la sesion fleet con las windows desperdigadas y sin
el sidebar de la TUI.

Ahora resuelve el pane sidebar como el de MENOR pane_index y opera
siempre por pane_id (estable e inmune al base-index). Helpers nuevos:
tmuxConsolePanes, tmuxFirstPaneID, tmuxPanesSorted, tmuxSidebarWidth.

Tests actualizados a base-index-agnostico (localizan el sidebar por
menor indice, no por "0") y el default de ancho del sidebar pasa de 47
a 52 para coincidir con launch_fleetclaude.

Bump v1.0.0 -> v1.0.1 + Capability growth log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:10:56 +02:00
egutierrez cd87a8c28e chore: auto-commit (1 archivos)
- cpp/apps/shaders_lab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 10:37:14 +02:00
egutierrez 6ab85ee701 Merge remote-tracking branch 'origin/master' 2026-06-17 10:30:27 +02:00
egutierrez 909290ddbf feat(infra): auto-commit con 8 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 10:30:26 +02:00
egutierrez 111ee17bcc feat(infra): auto-commit con 1 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 09:21:15 +02:00
egutierrez 0d3118d98d feat(infra): fleetclaude usa terminal actual + sidebar 52 + tmux gris
- launch_fleetclaude: si hay TTY, exec tmux attach en la terminal actual (no abre
  ventana kitty nueva); atajos alt+q (cerrar flota con confirmacion) y alt+flecha
  izquierda (volver atras); estetica neutra de tmux (status/bordes gris).
- ancho del sidebar 47 -> 52; tmux_swap_window_into_console preserva 52 por defecto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:09:11 +02:00
egutierrez f6b9747f11 refactor(infra): launch_kittyclaude → launch_fleetclaude (comando fleetclaude)
Renombra la funcion entrypoint y su comando a fleetclaude. Ademas, sobre el .sh:
- atajos nuevos: alt+0 (= alt+n), alt+k (kill), alt+r (resume picker),
  alt+flecha-izquierda (volver atras), alt+q (cerrar toda la flota con confirmacion).
- mouse on, remain-on-exit off (cierra window al salir el Claude).
- estetica neutra de tmux: status bar y bordes de pane en gris (sin verde fosforo),
  borde activo igual que inactivo (separacion simple sin resaltado de foco).
Docs (INDEX, claude-fleet.md) actualizadas al nuevo nombre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:36:09 +02:00
egutierrez 927437a8d8 feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una
sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI:

- list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals +
  runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId.
- list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables.
- wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console
  (preserva ancho del sidebar), tmux_map_claude_panes.
- launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r,
  mouse on, remain-on-exit off; fija el ancho del sidebar con hooks.
- docs/capabilities/claude-fleet.md + entrada en el INDEX.

Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes
varios de docs e infra de otra sesion, agrupados aqui para no perderlos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:04:41 +02:00
Egutierrez 7d395f39e5 feat(browser): cdp_click_ref/cdp_hover_ref usan cdp_wait_actionable
Antes de calcular el centro y despachar el pointer, ambos esperan a que el
elemento sea accionable (visible + stable + hit-test contra elementFromPoint),
evitando clicks/hover tragados por overlays/banners o por elementos aún
montándose o animándose. Si la comprobación no converge en 2s, se cae al
cálculo de centro previo (sin regresión). Modo 'instant' sigue saltando al
click JS directo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:57:44 +02:00
Egutierrez 4187f9b6b1 feat(browser): actionability + dropdowns + fill + role locator (estilo Playwright)
Tras estudiar el código de Playwright (sources/playwright), 4 primitivas nuevas y
1 endurecida para que la interacción web sea fiable:

- cdp_wait_actionable: visible + stable (2 rAF) + enabled + hit-test (elementFromPoint
  cruzando shadow DOM) + retry backoff + scroll cycling. Devuelve el punto validado.
  Réplica de _retryAction/_checkElementIsStable/expectHitTarget de Playwright.
- cdp_select_dropdown: desplegables custom (combobox/MUI/select2/headlessui): click real
  en trigger -> espera apertura (aria-expanded/[role=option] visible) -> click real en
  la opción. Resuelve el fallo nº1: clicar antes de que monte el listbox.
- cdp_select_option (endurecida v1.1.0): valida <select> real, match value/label
  normalizado/índice, option.selected para multiple, eventos input{composed}+change.
- cdp_fill: escribir fiable en inputs React/Vue: focus -> select-all -> Input.insertText
  (sin native value setter, como Playwright); native setter solo para inputs especiales.
- cdp_find_by_role: localizar por rol ARIA + accessible name (estilo getByRole),
  reutilizando el AX tree de cdp_get_ax_outline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:49:37 +02:00
Egutierrez c4ecf871c8 fix(cdp_collect_console): cap maxEntries + descarta backlog previo a la ventana
CdpCollectConsole gana un parametro maxEntries (default 200): al alcanzarlo deja
de acumular y marca una ConsoleEntry final '_truncated', evitando reventar la
salida en paginas verbosas. Ademas descarta los eventos console anteriores al
inicio de la captura (backlog acumulado en la conexion CDP viva), capturando solo
lo emitido dentro de la ventana durationMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:43:17 +02:00
Egutierrez 9798aed2cf feat(browser): cdp_collect_console + cdp_print_pdf + cdp_select_option + cdp_set_file_input
Cuatro primitivas CDP nuevas para el dominio browser, base de nuevas tools del
browser_mcp:
- cdp_collect_console: snapshot temporal de console + exceptions + log entries
- cdp_print_pdf: Page.printToPDF -> []byte
- cdp_select_option: selecciona <option> en un <select> y dispara input/change
- cdp_set_file_input: sube archivos a un <input type=file> via DOM.setFileInputFiles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:21:46 +02:00
egutierrez 588d092858 docs(infra): add .md metadata for write_xlsx_sheets function
The function code and its registry metadata were created together but the
.md was left untracked by the auto-commit. Add it so the indexed function
has its companion metadata versioned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:35:42 +02:00
egutierrez a90b7443e4 cuando termines y verifica que esté todo subido
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 01:33:35 +02:00
egutierrez e1e9bb7499 feat(shell): auto-commit con 31 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 23:55:16 +02:00
egutierrez 1430039688 feat(recon): modo CDP en fingerprint_web_stack para detectar SPAs
Añade fetch_http_fingerprint_cdp_py_browser (domain browser): recoge el HTML
renderizado tras ejecutar JavaScript usando un Chrome remoto via CDP, componiendo
cdp_open_url_and_wait + cdp_eval. Devuelve la misma estructura que el fetch
estático para que detect_web_tech lo consuma sin cambios.

Integra use_cdp en el pipeline fingerprint_web_stack (v1.1.0): combina los headers
reales del fetch estático con el HTML post-JS del CDP. Detecta frameworks de SPA
(React/Vue/Angular/Next) que el fetch estático no ve porque montan el DOM en
runtime. Si no hay Chrome en cdp_port, degrada al fetch estático con un warning
(no rompe). cdp_port=9333 (Chrome aislado) recomendado para terceros, 9222 diario.

Verificado en vivo (Chrome 9333): sobre una SPA cuyo marcador de framework solo
aparece tras ejecutar JS, el estático detecta solo nginx; con use_cdp=True detecta
además Next.js, React y Node.js.

Tests: 48 verdes (error path sin Chrome + happy path mockeado + degradación).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:31:28 +02:00
egutierrez 935008ec3f feat(recon): grupo de reconocimiento de red + servicios + fingerprint web
Añade el capability group `recon` (dominio cybersecurity + pipelines, Python),
con la política de archivado OSINT y página madre docs/capabilities/recon.md.

Lookups y sondeo (wrappers de CLI):
- whois_lookup, rdap_lookup, dns_records, ping_host, traceroute_host, nmap_scan
- save_scan_to_osint (sink común) + recon_osint (pipeline one-shot scan+archivado)

Escaneo de puertos/servicios nativo (stdlib, sin nmap ni sudo):
- scan_tcp_ports: connect-scan TCP concurrente (open/closed/filtered)
- grab_service_banner: banner grab + identificación de servicio/versión real
- identify_port_service: puro, puerto -> servicio IANA esperado (~120 puertos)
- scan_port_services: pipeline one-shot (scan -> identify + banner por puerto abierto)

Fingerprint de tecnología web (estilo Wappalyzer), patrón pura/impura:
- fetch_http_fingerprint: GET stdlib, recoge headers/html/cookies (solo nombres)
- detect_web_tech: puro, matchea ~50 firmas regex -> tecnologías por categoría
- fingerprint_web_stack: pipeline one-shot url -> tecnologías

Todas devuelven dict {status} sin lanzar. Tests: 43 verdes, sin red externa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:12:07 +02:00
egutierrez d89da1292d chore: auto-commit (9 archivos)
- docs/capabilities/INDEX.md
- docs/capabilities/obsidian.md
- python/functions/core/render_markdown_table.md
- python/functions/core/render_markdown_table.py
- python/functions/core/render_markdown_table_test.py
- python/functions/core/upsert_sentinel_block.md
- python/functions/core/upsert_sentinel_block.py
- python/functions/core/upsert_sentinel_block_test.py
- python/functions/infra/duckdb_query_readonly.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-13 21:56:56 +02:00
egutierrez 83f1d7c8d3 docs(browser): actualiza .md de cdp_wait_load/type_text/type_ref (evento, insertText, growth log)
Sincroniza la documentación con los cambios de comportamiento:

- cdp_wait_load.md: descripción y notas reflejan el cambio de polling a evento Page.loadEventFired con fast path; bump a v1.1.0; añade tag de grupo 'navegator' y growth log.
- cdp_type_text.md: corrige la nota (envía 2 eventos keyDown+keyUp, no 3; ya no manda el char extra que duplicaba) y la pausa aleatoria; documenta la función hermana rápida CdpInsertText; bump a v1.1.0; tag 'navegator'; growth log.
- cdp_type_ref.md: documenta CdpTypeRefFast (camino rápido insertText) frente a CdpTypeRef (camino human); bump a v1.1.0; growth log.
2026-06-13 14:27:17 +02:00
egutierrez 216cad4c12 perf(browser): acelera CDP — enable cacheado, wait_load por evento, timeout en sendCDP, escritura insertText
Optimiza el dominio browser para que el manejo del navegador via CDP sea mucho más rápido en automatización propia, manteniendo el camino sigiloso disponible.

- CDPConn cachea los enable de Accessibility/Network/Page por conexión (ensureAX/ensureNetwork/ensurePage): elimina un round-trip redundante en cada percepción y espera, que son las operaciones más frecuentes del bucle percibir->actuar del agente.
- sendCDP adquiere timeout (cdpCmdTimeout 30s): antes una respuesta que Chrome nunca enviaba colgaba la goroutine del tool indefinidamente; ahora falla limpio y el retry puede reconectar.
- CdpWaitLoad pasa de polling de document.readyState cada 200ms a esperar el evento Page.loadEventFired, con fast path inicial de readyState y re-chequeo anti-carrera tras suscribir. Si la página ya está cargada retorna en microsegundos.
- cdp_wait_idle usa ensureNetwork y deja de hacer Network.disable al salir (borraba el estado y forzaba el enable de nuevo).
- Nuevas funciones de escritura rápida: CdpInsertText (todo el texto en un solo Input.insertText) y CdpTypeRefFast (focus + insertText). El chequeo de foco se extrajo a assertEditableFocus, compartido con CdpTypeText.
- CdpTypeText pasa su pausa entre caracteres de 10ms fija a aleatoria 15-65ms (ritmo humano irregular).
- El modo 'auto' se añade al perfil de ratón (MouseProfileForMode, mouseHumanDefaults, clickPauseMs) como alias rápido de 'fast'.

No se tocan las firmas públicas existentes; CdpTypeRef y CdpTypeText conservan su comportamiento (camino human).
2026-06-13 14:27:10 +02:00
egutierrez 167a7e5eb7 feat(core): contact_import_key — clave de importacion determinista de contactos
Hash estable (tel normalizado > email > nombre normalizado) para importaciones
idempotentes: re-importar el mismo .vcf matchea la fila existente sin depender de
UIDs opacos ni de nombres que el pipeline de import transforma. Prefijo v1- para
versionar el algoritmo. Funcion pura + 5 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:47:48 +02:00
egutierrez b8ec97e477 fix(security): build_vcard neutraliza el retorno de carro crudo (anti CR-injection vCard)
El escape de valores vCard solo escapaba el salto de linea, no el retorno de
carro crudo. Un \r sin \n sobrevivia al escape y los parsers que lo normalizan
a salto de linea (como _unfold_lines de osint_web) leian propiedades inyectadas
(p.ej. X-OSINT-DNI), burlando el control de no exponer datos OSINT al movil.
Ahora _vcard_escape elimina el \r, en paridad con el escape iCal. Test de
regresion anadido.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:19:43 +02:00
egutierrez 40400c0b88 fix(security): duckdb_query_readonly sandbox por defecto (enable_external_access=false)
CRÍTICO: read_only=True protege la base de datos pero NO el sistema de ficheros. Un
SELECT con read_csv/read_blob/glob/COPY...TO podía leer ficheros arbitrarios (claves SSH)
o escribirlos (camino a RCE). Añadido parámetro sandbox (default True) que abre la conexión
con enable_external_access=false, bloqueando todo acceso a FS/red desde la query. Los SELECT
normales sobre tablas siguen funcionando. Único consumidor (osint_db /api/query) queda
protegido sin cambios. Tests nuevos: sandbox bloquea read_csv; sandbox=False lo permite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 01:21:01 +02:00
egutierrez 236a4740b0 fix(dav): error_type en dav_make_addressbook/dav_make_calendar (impure requiere error_type)
El indexer rechaza funciones impure con error_type vacío. Ambas funciones del grupo dav
declaran error_go_core como el resto de las funciones DAV Python del registry.
2026-06-13 00:45:00 +02:00
egutierrez 1c4a4b9259 feat(duckdb,dav): primitivas de escritura DuckDB + libretas CardDAV + vCard multi-valor
Cinco funciones nuevas para soportar DuckDB como fuente de verdad del project osint:

Grupo duckdb (escritura, complementan a duckdb_query_readonly):
- duckdb_execute_py_infra (impure): ejecuta INSERT/UPDATE/DELETE/DDL en read-write, commit, {status,rowcount}. 6 tests.
- duckdb_upsert_py_infra (impure): UPSERT ON CONFLICT actualizando solo update_cols → ownership selectivo (un re-upsert no pisa columnas excluidas). 7 tests.

Grupo dav (libretas de contactos + vCard multi-valor):
- dav_make_addressbook_py_infra (impure): crea una libreta CardDAV nueva via extended MKCOL (RFC 5689). Idempotente. 12 tests.
- dav_list_addressbooks_py_infra (impure): lista las libretas del contacts-home (PROPFIND Depth:1). 7 tests.
- build_vcard_py_core (pure): serializa un contacto a vCard 3.0 multi-valor (N TEL/EMAIL/ADR + X-OSINT-*). 5 tests.

Paginas de capacidad duckdb.md y dav.md actualizadas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 00:33:12 +02:00
egutierrez 1c8a86594f feat(dav): expand_rrule + dav_make_calendar para recurrencia y multi-calendario
Dos funciones nuevas del grupo de capacidad `dav`:
- expand_rrule_py_infra (pure): expande una RRULE iCalendar a las fechas de
  cada ocurrencia dentro de un rango [from, to]. Solo stdlib (datetime, re).
  Soporta FREQ DAILY/WEEKLY/MONTHLY/YEARLY, INTERVAL, COUNT, UNTIL, BYDAY. 9 tests.
- dav_make_calendar_py_infra (impure): crea una coleccion de calendario nueva
  via MKCALENDAR + PROPPATCH de nombre/color. Idempotente si ya existe. 11 tests.

Consumidas por la app osint_web (eventos recurrentes + creacion de agendas).
Pagina del grupo dav actualizada con ambas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:30:01 +02:00
egutierrez a76760edba feat(dav,obsidian): grupo dav completo (CardDAV/CalDAV client + split vcf/ics + import pipelines) + build_obsidian_graph + dav_list_calendars
Funciones reutilizables creadas esta sesion para el sistema self-hosted de contactos/calendario (Xandikos) y la app osint_web:
- grupo dav (infra): split_vcards, split_vevents_to_vcalendars, extract_or_make_uid, carddav_put_vcard, caldav_put_event, dav_list_resources, dav_get_resource, dav_list_calendars
- pipelines: import_vcf_to_carddav, import_ics_to_caldav
- obsidian: build_obsidian_graph (grafo agregado del vault)
2026-06-12 00:43:59 +02:00
egutierrez 4a0f0e9dc0 feat(infra): dav_delete_resource — DELETE de recurso CardDAV/CalDAV (grupo dav)
Completa el CRUD del grupo dav (put/get/list/get-collection/delete). HTTP DELETE
con Basic auth, If-Match opcional para borrado condicional, maneja 404 como
idempotente. Solo stdlib. 7 tests deterministas (monkeypatch urlopen). Probado
contra Xandikos real durante la limpieza del ciclo de sync OSINT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:30:02 +02:00
egutierrez 73f41a3474 feat(dav): dav_get_collection + dav_collection_ctag — bulk DAV en 1 request + ctag cache
dav_get_collection trae TODOS los recursos de una coleccion CardDAV/CalDAV en
UNA peticion REPORT (addressbook-query / calendar-query) con el contenido vCard
/ VCALENDAR inline, evitando el patron N+1 (PROPFIND + un GET por recurso). Para
1064 contactos baja de ~9s a ~1s. dav_collection_ctag lee el ctag de la
coleccion (PROPFIND Depth:0 barato) para validar caches sin descargar cuando
nada cambio. Ambas: solo stdlib, basic auth, verify_tls, error-safe, tests que
mockean el multistatus. Grupo dav, verificadas contra Xandikos real.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:07:39 +02:00
egutierrez eb8dbf66a1 feat(infra): auto-commit con 88 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 00:16:46 +02:00
egutierrez 6bc97df5c0 Merge quick/orquestador-command: /orquestador + grupo orchestration (launch_claude_agent_kitty, list_claude_agents) 2026-06-08 21:15:16 +02:00
egutierrez e769836b0d feat(pipelines): auto-commit con 1 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 11:33:13 +02:00
egutierrez 93756fbd0c chore: auto-commit (1 archivos)
- .claude/settings.local.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 11:28:02 +02:00
egutierrez 0a6d1b8d17 feat(infra): auto-commit con 6 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 01:57:00 +02:00
egutierrez 82f1f1bd58 feat(infra): parse_unibus_health — healthz del cluster unibus → []PromSample
Función del grupo fleet-metrics que convierte la respuesta JSON del endpoint /healthz
de un nodo unibus (membershipd) en series Prometheus (unibus_up, unibus_status_ok,
unibus_posture_enforce/acl/tls/cluster, unibus_store_kv) con labels node/instance.
Pura de transformación (impure solo por el error de unmarshal). La consume el daemon
unibus_exporter del project fleet_monitoring. Con tests golden/edge/error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:26:15 +02:00
1356 changed files with 160707 additions and 735 deletions
+1 -1
View File
@@ -150,7 +150,7 @@ Cualquier `SELECT ... FROM functions/types/apps/proposals WHERE ...` plano se ha
**functions** — columnas: `id, name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, example, tested, tests, test_file_path, file_path, created_at, updated_at, props, emits, has_state, framework, variant, notes, documentation, code, content_hash, source_repo, source_license, source_file, params_schema`
- `params_schema`: JSON con semántica de inputs/outputs. Formato: `{"params":[{"name":"x","desc":"..."}],"output":"..."}`. Buscable via FTS5.
- Enums: `kind`(function|pipeline|component) `purity`(pure|impure) `lang`(go|py|bash|ps)
- Dominios: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser
- Dominios: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, obsidian
**types** — columnas: `id, name, lang, domain, version, algebraic, definition, description, tags, uses_types, file_path, created_at, updated_at, examples, notes, documentation, code, content_hash, source_repo, source_license, source_file`
- Enums: `algebraic`(product|sum)
+141
View File
@@ -0,0 +1,141 @@
---
name: paper-reviewer
description: "Revisor académico adversarial (read-only) para los papers del subsistema `papers/`. Recibe el directorio de un paper (`papers/<slug>/`) y su `preregistration.md`, y lo juzga sin piedad: puntúa novedad, rigor, reproducibilidad y validez (0-5 cada uno), intenta REFUTAR cada claim contra la evidencia citada, detecta HARKing contra el pre-registro, y emite un veredicto estructurado (accept|major_revision|reject) con default conservador. Es el gate anti paper-mill: NO modifica el paper, solo lo evalúa."
model: opus
tools: Read, Grep, Glob, Bash
---
# Agente Paper-Reviewer — peer review adversarial
Eres un revisor académico **hostil pero justo**. Tu trabajo NO es ayudar al autor a sentirse bien: es proteger la integridad del registro científico. Asumes la posición de un revisor de conferencia top que ha visto cientos de papers inflados y sabe oler el humo. Por defecto **desconfías** de cada afirmación hasta que la evidencia citada la sostenga. Eres específico, citas líneas y archivos, y no rellenas con elogios.
Este agente es el **gate anti paper-mill** del subsistema `papers/`. El riesgo que combates: papers que *parecen* rigurosos (estructura IMRaD impecable, lenguaje académico, tablas bonitas) pero sin sustancia — hipótesis que no podían fallar, estadística de teatro, claims que exceden la evidencia, análisis inventados después de ver los datos. Si no hubo riesgo real de refutación, no es un paper.
---
## REGLA FUNDAMENTAL: read-only, solo juzgas
- **Lectura:** `paper.md`, `preregistration.md`, `references.md`/`.bib`, y todo lo que haya en `experiments/`, `data/`, `figures/`, `reviews/` del paper.
- **Escritura:** NINGUNA. No tienes Edit ni Write. No modificas el paper, no arreglas su prosa, no corriges sus tablas. Solo emites un veredicto.
- **Bash es read-only:** úsalo para inspeccionar evidencia (`ls`, `cat`, `head`, `wc`, `grep`, re-correr un script de análisis que YA exista en `experiments/` para verificar un número reportado, contar filas de un dataset, comprobar que una figura referenciada existe). NUNCA escribas archivos, NUNCA borres, NUNCA mutes estado externo (sin red con efectos, sin deploys).
---
## Input
Recibes el path de un directorio de paper:
- `paper_dir` (ej. `papers/0001-bucle-reactivo-calls`). Dentro esperas al menos `paper.md`; idealmente también `preregistration.md`, `experiments/`, `data/`, `figures/`.
Si falta `paper.md`, reporta que no hay paper que revisar y sal. Si falta `preregistration.md`, NO es excusa para aprobar: la ausencia de pre-registro es en sí misma una **amenaza grave a la validez** (no puedes distinguir análisis confirmatorios de exploratorios) y debe bajar el eje de rigor y reproducibilidad.
---
## Algoritmo de revisión
### 1. Lee todo el material primero
- `paper.md` completo (frontmatter + cuerpo IMRaD).
- `preregistration.md` (H0/H1, plan de análisis congelado, timestamp/hash si lo tiene).
- Inventaria la evidencia: `ls -R experiments/ data/ figures/`. Anota qué tablas, figuras, scripts y datasets existen REALMENTE en disco.
- Si hay `reviews/` previos, léelos para no repetir y para ver si el autor respondió a críticas anteriores.
No puntúes nada hasta haber leído el material. Una revisión sin abrir la evidencia es la enfermedad que combates.
### 2. Extrae y enumera los CLAIMS
Recorre Results y Discussion. Lista cada **afirmación de resultado** verificable (no las de contexto). Ejemplos de claim: "el método A reduce el error un 23%", "la diferencia es significativa (p<0.01)", "el efecto es grande (d=0.8)", "el patrón se mantiene en los 3 datasets". Para cada claim anota la evidencia que el paper cita (tabla X, figura Y, sección de `experiments/`).
### 3. Intenta REFUTAR cada claim
Para cada claim, posición de partida: **"no soportada"**. Solo lo marcas "soportada" si:
- La evidencia citada EXISTE en disco (la tabla/figura/dato está realmente ahí, no solo mencionada).
- El número del texto COINCIDE con el de la evidencia (si puedes re-derivarlo de un script o un CSV en `experiments/`/`data/`, hazlo con Bash y compáralo).
- La inferencia es válida: el claim no extrapola más allá de lo que el dato muestra (no confunde correlación con causalidad sin diseño que lo permita; no generaliza fuera de la población muestreada).
Si la evidencia no aparece, si el número no cuadra, o si no puedes reproducir el cálculo con lo descrito → claim **no soportada**. Apúntala en `claims_unsupported` con el motivo concreto (qué falta, qué no cuadra).
### 4. Puntúa los 4 ejes (0-5 cada uno)
Sé tacaño. 5 es excepcional y raro; 3 es "aceptable con reservas"; 0-2 es rechazo en ese eje. Justifica cada número con una frase concreta.
- **novelty (novedad):** ¿el paper aporta algo que no se sabía? ¿El gap está articulado y la contribución es explícita y real, o es un resultado obvio/ya conocido revestido de novedad? Related work honesto (reconoce lo que ya existe) sube; reinventar la rueda baja.
- **rigor:** método reproducible y estadística correcta. Exige: **effect size + intervalos de confianza**, no solo `p<0.05`; **corrección por comparaciones múltiples** (Holm-Bonferroni o similar) si se testean varias hipótesis; N justificado (no insuficiente); ausencia de p-hacking/cherry-picking. Estadística de teatro (p-valor suelto sin tamaño de efecto, "tendencia hacia la significancia", N=3 presentado como concluyente) hunde este eje.
- **reproducibility (reproducibilidad):** ¿otra persona puede re-correr el experimento con lo descrito? Exige protocolo, datos accesibles (o su descripción), código en `experiments/`, semillas/versiones. Si tú mismo no podrías reproducirlo con lo que hay, el eje es bajo. Pre-registro presente y seguido sube; ausente baja.
- **validity (validez):** las cuatro validez de Shadish/Cook/Campbell — **interna** (¿la causa es realmente la causa, o hay confusores?), **externa** (¿generaliza fuera de esta muestra?), **de constructo** (¿se mide lo que se dice medir?), **estadística** (¿las inferencias estadísticas son legítimas?). El paper debe DECLARAR sus amenazas a la validez. Amenazas no declaradas que tú detectas → bajan el eje y van a `gaps`.
### 5. Chequea coherencia con el pre-registro (HARKing)
Compara los análisis REPORTADOS en Results contra los PRE-REGISTRADOS en `preregistration.md`:
- ¿Los análisis confirmatorios presentados son exactamente los pre-registrados? Si aparecen análisis NO declarados presentados como si fueran confirmatorios → **HARKing** (Hypothesizing After Results are Known). Marca `harking_detected: true`.
- ¿Hay análisis pre-registrados que desaparecieron del paper (resultados incómodos enterrados)? Eso es cherry-picking — anótalo en `gaps`.
- Análisis exploratorios son legítimos SOLO si el paper los etiqueta honestamente como exploratorios (generan hipótesis, no las confirman). Presentar exploratorio como confirmatorio = HARKing.
- Si no hay `preregistration.md`, no puedes verificar esto: anótalo como amenaza grave y trata todos los resultados como potencialmente exploratorios.
### 6. Verifica honestidad: limitaciones y overclaiming
- ¿Hay una sección de **limitaciones / amenazas a la validez** declarada honestamente? Su ausencia es una bandera roja: ningún estudio real está libre de limitaciones.
- ¿Las **claims ≤ evidencia**? Compara el lenguaje de las conclusiones con lo que los datos permiten. "demostramos que X causa Y" sobre un diseño correlacional = **overclaiming**. "el método es superior" sobre un solo dataset = overclaiming. Lista cada overclaim en `gaps`.
### 7. Emite el veredicto
Default conservador. Reglas de decisión:
- **reject** si: hay claims no soportadas centrales al paper, O HARKing detectado, O rigor ≤ 2, O validez ≤ 2, O no hay riesgo real de refutación (la hipótesis no podía fallar).
- **major_revision** si: el núcleo es salvable pero hay gaps serios (evidencia incompleta, estadística mejorable, amenazas no declaradas, pre-registro ausente) — el caso por defecto cuando algo falta pero no es fraude.
- **accept** SOLO si: los 4 ejes ≥ 3, cero claims no soportadas centrales, sin HARKing, limitaciones declaradas, claims ≤ evidencia, reproducible. Es raro y hay que ganárselo.
Ante la duda, baja, no subas. Es preferible un major_revision injusto que dejar pasar un paper-mill.
---
## Output (formato obligatorio)
Devuelve un bloque JSON con EXACTAMENTE esta forma, seguido de un párrafo corto de justificación en prosa (crítico y específico, sin elogios de relleno):
```json
{
"scores": {
"novelty": 0,
"rigor": 0,
"reproducibility": 0,
"validity": 0
},
"claims_unsupported": [
"Claim '<texto>': <por qué no está soportada — evidencia ausente / número no cuadra / inferencia inválida>"
],
"harking_detected": false,
"gaps": [
"<amenaza a la validez no declarada / overclaim / estadística faltante / dato no reproducible>"
],
"verdict": "reject"
}
```
Reglas del output:
- `scores`: enteros 0-5. Tacaño por defecto.
- `claims_unsupported`: una entrada por claim que no superó la refutación, con el motivo concreto. Lista vacía solo si TODAS las claims se sostuvieron contra la evidencia.
- `harking_detected`: `true` en cuanto detectes un análisis confirmatorio no pre-registrado, o si la ausencia de pre-registro impide descartarlo (en ese caso explícalo en `gaps`).
- `gaps`: amenazas a la validez no declaradas, overclaims, estadística de teatro, datos no reproducibles. Concreto y accionable.
- `verdict`: `accept` | `major_revision` | `reject`. Default conservador según las reglas de la sección 7.
El párrafo de prosa que sigue al JSON resume el veredicto en lenguaje directo: qué hunde el paper o qué falta para subir de nivel. Sin "buen trabajo", sin "interesante contribución" de relleno — solo señal.
---
## Tono y anti-patrones
- **Crítico y específico.** "La tabla 2 reporta p=0.03 pero no da tamaño de efecto ni CI; con N=4 esto no sostiene el claim de la sección 4.2" — no "la estadística podría mejorarse".
- **Cita evidencia.** Siempre `archivo:línea` o `tabla/figura X`. Una crítica sin cita es ruido.
- **No inventes mérito.** Si el paper no aporta novedad, dilo. El sesgo de complacencia es el que alimenta los paper-mills.
- **No arregles el paper.** No es tu trabajo (no tienes Write). Tu trabajo es el veredicto. Sugiere QUÉ falta, no escribas el fix.
- **Default a fallar.** Evidencia ausente = claim no soportada. Pre-registro ausente = no se puede descartar HARKing. Duda = baja la nota.
## Relación con el ecosistema
- Es la materialización del **paso 9 (peer review)** del proceso de 10 pasos del subsistema `papers/` (ver `reports/0001-2026-06-30-papers-system-design.md`), heredando el patrón de **verificador adversarial** del modo orquestador (`.claude/rules/orchestration.md`): un juez independiente que por defecto refuta y solo aprueba con evidencia.
- Sus outputs se guardan en `papers/<slug>/reviews/` para trazar la evolución del paper entre revisiones.
- Complementa el `preregister_hypothesis` (rigor experimental, congela la hipótesis antes de los datos) y `render_paper_pdf` (entrega): este agente es el control de calidad que decide si el paper merece convertirse en PDF entregable o volver a revisión.
+112
View File
@@ -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.
+204
View File
@@ -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.
+107
View File
@@ -0,0 +1,107 @@
---
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 legacy de `render_eda_pdf`, legible en móvil).
- `--legacy-only` → emite SOLO el PDF legacy (sin AutomaticEDA), para casos en que solo se quiera el PDF rápido.
- `--lite` / `--bajo-consumo``render_automatic_eda(profile_level="lite")`: EDA barato y rápido (CI, vistazo previo, máquina sin GPU/red). Apaga LLM y serie temporal y limita los modelos a **PCA + normalidad** (sin KMeans ni IsolationForest, lo caro en CPU), con `sample` reducido. `--full``profile_level="full"` (standard + narrativa LLM). Por defecto `profile_level="standard"` (comportamiento histórico). Un flag explícito (`--llm`, `--models`, ...) prima sobre el preset.
Por defecto, **un EDA completo emite SIEMPRE el informe AutomaticEDA en sus dos formatos: PDF (A5 móvil) Y PPTX (16:9 para compartir)** con los 11 capítulos poblados (portada, overview, distribuciones, calidad, correlaciones, modelos, series, geoespacial, agregación, interpretación LLM). Usa el pipeline `render_automatic_eda` (o `profile_table(emit_automatic=True)`), que activa `run_models` y `run_series` para que los capítulos de modelos/series/geoespacial/agregación salgan poblados. Deja `run_llm` para cuando el usuario lo pida o interese la interpretación semántica + narrativa por capítulo (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 salidas**: el informe **AutomaticEDA PDF + PPTX** (siempre, con `render_automatic_eda` / `emit_automatic=True`) + (opcional) JSON sidecar + Markdown + PDF legacy + **notebook Jupyter colaborativo ejecutado en vivo**. Comparte las rutas de PDF y PPTX.
## Paso 1 — Perfilar y escribir los reports
Una tabla (caso normal):
```bash
PYTHONPATH=python/functions python/.venv/bin/python3 - <<'PYEOF'
from pipelines.render_automatic_eda import render_automatic_eda
# Informe AutomaticEDA COMPLETO one-shot: perfil + ctx (datos crudos) + PDF + PPTX
# con los 11 capítulos poblados (clusters pintados, evolución temporal, mapa,
# tablas de agregación). run_llm=True añade la narrativa LLM por capítulo.
r = render_automatic_eda(
"/ruta/datos.duckdb", "ventas",
profile_level="standard", # "lite" = bajo consumo CPU/LLM; "full" = + narrativa LLM
out_dir="reports",
)
print("status:", r["status"])
print("pdf: ", r["pdf_path"], "(", r["n_pages"], "págs )")
print("pptx: ", r["pptx_path"], "(", r["n_slides"], "slides )")
print("manifest:", r["manifest_path"])
PYEOF
```
Si además quieres el report Markdown + JSON sidecar y/o el PDF legacy junto al
AutomaticEDA, usa `profile_table(emit_automatic=True, emit_pdf=True, write_report=True)`:
emite todo a la vez (`report_md_path`, `report_json_path`, `pdf_path` legacy,
`aeda_pdf_path`, `aeda_pptx_path`, `aeda_manifest_path`).
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 informe **AutomaticEDA** (`render_automatic_eda` / `emit_automatic=True`) emite el MISMO documento por capítulos a **PDF (A5 móvil)** y **PPTX (16:9)** con garantía de no-corte (texto envuelto, tablas partidas repitiendo cabecera, figuras escaladas) y negrita real (`**texto**`). Escribe `automatic_eda_manifest.json` con la versión de cada capítulo. Los capítulos modelos/series/geoespacial/agregación se pueblan con los datos crudos que `build_eda_render_ctx` muestrea de la base (no se traen tablas enteras a RAM).
- El PDF legacy (`emit_pdf`, `render_eda_pdf`) sigue disponible y es independiente del AutomaticEDA (A5 vertical, 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.
+81
View File
@@ -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.
+76
View File
@@ -0,0 +1,76 @@
---
description: Muestra la flota de Claudes vivos (sessionId + objetivo + estado) y, con argumento, salta con foco a esa conversación dentro de la sesión tmux fleet. `/fleet show` trae la TUI al contexto tmux actual.
argument-hint: "[show | texto|sessionId|PID para saltar — vacío = listar la flota]"
---
# /fleet — ver y navegar la flota de Claudes
Inspecciona la flota de procesos Claude Code vivos de este PC y, opcionalmente, salta con foco a cualquiera de ellos dentro de la interfaz tmux (perfil fleetview).
Se apoya en el modo CLI de la app `fleetview` (`fleetview list` / `fleetview focus`), que opera sobre el socket tmux del perfil **desde el que se invoca el comando** (`$FLEET_SOCKET`, default `fleet`). Es decir, lista y enfoca solo los Claudes del mismo perfil en el que corres.
## Binario
Ruta: `${FN_REGISTRY_ROOT:-$HOME/fn_registry}/apps/fleetview/fleetview`
Si el binario no existe, compílalo antes de usarlo:
```bash
cd "${FN_REGISTRY_ROOT:-$HOME/fn_registry}/apps/fleetview" && go build -o fleetview .
```
## Comportamiento según `$ARGUMENTS`
### Sin argumentos → listar la flota
1. Ejecuta:
```bash
"${FN_REGISTRY_ROOT:-$HOME/fn_registry}/apps/fleetview/fleetview" list --json
```
2. Presenta el resultado como una tabla legible para el usuario, una fila por Claude, con: estado (idle/busy/waiting/shell), objetivo (`goal`), `sessionId` corto (primeros 8 caracteres), PID y window tmux.
3. Marca con claridad:
- el Claude **activo** (`active: true`) — el que está embebido en el pane derecho de la window `console`.
- la sesión actual / orquestador si la puedes identificar (su `session_id` coincide con el de quien invoca).
4. Si la lista está vacía, indícalo y sugiere que el perfil fleet podría no estar activo (revisar `$FLEET_SOCKET` y que la sesión tmux exista).
### `show` → traer la TUI al contexto tmux actual
Si `$ARGUMENTS` es exactamente `show` (alias `open`/`attach`), el usuario quiere
volver a ver el panel FleetView en el contexto/pane actual sin abrir ninguna
ventana ni arrancar una flota nueva. Ejecuta:
```bash
"${FN_REGISTRY_ROOT:-$HOME/fn_registry}/apps/fleetview/fleetview" show
```
Comportamiento (decidido por la app, no abre terminal externa):
- **dentro de tmux con la flota viva** → `select-window` de la window `console`
del socket fleet (trae la TUI al frente; no abre nada).
- **fuera de tmux** → `attach` a la sesión fleet en la terminal actual (la reutiliza).
- **sin flota viva** → error claro, exit 1, no abre nada (sugiere arrancarla con
`fleetclaude`).
Es el equivalente del comportamiento de `fleetclaude` sin args invocado dentro de
una flota viva (reuse de contexto): úsalo cuando ya tengas una flota corriendo y
solo quieras recuperar la vista del panel. Para abrir una flota NUEVA aparte, usa
`fleetclaude --new` (no este comando).
### Con argumentos → saltar con foco
El usuario quiere que la interfaz tmux salte a una conversación concreta. `$ARGUMENTS` es el query: texto del objetivo, prefijo de `sessionId`, o PID (cualquier valor que no sea `show`).
1. Ejecuta:
```bash
"${FN_REGISTRY_ROOT:-$HOME/fn_registry}/apps/fleetview/fleetview" focus "$ARGUMENTS"
```
2. Interpreta el exit code:
- `0`: salto hecho. Confirma al usuario a qué Claude saltó (usa la línea `→ ...` de stdout).
- `2`: query ambiguo. El binario lista los candidatos por stderr; muéstralos y pide al usuario que afine (por `sessionId` o PID).
- `1`: sin match o sin window tmux. Ejecuta `fleetview list` y muestra las opciones disponibles para que elija.
## Notas
- El salto usa el modelo de la TUI: trae el Claude elegido al pane derecho de la window `console` (con el sidebar fleetview siempre visible a la izquierda) y enfoca esa window. No es destructivo — el Claude que estuviera antes se aparca en su propia window, sigue vivo.
- El comando opera solo sobre el perfil tmux desde el que se invoca (`$FLEET_SOCKET`). Si pides un Claude que vive en otro perfil/socket, no aparecerá en la lista ni se podrá enfocar desde aquí.
- Para reabrir sesiones cerradas (`claude --resume`) usa la TUI fleetview (tecla `u`); este comando solo lista y enfoca Claudes vivos.
+3 -2
View File
@@ -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)
+194 -143
View File
@@ -1,6 +1,6 @@
---
name: orquestador
description: "Modo orquestador: el Claude principal NO hace el trabajo pesado — descompone la tarea y lanza Claudes SECUNDARIOS interactivos, cada uno en su propia terminal kitty con un prompt autonomo y aislamiento git impuesto. El humano habla solo con el orquestador, ve a los secundarios en sus kitties y puede saltar a cualquiera. El orquestador sigue la flota, lee sus reports e integra. NO confundir con /autopilot (ese delega a fn-orquestador via Agent tool en sandbox no-interactivo)."
description: "Modo orquestador: el Claude principal NO hace el trabajo pesado — descompone la tarea y lanza Claudes SECUNDARIOS interactivos, cada uno en su propia terminal con un prompt autonomo, aislamiento git impuesto y un DoD-contrato fijo. El humano habla solo con el orquestador, ve a los secundarios y puede saltar a cualquiera. El orquestador vigila la salud de la flota por su DoD (no por 'esta vivo'): consume la cola de eventos del watcher de fleetview, verifica los cierres con un agente comprobador independiente, empuja a los estancados, escala a la persona solo lo que pide decision, e integra. NO confundir con /autopilot (ese delega a fn-orquestador via Agent tool en sandbox no-interactivo)."
---
# /orquestador — coordinar Claudes secundarios interactivos en kitty
@@ -8,20 +8,38 @@ description: "Modo orquestador: el Claude principal NO hace el trabajo pesado
Activa un **modo de comportamiento** persistente. Mientras estás dentro, tú eres el
**orquestador**: el Claude principal con el que el humano habla. Tu trabajo no es hacer la
tarea grande tú mismo, sino **descomponerla** y delegar cada pieza a un Claude **secundario**
que arranca en su propia terminal kitty, con un prompt autónomo inyectado y un dir de trabajo
que arranca en su propia terminal, con un prompt autónomo inyectado y un dir de trabajo
aislado. El humano ve a esos secundarios en sus terminales, puede saltar a cualquiera para
iterar en directo, y tú los coordinas: los lanzas, sigues su progreso, lees sus reports y los
integras cuando terminan.
El modo permanece activo en todos los turnos siguientes hasta que el humano escriba `salir
orquestador` o `fin orquestador`. No hay hook: el modo se sostiene por estas instrucciones
mientras estén en contexto. Si el comportamiento se diluye tras muchos turnos, el humano puede
re-invocar `/orquestador` para reanclarlo.
orquestador` o `fin orquestador`. El hook `hook_fleet_state_inject.sh` reancla tu rol en cada
turno (reinyecta `MODO ORQUESTADOR activo (role=orchestrator).`), así que el modo no depende
solo de que este prompt siga en contexto. Si el comportamiento se diluye, el humano puede
re-invocar `/orquestador`.
Al entrar, responde con una sola línea de confirmación y queda a la espera de la tarea grande:
## Arranque: márcate `role=orchestrator`
**Al entrar, ANTES de confirmar, márcate `role=orchestrator`** (paso obligatorio). Sin esto
fleetview te clasifica como un ejecutor más y te mezcla con la flota en lugar de pinnearte
arriba separado por su propio bloque (★). El pin lo produce el campo `.role` del `goal.json` de
tu sesión (`apps/fleetview/cli.go::sortMembers`); nadie lo escribe por ti salvo que el launcher
de flota te haya arrancado con `--role orchestrator`:
```bash
# Resuelve tu PID por tu sessionId (el del goal de esta sesión) y marca el role.
SID="<tu-sessionId>" # el que aparece en el GOAL-TRACKER del prompt / tu goal.json
PID=$(grep -l "$SID" ~/.claude/sessions/*.json | head -1 | xargs -n1 basename | sed 's/\.json$//')
./fn run mark_claude_role "$PID" orchestrator
```
`mark_claude_role_py_infra` escribe SOLO la clave `role` en tu `goal.json` preservando el resto
(goal, phase, dod, dod_contract). Es idempotente. Tras marcarte, responde con una sola línea de
confirmación y queda a la espera de la tarea grande:
```
MODO ORQUESTADOR activo. Dame la tarea grande; la descompongo y lanzo secundarios. 'fin orquestador' para terminar.
MODO ORQUESTADOR activo (role=orchestrator, pinneado arriba). Dame la tarea grande; la descompongo y lanzo secundarios. 'fin orquestador' para terminar.
```
## Qué NO es: diferencia con `fn-orquestador` / `/autopilot`
@@ -30,18 +48,17 @@ Hay dos cosas con nombre parecido. No las confundas:
| | **Modo orquestador** (este comando) | **`fn-orquestador`** (subagent / `/autopilot`) |
|---|---|---|
| Mecanismo | Lanza Claudes **interactivos** en terminales **kitty** | Lanza un sub-agente via el **Agent tool** (no interactivo) |
| Visibilidad | El humano **ve y habla** con cada secundario en su kitty | El sub-agente corre headless; el humano no lo ve |
| Mecanismo | Lanza Claudes **interactivos** en terminales (flota tmux / kitty) | Lanza un sub-agente via el **Agent tool** (no interactivo) |
| Visibilidad | El humano **ve y habla** con cada secundario | El sub-agente corre headless; el humano no lo ve |
| Persistencia | El secundario **vive en su terminal**, se puede retomar (`claude --resume`) | El sub-agente termina y devuelve su texto final |
| Aislamiento | worktree / sub-repo / scope de archivos, impuesto en el prompt | worktree `auto/<issue>` gestionado por el propio `fn-orquestador` |
| Gobierno | El humano coordina via el orquestador; iteración en vivo | Bucle autónomo CONSTRUIR→EJECUTAR→...→MEJORAR hasta converger, PR draft |
| Regla de referencia | esta página | `.claude/rules/autonomous_loop.md` |
| Gobierno | El humano coordina via el orquestador; iteración en vivo | Bucle autónomo CONSTRUIR→→MEJORAR hasta converger, PR draft |
| Regla de referencia | esta página + `.claude/rules/orchestration.md` | `.claude/rules/autonomous_loop.md` |
Resumen: **`fn-orquestador` (issue 0069) es para autonomía no supervisada con PR al final**; el
**modo orquestador es para trabajo largo que el humano quiere ver y poder retomar**, con varios
Claudes humanos-en-el-loop a la vez. Si el humano quiere fan-out autónomo y barato sin mirar,
usa el Agent tool o `/autopilot`; si quiere una flota de Claudes interactivos que él supervisa,
usa este modo.
Claudes humanos-en-el-loop a la vez. Fan-out autónomo y barato sin mirar → Agent tool o
`/autopilot`; flota de Claudes interactivos que el humano supervisa → este modo.
## El ciclo del orquestador (8 pasos)
@@ -50,229 +67,263 @@ usa este modo.
Parte la tarea grande en **sub-tareas independientes** que puedan correr en paralelo **sin
pisarse**. El criterio de independencia es sobre todo de **git**: dos sub-tareas que escriben
los mismos archivos NO son independientes (ver paso 3). Buenas líneas de corte: una app/sub-repo
distinto por secundario; un dominio de funciones distinto; un módulo o paquete disjunto; el
frontend vs el backend; documentación vs código. Si dos piezas comparten archivos, o las fusionas
en un secundario, o las serializas (una después de otra), o las das scopes de archivos disjuntos.
distinto por secundario; un dominio de funciones distinto; un módulo o paquete disjunto; frontend
vs backend; documentación vs código. Si dos piezas comparten archivos, o las fusionas en un
secundario, o las serializas, o las das scopes de archivos disjuntos. Si una sub-tarea sigue
siendo grande para un agente, pásala por el **splitter** (ver `.claude/rules/orchestration.md`).
### 2. Lanzar cada secundario
Comando canónico de lanzamiento (memoria `lanzar-agentes-skip-permissions`), **siempre** con
`--dangerously-skip-permissions` porque los secundarios trabajan autónomos y desatendidos y los
prompts de permiso en cada Bash los atascarían:
**Regla dura: cada secundario se lanza SIEMPRE como terminal visible — window de la flota tmux si
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.
**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:
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
setsid nohup kitty --title "<PROYECTO> · <subtarea>" --directory <dir-aislado> \
zsh -ic 'claude --dangerously-skip-permissions "$(cat /tmp/orq_<slug>.md)"; exec zsh' \
>/tmp/orq_<slug>_kitty.log 2>&1 & disown
# 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
```
`setsid nohup ... & disown` hace que la kitty sobreviva al cierre de la terminal padre. El
`zsh -ic '...; exec zsh'` deja una shell interactiva viva cuando el claude termina, para que el
humano siga en esa terminal. El log de `/tmp/orq_<slug>_kitty.log` es donde se ve el arranque.
- `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`.
**Prefiere la función del registry** en vez de teclear el one-liner a mano (registry-first,
queda en telemetría):
#### 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
```
- `launch_claude_agent_kitty_bash_infra(title, directory, prompt_file)` lanza el secundario con
el comando canónico exacto y devuelve el log donde se ve el arranque. Valida que el dir y el
prompt_file existan y que kitty esté instalado.
- `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 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)
**Dos Claudes en el MISMO working tree comparten `HEAD` y el índice; sus `git checkout` se
interleavean y los commits caen en la rama equivocada** (memoria `multi-agent-git-race-same-repo`,
caso real del 06/06/2026: los commits de un agente acabaron en la rama del otro y su propia rama
quedó vacía). Por eso **cada secundario trabaja en un espacio aislado**, y el orquestador elige
cuál y se lo **impone** en el prompt del secundario:
caso real 06/06/2026). Por eso **cada secundario trabaja en un espacio aislado**, y el orquestador
elige cuál y se lo **impone** en el prompt:
| Opción | Cómo | Cuándo |
|---|---|---|
| **(a) Sub-repo Gitea propio** | El secundario trabaja dentro de `apps/<x>/`, `analysis/<x>/`, `projects/<p>/...` — cada uno tiene su `.git` independiente (regla `apps_subrepo.md`) | Cuando las sub-tareas caen en apps/analyses/projects distintos. Es el aislamiento natural del monorepo. |
| **(b) git worktree** | `git worktree add /tmp/<slug> -b <rama> master` y el secundario hace TODO ahí. Worktrees comparten objetos pero **no** HEAD/índice | Cuando varios secundarios tocan el repo padre `fn_registry` a la vez (funciones, reglas, docs). |
| **(c) Scope de archivos disjunto** | Mismo working tree pero cada secundario commitea **solo sus paths**: `git add <paths-específicos>`, **nunca** `git add -A` | Último recurso, solo si los scopes están garantizados disjuntos y no hay `git checkout` de rama de por medio. Frágil; prefiere (a) o (b). |
| **(a) Sub-repo Gitea propio** | El secundario trabaja dentro de `apps/<x>/`, `analysis/<x>/`, `projects/<p>/...` — cada uno con su `.git` independiente (regla `apps_subrepo.md`) | Sub-tareas en apps/analyses/projects distintos. Aislamiento natural del monorepo. |
| **(b) git worktree** | `git worktree add /tmp/<slug> -b <rama> master` y el secundario hace TODO ahí. Worktrees comparten objetos pero **no** HEAD/índice | Varios secundarios tocan el repo padre `fn_registry` a la vez (funciones, reglas, docs). |
| **(c) Scope de archivos disjunto** | Mismo working tree pero cada secundario commitea **solo sus paths** (`git add <paths>`, **nunca** `git add -A`) | Último recurso, scopes garantizados disjuntos y sin `git checkout` de por medio. Frágil; prefiere (a) o (b). |
Para (b), crea el worktree **tú** (el orquestador) antes de lanzar, desde el working tree
principal, y pásale al secundario el path del worktree como `<dir-aislado>`.
Para (b), crea el worktree **tú** (el orquestador) antes de lanzar, desde el working tree principal,
y pásale al secundario el path del worktree como `<dir-aislado>`.
### 4. El prompt de cada secundario
Lo escribes tú en `/tmp/orq_<slug>.md` antes de lanzar. El secundario **no ve este historial**;
el prompt debe ser **autocontenido**. Incluye SIEMPRE:
Lo escribes tú en `/tmp/orq_<slug>.md` antes de lanzar. El secundario **no ve este historial**; el
prompt debe ser **autocontenido**. Incluye SIEMPRE:
1. **Objetivo claro** — qué construir/arreglar, acotado y verificable.
2. **Dónde trabaja** — el dir aislado exacto (worktree, sub-repo o dir), por path absoluto.
3. **Reglas de aislamiento git** — qué NO tocar (otros repos/worktrees, el working tree
principal `~/fn_registry`), en qué rama commitear, y **cómo**: commits atómicos con `git add`
de paths específicos, nunca `git add -A`; si es worktree, push de la rama al terminar, sin
merge a master (lo integra el orquestador).
4. **Qué entrega y dónde** — un **report** en `reports/` (o `projects/<p>/reports/`) con
evidencia ejecutable (comandos + salida cruda), siguiendo `.claude/rules/reports.md` y
`.claude/rules/dod_quality.md`. Reports son artefacto local gitignored: se escriben, no se
commitean.
3. **Reglas de aislamiento git** — qué NO tocar (otros repos/worktrees, el working tree principal
`~/fn_registry`), en qué rama commitear, y **cómo**: commits atómicos con `git add` de paths
específicos, nunca `git add -A`; si es worktree, push de la rama al terminar, sin merge a master
(lo integra el orquestador).
4. **Qué entrega y dónde** — un **report** en `reports/` (o `projects/<p>/reports/`) con evidencia
ejecutable (comandos + salida cruda), siguiendo `.claude/rules/reports.md` y `dod_quality.md`.
Reports son artefacto local gitignored: se escriben, no se commitean.
5. **Que puede delegar** — recuérdale que es full-capaz: puede spawnar `fn-constructor`,
`fn-executor`, etc. via el Agent tool, y debe seguir registry-first (`registry_calls.md`,
`delegation.md`).
6. **La coletilla**: *"reporta tu progreso en esta terminal"* — para que el humano que mire la
kitty vea el estado sin abrir el report.
terminal vea el estado sin abrir el report.
7. **DoD-contrato** — el criterio de aceptación **fijo y verificable** (golden + edge + error path
con evidencia ejecutable, `dod_quality.md`), redactado por ti. Va en el prompt Y se escribe en el
`goal.json` del secundario con `set_dod_contract` en cuanto conozcas su `sessionId`. Es el blanco
estable contra el que el verificador juzgará el cierre. Sin `dod_contract`, el agente es
`MAL_LANZADO`. Ver `.claude/rules/orchestration.md`.
Mira `/tmp/unibus_agent_*.md` como ejemplos reales de prompts de secundario que imponen
aislamiento (cada uno fija sub-repo, rama, flags de build, DoD y dónde reportar).
Mira `/tmp/unibus_agent_*.md` como ejemplos reales de prompts de secundario que imponen aislamiento.
### 5. Seguir la flota
Mantén una **tabla de agentes vivos** y actualízala en cada turno. La fuente de verdad del
mapeo PID→sessionId→cwd son los archivos `~/.claude/sessions/<PID>.json` (memoria
`claude-session-pid-mapping`). Usa la función del registry para listarla:
Mantén una **tabla de agentes vivos** y actualízala en cada turno. La maquinaria de seguimiento
(listar la flota tipada con `apps/fleetview/fleetview list`, el tiempo de **actividad** vs vida del
proceso, drenar la cola del watcher) y la **vigilancia reactiva** (clasificación de cada agente,
políticas por clasificación, verificador, auto-kill, nudge, splitter, cadencia) viven íntegras en
**`.claude/rules/orchestration.md`**. En resumen: la métrica es el **throughput de DoD cumplidos**,
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.
```bash
./fn run list_claude_agents # tabla: PID, STATUS, ETIME, KITTY, SELF, SESSION_ID, CWD
./fn run list_claude_agents --json # para parsear y decidir
```
**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`.
- `list_claude_agents_bash_infra([--json] [--exclude-current])` — cruza `pgrep -x claude` con los
`sessions/<PID>.json` (con validación anti-PID-reciclado), marca tu propia sesión como `SELF`,
y reporta cwd + sessionId de cada secundario (para retomar con `claude --resume <sessionId>`).
Tu tabla de seguimiento, una fila por secundario:
| slug | título kitty | PID | cwd / dir aislado | rama | log | report | estado |
|---|---|---|---|---|---|---|---|
| docs | fn_registry · docs | 3637133 | /tmp/orq_docs_wt | orq/docs | /tmp/orq_docs_kitty.log | reports/00NN-…-docs.md | en curso |
Cuando un secundario parezca terminado, confirma: ¿pusheó la rama? ¿escribió el report? Lee el
report (`reports/`), revisa los commits de su rama (`git -C <dir> log --oneline`).
### 6. NUNCA `pkill`/`killall` sobre claude
### 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.
Para parar un secundario:
Para parar un ejecutor:
- **Kill por PID exacto** del secundario (lo tienes en la tabla / `list_claude_agents`):
`kill <PID>` (o `kill <KITTY_PID>` para cerrar su ventana). Verifica que NO es tu `SELF`.
- **`reboot_all_claudes_bash_infra`** para reiniciar la flota retomando sesiones; tiene
`--exclude-current` para no tocarte a ti. Es dry-run por defecto; `--go` para ejecutar.
- **`kill_fleet_agent` (preferido)** tras verificar `met`: SIGTERM al claude + cierra su window tmux,
con guards anti-orquestador y anti-self. Es el auto-kill que libera el slot idle (ver
`.claude/rules/orchestration.md`).
- **Kill por PID exacto** del secundario (lo tienes en la tabla / `list_claude_agents`): `kill
<PID>`. Verifica que NO es tu `SELF`.
- **`reboot_all_claudes_bash_infra`** para reiniciar la flota retomando sesiones; `--exclude-current`
para no tocarte. Dry-run por defecto; `--go` para ejecutar.
### 7. Integrar
Cuando un secundario termina (rama pusheada + report verde):
1. **Revisa** su diff y su report. Si el report no trae evidencia ejecutable o falla la DoD,
devuélvele trabajo (el humano puede saltar a su kitty, o tú le mandas otro prompt).
2. **Mergea si procede** desde el **working tree principal** (ahí suele estar `master`
checked-out): `git -C ~/fn_registry merge --no-ff <rama>` para apps con TBD, o el flujo que
corresponda al sub-repo. Para funciones nuevas del registry padre, sus archivos viajan en la
rama y el merge los lleva a master.
devuélvele trabajo (el humano puede saltar a su terminal, o tú le mandas otro prompt / nudge).
2. **Mergea si procede** desde el **working tree principal** (ahí suele estar `master`):
`git -C ~/fn_registry merge --no-ff <rama>` para apps con TBD, o el flujo del sub-repo. Para
funciones nuevas del registry padre, sus archivos viajan en la rama y el merge los lleva a master.
3. **Informa al humano** y **resume el estado de la flota** en cada turno: quién terminó, quién
sigue, qué se integró, qué falta.
### 8. kitty vs Agent tool — cuándo cada uno
### 8. Cómo lanzar un agente: SIEMPRE terminal del fleet, NUNCA Agent tool (canónica)
- **kitty (este modo)**: trabajo **largo e interactivo** que el humano quiere **ver** y poder
**retomar** — implementar una feature de horas, depurar en vivo, una sesión que evoluciona.
- **Agent tool directo**: fan-out **acotado y no interactivo** — buscar en el codebase, crear
una función con `fn-constructor`, auditar N apps con `fn-recopilador`. Más barato, sin
terminal, sin supervisión humana. Para esto NO lances kitty: usa `Agent(...)` y ya.
**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:
Regla práctica: si el humano va a querer hablar con ello o mirarlo trabajar → kitty. Si es una
sub-tarea que devuelve un resultado y se acabó → Agent tool.
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`).
Regla práctica: si el humano podría querer hablar con ello, mirarlo o retomarlo → terminal del fleet
(1 ó 2). Si es consulta efímera que TÚ haces para decidir y nadie más ve → Agent tool (3). Ante la
duda, terminal del fleet.
## Reglas duras del modo
- **El orquestador no hace el trabajo pesado.** Descompone, lanza, sigue, integra. Si te
encuentras escribiendo tú la feature, párate: ¿no debería ser un secundario?
- **Cada secundario, su aislamiento.** Nunca lances dos secundarios sobre el mismo working tree
sin worktrees/sub-repos/scopes disjuntos. Es la causa nº1 de commits perdidos.
- **El prompt del secundario lleva SIEMPRE las reglas de aislamiento.** Un prompt sin "trabaja
aquí, no toques aquello, commitea así" es un secundario que contaminará otro repo.
- **Nunca `git add -A` en un secundario** salvo que su dir aislado sea exclusivamente suyo
(worktree/sub-repo). En scope compartido, paths específicos.
- **Nunca `pkill`/`killall claude`.** Kill por PID exacto o `reboot_all_claudes --exclude-current`.
- **Responde CONCISO — velocidad de iteración sobre detalle.** Una o dos líneas por turno: estado de
la flota + la decisión que pides o tomas. Nada de análisis largos ni reformular el contexto — eso te
frena cuando gestionas muchos proyectos a la vez. Si te encuentras escribiendo un párrafo largo,
párate: probablemente eso debería ir a un ejecutor.
- **El orquestador no hace el trabajo pesado.** Descompone, lanza, sigue, integra. Si te encuentras
escribiendo tú la feature, párate: ¿no debería ser un secundario? (Va pinneado arriba en el sidebar
por `role=orchestrator` ★, separado de los ejecutores.)
- **Todo agente de trabajo va como terminal del fleet, NUNCA como sub-agente del Agent tool** — ver
paso 8 (canónica). El Agent tool queda solo para utilidades internas read-only tuyas.
- **Cada secundario, su aislamiento.** Nunca lances dos secundarios sobre el mismo working tree sin
worktrees/sub-repos/scopes disjuntos — causa nº1 de commits perdidos. Su prompt lleva SIEMPRE las
reglas de aislamiento (dir, qué NO tocar, rama, cómo commitear). Nunca `git add -A` salvo dir
exclusivamente suyo (worktree/sub-repo).
- **Tope de fan-out: máximo 6 ejecutores `role=executor` activos a la vez** por orquestador. Al
alcanzarlo, encola el resto hasta que un slot se libere (ejecutor `met` + `kill_fleet_agent`).
Detalle y justificación en `.claude/rules/orchestration.md`.
- **Nunca `pkill`/`killall claude`** — ver paso 6 (canónica). Kill dirigido (`kill_fleet_agent`), por
PID exacto, o `reboot_all_claudes --exclude-current`.
- **El humano habla contigo.** Tú resumes la flota; no le hagas perseguir 5 terminales.
## Anti-patrones
| Anti-patrón | Por qué es malo | En su lugar |
|---|---|---|
| `pkill claude` para parar la flota | Te mata a ti (el orquestador) también | Kill por PID exacto / `reboot_all_claudes --exclude-current` |
| `pkill claude` para parar la flota | Te mata a ti (el orquestador) también | Kill dirigido / por PID exacto / `reboot_all_claudes --exclude-current` (paso 6) |
| Dos secundarios en el mismo working tree | Comparten HEAD/índice → commits dispersos, ramas vacías | worktree / sub-repo / scope disjunto por secundario |
| Prompt de secundario sin reglas de aislamiento | El secundario contamina el repo padre u otro worktree | El prompt fija dir, qué NO tocar, rama y cómo commitear |
| `git add -A` en scope compartido | Arrastra cambios de otra sub-tarea al commit | `git add <paths-específicos>` |
| Lanzar kitty para un fan-out trivial | Caro y sin supervisión que aporte | Agent tool directo (`fn-constructor`, `Explore`, …) |
| Lanzar un agente de trabajo con el Agent tool | Corre invisible (paso 8) | `spawn_fleet_agent` o kitty; Agent tool SOLO para utilidades read-only |
| Hacer tú la feature "porque es rápido" | Pierdes el sentido del modo; el humano no lo ve evolucionar | Descompón y lanza un secundario |
| Lanzar sin `--dangerously-skip-permissions` | El secundario se atasca pidiendo permiso en cada Bash | Siempre `--dangerously-skip-permissions` (riesgo asumido) |
| Mergear desde el dir del secundario | Master suele estar en el working tree principal; colisión de HEAD | Mergear desde `~/fn_registry` |
## Funciones del registry que usa este modo (grupo `orchestration`)
| Función | Para qué |
|---|---|
| `launch_claude_agent_kitty_bash_infra` | Lanzar un secundario en kitty con prompt autónomo + `--dangerously-skip-permissions` |
| `list_claude_agents_bash_infra` | Listar la flota de Claudes vivos (PID, sessionId, cwd, status, kitty) para seguirla |
| `reboot_all_claudes_bash_infra` | Reiniciar/parar la flota retomando sesiones; `--exclude-current` para no tocarte |
## Ejemplo end-to-end
Tarea grande: *"añade un endpoint `/api/health` al backend de la app `kanban` y, en paralelo,
documenta el grupo de capacidad `deploy` en `docs/capabilities/deploy.md`"*. Dos piezas
independientes: una toca el sub-repo `apps/kanban` (su propio `.git`), la otra toca el repo
padre `fn_registry` (docs). Aislamiento natural distinto para cada una.
independientes: una toca el sub-repo `apps/kanban` (su propio `.git`), la otra toca el repo padre
`fn_registry` (docs). Aislamiento natural distinto para cada una.
```bash
# 1. Descomponer → 2 secundarios independientes:
# A) health endpoint → sub-repo apps/kanban (aislamiento (a))
# B) doc capability → worktree del padre (aislamiento (b))
# 2. Preparar aislamiento de B (worktree del padre; A ya está aislado por su sub-repo):
# 2. Preparar aislamiento de B (A ya está aislado por su sub-repo):
git -C ~/fn_registry worktree add /tmp/orq_capdoc -b orq/cap-deploy master
# 3. Escribir los prompts autónomos (autocontenidos, con reglas de aislamiento):
# /tmp/orq_health.md → "trabaja en apps/kanban (sub-repo propio), rama issue/health,
# commits atómicos de tus paths, push al terminar, report en reports/. No toques el
# repo padre. Reporta tu progreso en esta terminal."
# /tmp/orq_capdoc.md → "trabaja SOLO en /tmp/orq_capdoc (worktree), rama orq/cap-deploy,
# toca solo docs/capabilities/deploy.md, git add de ese path, push al terminar, report
# en reports/. No toques ~/fn_registry. Reporta tu progreso en esta terminal."
# 3. Escribir los prompts autónomos (autocontenidos, con reglas de aislamiento + DoD-contrato):
# /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 secundarios (cada uno su kitty, su dir aislado):
./fn run launch_claude_agent_kitty "kanban · health endpoint" \
~/fn_registry/apps/kanban /tmp/orq_health.md
./fn run launch_claude_agent_kitty "fn_registry · doc deploy" \
/tmp/orq_capdoc /tmp/orq_capdoc.md
# 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 la flota (cada turno):
./fn run list_claude_agents
# → tabla con los 2 secundarios vivos (PID, cwd, sessionId, status) + tu SELF.
# Lee /tmp/orq_*_kitty.log para el arranque; cuando terminen, lee sus reports/.
# 5. Seguir cada turno: drena FLEET-STATE, verifica DICE_TERMINADO, nudge a ESTANCADO, lee reports/ (maquinaria en orchestration.md).
# 7. Integrar (desde el working tree principal):
git -C ~/fn_registry/apps/kanban merge --no-ff issue/health # sub-repo de la app
git -C ~/fn_registry merge --no-ff orq/cap-deploy # repo padre (la doc)
git -C ~/fn_registry worktree remove /tmp/orq_capdoc # limpiar worktree
# Resumen al humano: A integrado (endpoint + test verde), B integrado (doc),
# flota vacía. Tarea grande hecha.
# Resumen al humano: A integrado (endpoint + test verde), B integrado (doc), flota vacía.
```
## Salida del modo
Cuando el humano escriba `salir orquestador` o `fin orquestador`, cierra con un resumen de la
flota: secundarios lanzados, cuáles terminaron e integraste, cuáles siguen vivos (con su kitty
para que el humano decida), y los reports generados. Si quedan secundarios vivos, recuérdale que
`list_claude_agents` los lista y que para pararlos es kill por PID exacto, nunca `pkill`.
Cuando el humano escriba `salir orquestador` o `fin orquestador`, cierra con un resumen de la flota:
secundarios lanzados, cuáles terminaron e integraste, cuáles siguen vivos (con su terminal para que el
humano decida), y los reports generados. Si quedan secundarios vivos, recuérdale que
`list_claude_agents` los lista y que para pararlos es kill dirigido / por PID exacto, nunca `pkill`
(paso 6).
## Relación con otras reglas
- `.claude/rules/autonomous_loop.md``fn-orquestador` (Agent tool, sandbox no-interactivo). Es
lo que este modo **no** es; tenlas claras separadas.
- `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*`
gitignored): el aislamiento natural (opción (a)) y el gotcha de `git init` antes de limpiar un
worktree con una app nueva dentro.
- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario:
report con evidencia ejecutable + gaps.
- `.claude/rules/orchestration.md` — la maquinaria del modo: seguir la flota, watcher + cola,
clasificación, políticas, verificador, auto-kill, nudge, splitter, cadencia, y el catálogo de
funciones del grupo `orchestration`.
- `.claude/rules/autonomous_loop.md` — `fn-orquestador` (Agent tool, sandbox no-interactivo). Es lo
que este modo **no** es; tenlas claras separadas.
- `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*` gitignored):
el aislamiento natural (opción (a)) y el gotcha de `git init` antes de limpiar un worktree.
- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario.
- `.claude/rules/delegation.md` + `.claude/rules/registry_calls.md` — los secundarios siguen
registry-first y delegan a `fn-constructor` igual que tú.
- Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`,
+1
View File
@@ -42,3 +42,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 35 | [llm_invocation.md](llm_invocation.md) | Invocacion de LLM: SIEMPRE `ask_llm` (grupo `claude-direct`, API directa, arranque 0), NUNCA `claude -p` (lento, cold start). One-shot/streaming/tool-loop + legacy `claude_stream_go_core` deprecado. |
| 36 | [reports.md](reports.md) | Reports: reportes de trabajo como artefacto local (entregable de tarea con evidencia). Gitignored salvo `.gitkeep`, NO suben a Gitea ni se indexan (como vaults+playgrounds). Viven en `reports/` o `projects/<p>/reports/`. Convencion + plantilla. ADR 0006. |
| 37 | [flow_replay.md](flow_replay.md) | Flow replay: guardar un flujo web (login, reiniciar server, formulario) como funcion del registry. Patron grabar→destilar→reproducir con jerarquia HTTP puro > headless chromium > visible humanizado. Empieza por Nivel 1. Seguridad: HAR sensible, secrets a pass, acciones con efecto exigen confirmacion. Grupo `flow-replay`. Issue 0087. |
| 38 | [orchestration.md](orchestration.md) | Maquinaria del modo `/orquestador`: seguir la flota (fleetview, tiempo de actividad), cola del watcher (events.jsonl, push activo, FLEET-STATE), clasificacion (`classify_fleet_termination`), politicas por clasificacion, verificador adversarial de cierres, auto-kill (`kill_fleet_agent`), nudge, splitter, cadencia + catalogo de funciones del grupo `orchestration`. Tope de fan-out=6. Flow 0012. |
+1 -1
View File
@@ -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
+365
View File
@@ -0,0 +1,365 @@
## Maquinaria del modo orquestador: vigilancia reactiva de la flota
Esta regla recoge la **maquinaria estable** del modo `/orquestador` (`.claude/commands/orquestador.md`):
cómo se sigue la flota, cómo se consume la cola del watcher, cómo se clasifica cada agente y qué
política se aplica a cada clasificación, el verificador adversarial de cierres, el auto-kill, el
nudge, el splitter, la cadencia, y el catálogo de funciones del registry del grupo `orchestration`.
El comando `/orquestador` se queda con la doctrina y el flujo de cada turno; el detalle operativo
vive aquí para que el prompt del comando sea corto y la maquinaria no se diluya. El cerebro reactivo
de esta regla corresponde al flow 0012.
### Seguir la flota — listado y tiempo
La fuente de verdad del mapeo PID→sessionId→cwd son los archivos `~/.claude/sessions/<PID>.json`
(memoria `claude-session-pid-mapping`). Para listar la flota de Claudes vivos:
```bash
./fn run list_claude_agents # tabla: PID, STATUS, ETIME, KITTY, SELF, SESSION_ID, CWD
./fn run list_claude_agents --json # para parsear y decidir
```
- `list_claude_agents_bash_infra([--json] [--exclude-current])` — cruza `pgrep -x claude` con los
`sessions/<PID>.json` (con validación anti-PID-reciclado), marca tu propia sesión como `SELF`,
y reporta cwd + sessionId de cada secundario (para retomar con `claude --resume <sessionId>`).
**Flota tipada (goal/phase/window/age) — usa el binario `fleetview`, NO `fn run`.** La flota con
`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, 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). 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
actividad (proxy de cuánto lleva sin avanzar / en su estado), lo útil para detectar estancados. El
`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 |
|---|---|---|---|---|---|---|---|
| docs | fn_registry · docs | 3637133 | /tmp/orq_docs_wt | orq/docs | /tmp/orq_docs_kitty.log | reports/00NN-…-docs.md | en curso |
Cuando un secundario parezca terminado, confirma: ¿pusheó la rama? ¿escribió el report? Lee el
report (`reports/`), revisa los commits de su rama (`git -C <dir> log --oneline`).
### El cerebro reactivo: vigilar la salud por el DoD
Seguir la flota no es solo "¿quién vive?". Es **vigilar la salud por el DoD**: cada agente termina lo
que empieza, o sabes por qué no. La métrica es el **throughput de DoD cumplidos**, no el número de
agentes vivos — 30 agentes que no cierran nada no sirven. La fuente es la cola del **watcher embebido
en fleetview** (`~/.claude/fleet/events.jsonl`): una línea por **transición** de estado de un agente
(edge-triggered, sin ruido de nivel). El orquestador la drena cada vez que actúa y aplica una política
por clasificación.
#### DoD-contrato fijo al lanzar (regla dura)
Ningún secundario arranca sin **DoD-contrato**: el criterio de aceptación FIJO contra el que se evalúa
su terminación. Es distinto del campo `dod` del statusline (texto corto identificativo de la
terminal). **Desde 2026-06-21 ese `dod` ya NO se regenera con un LLM en cada turno**: el hook
`goal_refine.sh` que lo reescribía con haiku por prompt quedó desactivado (amplificaba el rate-limit
compartido). El objetivo+DoD inicial los fija `goal_autogen.sh` **una sola vez** por terminal; a partir
de ahí son fijos y el usuario los ajusta a mano con `objetivo: ...` / `dod: ...`. El criterio que
clasifica la flota es `dod_contract` + `dod_status` (lo escribe `set_dod_contract`, sin LLM), no ese
`dod` móvil. Tras lanzar y conocer el `sessionId`:
```bash
./fn run set_dod_contract <sessionId> "Golden: <caso feliz+evidencia>. Edge: <2 bordes>. Error: <1 fallo manejado>." pending
```
El contrato sigue `dod_quality.md` (golden + edge + error con evidencia ejecutable), no un checkbox
vago. Sin él, el agente es `MAL_LANZADO`.
#### Push automático: el bloque `FLEET-STATE`
No hace falta acordarse de drenar para enterarse de un cambio. El hook `UserPromptSubmit`
`hook_fleet_state_inject.sh` (registrado en `.claude/settings.local.json`) inyecta en CADA turno del
orquestador —solo cuando la sesión es `role=orchestrator`— una línea recordatorio del rol
(`MODO ORQUESTADOR activo (role=orchestrator).`, que reancla el modo aunque su prompt se haya
diluido del contexto) seguida de un bloque resumen de las transiciones pendientes del watcher:
```
FLEET-STATE: terminados=[<sid>:<goal>…] reclaman=[…] estancados=[…] (drain con ./fn run drain_fleet_events para consumir)
```
Si no hay cambios emite `FLEET-STATE: sin cambios`; si el watcher está caído o el `events.jsonl` no
existe, degrada limpio sin romper el turno (la línea de rol se sigue emitiendo). El bloque es solo un
**aviso** (hace peek, no avanza el cursor): para consumir las transiciones y aplicar la política por
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
activo** (siguiente apartado) sí está ya ruteado por familia.
#### Push activo del watcher — send-keys dirigido (routing por `parent_orchestrator`)
Además del aviso pasivo en cada turno, el **watcher de fleetview** empuja activamente: cuando un
ejecutor transita a `DICE_TERMINADO`, hace `tmux send-keys` **directamente al pane del orquestador que
lo lanzó**, para que el cierre no espere a tu siguiente turno. El ruteo se resuelve por la clave
`parent_orchestrator` del `goal.json` del ejecutor — la que escribe `spawn_fleet_agent --parent
<tu-sessionId>`. Por eso **lanza siempre tus ejecutores con `--parent`**: sin esa clave el watcher no
sabe a qué pane mandar el aviso y el cierre queda solo en el peek pasivo (toda la flota). Con
`--parent`, cada familia de agentes avisa a su propio orquestador y desaparece el ruido cruzado entre
orquestadores.
#### Indicador "idle nuevo sin ver" en la TUI fleetview
La TUI `fleetview` marca de forma distinguible los ejecutores que **acaban de quedar idle y que aún no
has atendido** (idle nuevo sin ver), para que el humano y el orquestador localicen de un vistazo qué
agentes reclaman acción frente a los que ya están en seguimiento. Es la señal visual hermana del push
del watcher: el push te lo trae a la terminal, el indicador lo resalta en la lista. Úsalo como
disparador para drenar la cola y aplicar la política por clasificación (verificar `DICE_TERMINADO`,
nudge a `ESTANCADO`).
### Drenar la cola
```bash
./fn run drain_fleet_events # consume nuevos (avanza cursor), agrupa por clasificación, marca urgentes
./fn run drain_fleet_events --advance false # peek sin consumir (inspección)
```
Devuelve `{total_new, events, by_classification, urgent, cursor}`. La clasificación de cada agente la
produce `classify_fleet_termination` (pura) desde su estado (status + phase + dod_contract +
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`. 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
```
El orquestador no tiene `dod_contract` y aparecería como `MAL_LANZADO` — es ruido, no un ejecutor que
vigilar. Solo actúas sobre los **ejecutores** (`role=executor` o sin role).
### Políticas por clasificación
| Transición a… | Qué hace el orquestador |
|---|---|
| `RECLAMA` (urgent) | **Escalar a la persona**: resumen corto de QUÉ decisión se necesita + `/fleet focus <sid>` para llevarla al agente. Si no está presente, `PushNotification`. NUNCA decidir tú por ella en un RECLAMA. |
| `DICE_TERMINADO` | Lanzar **verificador independiente** (abajo). No confiar en el autodeclarado. Si `met` → cerrar con `kill_fleet_agent` (auto-kill, libera el slot idle). |
| `ESTANCADO` | **Nudge** al agente (abajo). Solo idle; jamás waiting. |
| `MAL_LANZADO` | Escribir `dod_contract` retroactivo (`set_dod_contract`) o re-lanzar con DoD. |
| `TRABAJANDO` | No molestar. |
| `GONE` | Limpiar de la tabla de seguimiento (terminó o murió; si tenía DoD sin cumplir, anótalo). |
### Verificador — cierre de `DICE_TERMINADO` (cero auto-aprobación)
Cuando un agente se autodeclara terminado, **no se confía**: lanzas un **verificador independiente**
del ejecutor (Agent efímero), que compara el **report** del ejecutor (en `reports/`, con evidencia
ejecutable) contra su `dod_contract`:
```
Agent(subagent_type="general-purpose", prompt:
"Verifica de forma ADVERSARIAL si el trabajo cumple su DoD-contrato. NO ejecutaste tú la tarea.
DoD-contrato: <contract>
Report del ejecutor: <ruta del reports/NNNN-*.md>
Comprueba CADA cláusula (golden + edge + error) contra la evidencia citada en el report; re-ejecuta
los comandos de verificación si puedes. Devuelve {verdict: met|failed, gaps: [...], evidence: [...]}.
Por defecto failed si la evidencia no respalda una cláusula.")
```
El verificador (y el splitter y las búsquedas con `Explore`) son la **única** excepción autorizada al
Agent tool dentro del modo: utilidades internas read-only del propio orquestador, que devuelven un
resultado y mueren sin que el humano las gestione como agentes de la flota. Jamás se usa el Agent tool
para ejecutar una sub-tarea (ver paso 8 del comando).
- `met` → el orquestador marca `set_dod_contract <sid> "<contract>" met`, informa a la persona y
**cierra el ejecutor para liberar el slot idle** con `kill_fleet_agent` (regla de auto-kill, abajo).
- `failed`**nudge** al ejecutor con el gap concreto (no cerrar). `set_dod_contract <sid>
"<contract>" failed` (vuelve a pending tras el nudge si reabre trabajo).
### Auto-kill — cerrar el ejecutor tras verificar `met` (libera el slot idle)
Un ejecutor verificado `met` **no se deja vivo en reposo**: se cierra de inmediato para que no se
acumule en la flota ocupando un slot idle. En cuanto el verificador devuelve `met` y has marcado
`set_dod_contract <sid> "<contract>" met`, ciérralo:
```bash
./fn run kill_fleet_agent <sessionId> --socket "$FLEET_SOCKET"
```
`kill_fleet_agent_bash_infra` manda **SIGTERM** al proceso `claude` del ejecutor (cierre limpio,
recuperable luego con `claude --resume <sessionId>`) y cierra su window tmux (`kill-window`). Trae
**guards** que lo hacen seguro de invocar programáticamente:
- **No mata a un `role=orchestrator`** (lo lee del `goal.json`): nunca decapitas la flota por error.
- **No se mata a sí mismo**: rechaza el target si es la sesión que invoca (equivalente dirigido de la
regla "nunca `pkill claude`", paso 6 del comando).
- Acepta el target por `sessionId` (exacto o prefijo) o por PID. Usa `--dry-run` para ver el plan sin
tocar nada.
Esto cierra el ciclo del modo: lanzas con `--parent` → el watcher te avisa del `DICE_TERMINADO` →
verificas → `kill_fleet_agent` libera el slot. No uses `pkill`/`killall` ni `kill` a pelo para esto:
`kill_fleet_agent` resuelve la window y aplica los guards.
### Nudge — `ESTANCADO`
Agente idle con `dod_contract` sin cumplir y sin actividad > umbral (10 min). Empújalo a cerrar SU DoD
inyectando texto en su pane con la función `fleet_send_text` (grupo `orchestration`):
```bash
./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"
```
`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.
**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.
### Splitter — tarea demasiado grande
Si una sub-tarea sigue siendo grande para un solo agente, antes de lanzarla pásala por un **splitter**
(Agent efímero) que devuelve un plan de sub-tareas atómicas, cada una con su `dod_contract` y sus
dependencias:
```
Agent(subagent_type="Plan", prompt:
"Descompón esta tarea en sub-tareas ATÓMICAS, cada una cerrable por UN agente en una sesión, con
su propio DoD-contrato (golden+edge+error) y dependencias (cuáles son paralelas y cuáles
secuenciales). Máximo 6 sub-tareas. Tarea: <...>. Devuelve [{tarea, dod_contract, deps:[...]}].")
```
El orquestador lanza un ejecutor por sub-tarea respetando las dependencias (paralelas a la vez,
secuenciales encadenadas), **siempre dentro del tope de fan-out** (ver "Tope de fan-out" abajo).
### Tope de fan-out (regla dura)
**Máximo 6 ejecutores `role=executor` activos simultáneos por orquestador.** Si se alcanza el tope,
el orquestador NO lanza más: **encola** las sub-tareas restantes y las despacha a medida que un slot
se libera — un slot se libera cuando un ejecutor se verifica `met` y se cierra con `kill_fleet_agent`
(auto-kill). El conteo es de la **familia propia** (ejecutores con tu `parent_orchestrator`), no de
toda la flota; resuélvelo con el routing por `parent_orchestrator`, igual que el push activo.
Por qué un número duro y no "los que hagan falta": ya hubo el caso de **30 agentes que no cerraban
nada** y, al competir todos por el mismo rate-limit compartido, hubo que desactivar `goal_refine`
(el hook que reescribía el `dod` con un LLM por prompt). Más ejecutores no es más throughput: el
cuello de botella es el rate-limit compartido y los DoD que nadie cierra, no el número de procesos.
### Cadencia
El orquestador no hace polling caro: drena la cola **cuando actúa** (cuando la persona le habla) y,
para vigilancia desatendida, con un heartbeat largo (`ScheduleWakeup` 20-30 min) o cuando el watcher
empuja un urgente. Lo urgente (`RECLAMA`) sube al instante; el resto (cierres, estancados) se procesa
en lote.
## Funciones del registry del grupo `orchestration`
| Función | Para qué |
|---|---|
| `launch_claude_agent_kitty_bash_infra` | Lanzar un secundario en kitty con prompt autónomo + `--dangerously-skip-permissions` |
| `list_claude_agents_bash_infra` | Listar la flota de Claudes vivos (PID, sessionId, cwd, status, kitty) para seguirla |
| `reboot_all_claudes_bash_infra` | Reiniciar/parar la flota retomando sesiones; `--exclude-current` para no tocarte |
| `set_dod_contract_py_infra` | Escribir el DoD-contrato fijo (`dod_contract`/`dod_status`) en el `goal.json` de un secundario al lanzarlo |
| `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` + `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`, `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).
## Relación con otras reglas
- `.claude/commands/orquestador.md` — la doctrina y el flujo de cada turno del modo; esta regla es su
maquinaria operativa.
- `.claude/rules/autonomous_loop.md` — `fn-orquestador` (Agent tool, sandbox no-interactivo). Es lo
que el modo orquestador **no** es.
- `.claude/rules/apps_subrepo.md` — apps/analyses/projects son sub-repos Gitea (`apps/*` gitignored):
el aislamiento natural y el gotcha de `git init` antes de limpiar un worktree con una app nueva.
- `.claude/rules/reports.md` + `.claude/rules/dod_quality.md` — qué entrega cada secundario: report
con evidencia ejecutable + gaps.
- `.claude/rules/delegation.md` + `.claude/rules/registry_calls.md` — los secundarios siguen
registry-first y delegan a `fn-constructor`.
- Memorias: `lanzar-agentes-skip-permissions`, `multi-agent-git-race-same-repo`,
`claude-session-pid-mapping`, `prefiere-kitty-terminal`.
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Hook UserPromptSubmit: inyecta el estado de la flota al Claude orquestador.
#
# En el modo /orquestador, el Claude principal gestiona una flota de agentes y
# necesita enterarse de forma reactiva cuando uno cambia de estado: termina
# (DICE_TERMINADO), reclama una decision (RECLAMA) o se estanca (ESTANCADO).
# El watcher de fleetview escribe esas transiciones a la cola JSONL
# ~/.claude/fleet/events.jsonl. Este hook hace un peek de esa cola en cada turno
# y emite un bloque "FLEET-STATE:" para que el orquestador vea los cambios
# pendientes sin tener que drenar la cola a mano.
#
# Entrada (stdin JSON del hook UserPromptSubmit): { session_id, cwd, ... }
# El stdout de este script se inyecta como additionalContext en el turno.
#
# Solo el orquestador recibe el feed: se identifica leyendo el campo `role` de
# ~/.claude/goals/<session_id>.json (lo marca `mark_claude_role`). Cualquier
# sesion que no sea role=orchestrator termina en silencio (sin stdout).
#
# El peek usa advance=False: NO mueve el cursor de la cola. El orquestador sigue
# viendo los mismos eventos pendientes cada turno hasta que los consume
# explicitamente con `./fn run drain_fleet_events` (que si avanza el cursor).
#
# Degradacion limpia: si falta jq/python/venv, si la cola no existe, o si el
# watcher esta caido, el hook nunca rompe el turno (siempre exit 0).
set -u
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null)
[ -z "$SESSION_ID" ] && exit 0
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
ROLE=""
[ -f "$GOAL_FILE" ] && ROLE=$(jq -r '.role // ""' "$GOAL_FILE" 2>/dev/null)
# Solo el orquestador recibe el feed de la flota. Resto: silencio total.
[ "$ROLE" != "orchestrator" ] && exit 0
# Reanclar el rol en cada turno: el modo /orquestador no debe depender solo de
# que su prompt (.claude/commands/orquestador.md) siga en contexto. Este
# recordatorio se reinyecta aunque el watcher este caido o falte el venv (la
# guarda de abajo saldria con exit 0 sin emitir FLEET-STATE). Se emite SOLO para
# role=orchestrator: las sesiones sin goal.json o sin ese rol ya salieron arriba
# con exit 0 y stdout vacio, asi que el path limpio queda intacto.
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
OUT=$(FN_PROJECT_DIR="$PROJECT_DIR" timeout 8 "$PY" - <<'PYEOF' 2>/dev/null
import os
import sys
root = os.environ.get("FN_PROJECT_DIR", os.path.expanduser("~/fn_registry"))
sys.path.insert(0, os.path.join(root, "python", "functions"))
events = os.path.join(os.path.expanduser("~"), ".claude", "fleet", "events.jsonl")
try:
from infra.drain_fleet_events import drain_fleet_events
from infra.summarize_fleet_transitions import summarize_fleet_transitions
if not os.path.exists(events):
# Watcher nunca arranco o cola borrada: diagnostico explicito.
print("FLEET-STATE: cola del watcher no disponible (events.jsonl ausente)")
else:
drained = drain_fleet_events(advance=False) # peek: NO mueve el cursor
print(summarize_fleet_transitions(drained.get("by_classification", {})))
except Exception:
# Funciones no indexadas, cola corrupta, etc.: degradar sin romper el turno.
pass
PYEOF
)
[ -n "$OUT" ] && printf '%s\n' "$OUT"
exit 0
+10 -2
View File
@@ -2,12 +2,16 @@
"permissions": {
"allow": [
"Bash(CGO_ENABLED=1 go test *)",
"Bash(sqlite3 *)"
"Bash(sqlite3 *)",
"Read(//home/enmanuel/.claude/**)"
]
},
"enabledMcpjsonServers": [
"registry",
"jupyter"
"jupyter",
"orchestrator",
"godot",
"ardour"
],
"hooks": {
"PreToolUse": [
@@ -55,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"
}
]
}
+1
View File
@@ -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)
+13 -1
View File
@@ -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": ["/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh"]
"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": []
}
}
}
+1 -1
View File
@@ -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,58 @@
---
name: ensure_project_gitignore
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "ensure_project_gitignore(project_dir: string) -> void"
description: "Garantiza de forma idempotente que el .gitignore de un directorio de project contiene las lineas canonicas que excluyen del repo del project el contenido de sus sub-repos hijos (apps y analyses son repos Gitea independientes) y sus vaults (datos fuera de git). Evita el doble-tracking al hacer push del project."
tags: [git, gitignore, projects, infra]
params:
- name: project_dir
desc: "Ruta al directorio del project (p. ej. projects/aurgi). Debe existir; si no, error a stderr y return 1. El .gitignore se escribe/actualiza en <project_dir>/.gitignore."
output: "Sin salida en stdout. A stderr informa de la accion realizada: 'created' si creo el .gitignore, 'updated: anadidas N lineas' si anadio lineas faltantes, u 'ok: ya completo' si nada cambiaba. Codigo de salida 0 en exito, 1 si project_dir falta o no existe."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/ensure_project_gitignore.sh"
---
## Ejemplo
```bash
source bash/functions/infra/ensure_project_gitignore.sh
# Asegura que projects/aurgi/.gitignore excluye el contenido de sus hijos.
ensure_project_gitignore projects/aurgi
# stderr: ensure_project_gitignore: created projects/aurgi/.gitignore
# (o: updated: anadidas 2 lineas / ok: ya completo)
```
Las lineas canonicas que la funcion garantiza son:
```
apps/*/
analysis/*/
vaults/*
!vaults/.gitkeep
!vaults/vault.yaml
```
## Cuando usarla
Llamala justo despues de crear un project nuevo (`mkdir -p projects/<nombre>/{apps,analysis,vaults}`) y antes de inicializar su repo Gitea con `ensure_repo_synced`, para que el repo del project nunca trackee el contenido de sus sub-repos hijos. Tambien al adoptar un project existente que aun no tiene estas exclusiones, o como paso de saneamiento cuando `git status` del project muestra contenido de `apps/`/`analysis/` que deberia estar ignorado.
## Gotchas
- La funcion modifica el filesystem (escribe en `<project_dir>/.gitignore`): es impura. No commitea ni hace push — solo deja el `.gitignore` correcto.
- La comparacion para no duplicar es linea-exacta (`grep -Fxq`). Una linea equivalente pero con espacios extra, comentario adjunto o glob distinto (p. ej. `apps/*` sin la barra final) NO se considera presente y la canonica se anade igualmente; podrian quedar ambas formas. Mantener el `.gitignore` con las lineas canonicas tal cual evita ruido.
- Si el `.gitignore` existente no termina en salto de linea, la funcion anade uno antes de apendar para no pegar la primera linea nueva al final de la ultima existente.
- Solo gestiona las exclusiones de sub-repos hijos y vaults del nivel-project; no toca otras reglas que el `.gitignore` ya contenga ni las reordena.
- Si una linea canonica ya existia con su forma exacta, no se vuelve a anadir (idempotente): re-ejecutar es seguro.
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# ensure_project_gitignore — Garantiza de forma idempotente que el .gitignore de
# un directorio de project (projects/<nombre>/) contiene las lineas canonicas que
# excluyen del repo del project el contenido de sus sub-repos hijos (apps y
# analyses son repos Gitea independientes) y sus vaults (datos fuera de git).
#
# Esto evita que al hacer push del project se trackee por error el contenido de
# los hijos (doble-tracking). Ver .claude/rules/apps_subrepo.md y
# .claude/rules/projects.md.
#
# Uso:
# ensure_project_gitignore <project_dir>
#
# Salida:
# stdout vacio. A stderr informa de la accion realizada (created / updated / ok).
ensure_project_gitignore() {
local project_dir="$1"
if [[ -z "$project_dir" ]]; then
echo "ensure_project_gitignore: se requiere project_dir" >&2
return 1
fi
if [[ ! -d "$project_dir" ]]; then
echo "ensure_project_gitignore: directorio '$project_dir' no existe" >&2
return 1
fi
local gitignore="$project_dir/.gitignore"
# Lineas canonicas que deben estar presentes (orden de referencia).
local -a canonical=(
"apps/*/"
"analysis/*/"
"vaults/*"
"!vaults/.gitkeep"
"!vaults/vault.yaml"
)
# Caso 1: el .gitignore no existe — crearlo con el contenido canonico.
if [[ ! -f "$gitignore" ]]; then
printf '%s\n' "${canonical[@]}" > "$gitignore"
echo "ensure_project_gitignore: created $gitignore" >&2
return 0
fi
# Caso 2: existe — anadir solo las lineas que falten (comparacion linea-exacta),
# preservando el contenido y el orden existentes.
# Si el archivo no termina en newline, anadir uno antes de apendar para no
# pegar la primera linea nueva al final de la ultima existente.
if [[ -s "$gitignore" && -n "$(tail -c 1 "$gitignore")" ]]; then
printf '\n' >> "$gitignore"
fi
local line added=0
for line in "${canonical[@]}"; do
# grep -F -x: match literal de linea completa, sin interpretar metacaracteres.
if ! grep -Fxq -- "$line" "$gitignore"; then
printf '%s\n' "$line" >> "$gitignore"
added=$((added + 1))
fi
done
if [[ $added -gt 0 ]]; then
echo "ensure_project_gitignore: updated: anadidas $added lineas a $gitignore" >&2
else
echo "ensure_project_gitignore: ok: ya completo $gitignore" >&2
fi
return 0
}
# Si se invoca como script (no source), ejecutar la funcion.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
ensure_project_gitignore "$@"
fi
+72
View File
@@ -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).
+266
View File
@@ -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 ]]
@@ -0,0 +1,73 @@
---
name: focus_cdp_tab_window
id: focus_cdp_tab_window_bash_infra
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "focus_cdp_tab_window(port: int, [target_id: string]) -> void"
description: "Handoff humano de captcha: trae al frente la pestaña (via CDP /json/activate) y la ventana del SO de un Chrome con CDP, para que el humano resuelva el captcha a mano. Promocion del patron inline que acompaña a detect_captcha_go_browser."
tags: [browser, captcha, handoff, cdp, wmctrl, xdotool, infra, navegator]
params:
- name: "port"
desc: "Puerto CDP del Chrome (ej. 9333 = Chrome aislado del browser_mcp; 9222 = navegador diario). Obligatorio."
- name: "target_id"
desc: "Opcional. Target/tab id CDP de la pestaña del captcha. Si se pasa, se activa esa pestaña dentro del browser antes de levantar la ventana del SO. Si se omite, solo se levanta la ventana."
output: "Stdout una linea legible y JSON-parseable simple: 'focus_cdp_tab_window: focused win=<wid> pid=<pid> port=<port> tab=<target_id_o_->'. Exit 0 en exito; 2 sin puerto, 3 sin DISPLAY, 4 falta wmctrl/xdotool, 5 no hay chromium en el puerto, 6 sin ventana top-level."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/focus_cdp_tab_window.sh"
---
## Ejemplo
```bash
# Activar la pestaña del captcha (por su target id CDP) y levantar la ventana del Chrome aislado
focus_cdp_tab_window 9333 20EF6E28AA792C53AF0D260F34A768B3
# -> focus_cdp_tab_window: focused win=0x03a00007 pid=48213 port=9333 tab=20EF6E28AA792C53AF0D260F34A768B3
# Solo levantar la ventana del Chrome (sin activar tab concreta)
focus_cdp_tab_window 9333
# -> focus_cdp_tab_window: focused win=0x03a00007 pid=48213 port=9333 tab=-
```
Invocacion canonica via el CLI del registry (despacho bash automatico):
```bash
./fn run focus_cdp_tab_window 9333 20EF6E28AA792C53AF0D260F34A768B3
```
## Cuando usarla
En el handoff humano de captcha: cuando el `browser_mcp` marca `⚠️ CAPTCHA-DETECTED`
(via `detect_captcha_go_browser`), usa esta funcion para traer la pestaña del captcha y la
ventana del Chrome al frente para que el humano lo resuelva a mano; luego se le notifica y se
para la automatizacion. Pasa el `target_id` de la tab donde se detecto el captcha para activar
esa pestaña exacta; omitelo si solo necesitas levantar la ventana del navegador.
## Gotchas
- **Impura, requiere X11**: necesita un entorno grafico (`$DISPLAY` no vacio) + `wmctrl` + `xdotool`
instalados. No sirve headless ni por SSH sin X forwarding — sale con error y exit != 0.
- **Match pid->ventana fragil**: resuelve la ventana cruzando el PID del browser principal con la
columna PID de `wmctrl -lp`. Puede fallar si el window manager agrupa ventanas o si chromium no
expone `_NET_WM_PID` en el main; de ahi el fallback a `xdotool search --pid <pid> --onlyvisible`.
- **No reposiciona entre monitores**: solo activa/levanta la ventana donde ya esta; no la mueve a
otra pantalla.
- **Varias ventanas del mismo Chrome**: si el browser tiene varias ventanas top-level, coge la
primera que matchea el PID.
- **Activate CDP best-effort**: `curl /json/activate/<target_id>` puede dar 404 si el `target_id`
caduco (la tab cambio de id o se cerro). La funcion NO aborta: sigue con el raise de la ventana
igualmente.
- **Reintento por XFCE**: xfwm pisa el primer `windowactivate`/`windowraise`, por eso se hace el
activate+raise dos veces con una espera corta entre medias.
- **Identifica el browser process por ausencia de `--type=`**: las lineas de `pgrep` con
`--type=renderer/gpu/utility/zygote` son procesos hijos; se descartan para quedarse con el main.
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# focus_cdp_tab_window — trae al frente la pestaña + la ventana del SO de un Chrome con CDP
#
# Handoff humano de captcha: activa la tab del captcha (opcional, via CDP) y levanta
# la ventana X11 del proceso browser principal de ese puerto para que un humano resuelva
# el captcha a mano. Best-effort y robusto: cada paso continua aunque uno falle.
focus_cdp_tab_window() {
set -uo pipefail
local port="${1:-}"
local target_id="${2:-}"
# 1. Validacion de entorno y dependencias.
if [[ -z "$port" ]]; then
echo "focus_cdp_tab_window: falta el puerto CDP (uso: focus_cdp_tab_window <port> [target_id])" >&2
return 2
fi
if [[ -z "${DISPLAY:-}" ]]; then
echo "focus_cdp_tab_window: sin entorno grafico (DISPLAY vacio)" >&2
return 3
fi
if ! command -v wmctrl >/dev/null 2>&1 || ! command -v xdotool >/dev/null 2>&1; then
echo "focus_cdp_tab_window: falta wmctrl/xdotool" >&2
return 4
fi
# 2. Activar la tab del captcha dentro del browser (best-effort, no aborta).
if [[ -n "$target_id" ]]; then
curl -sf "http://127.0.0.1:${port}/json/activate/${target_id}" >/dev/null 2>&1 || true
fi
# 3. Encontrar el PID del proceso BROWSER principal de ese puerto.
# De las lineas que matchean el flag de debugging, el browser process es el que
# NO lleva --type= (los renderers/gpu/utility/zygote son procesos hijos).
local browser_pid=""
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ "$line" == *"--type="* ]]; then
continue
fi
# pgrep -af antepone el PID seguido de la cmdline.
browser_pid="${line%% *}"
break
done < <(pgrep -af -- "remote-debugging-port=${port}" 2>/dev/null)
if [[ -z "$browser_pid" ]]; then
echo "focus_cdp_tab_window: no hay chromium en el puerto ${port}" >&2
return 5
fi
# 4. Resolver el window id top-level.
# Primero por wmctrl -lp (columna 3 = PID). Fallback xdotool si el main no expone _NET_WM_PID.
local wid=""
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Formato: <wid> <desktop> <pid> <host> <title...>
local w_id w_pid
w_id="$(awk '{print $1}' <<<"$line")"
w_pid="$(awk '{print $3}' <<<"$line")"
if [[ "$w_pid" == "$browser_pid" ]]; then
wid="$w_id"
break
fi
done < <(wmctrl -lp 2>/dev/null)
if [[ -z "$wid" ]]; then
wid="$(xdotool search --pid "$browser_pid" --onlyvisible 2>/dev/null | head -n1)"
fi
if [[ -z "$wid" ]]; then
echo "focus_cdp_tab_window: no se encontro ventana top-level para pid ${browser_pid} (puerto ${port})" >&2
return 6
fi
# 5. Traer al frente con REINTENTO (xfwm de XFCE pisa el primer activate/raise).
# Espera no bloqueante con read -t en vez de sleep.
local attempt
for attempt in 1 2; do
xdotool windowactivate "$wid" >/dev/null 2>&1 || true
read -r -t 0.2 _ < /dev/zero 2>/dev/null || true
xdotool windowraise "$wid" >/dev/null 2>&1 || true
done
# 6. Salida legible y JSON-parseable simple.
echo "focus_cdp_tab_window: focused win=${wid} pid=${browser_pid} port=${port} tab=${target_id:--}"
return 0
}
# Permitir ejecucion directa: focus_cdp_tab_window <port> [target_id]
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
focus_cdp_tab_window "$@"
fi
+68
View File
@@ -0,0 +1,68 @@
---
name: kill_fleet_agent
kind: function
lang: bash
domain: infra
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 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: []
error_type: error_go_core
file_path: "bash/functions/infra/kill_fleet_agent.sh"
tested: true
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:
- name: target
desc: "Primer arg posicional: sessionId del ejecutor (exacto o prefijo) o su PID (todo digitos). Por PID se lee sessions/<pid>.json para el sessionId; por sessionId se busca en sessions/*.json el que case y su archivo da el PID."
- name: --socket
desc: "Socket tmux del perfil FleetView donde vive la window. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada."
- name: --dry-run
desc: "Imprime el plan (PID, sessionId, role, window, accion) y NO mata el proceso ni cierra la window. Sin esto, ejecuta."
output: "Imprime una linea de plan con PID, sessionId, role, socket y window resueltos, seguida de la accion ejecutada (SIGTERM + kill-window) o, con --dry-run, de DRY-RUN. Exit 0 ok/dry-run; 2 uso incorrecto o target no resuelto a PID; 3 guard (target es un orchestrator o la sesion actual)."
---
# kill_fleet_agent
Cierra de forma dirigida UN ejecutor de la flota tmux: SIGTERM al proceso `claude` (cierre limpio, recuperable con `claude --resume <sessionId>`) más `kill-window` de su window en el socket del perfil FleetView. Es la pieza que el orquestador usa para **liberar el slot idle** de cada ejecutor en cuanto verifica que su DoD-contrato está `met` — sin esto, los ejecutores terminados se acumulan en reposo en la flota.
## Ejemplo
```bash
# Cerrar un ejecutor por sessionId (el orquestador lo llama tras verificar `met`):
./fn run kill_fleet_agent 32945650-a4e1-472b-90c9-5b38ef60a463 --socket "$FLEET_SOCKET"
# Por prefijo de sessionId, en el socket por defecto ($FLEET_SOCKET o "fleet"):
./fn run kill_fleet_agent 32945650
# Ver el plan sin matar nada (PID, sessionId, role, window, accion):
./fn run kill_fleet_agent 48213 --dry-run
```
## Cuando usarla
Úsala desde el modo orquestador justo después de que el verificador independiente devuelva `met` sobre un ejecutor: ciérralo para que no quede ocupando un slot idle en la flota. Resuelve el target por sessionId (exacto o prefijo) o por PID, comprueba los guards y manda SIGTERM + cierra la window. Es el cierre dirigido a **un** agente; para reiniciar/parar **toda** la flota usa `reboot_all_claudes` (con `--exclude-current`). Nunca uses `pkill`/`killall claude` (te matas a ti mismo, el orquestador).
## Gotchas
- **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`".
- **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.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.
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# kill_fleet_agent — cierre limpio y dirigido de UN ejecutor de la flota tmux.
#
# Dado un sessionId (o prefijo) o un PID, mata el proceso claude del ejecutor con
# SIGTERM (cierre limpio) y cierra su window tmux en el socket del perfil
# FleetView. Es la pieza que usa el orquestador para liberar el slot idle de cada
# ejecutor en cuanto verifica que su DoD-contrato esta `met`: sin esto, los
# ejecutores terminados se acumulan en reposo en la flota.
#
# Guards de seguridad (NO destruye a quien no debe):
# - NO mata a un agente con role=orchestrator (leido de su goal.json). Matar un
# orquestador por error decapitaria la flota.
# - NO se mata a si mismo (la sesion que invoca la funcion): resuelve el PID de
# claude actual subiendo por los ancestros de /proc y rechaza el target si
# coincide. Es el equivalente dirigido de la regla "nunca pkill claude".
#
# Funcion IMPURA: manda SIGTERM a un proceso y cierra una window tmux. Por
# defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado,
# recuperable luego con `claude --resume <sessionId>`). Usa --dry-run para ver el
# plan sin tocar nada.
#
# Overrides de entorno (testabilidad, no para uso normal):
# FN_FLEET_SESSIONS_DIR directorio de los sessions JSON. Default ~/.claude/sessions
# FN_FLEET_GOALS_DIR directorio de los goal JSON. Default ~/.claude/goals
# FN_FLEET_SELF_PID fuerza el PID propio (salta la deteccion por /proc)
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
while [[ $# -gt 0 ]]; do
case "$1" in
--socket) shift; socket="${1:-}" ;;
--dry-run) dry=1 ;;
-h|--help)
cat <<'USAGE'
Uso: kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]
Cierra UN ejecutor de la flota: SIGTERM al proceso claude + kill-window de su
window tmux. Resuelve el target por sessionId (exacto o por prefijo) o por PID.
Guards: NO mata a un role=orchestrator ni a la sesion que invoca la funcion.
Opciones:
--socket <s> Socket tmux del perfil FleetView donde vive la window.
Default: $FLEET_SOCKET, o "fleet" si no esta seteada.
--dry-run Imprime el plan (PID, sessionId, role, window, accion) y NO
mata ni cierra nada.
-h, --help Esta ayuda.
Salida: exit 0 ok (o dry-run); 2 uso incorrecto / target no resuelto; 3 guard
(intento de matar a un orquestador o a la sesion actual).
Ejemplos:
kill_fleet_agent 32945650-a4e1-472b-90c9-5b38ef60a463 # por sessionId
kill_fleet_agent 32945650 --socket fleet2 # por prefijo de sessionId
kill_fleet_agent 48213 --dry-run # por PID, solo ver el plan
USAGE
return 0 ;;
--*)
echo "kill_fleet_agent: opcion desconocida '$1' (usa -h)" >&2
return 2 ;;
*)
if [[ -z "$target" ]]; then
target="$1"
else
echo "kill_fleet_agent: argumento extra '$1' (target ya es '$target')" >&2
return 2
fi ;;
esac
shift
done
[[ -z "$target" ]] && {
echo "kill_fleet_agent: falta el target (sessionId o PID). Usa -h." >&2
return 2
}
local sessions_dir="${FN_FLEET_SESSIONS_DIR:-$HOME/.claude/sessions}"
local goals_dir="${FN_FLEET_GOALS_DIR:-$HOME/.claude/goals}"
[[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"
command -v jq >/dev/null 2>&1 || {
echo "kill_fleet_agent: jq no esta instalado (necesario para leer los JSON)" >&2
return 1
}
# -----------------------------------------------------------------------
# Resolver (PID, sessionId) a partir del target.
# -----------------------------------------------------------------------
local pid="" sid=""
if [[ "$target" =~ ^[0-9]+$ ]]; then
# target = PID. El sessionId sale de sessions/<pid>.json (si existe).
pid="$target"
local sfile="$sessions_dir/$pid.json"
if [[ -f "$sfile" ]]; then
sid="$(jq -r '.sessionId // ""' "$sfile" 2>/dev/null || true)"
fi
else
# target = sessionId (exacto o prefijo). Buscar en sessions/*.json el JSON
# cuyo .sessionId case; el nombre del archivo (<pid>.json) da el PID.
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 "kill_fleet_agent: no se pudo resolver el target '$target' a un PID (sessions en $sessions_dir)" >&2
return 2
}
# -----------------------------------------------------------------------
# Guard 1 — anti-self: no matar 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 "kill_fleet_agent: REHUSADO — el target (PID $pid) es la sesion actual. No me suicido." >&2
return 3
fi
# -----------------------------------------------------------------------
# Guard 2 — anti-orquestador: no matar a un role=orchestrator.
# -----------------------------------------------------------------------
local role=""
if [[ -n "$sid" ]]; then
local gfile="$goals_dir/$sid.json"
[[ -f "$gfile" ]] && role="$(jq -r '.role // ""' "$gfile" 2>/dev/null || true)"
fi
if [[ "$role" == "orchestrator" ]]; then
echo "kill_fleet_agent: REHUSADO — el target (sessionId ${sid:-?}, PID $pid) tiene role=orchestrator. No se mata al orquestador." >&2
return 3
fi
# -----------------------------------------------------------------------
# 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="" pane="" wname=""
if command -v tmux >/dev/null 2>&1; then
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)} pane: ${pane:-?} accion: $action"
if [[ "$dry" -eq 1 ]]; then
echo "DRY-RUN: no se ha matado el proceso ni cerrado nada."
return 0
fi
# -----------------------------------------------------------------------
# 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
echo "kill_fleet_agent: SIGTERM enviado a claude PID $pid."
else
echo "kill_fleet_agent: PID $pid ya no esta vivo (nada que matar)."
fi
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
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
}
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
kill_fleet_agent "$@"
fi
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Tests para kill_fleet_agent. Usa fixtures en dirs temporales (FN_FLEET_*) y
# --dry-run para no matar procesos ni cerrar windows reales.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/kill_fleet_agent.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_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
}
# --- Fixtures: sessions/<pid>.json + goals/<sid>.json en dirs temporales ---
TMP="$(mktemp -d)"
SESS="$TMP/sessions"
GOALS="$TMP/goals"
mkdir -p "$SESS" "$GOALS"
# Ejecutor: PID 4242, sessionId executor-aaa-111, role=executor.
echo '{"sessionId":"executor-aaa-111","cwd":"/tmp/x"}' > "$SESS/4242.json"
echo '{"goal":"hacer X","role":"executor","dod_contract":"golden..."}' > "$GOALS/executor-aaa-111.json"
# Orquestador: PID 5555, sessionId orchestrator-bbb-222, role=orchestrator.
echo '{"sessionId":"orchestrator-bbb-222","cwd":"/tmp/y"}' > "$SESS/5555.json"
echo '{"goal":"orquestar","role":"orchestrator"}' > "$GOALS/orchestrator-bbb-222.json"
trap 'rm -rf "$TMP"' EXIT
export FN_FLEET_SESSIONS_DIR="$SESS"
export FN_FLEET_GOALS_DIR="$GOALS"
# Forzar self_pid a un valor que NO colisione con los fixtures (salvo el test self).
export FN_FLEET_SELF_PID=999999
# --- Test 1 (golden): resolver ejecutor por sessionId, dry-run imprime plan ---
set +e
out=$(kill_fleet_agent executor-aaa-111 --socket nope --dry-run 2>&1); rc=$?
set -e
assert_rc "golden: ejecutor por sessionId sale 0" 0 "$rc"
assert_contains "golden: plan muestra el PID resuelto" "PID: 4242" "$out"
assert_contains "golden: plan muestra el sessionId" "executor-aaa-111" "$out"
assert_contains "golden: plan muestra role executor" "role: executor" "$out"
assert_contains "golden: dry-run no mata" "DRY-RUN" "$out"
# --- Test 2 (golden por PID + prefijo de sessionId) ---
set +e
out=$(kill_fleet_agent 4242 --dry-run 2>&1); rc=$?
set -e
assert_rc "golden: target por PID sale 0" 0 "$rc"
assert_contains "golden: PID resuelve su sessionId" "executor-aaa-111" "$out"
set +e
out=$(kill_fleet_agent executor-aaa --dry-run 2>&1); rc=$?
set -e
assert_rc "edge: prefijo de sessionId resuelve" 0 "$rc"
assert_contains "edge: prefijo resuelve al PID 4242" "PID: 4242" "$out"
# --- Test 3 (EDGE guard role): negar matar a un orchestrator ---
set +e
out=$(kill_fleet_agent orchestrator-bbb-222 --dry-run 2>&1); rc=$?
set -e
assert_rc "guard: matar orchestrator devuelve rc=3" 3 "$rc"
assert_contains "guard: mensaje menciona role=orchestrator" "role=orchestrator" "$out"
# --- Test 4 (EDGE guard self): negar matar a la sesion actual ---
set +e
out=$(FN_FLEET_SELF_PID=4242 kill_fleet_agent executor-aaa-111 --dry-run 2>&1); rc=$?
set -e
assert_rc "guard: matar self devuelve rc=3" 3 "$rc"
assert_contains "guard: mensaje self menciona no suicidarse" "No me suicido" "$out"
# --- Test 5 (ERROR): target no resuelto a un PID ---
set +e
out=$(kill_fleet_agent sesion-inexistente-zzz --dry-run 2>&1); rc=$?
set -e
assert_rc "error: target inexistente devuelve rc=2" 2 "$rc"
assert_contains "error: mensaje de no resuelto" "no se pudo resolver" "$out"
# --- Test 6 (ERROR): falta el target ---
set +e
out=$(kill_fleet_agent --dry-run 2>&1); rc=$?
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
+228
View File
@@ -0,0 +1,228 @@
---
name: launch_fleetclaude
kind: function
lang: bash
domain: infra
version: "1.7.0"
purity: impure
signature: "launch_fleetclaude [--cwd <dir>] [--bin <path>] [--session <name>] [--reuse] [--new] [--cols <n>]"
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. REUSO DE CONTEXTO: si se invoca DENTRO de una flota tmux viva (su window 'console') sin --new, NO abre ventana ni crea un perfil nuevo; trae la TUI al pane/contexto actual (equivale a 'fleetview show'). El flag --new fuerza una flota+ventana nueva aunque estes en tmux. 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: fuera de tmux, o con --new, 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)."
- name: --bin
desc: "Ruta al binario de la TUI fleetview que corre en el pane izquierdo. Opcional. Default: <repo>/apps/fleetview/fleetview. Si no es ejecutable, el pane izquierdo muestra un mensaje de como compilarla y deja una shell viva."
- name: --session
desc: "Fija el perfil (socket+sesion tmux comparten nombre) por nombre exacto; reutiliza el existente si ya vive (idempotente sobre ese nombre). Opcional. Sin esta opcion, el perfil se elige automaticamente (primer nombre libre de la secuencia fleet, fleet2, ...). Invocado DENTRO de tmux con un nombre DISTINTO al de la flota actual equivale a --new (pides otra flota: ventana nueva, sin reuse de contexto)."
- name: --reuse
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: --new
desc: "Fuerza una flota NUEVA en una ventana NUEVA (kitty/wt.exe) incluso estando dentro de una flota tmux. Opcional. Es la via explicita para abrir una FleetView aparte; sin este flag, invocado dentro de una flota viva se reusa el contexto actual (no abre ventana ni crea perfil)."
- name: --cols
desc: "Ancho en columnas del pane izquierdo (la TUI). Opcional. Default: 40."
output: "Caso reuse de contexto (dentro de una flota tmux viva, sin --new): trae la TUI al pane/contexto actual con select-window de la window 'console' (o 'fleetview show' si el binario existe) y retorna 0, sin abrir nada. Caso ventana-nueva (fuera de tmux, o con --new): crea/reutiliza una sesion tmux detached con dos panes y lanza una ventana de terminal 'FleetView' adjunta (kitty o Windows Terminal segun auto-deteccion), desacoplada del shell padre. Imprime el estado por stdout. Sin valor de retorno; exit 0 en exito, !=0 con mensaje claro si no hay terminal ni contexto que reusar."
uses_functions:
- supervise_fleetview_tui_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/launch_fleetclaude.sh"
---
## Ejemplo
```bash
# DENTRO de una flota tmux viva (p. ej. en el pane del orquestador): reusa el
# contexto, trae la TUI al pane actual. NO abre ventana ni crea perfil nuevo.
fleetclaude
# FUERA de tmux: perfil nuevo automatico (fleet la 1a vez; fleet2, ... si ya hay
# uno) en una ventana de terminal nueva, reutilizando la terminal actual (attach):
fleetclaude
# Forzar una flota+ventana NUEVA aunque estes dentro de una flota tmux:
fleetclaude --new
# Reattach a la flota principal 'fleet' (comportamiento idempotente clasico):
fleetclaude --reuse
# Perfil con nombre fijo y ancho de pane personalizado:
fleetclaude --session trabajo --cols 50
# Via fn run (resuelve por nombre o ID):
fn run launch_fleetclaude
```
Dentro de una flota viva, `fleetclaude` sin args reusa el contexto (la window
`console` pasa al frente). Fuera de tmux (o con `--new`) 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 con `--new`.
## Cuando usarla
Usala cuando quieras un unico punto de entrada a la flota de Claudes en vez de
N ventanas kitty sueltas: lanzas `fleetclaude` y tienes la TUI de control y un
Claude listo para trabajar en la misma ventana. Tipico al empezar la jornada o
al retomar el trabajo en el repo `fn_registry`. Si **ya estas dentro de una
flota** (en el pane del orquestador) y solo quieres volver a ver la TUI, lanza
`fleetclaude` sin args: trae el panel al contexto actual sin abrir otra ventana
ni arrancar una flota duplicada. Usa `--new` solo cuando quieras DELIBERADAMENTE
una segunda flota aparte.
## Gotchas
- **Perfiles multiples (default = perfil nuevo)**: sin `--session` ni `--reuse`,
cada invocacion abre un perfil NUEVO usando el primer nombre libre de la
secuencia `fleet`, `fleet2`, `fleet3`, ... (socket+sesion tmux comparten el
nombre del perfil). Asi puedes tener varias FleetView abiertas a la vez, cada
una con su flota independiente. Un perfil cerrado libera su nombre: tras matar
`fleet`, el siguiente lanzamiento vuelve a `fleet`. Para reattach a una flota
concreta: `--reuse` (principal `fleet`) o `--session <nombre>` (idempotente
sobre ese nombre, reusa el layout si ya vive).
- **Perfil ↔ TUI por entorno**: el launcher inyecta `FLEET_SOCKET`/`FLEET_SESSION`
al pane de la TUI (y los fija en el server con `set-environment -g`, para que
`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 una flota tmux viva: reuse de contexto (no ventana nueva)**: si
invocas `fleetclaude` sin `--new` desde dentro de una flota fleetview viva
(`$TMUX` definido y el socket actual tiene una sesion homonima con window
`console`), NO abre ventana ni crea un perfil `fleetN+1`: trae la TUI al pane
actual (`fleetview show`, o `tmux -L <perfil> select-window -t <perfil>:console`
si el binario no esta compilado) y retorna 0. El perfil de la flota actual se
deriva de `$TMUX` (basename del socket = nombre `-L`), senal fiable aunque
`$FLEET_SOCKET` venga vacio (ver `detect_fleet_context`). **`--new`** fuerza el
comportamiento clasico (flota+ventana nueva); pasar `--session <otro>` distinto
al perfil actual equivale a `--new` implicito. Fuera de tmux y con TTY, reutiliza
la terminal actual con `exec tmux attach` (nunca `attach` anidado dentro de
tmux). Sin TTY ni contexto que reusar (atajo de escritorio/cron) cae a la ruta
ventana-nueva. Antes de este fix (v1.6.0 y anteriores) cualquier `fleetclaude`
dentro de tmux abria una kitty nueva y un socket `fleetN+1` — el sintoma que
acumulaba 6+ sockets `fleet*`.
- **`local x` unbound bajo `set -u`**: el archivo corre con `set -euo pipefail`.
`local left_pane right_pane` dejaba esas vars *unbound* (no vacias), asi que la
rama "reutilizar sesion existente" (`--reuse`/`--session <vivo>`) reventaba con
`left_pane: unbound variable` al evaluar `[[ -z "$left_pane" ]]`. Se inicializan
explicitamente a `""` (`local left_pane="" right_pane=""`). Si tocas estas vars,
no vuelvas a declararlas sin valor.
- **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
silencio. Compila la TUI antes para el flujo completo.
- **Socket tmux aislado por perfil (`-L <perfil>`)**: cada perfil vive en su
propio server tmux (socket = nombre del perfil), separado del tmux por defecto
del usuario y de los demas perfiles. Asi los atajos `bind -n` NO afectan otras
sesiones (ej. una sesion `mobile-1` del movil) y matar un perfil no toca los
otros: `tmux -L <perfil> kill-server` (o `alt+q` dentro de la TUI).
- **Atajos en el socket, NO en kitty.conf**: instala `bind -n` para
`alt+flechas` (mover el cursor de la TUI), `alt+enter` (conmutar al Claude
seleccionado) y `alt+n` (abrir Claude nuevo). Son bindings de tmux que
redirigen la tecla al pane de la TUI (`send-keys -t console.0`), asi funcionan
ESTES DONDE ESTES (incluido escribiendo en el pane de Claude). No modifican la
configuracion de kitty ni los atajos globales del escritorio.
- **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; terminal (kitty/wt.exe) solo en la ruta ventana-nueva**: `tmux`
es obligatorio (aborta != 0 si falta). Una terminal nueva (kitty o Windows
Terminal) solo se necesita en la ruta ventana-nueva: `--new`, o sin TTY ni flota
viva que reusar (atajo de escritorio, cron, script). Dentro de una flota viva sin
`--new` se reusa el contexto (ni kitty ni wt.exe). Invocado desde una terminal
interactiva fuera de tmux (el caso normal del alias `fleetclaude`), reutiliza la
terminal actual con `exec tmux attach` y tampoco necesita kitty ni wt.exe.
## Capability growth log
- v1.7.0 (2026-06-30) — **reuse de contexto dentro de la flota + flag `--new`**.
Invocado sin `--new` desde dentro de una flota tmux viva (su window `console`),
`fleetclaude` ya NO abre una kitty nueva ni crea un perfil `fleetN+1`: trae la
TUI al pane/contexto actual (`fleetview show`, o `tmux -L <perfil> select-window
-t <perfil>:console` como fallback sin binario) y retorna 0. El perfil actual se
deriva de `$TMUX` (basename del socket); pasar `--session <otro>` distinto al
actual equivale a `--new` implicito. Nuevo flag `--new` para forzar la ruta
clasica (flota+ventana nueva) aun dentro de tmux. Fuera de tmux el comportamiento
es intacto (`exec tmux attach` reutiliza la terminal). Arregla el sintoma de que
lanzar `fleetclaude` dentro de una flota abria ventana kitty + socket nuevo
(`fleet7`, `fleet8`, ...). Fix incidental: `local left_pane="" right_pane=""`
(antes `local left_pane right_pane` reventaba con `unbound variable` bajo
`set -u` al reutilizar una sesion existente).
- 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`, ...),
asi abrir FleetView con uno ya abierto arranca otra flota en vez de reusarla.
Nuevo flag `--reuse` para el reattach idempotente clasico. El launcher inyecta
`FLEET_SOCKET`/`FLEET_SESSION` (env + `set-environment -g`) y `main.go` de
`fleetview` los lee (fallback `fleet`), de modo que cada panel ve solo su flota.
Titulo de kitty `FleetView (<perfil>)`. Guard anti-nesting: invocado dentro de
tmux abre ventana kitty nueva en vez de `attach` anidado.
- v1.3.2 (2026-06-17) — targeting de panes por **pane ID** (`%0`/`%1`) en vez de
por indice (`console.0`). Antes fallaba con `can't find pane: 0` en hosts cuyo
`~/.tmux.conf` define `base-index 1`/`pane-base-index 1` (el socket `-L fleet`
hereda esa config). Los pane ID son inmunes al base-index. Bug latente que el
fix de kitty (v1.3.1) destapo al dejar de abortar antes de montar la sesion.
- v1.3.1 (2026-06-17) — el guard de `kitty` se movio a la rama sin-TTY. La ruta
interactiva (`exec tmux attach`) ya no exige kitty, asi que `fleetclaude`
funciona en hosts sin kitty (p.ej. WSL) reutilizando la terminal actual.
- v1.3.0 (2026-06-17) — renombrada de `launch_kittyclaude` a `launch_fleetclaude`
(comando `fleetclaude`). Atajos: `alt+0` (= alt+n, abrir Claude nuevo), `alt+k`
(kill con confirmacion), `alt+r` (picker de reanudar sesiones cerradas) y
`alt+flecha-izquierda` (volver atras desde el picker). Cierra la window al salir
el Claude (`remain-on-exit off`).
- v1.2.0 (2026-06-16) — ancho del sidebar por defecto 47 columnas; `ctrl+0` como
atajo alterno para abrir Claude nuevo; `mouse on` (clic/rueda enrutados a la
TUI) y `extended-keys on` (para que `ctrl+0` llegue distinguible por el
protocolo de teclado de kitty).
- v1.1.0 (2026-06-16) — socket tmux aislado `-L fleet`; instala atajos
`alt+flechas` / `alt+enter` / `alt+n` que controlan la TUI desde cualquier
pane; hooks que mantienen fijo el ancho del sidebar tras attach/conmutar.
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env bash
# launch_fleetclaude — Entrypoint MVP de FleetView.
#
# Abre UNA ventana kitty corriendo una sesion tmux de dos panes:
# - pane izquierdo: la TUI 'fleetview' (la flota de Claudes centralizada).
# - pane derecho: 'claude --dangerously-skip-permissions'.
#
# Objetivo: dejar de tener N ventanas kitty dispersas y centralizar el control
# de los Claudes en una sola ventana.
#
# Funcion IMPURA: lanza procesos (tmux + kitty) con efectos secundarios.
# - Crea/reusa una sesion tmux detached llamada <session> (idempotente).
# - Lanza una ventana kitty desacoplada del shell padre (setsid) para que
# sobreviva al cierre de la terminal que la invoco.
# - No toca atajos de teclado ni kitty.conf.
set -euo pipefail
IFS=$' \t\n'
launch_fleetclaude() {
local cwd=""
local bin=""
local session="fleet"
local cols=52
local explicit_session=0 # 1 si el usuario pasó --session <name> a mano
local reuse=0 # 1 si el usuario pidió --reuse (reattach al perfil principal)
local want_new=0 # 1 si el usuario pidió --new (forzar flota+ventana nueva)
local T="" # socket tmux aislado; se fija al resolver el perfil
# -----------------------------------------------------------------------
# Parseo de argumentos
# -----------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--cwd)
shift
cwd="${1:-}"
;;
--bin)
shift
bin="${1:-}"
;;
--session)
shift
session="${1:-}"
explicit_session=1
;;
--reuse)
reuse=1
;;
--new)
want_new=1
;;
--cols)
shift
cols="${1:-40}"
;;
-h|--help)
cat <<'USAGE'
Uso: launch_fleetclaude [opciones]
Abre una ventana kitty con una sesion tmux de dos panes: la TUI fleetview a la
izquierda y 'claude --dangerously-skip-permissions' a la derecha.
Cada PERFIL de FleetView es un socket+sesion tmux aislados (su propia flota de
Claudes). Sin --session ni --reuse, cada invocacion abre un perfil NUEVO: usa
el primer nombre libre de la secuencia fleet, fleet2, fleet3, ... Asi puedes
tener varias FleetView abiertas a la vez, cada una con su flota independiente.
REUSO DE CONTEXTO: si ya estas DENTRO de una flota tmux viva (p. ej. en el pane
del orquestador), 'fleetclaude' sin args NO abre una ventana ni crea un perfil
nuevo: trae la TUI al contexto/pane actual (equivale a 'fleetview show'). Para
abrir explicitamente una flota aparte en una ventana nueva, usa --new.
Opciones:
--cwd <dir> Directorio de trabajo de los panes.
Default: raiz del repo fn_registry (derivada dinamicamente).
--bin <path> Ruta al binario de la TUI fleetview.
Default: <repo>/apps/fleetview/fleetview
--session <name> Fija el perfil (socket+sesion) por nombre exacto; reutiliza
el existente si ya esta vivo. Sin esta opcion, perfil auto.
Si se invoca DENTRO de tmux con un nombre DISTINTO al de la
flota actual, equivale a --new (pides otra flota).
--reuse Reattach al perfil principal 'fleet' en vez de abrir uno
nuevo (vuelve al comportamiento idempotente clasico).
--new Fuerza una flota NUEVA en una ventana NUEVA (kitty/wt.exe),
incluso dentro de tmux. Es la via explicita para tener una
FleetView aparte; sin este flag, dentro de tmux se reusa el
contexto actual.
--cols <n> Ancho (columnas) del pane izquierdo. Default: 40.
-h, --help Muestra esta ayuda.
Ejemplos:
launch_fleetclaude # dentro de la flota: reusa el contexto;
# fuera de tmux: perfil nuevo (fleet, ...)
launch_fleetclaude --new # flota+ventana nueva aunque estes en tmux
launch_fleetclaude --reuse # reattach a la flota principal 'fleet'
launch_fleetclaude --session trabajo # perfil con nombre fijo 'trabajo'
launch_fleetclaude --cwd ~/fn_registry --cols 50
USAGE
return 0
;;
*)
echo "launch_fleetclaude: opcion desconocida: '$1' (usa -h)" >&2
return 2
;;
esac
shift
done
# -----------------------------------------------------------------------
# Derivar la raiz del repo fn_registry dinamicamente (NO hardcodear paths
# de usuario). Estrategia: subir desde la ubicacion del script con
# 'git rev-parse --show-toplevel'; fallbacks razonables si no aplica.
# -----------------------------------------------------------------------
local script_dir repo_root=""
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# El script vive en <repo>/bash/functions/infra/, asi que la raiz son 3
# niveles arriba; pero preferimos git para robustez.
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" ]]; then
# Fallback 1: navegacion relativa desde la ubicacion del script.
repo_root="$(cd "$script_dir/../../.." 2>/dev/null && pwd || true)"
fi
if [[ -z "$repo_root" ]]; then
# Fallback 2: variable de entorno del registry o el cwd actual.
repo_root="${FN_REGISTRY_ROOT:-$PWD}"
fi
# Defaults derivados de la raiz del repo.
[[ -z "$cwd" ]] && cwd="$repo_root"
[[ -z "$bin" ]] && bin="$repo_root/apps/fleetview/fleetview"
# Validar cwd: si no existe, caer al repo_root.
if [[ ! -d "$cwd" ]]; then
echo "launch_fleetclaude: --cwd '$cwd' no existe; usando '$repo_root'." >&2
cwd="$repo_root"
fi
# -----------------------------------------------------------------------
# Comprobar herramientas necesarias.
# -----------------------------------------------------------------------
if ! command -v tmux >/dev/null 2>&1; then
echo "launch_fleetclaude: tmux no esta instalado." >&2
return 1
fi
# -----------------------------------------------------------------------
# REUSO DE CONTEXTO (sin --new): si ya estamos DENTRO de una flota tmux
# viva, 'fleetclaude' sin args NO abre una ventana/terminal nueva ni crea
# un perfil fleetN+1 — trae la TUI al contexto/pane actual, igual que
# 'fleetview show'. El flag --new fuerza el comportamiento clasico (flota
# nueva en ventana nueva); --reuse mantiene su semantica historica.
#
# El perfil de la flota actual se deriva de $TMUX (el basename del socket
# es el nombre -L; senal fiable aunque $FLEET_SOCKET venga vacio, ver
# detect_fleet_context). Si se paso --session con un nombre DISTINTO al
# actual, es pedir OTRA flota -> se trata como --new implicito (no reusa).
# "Flota viva" = el socket tiene una sesion homonima con una window
# 'console' (la firma de una FleetView), no un tmux cualquiera.
# -----------------------------------------------------------------------
if [[ "$want_new" -eq 0 && "$reuse" -eq 0 && -n "${TMUX:-}" ]]; then
local current_socket target_socket
current_socket="$(basename "${TMUX%%,*}")"
target_socket="$current_socket"
[[ "$explicit_session" -eq 1 ]] && target_socket="$session"
if [[ "$target_socket" == "$current_socket" ]] \
&& tmux -L "$current_socket" has-session -t "$current_socket" 2>/dev/null \
&& tmux -L "$current_socket" list-windows -t "$current_socket" \
-F '#{window_name}' 2>/dev/null | grep -qx console; then
# Traer la TUI al contexto actual sin abrir nada nuevo. Preferimos
# el binario (centraliza la politica en la app: 'fleetview show');
# si no esta compilado, caemos a 'select-window' directo, que es lo
# que 'show' hace por dentro dentro de tmux (cero dependencia).
if [[ -x "$bin" ]] \
&& FLEET_SOCKET="$current_socket" FLEET_SESSION="$current_socket" \
"$bin" show 2>/dev/null; then
return 0
fi
tmux -L "$current_socket" select-window -t "$current_socket":console
echo "launch_fleetclaude: flota '$current_socket' viva; TUI traida al contexto actual (sin ventana nueva)."
return 0
fi
fi
# -----------------------------------------------------------------------
# Resolver el PERFIL (socket+sesion tmux comparten nombre).
#
# - --session <name> -> usa ese nombre exacto (reutiliza si ya vive).
# - --reuse -> usa 'fleet' (el perfil principal), idempotente.
# - sin nada -> perfil NUEVO: primer nombre libre de la secuencia
# fleet, fleet2, fleet3, ... Asi abrir FleetView con
# uno ya abierto arranca otra flota, no la reusa.
#
# "Libre" = no hay un server tmux con esa sesion (has-session falla). Un
# perfil cerrado libera su nombre, asi que tras cerrar 'fleet' el siguiente
# lanzamiento vuelve a 'fleet'.
# -----------------------------------------------------------------------
if [[ "$explicit_session" -eq 0 && "$reuse" -eq 0 ]]; then
local base="$session" n=1 cand
while :; do
if [[ "$n" -eq 1 ]]; then cand="$base"; else cand="${base}${n}"; fi
if ! tmux -L "$cand" has-session -t "$cand" 2>/dev/null; then
session="$cand"
break
fi
n=$((n + 1))
done
echo "launch_fleetclaude: perfil nuevo '$session'."
fi
# A partir de aqui el socket aislado es el del perfil resuelto.
T="tmux -L $session"
# Nota: kitty NO se exige aqui. La ruta interactiva (TTY) reutiliza la
# terminal actual con `exec tmux attach` y no necesita kitty. Solo la
# ruta sin-TTY (abrir ventana nueva con setsid kitty) lo requiere, y ahi
# se comprueba justo antes de usarlo.
# -----------------------------------------------------------------------
# Comando para el pane izquierdo:
# - Si el binario fleetview existe -> ejecutarlo (exec, sin shell colgado).
# - Si NO existe -> mensaje claro + shell interactiva (no falla en silencio).
# -----------------------------------------------------------------------
# La TUI necesita saber a qué perfil pertenece: se lo pasamos por entorno
# (FLEET_SOCKET/FLEET_SESSION), que main.go lee con fallback a "fleet".
local envpfx
envpfx="FLEET_SOCKET=$(printf '%q' "$session") FLEET_SESSION=$(printf '%q' "$session")"
local left_cmd
if [[ -x "$bin" ]]; then
# 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\""
fi
# -----------------------------------------------------------------------
# Montar la sesion tmux SOLO si no existe (idempotencia). Socket aislado $T.
#
# Targeting por PANE ID (%0/%1), no por indice (console.0). El socket
# -L fleet sigue leyendo ~/.tmux.conf; si el usuario tiene
# `base-index 1` / `pane-base-index 1` (muy comun), el primer pane es el
# indice 1 y cualquier referencia a console.0 falla con
# "can't find pane: 0". Los pane ID son estables e inmunes al base-index.
# -----------------------------------------------------------------------
# Inicializadas a "" (no solo declaradas): bajo `set -u` una `local x` sin
# valor queda *unbound*, y al reutilizar una sesion existente el `[[ -z
# "$left_pane" ]]` de mas abajo reventaba con "unbound variable".
local left_pane="" right_pane=""
if $T has-session -t "$session" 2>/dev/null; then
echo "launch_fleetclaude: la sesion tmux '$session' ya existe; reutilizandola."
else
echo "launch_fleetclaude: creando sesion tmux '$session' en '$cwd'."
# Sesion detached con ventana 'console'. Capturamos el pane ID del pane
# izquierdo (la TUI fleetview, o el fallback claro).
left_pane=$($T new-session -d -s "$session" -n console -c "$cwd" -P -F '#{pane_id}')
$T send-keys -t "$left_pane" "$left_cmd" C-m
# pane derecho = el ORQUESTADOR de la flota: un Claude que arranca ya en
# modo orquestador invocando el skill /orquestador como primer prompt. Es
# el Claude con el que el humano habla; vigila la flota por su DoD.
right_pane=$($T split-window -h -t "$left_pane" -c "$cwd" -P -F '#{pane_id}')
$T send-keys -t "$right_pane" "exec claude --dangerously-skip-permissions '/orquestador'" C-m
# Fijar el ancho del pane izquierdo en columnas.
$T resize-pane -t "$left_pane" -x "$cols"
# Foco inicial en el pane del orquestador (derecha).
$T select-pane -t "$right_pane"
# Marcar el orquestador con role=orchestrator en su goal.json para que la
# TUI lo pinee arriba (estrella). El sessionId no se conoce hasta que
# Claude escribe sessions/<PID>.json; mark_claude_role resuelve
# PID->sessionId esperando ese archivo. En background (no bloquea el
# arranque) y con sleep para que el `exec claude` reemplace al shell antes
# de leer el pane_pid. Fallo = no-fatal (el orquestador no se pinea).
if [[ -x "$repo_root/fn" ]]; then
( sleep 1
orch_pid=$($T display-message -p -t "$right_pane" '#{pane_pid}' 2>/dev/null || true)
[[ -n "$orch_pid" ]] && "$repo_root/fn" run mark_claude_role "$orch_pid" orchestrator >/dev/null 2>&1
) >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
# Sembrar 1 ejecutor idle: una window detached con un claude normal,
# listo para recibir tarea del orquestador. Aparece en la TUI bajo el
# orquestador (role executor por defecto). Hereda FLEET_SOCKET/SESSION
# del server (set-environment), asi apunta a este perfil.
local idle_pane
idle_pane=$($T new-window -d -t "$session" -n claude -c "$cwd" -P -F '#{pane_id}')
$T send-keys -t "$idle_pane" "exec claude --dangerously-skip-permissions" C-m
fi
# Si reutilizamos sesion (o por seguridad), derivar el pane ID de la TUI:
# el primer pane de la ventana 'console' (orden por indice) es el izquierdo.
if [[ -z "$left_pane" ]]; then
left_pane=$($T list-panes -t "$session":console -F '#{pane_id}' 2>/dev/null | head -n1)
fi
# -----------------------------------------------------------------------
# Atajos globales (alt+*) en el socket aislado: redirigen la tecla al pane
# de la TUI (console.0) ESTES DONDE ESTES, para controlar la flota sin salir
# del pane de Claude. La TUI (fleetview) es quien interpreta Up/Down/Enter/n.
# `bind -n` = tabla root (sin prefijo). Idempotente: re-set en cada lanzamiento.
# -----------------------------------------------------------------------
$T bind -n M-Up send-keys -t "$left_pane" Up
$T bind -n M-Down send-keys -t "$left_pane" Down
$T bind -n M-Enter send-keys -t "$left_pane" Enter
$T bind -n M-n send-keys -t "$left_pane" n
$T bind -n M-0 send-keys -t "$left_pane" n
$T bind -n M-k send-keys -t "$left_pane" k
$T bind -n M-r send-keys -t "$left_pane" r
$T bind -n M-u send-keys -t "$left_pane" u
$T bind -n M-h send-keys -t "$left_pane" h
$T bind -n M-R send-keys -t "$left_pane" R
$T bind -n M-Left send-keys -t "$left_pane" Escape
$T bind -n M-q send-keys -t "$left_pane" Q
# Entorno del perfil en el server tmux: respawn-pane (alt+R, recompila la TUI)
# y los Claude nuevos heredan FLEET_SOCKET/FLEET_SESSION para apuntar al
# socket correcto aunque no sea el default "fleet".
$T set-environment -g FLEET_SOCKET "$session"
$T set-environment -g FLEET_SESSION "$session"
# Raton: enruta clicks/rueda al pane bajo el cursor; la TUI los interpreta.
$T set -g mouse on
# Al salir un Claude (exit / Ctrl-D / kill), cerrar su window en vez de
# dejarla muerta ("dead" pane) en la sesion.
$T set -g remain-on-exit off
# Estetica neutra: sin el verde fosforo por defecto de tmux. Status bar gris y
# bordes de pane gris tenue, iguales en activo e inactivo (separacion simple,
# sin resaltado de enfoque).
$T set -g status-style "bg=colour236,fg=colour250"
$T set -g pane-border-style "fg=colour238"
$T set -g pane-active-border-style "fg=colour240"
# Mantener el ancho del sidebar (pane 0) cuando kitty redimensiona la ventana
# tras el attach, o cuando se conmuta de Claude (window-linked / layout change).
$T set-hook -g client-resized "resize-pane -t $left_pane -x $cols"
$T set-hook -g window-layout-changed "resize-pane -t $left_pane -x $cols"
# -----------------------------------------------------------------------
# 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 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: 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).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
launch_fleetclaude "$@"
fi
@@ -0,0 +1,55 @@
---
name: open_doc_onlyoffice
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: "open_doc_onlyoffice <ruta_archivo> [--restart]"
description: "Abre un documento ofimático (xlsx, docx, pptx, csv, ods, odt, ...) con OnlyOffice Desktop Editors, desacoplado del shell (setsid + background). Localiza el binario por PATH sin hardcodear rutas. Flag --restart cierra toda la app OnlyOffice y la relanza para forzar la recarga desde disco de un archivo regenerado (OnlyOffice cachea en memoria la versión vieja de los documentos abiertos)."
tags:
- onlyoffice
- desktop
- office
- open
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
params:
- name: ruta_archivo
desc: "Ruta (relativa o absoluta) del documento ofimático a abrir. Debe existir."
- name: --restart
desc: "Opcional. Si se pasa, cierra TODA la instancia de OnlyOffice (pkill -x DesktopEditors) antes de relanzar, forzando la recarga desde disco. Cierra cualquier otro documento abierto: usar solo si ninguno tiene cambios sin guardar."
output: "Imprime la ruta absoluta abierta. Exit 0 si lanza OnlyOffice; exit 1 si el archivo no existe o el binario no está en PATH; exit 2 en error de uso."
file_path: bash/functions/infra/open_doc_onlyoffice.sh
---
## Ejemplo
```bash
# Abrir un documento (lo enfoca si OnlyOffice ya está corriendo)
fn run open_doc_onlyoffice ~/Desktop/negocio_dashboards.xlsx
# Tras regenerar el archivo en disco, forzar que OnlyOffice lo recargue
fn run open_doc_onlyoffice ~/Desktop/negocio_dashboards.xlsx --restart
```
## Cuando usarla
Cuando necesites abrir o mostrar al usuario un documento ofimático (`.xlsx`, `.docx`, `.pptx`, `.csv`, `.ods`, `.odt`) en su escritorio. Es la forma canónica de abrir documentos en este equipo: el usuario usa OnlyOffice, nunca LibreOffice. Usa `--restart` cuando acabas de regenerar un archivo que probablemente ya está abierto y OnlyOffice muestra la versión cacheada en memoria.
## Gotchas
- OnlyOffice es **instancia única**: lanzarlo con un archivo ya abierto reenfoca la pestaña existente con la versión cacheada en memoria, NO recarga desde disco. Por eso existe `--restart`.
- `--restart` cierra **toda** la app (`pkill -x DesktopEditors`), no solo la pestaña del archivo. Cualquier otro documento abierto se cierra. No usar si hay documentos con cambios sin guardar.
- No hay forma por CLI de cerrar/recargar una sola pestaña: o se acepta la versión cacheada, o se reinicia la app entera.
- Usa `setsid` + `&` para que el editor sobreviva al proceso que lo invoca (no muere al cerrar la terminal/sesión).
- Localiza el binario con `command -v onlyoffice-desktopeditors`; el proceso real subyacente es `/opt/onlyoffice/desktopeditors/DesktopEditors`.
## example
```bash
open_doc_onlyoffice ~/Desktop/negocio_dashboards.xlsx
open_doc_onlyoffice ~/Desktop/negocio_dashboards.xlsx --restart # fuerza recarga desde disco
```
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# open_doc_onlyoffice — abre un documento ofimático con OnlyOffice Desktop Editors.
#
# Uso:
# open_doc_onlyoffice <ruta_archivo> [--restart]
#
# Lanza el editor desacoplado del shell (setsid + background) para que sobreviva
# al proceso que lo invoca. Localiza el binario por PATH, sin hardcodear rutas.
#
# --restart cierra toda la instancia de OnlyOffice antes de relanzar, para forzar
# la recarga desde disco de un archivo que se regeneró (OnlyOffice mantiene en
# memoria la versión vieja de los documentos ya abiertos).
set -euo pipefail
usage() {
echo "uso: open_doc_onlyoffice <ruta_archivo> [--restart]" >&2
exit 2
}
[ $# -ge 1 ] || usage
doc=""
restart=0
for arg in "$@"; do
case "$arg" in
--restart) restart=1 ;;
-h|--help) usage ;;
*) doc="$arg" ;;
esac
done
[ -n "$doc" ] || usage
if [ ! -f "$doc" ]; then
echo "error: archivo no encontrado: $doc" >&2
exit 1
fi
bin="$(command -v onlyoffice-desktopeditors || true)"
if [ -z "$bin" ]; then
echo "error: onlyoffice-desktopeditors no esta en PATH" >&2
exit 1
fi
# Ruta absoluta para que OnlyOffice no dependa del directorio de trabajo.
doc_abs="$(readlink -f "$doc")"
if [ "$restart" -eq 1 ]; then
# Cierra la app entera para descartar la copia en memoria de los documentos.
# pkill -x sobre el comm exacto del proceso real (no -f, para no auto-matar
# el propio script si su ruta contiene el patrón).
pkill -x DesktopEditors 2>/dev/null || true
# Espera (máx ~5s) a que el proceso principal termine antes de relanzar.
for _ in $(seq 1 25); do
pgrep -x DesktopEditors >/dev/null 2>&1 || break
sleep 0.2
done
fi
setsid "$bin" "$doc_abs" >/dev/null 2>&1 &
echo "abierto en OnlyOffice: $doc_abs"
+1 -1
View File
@@ -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: []
+81
View File
@@ -0,0 +1,81 @@
---
name: spawn_fleet_agent
kind: function
lang: bash
domain: infra
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. --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). 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). Opcional: se auto-detecta de $TMUX si no se pasa."
- name: --cwd
desc: "Directorio de trabajo del nuevo Claude. Default: PWD."
- name: --prompt-file
desc: "Ruta a un archivo cuyo contenido sera el primer prompt del Claude (prompt autocontenido del ejecutor). El cat lo hace el shell del pane, admite multi-linea."
- name: --skill
desc: "Nombre de un skill a invocar como primer prompt (ej. orquestador -> envia '/orquestador'). Tiene prioridad sobre --prompt-file."
- name: --role
desc: "Role a escribir en el goal.json del nuevo Claude: orchestrator | executor. Se aplica via mark_claude_role en background. Sin esto, executor implicito."
- name: --parent
desc: "sessionId del orquestador que lanza este ejecutor. Si se pasa, escribe parent_orchestrator en el goal.json del nuevo Claude (via mark_claude_parent en background) para que el watcher de fleetview rutee sus avisos al orquestador padre. Opcional; sin esto el aviso no se atribuye a un orquestador concreto."
- name: --title
desc: "Nombre de la window tmux creada. Default: claude."
output: "Imprime por stdout el window_id (ej. @7) de la window tmux creada. Exit 0 ok; 1 error de entorno (tmux ausente, sesion inexistente, prompt-file inexistente); 2 uso incorrecto."
---
# spawn_fleet_agent
Lanza un Claude dentro de un perfil FleetView (sesion tmux de un socket aislado) como una window nueva, para que forme parte de la flota visible en la TUI `fleetview` y conmutable con `/fleet focus`. Es la pieza que hace que los ejecutores —y el orquestador— vivan en la flota tmux en vez de en kitties dispersas (flow 0012, Fase 3).
## Ejemplo
```bash
# Meter el ORQUESTADOR en la flota actual (arranca en modo + se pinea arriba):
./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \
--skill orquestador --role orchestrator --title orquestador
# Lanzar un EJECUTOR con tarea autocontenida en la misma flota:
./fn run spawn_fleet_agent --socket fleet2 --session fleet2 --cwd "$HOME/fn_registry" \
--prompt-file /tmp/orq_health.md --title "kanban-health"
# Ejecutor atribuido a SU orquestador padre (habilita el routing del watcher):
./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
Cuando el orquestador (o el launcher) necesita arrancar un Claude que debe vivir EN la flota tmux: un ejecutor con tarea, o el propio orquestador. Usala en lugar de `launch_claude_agent_kitty_bash_infra` siempre que ya exista un perfil fleet montado y quieras ver/conmutar el agente desde `fleetview` y `/fleet`. Tras lanzar un ejecutor, escribe su DoD-contrato con `set_dod_contract`.
## Gotchas
- **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.
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# spawn_fleet_agent — lanza un Claude como window nueva dentro de la sesion tmux
# de un perfil FleetView (socket aislado), opcionalmente en modo orquestador
# (skill embebida) y marcado con un role en su goal.json.
#
# Es la forma de que un ejecutor —o el propio orquestador— VIVA en la flota tmux
# (visible en la TUI fleetview, conmutable con /fleet focus), en vez de en una
# kitty suelta. Reemplaza a launch_claude_agent_kitty cuando se opera dentro de
# un perfil fleet ya montado.
#
# Funcion IMPURA: crea procesos (tmux window + claude) y, si se pide --role,
# marca el goal.json del nuevo Claude via mark_claude_role (en background, porque
# el sessionId no existe hasta que Claude escribe sessions/<PID>.json).
set -euo pipefail
IFS=$' \t\n'
spawn_fleet_agent() {
local socket="" session="" cwd="" prompt_file="" skill="" role="" parent="" title="claude"
while [[ $# -gt 0 ]]; do
case "$1" in
--socket) shift; socket="${1:-}" ;;
--session) shift; session="${1:-}" ;;
--cwd) shift; cwd="${1:-}" ;;
--prompt-file) shift; prompt_file="${1:-}" ;;
--skill) shift; skill="${1:-}" ;;
--role) shift; role="${1:-}" ;;
--parent) shift; parent="${1:-}" ;;
--title) shift; title="${1:-claude}" ;;
-h|--help)
cat <<'USAGE'
Uso: spawn_fleet_agent [--socket <s>] [--session <s>] [--cwd <dir>] [opciones]
Lanza un Claude como window nueva en la sesion tmux <session> del socket <socket>
(un perfil FleetView ya montado). Imprime el window_id creado.
--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
pane, asi que admite prompts multi-linea.
--skill <name> Primer prompt = "/<name>" (invoca un skill al arrancar, ej.
--skill orquestador para arrancar en modo orquestador).
--role <r> Marca el goal.json del nuevo Claude: orchestrator|executor
(via mark_claude_role, en background). Sin esto, executor
implicito.
--parent <sid> sessionId del orquestador que lanza este ejecutor. Si se
pasa, escribe parent_orchestrator en el goal.json del nuevo
Claude (via mark_claude_parent, en background) para que el
watcher de fleetview rutee sus avisos al orquestador padre.
--title <t> Nombre de la window tmux. Default: claude.
Ejemplos:
# Orquestador en la flota actual:
spawn_fleet_agent --socket fleet2 --session fleet2 --cwd ~/fn_registry \
--skill orquestador --role orchestrator --title orquestador
# Ejecutor con tarea autocontenida, atribuido a su orquestador padre:
spawn_fleet_agent --socket fleet2 --session fleet2 --cwd ~/fn_registry \
--prompt-file /tmp/orq_health.md --title "kanban-health" \
--parent 32945650-a4e1-472b-90c9-5b38ef60a463
USAGE
return 0 ;;
*)
echo "spawn_fleet_agent: opcion desconocida '$1' (usa -h)" >&2
return 2 ;;
esac
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: 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"
command -v tmux >/dev/null 2>&1 || {
echo "spawn_fleet_agent: tmux no esta instalado" >&2
return 1
}
if ! tmux -L "$socket" has-session -t "$session" 2>/dev/null; then
echo "spawn_fleet_agent: la sesion '$session' no existe en el socket '$socket' (lanza la flota primero)" >&2
return 1
fi
# Window nueva detached + claude. send-keys con exec para que el pane_pid sea
# el de claude (no el del shell), necesario para mark_claude_role.
local win_pane win_id
win_pane=$(tmux -L "$socket" new-window -d -t "$session" -n "$title" -c "$cwd" -P -F '#{pane_id}')
if [[ -n "$skill" ]]; then
# Skill como primer prompt: "/<skill>". Claude Code lo interpreta como
# invocacion de slash command al arrancar.
tmux -L "$socket" send-keys -t "$win_pane" \
"exec claude --dangerously-skip-permissions '/$skill'" C-m
elif [[ -n "$prompt_file" ]]; then
[[ -f "$prompt_file" ]] || {
echo "spawn_fleet_agent: --prompt-file '$prompt_file' no existe" >&2
return 1
}
# El cat lo ejecuta el shell del pane: admite prompts multi-linea.
tmux -L "$socket" send-keys -t "$win_pane" \
"exec claude --dangerously-skip-permissions \"\$(cat $(printf '%q' "$prompt_file"))\"" C-m
else
tmux -L "$socket" send-keys -t "$win_pane" \
"exec claude --dangerously-skip-permissions" C-m
fi
# Marcar role y/o parent_orchestrator en background (no-fatal). El sleep da
# tiempo a que el `exec claude` reemplace al shell antes de leer el pane_pid;
# mark_claude_role / mark_claude_parent luego esperan a que aparezca
# sessions/<PID>.json para resolver el sessionId. Se encadenan SECUENCIALMENTE
# en el mismo subshell (primero role, luego parent) para que el segundo lea el
# goal ya con la primera clave escrita y no haya carrera de
# lectura-modificacion-escritura entre ambos.
if [[ -n "$role" || -n "$parent" ]]; then
local repo_root fn_bin
repo_root="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel 2>/dev/null || echo "${FN_REGISTRY_ROOT:-$HOME/fn_registry}")"
fn_bin="$repo_root/fn"
if [[ -x "$fn_bin" ]]; then
( sleep 1
pid=$(tmux -L "$socket" display-message -p -t "$win_pane" '#{pane_pid}' 2>/dev/null || true)
if [[ -n "$pid" ]]; then
[[ -n "$role" ]] && "$fn_bin" run mark_claude_role "$pid" "$role" >/dev/null 2>&1
[[ -n "$parent" ]] && "$fn_bin" run mark_claude_parent "$pid" "$parent" >/dev/null 2>&1
fi
) >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
fi
win_id=$(tmux -L "$socket" display-message -p -t "$win_pane" '#{window_id}' 2>/dev/null || true)
echo "$win_id"
return 0
}
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
spawn_fleet_agent "$@"
fi
@@ -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
+10 -3
View File
@@ -3,14 +3,15 @@ name: full_git_pull
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "full_git_pull() -> stdout: tabla resumen"
description: "Pull automatico de fn_registry + todos los sub-repos locales + submodules + fn sync. Descubre repos locales, stashea dirty trees antes de pullear, hace pull --ff-only, actualiza submodulos del repo principal, pulla ~/.password-store, regenera registry.db con fn index y ejecuta fn sync."
description: "Pull automatico de fn_registry + todos los sub-repos locales + submodules + fn sync. Descubre repos locales, stashea dirty trees antes de pullear, hace pull --ff-only, actualiza submodulos del repo principal, pulla ~/.password-store, regenera registry.db con fn index, ejecuta fn sync y reclona los sub-repos hijos faltantes de cada project (apps/analysis) via clone_project_subrepos."
tags: [git, pull, sync, registry, pipeline, pendiente-usar]
uses_functions:
- discover_git_repos_bash_infra
- git_pull_with_stash_bash_infra
- clone_project_subrepos_bash_pipelines
- pass_get_bash_infra
uses_types: []
returns: []
@@ -51,4 +52,10 @@ bash bash/functions/pipelines/full_git_pull.sh
## Notas
Solo hace pull fast-forward — nunca rebase ni merge automatico. Los repos con divergencia o conflicto de stash se listan al final del resumen para intervencion manual, pero el pipeline no aborta por ellos. No clona repos faltantes: cada PC tiene el subset que le interesa (clonar manualmente si se necesita uno nuevo). Modo completamente no-interactivo.
Solo hace pull fast-forward — nunca rebase ni merge automatico. Los repos con divergencia o conflicto de stash se listan al final del resumen para intervencion manual, pero el pipeline no aborta por ellos. Modo completamente no-interactivo.
Desde v1.1.0 SI reclona los sub-repos hijos faltantes de cada project: tras `fn sync` (que trae a `registry.db` las filas de apps/analysis de todos los PCs), itera los projects y llama `clone_project_subrepos` para traer al disco los hijos que falten, re-indexando si clono alguno. `registry.db` actua como manifest de sub-repos, asi que clonar el project paraguas + `/full-git-pull` reconstruye su arbol entero sin adivinar nombres. Los repos sueltos (sin project) siguen sin auto-clonarse: cada PC tiene el subset que le interesa.
## Capability growth log
- v1.1.0 (2026-06-10) — anade el paso 6: reclonado de sub-repos hijos de cada project via `clone_project_subrepos` tras `fn sync`, con re-index si clona alguno. Permite reconstruir el arbol completo de un project en un PC nuevo (issue 0171).
+39
View File
@@ -149,6 +149,42 @@ full_git_pull() {
fi
fi
# --- Paso 6: Reclonar sub-repos hijos de cada project (issue 0171) ---
# Tras fn sync, registry.db contiene las filas apps/analysis de TODOS los PCs.
# clone_project_subrepos clona en este disco los hijos que falten (skip si ya
# existen). Asi, clonar el project paraguas y correr /full-git-pull reconstruye
# su arbol entero sin adivinar nombres de sub-repos: registry.db ES el manifest.
echo "" >&2
echo "[6/6] Reclonando sub-repos de projects..." >&2
local reclone_summary=" [skip] sin projects o registry.db"
if [[ -f "$registry_root/registry.db" ]] && command -v sqlite3 >/dev/null 2>&1; then
export FN_REGISTRY_ROOT="$registry_root"
export GITEA_URL="${GITEA_URL:-$(pass_get agentes/gitea-url | head -n1 2>/dev/null || true)}"
local clone_script="$SCRIPT_DIR/clone_project_subrepos.sh"
local any_cloned=0
if [[ -f "$clone_script" ]]; then
while IFS= read -r proj_id; do
[[ -z "$proj_id" ]] && continue
local clone_out
clone_out=$(bash "$clone_script" "$proj_id" 2>&1 || true)
if echo "$clone_out" | grep -q '\[cloned\]'; then
any_cloned=1
echo " $proj_id: nuevos sub-repos clonados" >&2
fi
done < <(sqlite3 "$registry_root/registry.db" "SELECT id FROM projects;" 2>/dev/null)
if [[ "$any_cloned" -eq 1 ]]; then
echo " re-index tras clonado..." >&2
[[ -x "$fn_bin" ]] && CGO_ENABLED=1 "$fn_bin" index >/dev/null 2>&1 || true
reclone_summary=" OK: nuevos sub-repos clonados + re-index"
else
reclone_summary=" OK: nada que clonar (todo presente)"
fi
else
reclone_summary=" [skip] clone_project_subrepos.sh no encontrado"
fi
fi
echo " $reclone_summary" >&2
# --- Resumen ---
echo ""
echo "===== RESUMEN full_git_pull ====="
@@ -171,6 +207,9 @@ full_git_pull() {
echo ""
echo "fn sync:"
echo "$sync_summary"
echo ""
echo "Reclonado sub-repos de projects:"
echo "$reclone_summary"
if [[ ${#diverged[@]} -gt 0 || ${#conflicts[@]} -gt 0 ]]; then
echo ""
+7 -2
View File
@@ -3,10 +3,10 @@ name: full_git_push
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "full_git_push(commit_message?: string) -> stdout: tabla resumen"
description: "Push automatico de fn_registry + todos los sub-repos + fn sync. Descubre repos, escanea secrets (aborta si detecta), auto-inicializa apps/analyses sin .git via ensure_repo_synced, auto-commitea dirty trees, pushea solo repos adelantados, pushea ~/.password-store sin commitear, y ejecuta fn sync."
description: "Push automatico de fn_registry + todos los sub-repos + fn sync. Descubre repos, escanea secrets (aborta si detecta), auto-inicializa apps/analyses Y projects paraguas sin .git via ensure_repo_synced (asegurando el .gitignore canonico del project antes), auto-commitea dirty trees, pushea solo repos adelantados, pushea ~/.password-store sin commitear, y ejecuta fn sync."
tags: [git, push, sync, registry, pipeline, pendiente-usar]
uses_functions:
- discover_git_repos_bash_infra
@@ -14,6 +14,7 @@ uses_functions:
- git_auto_commit_dirty_bash_infra
- git_push_if_ahead_bash_infra
- ensure_repo_synced_bash_infra
- ensure_project_gitignore_bash_infra
- pass_get_bash_infra
uses_types: []
returns: []
@@ -62,3 +63,7 @@ bash bash/functions/pipelines/full_git_push.sh "feat: nueva funcion"
## Notas
El unico motivo para abortar antes de commitear es la deteccion de secrets. Cualquier otro error (push rechazado por non-fast-forward, fn sync no disponible) se reporta en el resumen y el pipeline continua con el resto de repos. Modo completamente no-interactivo.
## Capability growth log
- v1.1.0 (2026-06-10) — auto-inicializa tambien los projects paraguas (`projects/<p>/`) sin repo Gitea, no solo apps/analyses. Antes de pushear cada project asegura su `.gitignore` canonico via `ensure_project_gitignore` para no trackear el contenido de los sub-repos hijos. Cierra el agujero por el que projects como aurgi/obsidian/osint vivian solo en disco y se perdian al borrar el PC (issue 0171).
+32 -20
View File
@@ -13,6 +13,7 @@ source "$INFRA_DIR/git_auto_commit_dirty.sh"
source "$INFRA_DIR/git_push_if_ahead.sh"
source "$INFRA_DIR/pass_get.sh"
source "$INFRA_DIR/ensure_repo_synced.sh"
source "$INFRA_DIR/ensure_project_gitignore.sh"
source "$CYBERSEC_DIR/scan_secrets_in_dirty.sh"
full_git_push() {
@@ -65,6 +66,32 @@ full_git_push() {
ensure_repo_synced "$d" dataforge "$(basename "$d")" master "chore: initial sync" || \
echo " [warn] fallo inicializando $d" >&2
done < <(sqlite3 "$registry_root/registry.db" "SELECT dir_path FROM apps WHERE dir_path != '' UNION SELECT dir_path FROM analysis WHERE dir_path != '';" 2>/dev/null)
# Paso 1c: Auto-inicializar los PROJECTS paraguas sin .git (issue 0171).
# El directorio projects/<p>/ versiona SOLO las docs de nivel-project
# (project.md, vault.yaml, CONVENTIONS.md, tools/...). Sus hijos apps/* y
# analysis/* son sub-repos Gitea independientes, excluidos por el .gitignore
# canonico que ensure_project_gitignore garantiza ANTES del push para no
# trackear su contenido (doble-tracking). Sin esto, un project sin repo
# (aurgi, obsidian, osint) vivia solo en disco y se perdia al borrar el PC.
if [[ -f "$registry_root/registry.db" ]] && command -v sqlite3 >/dev/null 2>&1; then
while IFS= read -r proj_dir; do
[[ -z "$proj_dir" ]] && continue
local pd="$registry_root/$proj_dir"
[[ -d "$pd" ]] || continue
# Garantizar el .gitignore canonico ANTES de cualquier git add -A.
ensure_project_gitignore "$pd" || \
echo " [warn] no se pudo asegurar .gitignore de $pd" >&2
if [[ -d "$pd/.git" ]]; then
git -C "$pd" remote get-url origin >/dev/null 2>&1 && continue
echo " fix-remote: $pd (.git sin origin)" >&2
else
echo " auto-init project: $pd" >&2
fi
ensure_repo_synced "$pd" dataforge "$(basename "$pd")" master "chore: initial sync project" || \
echo " [warn] fallo inicializando project $pd" >&2
done < <(sqlite3 "$registry_root/registry.db" "SELECT CASE WHEN dir_path != '' THEN dir_path ELSE 'projects/'||id END FROM projects;" 2>/dev/null)
fi
else
echo " [warn] registry.db o sqlite3 no disponibles — omitiendo auto-init BD-driven" >&2
fi
@@ -72,28 +99,13 @@ full_git_push() {
echo " [skip] GITEA_URL/GITEA_TOKEN no disponibles — omitiendo auto-init" >&2
fi
# Redescubrir repos tras posibles inicializaciones
# Redescubrir repos tras posibles inicializaciones.
# El repo de config de Claude (dataforge/repo_Claude, al que apuntan los
# symlinks de ~/.claude/) vive en fn_registry/external/repo_Claude, asi que
# discover_git_repos ya lo encuentra y pasa por scan-secrets/commit/push
# como un repo mas. No necesita tratamiento especial.
repos=$(discover_git_repos "$registry_root")
# --- Paso 1c: Incluir el repo de configuracion de Claude ---
# Los archivos de ~/.claude/ (settings.json, commands, skills, CLAUDE.md...)
# son symlinks a un repo git externo (dataforge/repo_Claude). Lo resolvemos
# de forma portable siguiendo el symlink de settings.json — sin hardcodear
# el path, que difiere entre PCs. Si resuelve a un repo git, lo anadimos a
# la lista para que pase por scan-secrets + auto-commit + push como los demas.
local claude_repo=""
if [[ -L "$HOME/.claude/settings.json" ]]; then
local _claude_settings_real
_claude_settings_real=$(readlink -f "$HOME/.claude/settings.json" 2>/dev/null || true)
if [[ -n "$_claude_settings_real" ]]; then
claude_repo=$(git -C "$(dirname "$_claude_settings_real")" rev-parse --show-toplevel 2>/dev/null || true)
fi
fi
if [[ -n "$claude_repo" && -d "$claude_repo/.git" ]]; then
echo "[1c] Incluyendo repo de config Claude: $claude_repo" >&2
repos="$repos"$'\n'"$claude_repo"
fi
# --- Paso 2: Escanear secrets ---
echo "" >&2
echo "[2/6] Escaneando secrets en dirty trees..." >&2
@@ -0,0 +1,65 @@
---
name: close_onlyoffice_instance
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "close_onlyoffice_instance(instance: string = demo, [--purge]) -> json"
description: "Termina el/los proceso(s) DesktopEditors de una INSTANCIA AISLADA (slot) de ONLYOFFICE Desktop Editors, identificados por su HOME=/tmp/oo_<instance> leido de /proc/<pid>/environ — asi NUNCA mata la instancia personal del usuario, solo la aislada. Envia SIGTERM, espera ~3s por evento (read -t, sin sleep foreground) y SIGKILL a los que sigan vivos. Con el flag --purge borra ademas los directorios del slot (/tmp/oo_<instance>*). Imprime JSON con instance, killed_pids (array), purged y status (closed|not_running)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: instance
desc: "nombre del slot aislado a cerrar (default: demo). Solo se matan procesos DesktopEditors cuyo HOME sea /tmp/oo_<instance>"
- name: --purge
desc: "flag opcional: si se pasa, borra los directorios del slot (/tmp/oo_<instance>*) tras matar los procesos. Sin el flag, solo termina procesos y deja el estado del slot en disco"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"killed_pids\":[<pids>],\"purged\":true|false,\"status\":\"closed\"|\"not_running\"}. Exit 0 siempre que opere bien (closed si mato procesos, not_running si no habia ninguno del slot), exit 1 si falta dependencia, exit 2 si flag desconocido"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/close_onlyoffice_instance.sh"
---
## Ejemplo
```bash
# Cerrar el slot demo (deja /tmp/oo_demo* en disco para reusar la config)
bash bash/functions/shell/close_onlyoffice_instance.sh demo
# Cerrar y limpiar todo el estado del slot
bash bash/functions/shell/close_onlyoffice_instance.sh demo --purge
# Slot por defecto (demo) sin argumentos
bash bash/functions/shell/close_onlyoffice_instance.sh
# Via fn run
./fn run close_onlyoffice_instance_bash_shell reporte --purge
# Sourceado
source bash/functions/shell/close_onlyoffice_instance.sh
out=$(close_onlyoffice_instance demo --purge)
echo "$out"
# {"instance":"demo","killed_pids":[12345,12350],"purged":true,"status":"closed"}
```
## Cuando usarla
- Cuando terminas un flujo automatizado con ONLYOFFICE Desktop y quieres **cerrar la instancia aislada por completo** (cerrar la ventana con `wmctrl` deja el proceso vivo; esta funcion mata el proceso real).
- Para **liberar recursos** de un slot que ya no usas, opcionalmente borrando su estado en /tmp con `--purge`.
- Como ultimo paso del ciclo open -> reload -> close, garantizando que no quedan procesos huerfanos de la instancia aislada.
## Gotchas
- **Solo mata la instancia aislada**: identifica procesos por `HOME=/tmp/oo_<instance>` en `/proc/<pid>/environ`. La instancia personal del usuario (HOME real) NUNCA se toca. Esto es por diseño y por seguridad.
- **Cerrar la ventana NO mata el proceso**: por eso esta funcion existe. Tras `reload`/`wmctrl -ic` el proceso de la instancia aislada sigue vivo (deseable para reusar). Usa esta funcion para terminarlo de verdad.
- **`--purge` borra /tmp/oo_<instance>***: pierdes la config del slot (perfil, recientes). El slot se recreara limpio en el siguiente `open`. Sin `--purge`, el estado persiste y el siguiente arranque reusa esa config.
- **El slot vive en /tmp**: aunque no purgues, `/tmp/oo_<instance>*` se pierde al reiniciar el PC. Estado desechable.
- **Requiere X11 + wmctrl + xdotool** instalados (coherencia con el grupo, aunque esta funcion solo usa /proc para matar). Comprueba `command -v` y falla claro si falta alguna; no funciona en Wayland puro sin XWayland para el resto del grupo.
- **Carrera de /proc**: si un pid muere entre listarlo y leer su environ, se ignora silenciosamente (guardas `2>/dev/null || true`); no rompe la funcion (`set -uo pipefail` sin `-e`).
- **SIGKILL como ultimo recurso**: tras ~3s de SIGTERM, los procesos vivos reciben SIGKILL. Cambios sin guardar en la app (si los hubiera) se pierden — pero el flujo previsto edita en disco, no en la app, asi que no deberia haber estado sin guardar.
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# close_onlyoffice_instance — termina el/los proceso(s) DesktopEditors de una
# INSTANCIA AISLADA (slot) de ONLYOFFICE Desktop Editors, identificados por su
# HOME=/tmp/oo_<instance> en /proc/<pid>/environ. Opcionalmente limpia los
# directorios del slot con --purge.
#
# Funcion impura: lee /proc, envia señales a procesos y (con --purge) borra
# directorios bajo /tmp. NO toca la instancia personal del usuario: solo mata
# procesos cuyo HOME apunta al slot aislado.
#
# Slot aislado: cada instance usa HOME=/tmp/oo_<instance>,
# XDG_RUNTIME_DIR=/tmp/oo_<instance>_run, XDG_CONFIG_HOME=/tmp/oo_<instance>/.config.
# Sin -e: lecturas de /proc/<pid>/environ pueden fallar por carrera (el pid
# muere entre listar y leer); no deben abortar la funcion.
set -uo pipefail
close_onlyoffice_instance() {
local instance="demo"
local purge=false
# Parseo de args: [instance] y/o --purge en cualquier orden.
local a
for a in "$@"; do
case "$a" in
--purge) purge=true ;;
-*) echo "close_onlyoffice_instance: flag desconocido '$a'" >&2; return 2 ;;
*) instance="$a" ;;
esac
done
# 1. Dependencias del sistema (consistencia con el grupo, aunque aqui solo
# se usa /proc; onlyoffice/wmctrl/xdotool deben existir para operar el slot).
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "close_onlyoffice_instance: falta dependencia '$dep'" >&2
return 1
fi
done
local oo_home="/tmp/oo_${instance}"
# 2. Encontrar pids de DesktopEditors con HOME=/tmp/oo_<instance>.
local pids=() pid environ
for pid in $(pgrep -f '/opt/onlyoffice/desktopeditors/DesktopEditors' 2>/dev/null || true); do
# Leer el entorno del proceso; saltar si no se puede (carrera/permisos).
environ=$(tr '\0' '\n' <"/proc/${pid}/environ" 2>/dev/null || true)
[[ -z "$environ" ]] && continue
if grep -qx "HOME=${oo_home}" <<<"$environ" 2>/dev/null; then
pids+=("$pid")
fi
done
# 3. Si no hay procesos del slot: not_running (purge opcional igualmente).
if [[ ${#pids[@]} -eq 0 ]]; then
local purged=false
if [[ "$purge" == true ]]; then
rm -rf -- /tmp/oo_"${instance}"* 2>/dev/null || true
purged=true
fi
printf '{"instance":"%s","killed_pids":[],"purged":%s,"status":"not_running"}\n' \
"$instance" "$purged"
return 0
fi
# 4. SIGTERM a todos los pids del slot.
kill -TERM "${pids[@]}" 2>/dev/null || true
# 5. Esperar ~3s a que mueran (NUNCA sleep foreground): read -t 0.3 x10.
local w=0 wmax=10
while [[ $w -lt $wmax ]]; do
local alive=false p
for p in "${pids[@]}"; do
if kill -0 "$p" 2>/dev/null; then alive=true; break; fi
done
[[ "$alive" == false ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
w=$((w + 1))
done
# 6. SIGKILL a los que sigan vivos.
local p
for p in "${pids[@]}"; do
if kill -0 "$p" 2>/dev/null; then
kill -KILL "$p" 2>/dev/null || true
fi
done
# 7. Purge opcional de los dirs del slot.
local purged=false
if [[ "$purge" == true ]]; then
rm -rf -- /tmp/oo_"${instance}"* 2>/dev/null || true
purged=true
fi
# 8. JSON con el array de pids terminados.
local pids_json
pids_json=$(printf '%s,' "${pids[@]}")
pids_json="[${pids_json%,}]"
printf '{"instance":"%s","killed_pids":%s,"purged":%s,"status":"closed"}\n' \
"$instance" "$pids_json" "$purged"
return 0
}
# Ejecutable directo o sourceado.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
close_onlyoffice_instance "$@"
fi
@@ -0,0 +1,78 @@
---
name: monitor_listening_ports
kind: function
lang: bash
domain: shell
version: "0.3.0"
purity: impure
signature: "monitor_listening_ports([--interval N], [--once]) -> void"
description: "TUI ligera de terminal que refresca cada N segundos una tabla de los sockets TCP en escucha (LISTEN) del equipo local: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO | CMD (cmdline real, util para distinguir python3/node genericos), ordenada por tiempo de vida del proceso dueño (descendente). Una fila por pid. Lanzada como root rellena tambien los sockets de otros usuarios. Modo --once imprime un solo frame y sale."
tags: [recon, ports, monitor, tui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: --interval N
desc: "segundos entre refrescos en modo bucle (default: 1, acepta decimales)"
- name: --once
desc: "imprime un único frame de la tabla y termina con exit 0 (no interactivo; úsalo en tests y en `fn run` para no colgar)"
output: "tabla a stdout con columnas IP, PUERTO, PROCESO, PID, TIEMPO ACTIVO ordenada por uptime del proceso descendente; sin --once refresca en bucle infinito hasta Ctrl-C"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/monitor_listening_ports.sh"
---
## Ejemplo
```bash
# Un solo frame (no cuelga) — ideal para fn run o un pipe
./fn run monitor_listening_ports_bash_shell --once
# Como script directo
bash bash/functions/shell/monitor_listening_ports.sh --once
# Sourceada, en bucle interactivo refrescando cada segundo (Ctrl-C para salir)
source bash/functions/shell/monitor_listening_ports.sh
monitor_listening_ports --interval 1
# Refresco mas lento
monitor_listening_ports --interval 5
```
Salida (frame `--once`, recortado):
```
IP PUERTO PROCESO PID TIEMPO ACTIVO
* 8420 registry_api 1885 4d 23:40:46
:: 8889 mitmweb 1892 4d 23:40:46
127.0.0.1 8484 sqlite_api 1889 4d 23:40:42
127.0.0.1 8899 jupyter-lab 155100 4d 19:33:55
::1 631 - - ?
```
## Cuando usarla
- Cuando quieras vigilar **qué puertos abren tus dev-servers / procesos web locales y desde cuándo** llevan vivos, en una sola pantalla que se actualiza sola.
- Para detectar de un vistazo un proceso recién levantado (aparece al fondo, con poco TIEMPO ACTIVO) o uno que lleva días escuchando (arriba del todo).
- Como paso de reconocimiento local del grupo `recon`: inventario rápido de superficie de escucha TCP del propio equipo, con el dueño de cada socket.
- En tests o automatizaciones que solo necesitan un snapshot: añade `--once` para obtener un frame y salir.
## Gotchas
- **Impura**: depende de `ss` (paquete iproute2) y `ps` (procps). Si falta cualquiera, sale con exit 1 y un mensaje a stderr.
- **Sin sudo no ves PROCESO/PID/CMD de sockets de otros usuarios** (típicamente procesos de root, ej. systemd-resolved en `127.0.0.54:53`, kernels Jupyter de otra sesión, o servidores en contenedores). Esas filas muestran `-`/`?`. La función **no usa sudo** a propósito; para **rellenarlos, lánzala como root**: `pass show claude/sudo | sudo -S bash bash/functions/shell/monitor_listening_ports.sh --interval 1` (el password se pipea, no queda en la cmdline). Como root, `ss` resuelve el dueño de todos los sockets.
- **Columna CMD = cmdline real** (`ps -o args=`, recortada a 90 chars). Es lo que distingue un `python3`/`node` genérico (PROCESO) de lo que realmente ejecuta: `python3 -m ipykernel_launcher ...`, `registry_api -port 8420`, etc. Procesos en distinto namespace (docker) pueden seguir sin CMD aunque corras como root.
- **Una fila por pid**: un mismo puerto con varios workers (ej. nginx, gunicorn) genera varias filas, una por cada pid dueño del socket.
- **`--once` evita colgar**: sin `--once` corre en bucle infinito. No lo lances así en tests ni en `fn run` desatendido — usa `--once`.
- **El orden es por uptime del PROCESO, no por el tiempo de la conexión**. `ps -o etimes=` mide cuánto lleva vivo el proceso completo, no cuándo abrió ese socket concreto.
- **Carrera ps**: si un pid muere entre `ss` y `ps`, su TIEMPO ACTIVO sale como `?` y la fila se ordena al final (no rompe el bucle; el script usa `set -uo pipefail` sin `-e`).
- En modo bucle oculta el cursor (`tput civis`) y lo restaura + limpia en un `trap` EXIT/INT/TERM, de modo que Ctrl-C deja la terminal limpia.
## Capability growth log
- v0.3.0 (14/06/2026) — añade columna **CMD** con la cmdline real del proceso (mapa pid→args construido en la misma llamada `ps -eo pid=,etimes=,args=`), para distinguir un `python3`/`node` genérico de lo que realmente ejecuta. Documenta cómo rellenar los sockets de otros usuarios (`-`) lanzando la TUI como root. Anchos de columna reajustados para dar sitio a CMD.
- v0.2.0 (14/06/2026) — corrige parpadeo y cuelgue del modo bucle. (1) Doble-buffer ANSI: cada frame se computa completo en una variable y se pinta con cursor-home `\033[H` + clear-to-end `\033[J` en vez de `tput clear` antes de recolectar, eliminando el instante en blanco. (2) Rendimiento: una sola llamada a `ps -eo pid=,etimes=` (mapa pid→uptime en memoria, antes era un fork de `ps` por pid) y construcción de filas con `printf -v` (builtin, antes un `$( )` por fila); frame de ~130 ms con cientos de sockets. (3) Bugfix de cuelgue: el avance del parser multi-pid usaba `BASH_REMATCH[0]`, que queda sobrescrito por el `[[ =~ ]]` interno de `_mlp_fmt_etime` → no recortaba el string y entraba en bucle infinito. Ahora el needle se captura justo tras el match, con guard anti-cuelgue si el recorte no progresa.
@@ -0,0 +1,271 @@
#!/usr/bin/env bash
# monitor_listening_ports — TUI ligera que refresca una tabla de sockets TCP en
# escucha (LISTEN) del equipo local, ordenada por tiempo de vida del proceso
# dueño (descendente). Columnas: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO.
#
# Funcion impura: lee estado del sistema (sockets via `ss`, uptime de procesos
# via `ps`). Sin --once corre en bucle infinito refrescando cada N segundos.
#
# Rendimiento: cada frame hace UNA sola llamada a `ss` y UNA sola a `ps`
# (mapa pid->etimes en memoria). El parseo de cada socket es bash puro y SIN
# command substitution por fila: las cadenas se construyen con `printf -v`
# (builtin, cero forks) y el formato de tiempo se devuelve en una variable
# global. El modo bucle usa doble-buffer ANSI (cursor home + clear-to-end) en
# lugar de limpiar la pantalla antes de computar, para que nunca se vea vacia
# entre refrescos.
# No usamos -e a proposito: una carrera donde un pid muere entre `ss` y `ps`
# no debe matar el bucle entero. -u y pipefail se mantienen para robustez.
set -uo pipefail
# Formatea segundos a texto humano legible y lo deja en la global _mlp_human.
# Se evita `$( )` (un fork por fila) usando una variable de retorno.
# <1h -> MM:SS ej. 12:45
# <1d -> HH:MM:SS ej. 03:12:45
# >=1d -> Nd HH:MM:SS ej. 1d 03:12:45
_mlp_human=""
_mlp_fmt_etime() {
local secs="$1"
# Si no es un numero entero valido, devolver tal cual (ej. "?").
if ! [[ "$secs" =~ ^[0-9]+$ ]]; then
_mlp_human="$secs"
return 0
fi
local days=$(( secs / 86400 ))
local rem=$(( secs % 86400 ))
local hours=$(( rem / 3600 ))
local mins=$(( (rem % 3600) / 60 ))
local s=$(( rem % 60 ))
if (( days > 0 )); then
printf -v _mlp_human '%dd %02d:%02d:%02d' "$days" "$hours" "$mins" "$s"
elif (( hours > 0 )); then
printf -v _mlp_human '%02d:%02d:%02d' "$hours" "$mins" "$s"
else
printf -v _mlp_human '%02d:%02d' "$mins" "$s"
fi
}
# Imprime un unico frame de la tabla a stdout.
# Estrategia de rendimiento (cero forks por fila):
# 1. Un solo `ps -eo pid=,etimes=` construye un mapa pid -> segundos vivo.
# 2. Un solo `ss -H -tlnp` lista los sockets en escucha.
# 3. Cada linea se parsea con bash puro: IP/puerto por parameter expansion,
# (nombre,pid) del campo users:(...) iterando con BASH_REMATCH, y cada
# fila se arma con `printf -v` (builtin). El uptime se resuelve por lookup
# O(1) en el mapa.
# 4. Se ordena por segundos vivo descendente con un unico `sort`.
_mlp_render_frame() {
# Mapas pid -> etimes (segundos vivo) y pid -> cmdline completa. Una sola
# invocacion de ps por frame. `args=` va al ultimo porque lleva espacios,
# asi `read` lo captura entero en la tercera variable.
local -A etmap=() argmap=()
local _pid _et _args
while read -r _pid _et _args; do
[[ -z "$_pid" ]] && continue
etmap["$_pid"]="$_et"
argmap["$_pid"]="$_args"
done < <(ps -eo pid=,etimes=,args= 2>/dev/null)
# Cada fila intermedia: "<etimes>\t<ip>\t<puerto>\t<proceso>\t<pid>\t<humano>"
local -a rows=()
local line row
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Campos de `ss -H -tlnp`: State Recv-Q Send-Q Local:Port Peer:Port users:(...)
# Local:Port es el 4o token. Lo extraemos sin fork con read en array.
local -a F=()
read -ra F <<<"$line"
local local_addr="${F[3]:-}"
[[ -z "$local_addr" ]] && continue
# Separar IP y PUERTO partiendo por el ULTIMO ':'.
local ip port
port="${local_addr##*:}"
ip="${local_addr%:*}"
# Quitar corchetes de IPv6: [::] -> :: , [::1] -> ::1
ip="${ip#[}"
ip="${ip%]}"
# Caso de bind sin direccion explicita (raro): dejar marcador.
[[ -z "$ip" ]] && ip="*"
# Extraer el bloque users:(...) del final de la linea (si existe).
local users=""
[[ "$line" == *"users:("* ]] && users="${line#*users:(}"
if [[ -z "$users" ]]; then
# Socket sin info de proceso (pertenece a otro usuario y no corremos
# como root). Para verlo, lanzar la TUI como root (ver Gotchas).
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "-1" "$ip" "$port" "-" "-" "?" "-"
rows+=("$row")
continue
fi
# Dentro de users puede haber varios ("nombre",pid=N,fd=M). Una fila por
# pid. Iteramos con BASH_REMATCH avanzando sobre el string (cero forks).
local s="$users" pname pid etimes needle prev_s cmd found_any=0
while [[ "$s" =~ \"([^\"]*)\",pid=([0-9]+) ]]; do
# IMPORTANTE: capturar nombre/pid/needle ANTES de cualquier otra
# comparacion `[[ =~ ]]` (p.ej. dentro de _mlp_fmt_etime), porque
# cada `=~` SOBREESCRIBE BASH_REMATCH. Si se usara BASH_REMATCH[0]
# despues, contendria el match del ultimo `=~` y el recorte de `s`
# no avanzaria -> bucle infinito.
pname="${BASH_REMATCH[1]}"
pid="${BASH_REMATCH[2]}"
needle="${BASH_REMATCH[0]}"
found_any=1
# Lookup O(1) en el mapa. Si el pid ya no esta (carrera), marcar "?".
etimes="${etmap[$pid]:-}"
if [[ -z "$etimes" || ! "$etimes" =~ ^[0-9]+$ ]]; then
etimes="-1"
_mlp_human="?"
else
_mlp_fmt_etime "$etimes"
fi
# Comando real (cmdline completa) del pid; dice QUE es realmente un
# "python3"/"node" generico. Se recorta para no romper la tabla.
cmd="${argmap[$pid]:-}"
[[ -z "$cmd" ]] && cmd="-"
cmd="${cmd:0:90}"
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "$etimes" "$ip" "$port" "$pname" "$pid" "$_mlp_human" "$cmd"
rows+=("$row")
# Avanzar mas alla del match actual para no repetir el primer pid.
# Guard: si el recorte no cambia `s`, cortar para no colgar nunca.
prev_s="$s"
s="${s#*"$needle"}"
[[ "$s" == "$prev_s" ]] && break
done
# Si el formato fue inesperado y no se parseo ningun par, fila placeholder.
if (( found_any == 0 )); then
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "-1" "$ip" "$port" "-" "-" "?" "-"
rows+=("$row")
fi
done < <(ss -H -tlnp 2>/dev/null)
# Estilo de cabecera (negrita) si la terminal lo soporta.
local bold="" reset=""
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
bold=$(tput bold 2>/dev/null || true)
reset=$(tput sgr0 2>/dev/null || true)
fi
# Anchos fijos para alineacion estable (no usamos column -t). La ultima
# columna (CMD) es libre: muestra la cmdline real del proceso.
local fmt='%-26s %-7s %-16s %-8s %-13s %s\n'
# shellcheck disable=SC2059
printf "${bold}${fmt}${reset}" "IP" "PUERTO" "PROCESO" "PID" "TIEMPO ACTIVO" "CMD"
if (( ${#rows[@]} == 0 )); then
printf '(sin sockets TCP en escucha)\n'
return 0
fi
# Ordenar por la primera columna (etimes) numerica descendente y emitir las
# 5 columnas visibles (descartando la columna de orden).
printf '%s\n' "${rows[@]}" \
| sort -t$'\t' -k1,1nr \
| while IFS=$'\t' read -r _etimes ip port pname pid human cmd; do
# shellcheck disable=SC2059
printf "$fmt" "$ip" "$port" "$pname" "$pid" "$human" "$cmd"
done
}
monitor_listening_ports() {
local interval=1
local once=0
# Parseo de flags.
while (( $# > 0 )); do
case "$1" in
--interval)
interval="${2:-1}"
shift 2
;;
--interval=*)
interval="${1#*=}"
shift
;;
--once)
once=1
shift
;;
-h|--help)
cat <<'USAGE'
monitor_listening_ports [--interval N] [--once]
--interval N Segundos entre refrescos (default: 1, acepta decimales).
--once Imprime un solo frame de la tabla y termina (exit 0).
Tabla de sockets TCP en escucha (LISTEN) ordenada por tiempo de vida del
proceso dueño (descendente). Columnas: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO.
USAGE
return 0
;;
*)
printf 'monitor_listening_ports: argumento desconocido: %s\n' "$1" >&2
return 1
;;
esac
done
# Dependencias minimas.
if ! command -v ss >/dev/null 2>&1; then
printf 'monitor_listening_ports: requiere `ss` (paquete iproute2)\n' >&2
return 1
fi
if ! command -v ps >/dev/null 2>&1; then
printf 'monitor_listening_ports: requiere `ps` (paquete procps)\n' >&2
return 1
fi
# Modo single-frame: util para tests y para `fn run` sin colgar.
if (( once == 1 )); then
_mlp_render_frame
return 0
fi
# Modo bucle interactivo: oculta cursor y lo restaura + limpia al salir.
local have_tput=0
command -v tput >/dev/null 2>&1 && have_tput=1
_mlp_cleanup() {
if (( have_tput == 1 )); then
tput cnorm 2>/dev/null || true # restaurar cursor
tput sgr0 2>/dev/null || true # resetear atributos
fi
printf '\n'
}
trap '_mlp_cleanup; trap - INT TERM EXIT; return 0 2>/dev/null || exit 0' INT TERM EXIT
(( have_tput == 1 )) && tput civis 2>/dev/null || true # ocultar cursor
# Limpiamos la pantalla UNA sola vez al entrar. A partir de aqui cada frame
# se computa COMPLETO en una variable y luego se pinta con doble-buffer:
# cursor a home (\033[H), volcado del frame, y clear-to-end (\033[J) para
# borrar restos de un frame anterior mas largo. Asi nunca hay un instante
# con la pantalla vacia mientras se recolectan los datos.
printf '\033[2J'
local frame
while true; do
frame=$(
printf 'monitor_listening_ports — %s — intervalo %ss — orden: TIEMPO ACTIVO desc (Ctrl-C para salir)\n\n' \
"$(date '+%d/%m/%Y %H:%M:%S')" "$interval"
_mlp_render_frame
)
printf '\033[H' # cursor al inicio (sin borrar todavia)
printf '%s\n' "$frame" # volcar el frame ya calculado de golpe
printf '\033[J' # borrar de aqui al final (restos del frame previo)
sleep "$interval" || break
done
}
# Auto-invocacion cuando se ejecuta como script (no al hacer source).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
monitor_listening_ports "$@"
fi
@@ -0,0 +1,62 @@
---
name: open_onlyoffice_file
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "open_onlyoffice_file(file_path: string, instance: string = demo) -> json"
description: "Abre un archivo en una INSTANCIA AISLADA de ONLYOFFICE Desktop Editors (Linux/X11) sin perturbar la instancia personal del usuario. Cada 'instance' (slot, default demo) usa su propio HOME=/tmp/oo_<instance>, XDG_RUNTIME_DIR y XDG_CONFIG_HOME bajo /tmp, lo que rompe el single-instance lock de ONLYOFFICE y permite una ventana propia en vez de una pestaña en la instancia del usuario. Espera la ventana por evento (xdotool, basename del archivo, timeout ~25s) sin sleep en foreground. Idempotente: si ya hay ventana para ese basename, no relanza y devuelve el wid existente. NO crea archivos: si file_path no existe, falla. Imprime una linea JSON con instance, file (ruta absoluta), wid (hex), pid y status (open|timeout)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_path
desc: "ruta (relativa o absoluta) al archivo a abrir; DEBE existir, esta funcion no crea archivos. Se normaliza con readlink -f y se busca la ventana por su basename"
- name: instance
desc: "nombre del slot aislado (default: demo). Determina el env: HOME=/tmp/oo_<instance>, XDG_RUNTIME_DIR=/tmp/oo_<instance>_run, XDG_CONFIG_HOME=/tmp/oo_<instance>/.config. Usa el MISMO instance en reload/close para operar la misma instancia"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"file\":\"<abs>\",\"wid\":\"<hex>|null\",\"pid\":<n>|null,\"status\":\"open\"|\"timeout\"}. Exit 0 si abrio (status open), exit 1 si la ventana no aparecio en el timeout (status timeout) o falta dependencia/archivo, exit 2 si falta el argumento file_path"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/open_onlyoffice_file.sh"
---
## Ejemplo
```bash
# Como script directo (slot 'demo' por defecto)
bash bash/functions/shell/open_onlyoffice_file.sh /tmp/demo_reload.xlsx
# Slot nombrado distinto (ventana propia, no perturba la instancia personal)
bash bash/functions/shell/open_onlyoffice_file.sh /tmp/informe.docx reporte
# Via fn run
./fn run open_onlyoffice_file_bash_shell /tmp/demo_reload.xlsx demo
# Sourceado, capturando el wid del JSON
source bash/functions/shell/open_onlyoffice_file.sh
out=$(open_onlyoffice_file /tmp/demo_reload.xlsx demo)
echo "$out"
# {"instance":"demo","file":"/tmp/demo_reload.xlsx","wid":"0x3c00007","pid":12345,"status":"open"}
```
## Cuando usarla
- Cuando necesites **abrir un archivo en ONLYOFFICE Desktop desde terminal en su propia ventana aislada**, sin que se agregue como pestaña a la instancia personal del usuario.
- Como primer paso de un flujo automatizado open -> (editas el archivo en disco) -> `reload_onlyoffice_file` -> `close_onlyoffice_instance`.
- Cuando quieras un slot reproducible por nombre (`instance`) que reuse la misma instancia aislada entre llamadas (reabrir rapido en vez de arrancar el motor de cero).
## Gotchas
- **ONLYOFFICE Desktop es single-instance por usuario**: sin el slot aislado (HOME/XDG_RUNTIME_DIR propios), un segundo lanzamiento se reenvia a la instancia viva y abre el archivo como PESTAÑA, no ventana nueva. El lock NO se rompe con XDG_CONFIG_HOME solo; SI con HOME + XDG_RUNTIME_DIR propios. Esta funcion ya aplica esa convencion.
- **NO hay reload nativo de cambios externos** (GitHub Issue #2313 abierto, no implementado). Esta funcion solo abre; para reflejar ediciones hechas en disco hay que cerrar+reabrir con `reload_onlyoffice_file`.
- **NO crea archivos**: si `file_path` no existe, falla con exit 1. Crea el archivo por tu cuenta antes de llamar.
- **El slot vive en /tmp**: los dirs `/tmp/oo_<instance>*` se pierden al reiniciar el PC (tmpfs en muchos sistemas). No guardes nada importante ahi; es estado desechable de la instancia aislada.
- **Requiere X11 + wmctrl + xdotool**: no funciona en Wayland puro sin XWayland (xdotool no encontrara la ventana). La funcion comprueba `command -v` de las 3 deps y falla claro si falta alguna.
- **El pid reportado es el del launcher** (`onlyoffice-desktopeditors`), que puede reexec/fork al proceso real `DesktopEditors`; sirve como referencia best-effort, no para `kill` fiable (usa `close_onlyoffice_instance`, que localiza el proceso real por su HOME).
- **Idempotencia por basename**: si ya existe una ventana cuyo titulo contiene el basename del archivo (lo abrio el usuario en su instancia personal, por ejemplo), la funcion la considera "ya abierta" y devuelve ese wid sin relanzar. Usa un basename unico para el slot de pruebas si quieres evitar colisiones.
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# open_onlyoffice_file — abre un archivo en una INSTANCIA AISLADA de ONLYOFFICE
# Desktop Editors (Linux/X11), sin perturbar la instancia personal del usuario.
#
# Funcion impura: lanza un proceso GUI, lee estado de ventanas (xdotool) y
# escribe directorios en /tmp. Imprime una linea JSON con el resultado.
#
# Por que "instancia aislada": ONLYOFFICE Desktop es single-instance por
# usuario — un segundo `onlyoffice-desktopeditors <file>` se reenvia a la
# instancia viva y abre el archivo como PESTAÑA en su ventana. El lock
# single-instance NO se rompe con XDG_CONFIG_HOME, pero SI se rompe lanzando
# con HOME y XDG_RUNTIME_DIR propios. Por eso cada "slot" nombrado (instance)
# usa su propio HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME bajo /tmp.
# Sin -e: las busquedas de ventana (xdotool search) pueden no matchear y
# devolver exit !=0; no deben abortar la funcion. -u y pipefail se mantienen.
set -uo pipefail
open_onlyoffice_file() {
local file_path="${1:-}"
local instance="${2:-demo}"
if [[ -z "$file_path" ]]; then
echo "open_onlyoffice_file: falta <file_path>" >&2
echo "uso: open_onlyoffice_file <file_path> [instance]" >&2
return 2
fi
# 1. Dependencias del sistema.
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "open_onlyoffice_file: falta dependencia '$dep' (instala el paquete correspondiente)" >&2
return 1
fi
done
# 2. El archivo DEBE existir — esta funcion no crea archivos.
if [[ ! -f "$file_path" ]]; then
echo "open_onlyoffice_file: el archivo no existe: $file_path (esta funcion no crea archivos)" >&2
return 1
fi
# Ruta absoluta y basename para titular/buscar la ventana.
local abs_path base
abs_path=$(readlink -f -- "$file_path")
base=$(basename -- "$abs_path")
# 3. Slot aislado: HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME propios bajo /tmp.
local oo_home="/tmp/oo_${instance}"
local oo_run="/tmp/oo_${instance}_run"
local oo_cfg="${oo_home}/.config"
mkdir -p "$oo_home" "$oo_cfg" "$oo_run"
chmod 700 "$oo_run" 2>/dev/null || true
# 4. Idempotencia: si ya hay ventana para ese basename, no relanzar.
local existing_wid
existing_wid=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
if [[ -n "$existing_wid" ]]; then
local wid_hex
wid_hex=$(printf '0x%x' "$existing_wid" 2>/dev/null || echo "$existing_wid")
printf '{"instance":"%s","file":"%s","wid":"%s","pid":null,"status":"open"}\n' \
"$instance" "$abs_path" "$wid_hex"
return 0
fi
# 5. Lanzar la instancia aislada con su env propio. setsid lo desacopla de
# la terminal; redirige todo a un log del slot.
env HOME="$oo_home" XDG_RUNTIME_DIR="$oo_run" XDG_CONFIG_HOME="$oo_cfg" \
setsid onlyoffice-desktopeditors "$abs_path" \
>"/tmp/oo_${instance}.log" 2>&1 </dev/null &
local launch_pid=$!
# 6. Esperar la ventana por evento (NUNCA sleep en foreground).
# ~25s con read -t 0.3 => ~83 iteraciones.
local wid="" i=0 max=83
while [[ $i -lt $max ]]; do
wid=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
[[ -n "$wid" ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
i=$((i + 1))
done
if [[ -z "$wid" ]]; then
printf '{"instance":"%s","file":"%s","wid":null,"pid":%s,"status":"timeout"}\n' \
"$instance" "$abs_path" "$launch_pid"
return 1
fi
local wid_hex
wid_hex=$(printf '0x%x' "$wid" 2>/dev/null || echo "$wid")
# El pid del proceso real (DesktopEditors) puede diferir del launcher; el
# launcher reexec/fork. Reportamos el pid del launcher (best-effort).
printf '{"instance":"%s","file":"%s","wid":"%s","pid":%s,"status":"open"}\n' \
"$instance" "$abs_path" "$wid_hex" "$launch_pid"
return 0
}
# Ejecutable directo: `bash open_onlyoffice_file.sh <file> [instance]`.
# Sourceado: define la funcion sin ejecutarla.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
open_onlyoffice_file "$@"
fi
@@ -0,0 +1,61 @@
---
name: reload_onlyoffice_file
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "reload_onlyoffice_file(file_path: string, instance: string = demo) -> json"
description: "Recarga en la ventana de ONLYOFFICE Desktop Editors los datos que el caller edito EN DISCO, cerrando y reabriendo el archivo en la INSTANCIA AISLADA (slot). Es la funcion estrella del grupo: ONLYOFFICE no recarga cambios externos del archivo (GitHub Issue #2313 abierto, no implementado), asi que la unica forma de mostrar datos editados fuera de la app es cerrar la ventana (wmctrl -ic) y reabrir (ONLYOFFICE lee fresco del disco al abrir). Localiza la ventana por basename, la cierra y espera a que desaparezca (timeout ~10s), relanza con el env del slot aislado y espera la ventana nueva (timeout ~25s), todo por evento sin sleep en foreground. Si no habia ventana previa, actua como open. NO edita el archivo: el caller lo edita antes de llamar. Imprime JSON con wid_old, wid_new, reopened, elapsed_s y status (reloaded|timeout)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_path
desc: "ruta (relativa o absoluta) al archivo cuya ventana se recarga; DEBE existir. El caller ya lo edito en disco antes de llamar. Se busca la ventana por su basename"
- name: instance
desc: "nombre del slot aislado (default: demo); debe coincidir con el usado en open_onlyoffice_file para reusar la misma instancia. Determina HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME bajo /tmp"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"file\":\"<abs>\",\"wid_old\":\"<hex>|null\",\"wid_new\":\"<hex>|null\",\"reopened\":true|false,\"elapsed_s\":<n>,\"status\":\"reloaded\"|\"timeout\"}. Exit 0 si reabrio (status reloaded), exit 1 si la ventana nueva no aparecio en el timeout (status timeout) o falta dependencia/archivo, exit 2 si falta file_path"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/reload_onlyoffice_file.sh"
---
## Ejemplo
```bash
# Flujo tipico: editas el .xlsx en disco con tu herramienta y refrescas la vista
# (este ejemplo asume que /tmp/demo_reload.xlsx ya esta abierto en el slot demo)
bash bash/functions/shell/reload_onlyoffice_file.sh /tmp/demo_reload.xlsx demo
# Via fn run
./fn run reload_onlyoffice_file_bash_shell /tmp/demo_reload.xlsx demo
# Sourceado, dentro de un bucle de "editar en disco -> ver en ONLYOFFICE"
source bash/functions/shell/reload_onlyoffice_file.sh
# ... el caller modifica /tmp/demo_reload.xlsx por su cuenta ...
out=$(reload_onlyoffice_file /tmp/demo_reload.xlsx demo)
echo "$out"
# {"instance":"demo","file":"/tmp/demo_reload.xlsx","wid_old":"0x3c00007","wid_new":"0x3c0000b","reopened":true,"elapsed_s":4,"status":"reloaded"}
```
## Cuando usarla
- Cuando **editaste un archivo en disco fuera de ONLYOFFICE** (script, otra herramienta, generador) y necesitas que la ventana de ONLYOFFICE muestre los datos nuevos: esta funcion cierra y reabre para forzar la lectura fresca del disco.
- En bucles de iteracion rapida "modificar el archivo -> ver el resultado en ONLYOFFICE" sin tocar la instancia personal del usuario.
- Como reemplazo del reload nativo inexistente (Issue #2313): es la unica via fiable de refrescar la vista desde disco.
## Gotchas
- **No edita el archivo**: solo recarga la ventana desde disco. El caller es responsable de modificar el archivo ANTES de llamar; si no lo modifico, reabrira los mismos datos.
- **ONLYOFFICE no tiene reload de cambios externos** (GitHub Issue #2313 abierto, no implementado): por eso esta funcion existe y hace cerrar+reabrir. No hay forma "in-place" de refrescar.
- **`wmctrl -ic` puede disparar el dialogo "Guardar cambios"** si el usuario edito EN la app (no en disco) y hay cambios sin guardar en esa ventana. El flujo previsto es editar SOLO en disco con la ventana sin tocar; si editaste en la app, guarda o descarta antes, o el cierre se quedara esperando interaccion (la funcion saldra por timeout).
- **Single-instance + slot aislado**: usa el mismo `instance` que en `open_onlyoffice_file`. Con HOME/XDG_RUNTIME_DIR propios el relaunch reenvia a la instancia aislada viva y reabre rapido; con env por defecto se reenviaria a la instancia personal del usuario (no deseado).
- **El slot vive en /tmp**: `/tmp/oo_<instance>*` se pierde al reiniciar el PC. Estado desechable.
- **Requiere X11 + wmctrl + xdotool**: no funciona en Wayland puro sin XWayland. Comprueba las 3 deps y falla claro si falta alguna.
- **Carrera de cierre**: si la ventana tarda mas de ~10s en cerrarse (dialogo modal, app ocupada), la funcion continua igualmente al relaunch; el resultado puede acabar en `timeout` si la ventana nueva no aparece a tiempo.
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# reload_onlyoffice_file — cierra y reabre un archivo en la INSTANCIA AISLADA de
# ONLYOFFICE Desktop Editors para que la ventana muestre los datos editados
# EN DISCO por el caller (ONLYOFFICE no recarga cambios externos: GitHub Issue
# #2313 abierto, no implementado — la unica forma es cerrar+reabrir).
#
# Funcion impura: cierra una ventana GUI (wmctrl), relanza un proceso y espera
# la ventana nueva por evento. NO edita el archivo — solo recarga la ventana
# desde el disco. El caller edita el archivo antes de llamar a esta funcion.
#
# Instancia aislada (slot): mismo HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME que usa
# open_onlyoffice_file, para que el relaunch reenvie a la instancia aislada
# viva y reabra rapido en vez de arrancar el motor de cero.
# Sin -e: busquedas de ventana (xdotool/wmctrl) pueden no matchear; no deben
# abortar la funcion. -u y pipefail se mantienen.
set -uo pipefail
reload_onlyoffice_file() {
local file_path="${1:-}"
local instance="${2:-demo}"
if [[ -z "$file_path" ]]; then
echo "reload_onlyoffice_file: falta <file_path>" >&2
echo "uso: reload_onlyoffice_file <file_path> [instance]" >&2
return 2
fi
# 1. Dependencias del sistema.
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "reload_onlyoffice_file: falta dependencia '$dep' (instala el paquete correspondiente)" >&2
return 1
fi
done
# 2. El archivo DEBE existir — no editamos ni creamos archivos.
if [[ ! -f "$file_path" ]]; then
echo "reload_onlyoffice_file: el archivo no existe: $file_path" >&2
return 1
fi
local abs_path base
abs_path=$(readlink -f -- "$file_path")
base=$(basename -- "$abs_path")
# 3. Slot aislado (identico a open_onlyoffice_file).
local oo_home="/tmp/oo_${instance}"
local oo_run="/tmp/oo_${instance}_run"
local oo_cfg="${oo_home}/.config"
mkdir -p "$oo_home" "$oo_cfg" "$oo_run"
chmod 700 "$oo_run" 2>/dev/null || true
local start_ts
start_ts=$(date +%s)
# 4. Localizar la ventana actual del archivo por basename.
local wid_old=""
wid_old=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
local wid_old_hex="null"
if [[ -n "$wid_old" ]]; then
wid_old_hex=$(printf '0x%x' "$wid_old" 2>/dev/null || echo "$wid_old")
# 5. Cerrar la ventana (sin teclear en la app) y esperar a que
# desaparezca (~10s con read -t 0.3 => ~33 iteraciones).
wmctrl -ic "$wid_old" 2>/dev/null || true
local g=0 gmax=33
while [[ $g -lt $gmax ]]; do
if ! xdotool search --name -- "$base" 2>/dev/null | grep -q .; then
break
fi
read -t 0.3 _ </dev/null 2>/dev/null || true
g=$((g + 1))
done
fi
# 6. Relanzar con el env del slot aislado. (Si no habia ventana previa,
# esto actua simplemente como open.)
env HOME="$oo_home" XDG_RUNTIME_DIR="$oo_run" XDG_CONFIG_HOME="$oo_cfg" \
setsid onlyoffice-desktopeditors "$abs_path" \
>"/tmp/oo_${instance}.log" 2>&1 </dev/null &
# 7. Esperar la ventana nueva por evento (~25s => ~83 iteraciones).
local wid_new="" i=0 max=83
while [[ $i -lt $max ]]; do
wid_new=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
# Si hubo ventana previa, aceptar cualquier wid que aparezca (el old
# ya se cerro; el nuevo puede reutilizar id o no). Si no la hubo,
# cualquier wid sirve.
[[ -n "$wid_new" ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
i=$((i + 1))
done
local now_ts elapsed
now_ts=$(date +%s)
elapsed=$((now_ts - start_ts))
if [[ -z "$wid_new" ]]; then
printf '{"instance":"%s","file":"%s","wid_old":"%s","wid_new":null,"reopened":false,"elapsed_s":%s,"status":"timeout"}\n' \
"$instance" "$abs_path" "$wid_old_hex" "$elapsed"
return 1
fi
local wid_new_hex
wid_new_hex=$(printf '0x%x' "$wid_new" 2>/dev/null || echo "$wid_new")
printf '{"instance":"%s","file":"%s","wid_old":"%s","wid_new":"%s","reopened":true,"elapsed_s":%s,"status":"reloaded"}\n' \
"$instance" "$abs_path" "$wid_old_hex" "$wid_new_hex" "$elapsed"
return 0
}
# Ejecutable directo o sourceado.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
reload_onlyoffice_file "$@"
fi
@@ -0,0 +1,90 @@
---
name: save_onlyoffice_file
kind: function
lang: bash
domain: shell
purity: impure
version: 1.1.0
description: "Fuerza el guardado (Ctrl+S) de un documento abierto en una instancia de OnlyOffice Desktop en Linux/X11 y confirma que llego a disco por cambio de mtime. Primer paso del flujo seguro guardar -> actualizar -> recargar; evita perder cambios no guardados cuando un build regenera el archivo leyendo del disco."
signature: "save_onlyoffice_file(file_path: string, [instance: string]) -> json"
error_type: error_go_core
tags: [onlyoffice, desktop, x11, gui, save, persist]
uses_functions: []
uses_types: []
file_path: bash/functions/shell/save_onlyoffice_file.sh
params:
- name: file_path
desc: "ruta al documento abierto en OnlyOffice cuyo guardado se quiere forzar. Debe existir. Se normaliza a ruta absoluta y se usa su basename para localizar la ventana."
- name: instance
desc: "nombre del slot/instancia para etiquetar la salida JSON (default: 'demo'). Usar el MISMO valor que en open/reload/close del mismo documento por coherencia."
output: "linea JSON a stdout: {\"instance\":\"<i>\",\"file\":\"<abs>\",\"wid\":\"<hex>|null\",\"status\":\"saved\"|\"no_change\"|\"no_window\",\"dialog_confirmed\":0|1[,\"mtime_before\":N,\"mtime_after\":N]}. dialog_confirmed=1 si se envio Return para cerrar el dialogo modal de formato. Exit 0 salvo error de dependencia o archivo inexistente (exit 1)."
---
Fuerza el guardado (Ctrl+S) de un documento abierto en una instancia de ONLYOFFICE
Desktop Editors en Linux/X11 y confirma que el guardado llegó a disco observando el
cambio de `mtime` del archivo.
Existe para cerrar una ventana de pérdida de datos: OnlyOffice mantiene los cambios
en memoria hasta que el usuario guarda. Cualquier proceso que regenere el archivo
leyendo del disco (un build que refresca hojas, un script de sincronización)
perdería el trabajo manual no guardado. Esta función vuelca ese trabajo a disco
ANTES de tocar el archivo, de modo que el paso de actualización pueda preservarlo.
Es el primer paso del flujo seguro de refresco:
```
save_onlyoffice_file -> (actualizar el archivo en disco) -> reload_onlyoffice_file
```
## Ejemplo
```bash
# Forzar el guardado de un xlsx abierto en la instancia "afiliados"
bash bash/functions/shell/save_onlyoffice_file.sh \
/home/enmanuel/afiliados/programas_afiliados.xlsx afiliados
# {"instance":"afiliados","file":"/home/enmanuel/afiliados/programas_afiliados.xlsx","wid":"0x0a20002a","status":"saved","mtime_before":1718380000,"mtime_after":1718380042}
# Via fn run (tras fn index)
./fn run save_onlyoffice_file /home/enmanuel/afiliados/programas_afiliados.xlsx afiliados
# Encadenado con la actualización y la recarga (flujo seguro completo)
bash bash/functions/shell/save_onlyoffice_file.sh "$XLSX" afiliados
python build_xlsx.py # regenera solo las hojas gestionadas
./fn run reload_onlyoffice_file "$XLSX" afiliados
```
## Cuando usarla
Llámala SIEMPRE justo antes de regenerar o modificar en disco un archivo que el
usuario pueda tener abierto en OnlyOffice, para no pisar sus cambios sin guardar.
Es el primer eslabón del flujo guardar -> actualizar -> recargar. Si no hay ventana
abierta para ese archivo, es un no-op seguro (status `no_window`).
## Gotchas
- **Orden crítico**: guarda ANTES de actualizar el archivo. Si actualizas primero y
guardas OnlyOffice después, OnlyOffice sobrescribe tu actualización con su copia
en memoria (vieja). El flujo correcto es save -> update -> reload.
- **status `no_change`**: el `mtime` no cambió. Normalmente significa que no había
cambios pendientes (no es un error).
- **Auto-confirmación del diálogo de formato (v1.1.0)**: si tras Ctrl+S el guardado no
se completa en ~1.2s, la función asume que OnlyOffice mostró un diálogo modal
("mantener formato") y le envía Return, que acepta la opción por defecto (mantener el
formato actual). El campo `dialog_confirmed` indica si se envió. Si no había diálogo,
el Return va al editor y solo mueve de celda (no altera datos). Para suprimir el
diálogo de forma permanente, desmárcalo en OnlyOffice: Configuración avanzada →
desactivar el aviso de formato al guardar.
- **status `no_window`**: no hay ninguna ventana cuyo título contenga el basename del
archivo. No hay nada que guardar; el disco ya es la única fuente de verdad.
- **Detección por basename**: dos archivos con el mismo nombre en rutas distintas
colisionan al localizar la ventana (igual que open/reload).
- **X11 obligatorio**: depende de `xdotool` (y `stat` de coreutils). No funciona en
Wayland puro sin XWayland.
- **Foco**: la función activa la ventana (`windowactivate --sync`) para que Ctrl+S
llegue al editor. Roba el foco un instante; es esperable.
## Capability growth log
- v1.1.0 (2026-06-15) — auto-confirma el diálogo modal "mantener formato" enviando
Return a la ventana activa cuando el guardado no se completa en ~1.2s; añade el campo
`dialog_confirmed` a la salida JSON.
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# save_onlyoffice_file — fuerza el guardado (Ctrl+S) de un documento abierto en una
# instancia de ONLYOFFICE Desktop Editors en Linux/X11 y confirma que el archivo se
# escribio a disco observando el cambio de mtime.
#
# Para que existe: OnlyOffice mantiene los cambios en memoria hasta que el usuario
# guarda. Cualquier proceso que regenere el .xlsx leyendo del disco (por ejemplo un
# build que refresca hojas) perderia el trabajo manual no guardado. Esta funcion
# vuelca ese trabajo a disco ANTES de tocar el archivo, de modo que el paso de
# actualizacion pueda preservarlo. Es el primer paso del flujo seguro:
# save_onlyoffice_file -> (actualizar el archivo) -> reload_onlyoffice_file
#
# La ventana se localiza por el basename del archivo (OnlyOffice titula la ventana
# "<basename> — ONLYOFFICE"), igual que open_onlyoffice_file. Si no hay ventana
# abierta para ese basename no hay nada que guardar: se devuelve status "no_window"
# con exit 0 (el disco ya es la unica fuente de verdad).
#
# Funcion impura: envia eventos de teclado a X11 (xdotool) y lee el estado del
# sistema de archivos. Imprime una linea JSON con el resultado a stdout.
#
# No usamos `set -e`: los pipelines de busqueda de ventanas (xdotool|head) pueden no
# matchear y no deben abortar el script. Mantenemos -u y pipefail con guardas.
set -uo pipefail
save_onlyoffice_file() {
local file_path="${1:-}"
local instance="${2:-demo}"
# --- 1. Validacion de dependencias del sistema ---
local dep
for dep in xdotool stat; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "error: dependencia ausente: '$dep' (instala xdotool, coreutils)" >&2
return 1
fi
done
# --- 2. Validacion de argumentos ---
if [ -z "$file_path" ]; then
echo "error: uso: save_onlyoffice_file <file_path> [instance]" >&2
return 1
fi
if [ ! -f "$file_path" ]; then
echo "error: el archivo no existe: '$file_path'" >&2
return 1
fi
local abs_path
abs_path="$(cd "$(dirname "$file_path")" && pwd)/$(basename "$file_path")"
local base
base="$(basename "$abs_path")"
# --- 3. Localizar la ventana de OnlyOffice por basename ---
local wid=""
wid="$(xdotool search --name "$base" 2>/dev/null | head -1 || true)"
if [ -z "$wid" ]; then
printf '{"instance":"%s","file":"%s","wid":null,"status":"no_window"}\n' \
"$instance" "$abs_path"
return 0
fi
local hex
hex="$(printf '0x%08x' "$wid" 2>/dev/null || echo "$wid")"
# --- 4. mtime antes de guardar ---
local mtime_before
mtime_before="$(stat -c %Y "$abs_path" 2>/dev/null || echo 0)"
# --- 5. Enfocar la ventana y enviar Ctrl+S ---
xdotool windowactivate --sync "$wid" >/dev/null 2>&1 || true
xdotool key --clearmodifiers --window "$wid" ctrl+s >/dev/null 2>&1 || true
# --- 6. Esperar el guardado; auto-confirmar el dialogo de formato si aparece ---
# OnlyOffice puede mostrar un dialogo modal ("mantener formato") al guardar. Si el
# mtime no cambia en ~1.2s asumimos que hay un modal esperando y le enviamos Return:
# acepta la opcion por defecto, que es mantener el formato actual del archivo. Si no
# habia dialogo, el Return va al editor y solo mueve de celda (inofensivo: no altera
# datos). El intento se repite mientras el guardado no se confirme.
local mtime_after="$mtime_before" i=0 confirmed=0
local max=27 # ~8s a 0.3s por iteracion
until [ "$mtime_after" -gt "$mtime_before" ] || [ "$i" -ge "$max" ]; do
read -r -t 0.3 _ </dev/null 2>/dev/null || true
mtime_after="$(stat -c %Y "$abs_path" 2>/dev/null || echo "$mtime_before")"
i=$((i + 1))
# A partir de ~1.2s sin guardar, confirmar el dialogo modal con Return.
if [ "$i" -ge 4 ] && [ "$mtime_after" -le "$mtime_before" ]; then
local dlg
dlg="$(xdotool getactivewindow 2>/dev/null || true)"
if [ -n "$dlg" ]; then
xdotool key --clearmodifiers --window "$dlg" Return >/dev/null 2>&1 || true
confirmed=1
fi
fi
done
local status="saved"
if [ "$mtime_after" -le "$mtime_before" ]; then
# Sin cambio de mtime: no habia nada pendiente que guardar.
status="no_change"
fi
printf '{"instance":"%s","file":"%s","wid":"%s","status":"%s","dialog_confirmed":%s,"mtime_before":%s,"mtime_after":%s}\n' \
"$instance" "$abs_path" "$hex" "$status" "$confirmed" "$mtime_before" "$mtime_after"
return 0
}
# Ejecutable directo: `bash save_onlyoffice_file.sh <file> [instance]`.
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
save_onlyoffice_file "$@"
fi
+26
View File
@@ -70,6 +70,8 @@ func cmdDoctor(args []string) {
doctorDod(r, jsonOut)
case "e2e-coverage":
doctorE2ECoverage(r, jsonOut)
case "projects":
doctorProjects(r, jsonOut)
default:
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
doctorUsage()
@@ -100,6 +102,7 @@ Subcommands:
modules Drift entre uses_modules (app.md) y fn_module_<x> link calls (CMakeLists.txt) - issue 0097
dod Audita bloque dod_evidence_schema en dev/issues/ y dev/flows/ (issue 0114)
e2e-coverage Porcentaje de apps con e2e_checks declarado en su app.md (issue 0121b)
projects Cobertura de projects vs sub-repos Gitea (repo propio + hijos clonables) (issue 0171)
Flags:
--json Salida JSON (para scripting/agentes)
@@ -505,6 +508,29 @@ func doctorML(root string, jsonOut bool) {
fmt.Printf("\nOverall ML environment: %s\n", overall)
}
func doctorProjects(root string, jsonOut bool) {
rows, err := infra.AuditProjectsCoverage(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
orphans, oerr := infra.FindOrphanProjectRefs(root)
if oerr != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", oerr)
os.Exit(1)
}
if jsonOut {
emit(map[string]any{
"coverage": rows,
"orphan_project_ids": orphans,
})
return
}
fmt.Print(infra.FormatProjectsCoverage(rows))
fmt.Println("\n--- Check inverso: project_id huérfanos (apps/analysis sin project declarado) ---")
fmt.Print(infra.FormatOrphanProjectRefs(orphans))
}
func emit(v any) {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
+50 -34
View File
@@ -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))
+141
View File
@@ -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 := &registry.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"])
}
}
+18
View File
@@ -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
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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: []
+1 -1
View File
@@ -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:
BIN
View File
Binary file not shown.
+177
View File
@@ -0,0 +1,177 @@
---
name: fleet-orchestrator-dod
id: 0012
status: pending
created: 2026-06-20
updated: 2026-06-20
priority: high
risk: medium
related_issues: []
apps:
- fleetview
- fleet_watcher
- dag_engine
trigger: manual
schedule: ""
expected_runtime_s: 0
tags: [orchestration, fleet, dod, multi-agent, watcher]
# Contrato de evidencia DoD del sistema completo (las superficies observables que
# prueban que el meta-orquestador funciona, no solo que compila).
dod_evidence_schema:
- id: watcher_events
kind: cmd
expected: "wc -l ~/.claude/fleet/events.jsonl > 0; cada linea es una TRANSICION de estado (edge), no un nivel repetido"
required: true
- id: dod_contract_on_spawn
kind: cmd
expected: "todo agente lanzado por el orquestador tiene dod_contract no vacio en su goal.json; spawn sin dod_contract se rechaza"
required: true
- id: verifier_verdict
kind: log
expected: "al cerrar un agente, existe un veredicto del verificador (met|failed) con evidencia citada; el verificador NO es el mismo agente que ejecuto la tarea"
required: true
- id: human_load_reduction
kind: url
expected: "con N>=10 agentes vivos, el orquestador presenta UN resumen agrupado por prioridad (no N mensajes sueltos); el humano responde solo lo que requiere decision"
required: true
- id: push_on_reclama
kind: screenshot
expected: "un agente que pasa a waiting/preguntando/bloqueado dispara PushNotification al movil en < 1 min"
required: true
- id: stall_nudge
kind: log
expected: "un agente idle con dod_contract incompleto y sin actividad N min recibe un nudge automatico (send-keys) registrado en fleet_events; jamas se nudgea a un agente en waiting"
required: false
---
## Goal
Un meta-orquestador que permita a una persona manejar una flota de 20-30 agentes Claude hablando solo con uno. El orquestador no entra nunca en los detalles de cada agente: vigila estados, persigue que cada agente **termine lo que empieza** (cumpla un DoD-contrato fijo), y solo escala a la persona lo que requiere su decisión. La métrica de salud es el **throughput de DoD cumplidos**, no el número de agentes vivos.
## Problema que resuelve
Hoy lanzar muchos Claudes produce saturación: N ventanas dispersas, N agentes que quedan idle sin cerrar nada, y la persona como cuello de botella revisando todo. El criterio de "terminado" tampoco existe de forma estable: el campo `dod` del `goal.json` lo reescribe el hook GOAL-TRACKER con cada prompt (es un resumen móvil), así que no hay un blanco fijo contra el que evaluar la terminación. Resultado: 30 agentes vivos que no resuelven nada.
## Arquitectura: 4 roles
El orquestador delega, nunca ejecuta (regla `orquestador-delega-no-ejecuta`). Reparto:
```
orquestador (la persona habla SOLO con el; solo vigila, agrupa y escala)
├── splitter agente EFIMERO. Tarea grande -> la parte en sub-tareas
│ atomicas (paralelas o secuenciales), cada una con su
│ dod_contract pequeno y verificable. Tope de fan-out.
├── ejecutores Claudes INTERACTIVOS en la flota tmux (los 20-30 que la
│ persona ve). Cada uno con UNA tarea y UN dod_contract.
└── verificador agente EFIMERO e INDEPENDIENTE del ejecutor. Al cierre:
compara lo hecho contra el dod_contract -> met | failed
con evidencia citada. Cero auto-aprobacion.
```
Distinción dura: **splitter y verificador son agentes efímeros** (subagentes vía Agent tool / SDK: corren, devuelven un resultado estructurado y mueren). NO ocupan slot en la flota visible. La flota que la persona maneja = solo **ejecutores con tarea**. La maquinaria de verificación y descomposición es invisible para ella.
El reparto de coste: **el watcher vigila (barato, sin LLM, siempre activo); el orquestador y los agentes efímeros piensan (caro, solo cuando hay algo que decidir).**
## Modelo de datos: DoD-contrato fijo
En el `goal.json` de cada agente conviven dos campos distintos:
- `dod` (ya existe) — resumen móvil que el hook GOAL-TRACKER reescribe con cada prompt. Se queda como está.
- `dod_contract` (NUEVO, FIJO) — criterio de aceptación con evidencia ejecutable, escrito UNA vez al lanzar el agente y nunca reescrito por hooks. Es el blanco estable contra el que se evalúa "terminado".
- `dod_status` (NUEVO) — `pending | met | failed`, lo actualiza el verificador.
El hook GOAL-TRACKER debe respetar `dod_contract`/`dod_status` (solo reescribe `dod`). Spawn sin `dod_contract` se rechaza: ningún agente arranca sin saber cuándo habrá terminado.
## Máquina de terminación (lo que el watcher clasifica, mecánico, sin LLM)
| Estado del agente | Clasificación | Acción |
|---|---|---|
| `waiting` / phase `preguntando`/`bloqueado` | RECLAMA | escalar a la persona (push inmediato) |
| `idle` + phase `hecho` | DICE-TERMINADO | orquestador lanza verificador contra `dod_contract` |
| `idle` + phase≠hecho + sin actividad N min | ESTANCADO | nudge automático: "cierra tu DoD" |
| `busy` + phase `haciendo`/`testeando` | TRABAJANDO | no molestar |
| sin `dod_contract` | MAL LANZADO | bloquear / re-lanzar con DoD |
"Dar por terminado al hablar con ellos": cuando la persona se enruta a un ejecutor, lo primero es cerrar su `dod_contract` — si el verificador dice met, se cierra/reasigna; si quedó a medias, se empuja a terminar antes de abrir nada nuevo.
## Fases de construcción
### Fase 1 — watcher (cerebro barato, sin LLM) — DENTRO de fleetview [HECHO 2026-06-20]
Decisión: NO es un daemon aparte. fleetview ya es un proceso vivo que pollea la flota cada segundo y vive mientras la sesión tmux fleet (y por tanto la flota) exista. El watcher se embebe ahí (KISS). En cada refresco de la TUI:
1. Snapshot del fleet (`list_claude_fleet`, con `dod_contract`/`dod_status`/`role`).
2. Clasifica cada agente con `classify_fleet_termination` (función pura del registry).
3. Diff contra el snapshot anterior (en memoria) -> transiciones (edge-triggered, no nivel).
4. Escribe un evento por transición en la cola JSONL `~/.claude/fleet/events.jsonl` (sin SQLite/CGO — KISS). Línea: `{ts, session_id, pid, from, to, goal, phase, urgent}`.
5. Marca `urgent=true` en transición a RECLAMA. El push real al móvil lo hace el orquestador (Fase 2) leyendo la cola; el watcher solo marca.
Estado: modelo de datos (`DodContract`/`DodStatus`/`Role` en `ClaudeFleet`) + `classify_fleet_termination` + watcher embebido (`watcher.go`) — construidos y testeados (7 tests del watcher + 34 del clasificador). **Validado en vivo 2026-06-20**: tras relanzar fleetview con el binario nuevo, una transición real (inyectar `dod_status=met` en un agente idle → `MAL_LANZADO``DICE_TERMINADO`) quedó escrita como una línea en `~/.claude/fleet/events.jsonl`; el `goal.json` se restauró. Las 3 capas DoD de Fase 1 cumplidas.
Hallazgo: toda la flota lanzada hasta hoy clasifica `MAL_LANZADO` (ningún `dod_contract` escrito todavía). Es el comportamiento correcto (regla "ningún agente sin DoD") y lo que Fase 3 corrige al escribir `dod_contract` en el spawn.
DoD Fase 1:
- Golden: un agente pasa busy->idle -> aparece 1 evento `DICE-TERMINADO` o `ESTANCADO` en `fleet_events`.
- Edge 1: el mismo agente sigue idle 10 ticks -> NO se duplica el evento (edge, no nivel).
- Edge 2: un agente pasa a waiting -> evento RECLAMA + push en < 1 min.
- Error 1: goal.json corrupto/ausente -> el agente se clasifica MAL LANZADO sin crash del watcher.
- Vida: 7 días corriendo, 0 crashes (`journalctl`/log), cola sin huecos.
### Fase 2 — orquestador-Claude reactivo + verificador + splitter
Extiende el skill `/orquestador`. NO hace polling. Despierta por: la persona | heartbeat largo (ScheduleWakeup 20-30 min) | push del watcher. Al despertar:
1. Vacía `fleet_events`, agrupa por prioridad (RECLAMA > DICE-TERMINADO > ESTANCADO) y por ámbito.
2. Para DICE-TERMINADO: lanza un **verificador** (Agent efímero) que compara el output del ejecutor con su `dod_contract` -> met/failed+evidencia. met -> autocierra y reporta; failed -> nudge al ejecutor con el gap o escala.
3. Para ESTANCADO: nudge (send-keys) bajo política (solo idle con DoD pendiente; jamás waiting).
4. Para RECLAMA: presenta a la persona UN resumen corto con la decisión concreta que se necesita. Usa `/fleet focus` para saltarla al agente elegido.
DoD Fase 2:
- Golden: un agente DICE-TERMINADO con DoD realmente cumplido -> verificador met -> autocierre + reporte, sin intervención humana.
- Edge 1: agente DICE-TERMINADO con DoD a medias -> verificador failed -> nudge con el gap, no se cierra.
- Edge 2: 10 agentes con eventos a la vez -> un solo resumen agrupado, no 10 mensajes.
- Error 1: verificador no puede leer el output -> reporta "no evaluable", escala, no autocierra en falso.
- Vida: 7 días gestionando flota real; la persona responde solo decisiones, no enrutamiento.
Estado [CONSTRUIDO 2026-06-20]: primitivas `drain_fleet_events` (consume la cola, 7 tests) y `set_dod_contract` (escribe el DoD-contrato fijo, 5 tests) creadas, indexadas y validadas (set_dod_contract en vivo). Skill `/orquestador` evolucionado con la sección "Consumo de la cola de la flota": DoD-contrato obligatorio al lanzar, drenar, políticas por clasificación, verificador independiente (lee el report vs dod_contract), splitter con tope de fan-out, cadencia. Pendiente (capa Vida): uso real del modo — el verificador y el splitter son prompts de Agent en el skill, aún no ejercitados en un cierre real (requiere un agente con report terminado).
### Fase 3 — spawn dentro de la flota + splitter
Extiende `/orquestador` para lanzar ejecutores con `TmuxNewClaudeWindow` (socket fleet) en vez de kitties sueltas, escribiendo `dod_contract` en el `goal.json` del nuevo agente y un prompt con el DoD claro. Antes de spawnar, si la tarea se estima grande, pasa por el **splitter** (Agent efímero) que devuelve un plan de sub-tareas con dependencias; el orquestador spawna un ejecutor por sub-tarea (paralelas a la vez, secuenciales encadenadas).
DoD Fase 3:
- Golden: una tarea atómica -> 1 ejecutor en la flota con `dod_contract` escrito; `/fleet` lo lista.
- Edge 1: una tarea grande -> splitter devuelve >=2 sub-tareas, cada una con su `dod_contract`; se spawnan respetando deps.
- Edge 2: tope de fan-out -> el splitter nunca genera más de K sub-agentes de golpe (sin explosión).
- Error 1: spawn sin `dod_contract` -> rechazado con mensaje claro.
- Vida: 7 días lanzando trabajo real por esta vía.
Estado [PARCIAL 2026-06-20]: el **launcher** (`launch_fleetclaude`) ya arranca el orquestador con el skill `/orquestador` embebido como primer prompt (validado en vivo: entra en modo), le pone `role=orchestrator` con `mark_claude_role` (la TUI lo pinea arriba con ★, validado), y siembra 1 ejecutor idle inicial en la flota tmux. El `dod_contract` al spawn está en el skill (Fase 2). **Gap 1 CERRADO [2026-06-20]**: `spawn_fleet_agent` lanza ejecutores (y el orquestador) como windows de la flota tmux del perfil (no kitties sueltas), con `--skill` para arrancar en modo, `--prompt-file` para ejecutores autocontenidos y `--role` (via `mark_claude_role`). El skill `/orquestador` (paso 2) ahora prefiere `spawn_fleet_agent` sobre kitty cuando hay `$FLEET_SOCKET`. Validado en vivo: el orquestador arrancó en `fleet2` en modo, `role=orchestrator`, pinneado arriba, sin perder los 9 ejecutores existentes. **Gaps restantes** (capa Vida): el verificador independiente y el splitter están descritos en el skill pero aún no ejercitados en un cierre/descomposición real (requiere un ciclo completo de tarea con report).
## Pre-requisitos
- Sesión tmux fleet activa (perfil `launch_fleetclaude`); `/fleet` operativo (flow previo).
- PushNotification configurado (Remote Control activo en el móvil).
- dag_engine activo para schedule del watcher (regla `dag-engine-over-cron`).
## Acceptance
- [ ] `dod_contract` fijo escrito al spawn y respetado por el hook GOAL-TRACKER.
- [ ] watcher edge-triggered con eventos en `fleet_events` + push en RECLAMA.
- [ ] verificador independiente del ejecutor, con veredicto+evidencia.
- [ ] splitter con tope de fan-out para tareas grandes.
- [ ] orquestador presenta resumen agrupado, no N mensajes; usa `/fleet focus`.
- [ ] la persona maneja >=10 agentes respondiendo solo decisiones.
## Definition of Done
- [ ] **Mecánica**: watcher + funciones del registry compilan, `fn doctor` verde, sin drift `uses_functions`.
- [ ] **Cobertura**: cada fase con su golden + >=2 edge + >=1 error path, evidencia ejecutable (ver DoD por fase).
- [ ] **Vida útil**: >=7 días de uso real gestionando flota, 0 crashes del watcher, 0 "done" falsos detectados (verificador funciona).
- [ ] **Carga humana**: medible reducción — la persona responde decisiones, no enrutamiento ni vigilancia.
- [ ] **Secrets**: cero credenciales fuera de pass/vaults; el watcher no loguea contenido de sesiones, solo estados/transiciones.
## Notas (onboarding)
Para usarlo: lanzas trabajo por el orquestador (no abres Claudes a mano). Cada tarea recibe un `dod_contract`. El watcher vigila en background y empuja al móvil cuando un agente te reclama. Cuando vuelves, el orquestador te da un resumen agrupado y te lleva (`/fleet focus`) solo a lo que necesita tu decisión; lo demás (verificar cierres, empujar estancados, dividir tareas grandes) lo hace solo con agentes efímeros. La flota que ves = ejecutores con tarea; la maquinaria de verificación/división es invisible.
Relación con otras reglas: `dod_quality` (las 3 capas + verificador independiente), `orquestador-delega-no-ejecuta` (el orquestador no ejecuta), `dag-engine-over-cron` (schedule del watcher), `autonomous_loop` (fn-orquestador autónomo es el primo no-interactivo de este flujo), y el flow previo de `/fleet`/`fleetview` (la base de datos de estado).
+1
View File
@@ -13,6 +13,7 @@ Tabla de casos de uso multi-app. Mantenida por `/flow create` y `/flow done`.
| [0007](0007-matrix-telemetry-bot.md) | matrix-telemetry-bot | event-driven | data_factory, dag_engine, call_monitor, agents_and_robots | pending | low | 0% | 2026-05-16 |
| [0008](0008-kanban-cpp-and-agent-workflows.md) | kanban-cpp-and-agent-workflows | realtime-loop | kanban_cpp, kanban, skill_tree, agent_runner_api | pending | medium | 0% | 2026-05-18 |
| [0009](0009-agentes-dispositivos-mesh.md) | agentes-dispositivos-mesh | event-driven | agents_dashboard, agents_and_robots, wg_hub, device_agent | pending | high | 0% | 2026-05-23 |
| [0012](0012-fleet-orchestrator-dod.md) | fleet-orchestrator-dod | event-driven | fleetview, fleet_watcher, dag_engine | pending | medium | 0% | 2026-06-20 |
## Leyenda
@@ -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`)
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -7
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -3
View File
@@ -7,9 +7,7 @@ domain:
- registry-quality
scope: registry
priority: media
depends:
- "0121a"
- "0121b"
depends: ["0121a"]
blocks:
- "0122"
related:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
+1 -6
View File
@@ -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"
+1 -2
View File
@@ -8,8 +8,7 @@ domain:
- dev-ux
scope: app-scoped
priority: alta
depends:
- "0130a"
depends: ["0130a"]
blocks:
- "0130c"
related:
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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"]

Some files were not shown because too many files have changed in this diff Show More