2 Commits

Author SHA1 Message Date
egutierrez 18bdfc7bfd chore(issues): cerrar issue 0076 — gradle_run android-sdk detection fixed
Co-Authored-By: fn-orquestador <noreply@anthropic.com>
2026-05-15 14:01:43 +02:00
egutierrez 27ae829a1e fix(infra): gradle_run detecta android-sdk (install_android_sdk default) en orden correcto
ANDROID_HOME resolution ahora busca en orden:
  1. $HOME/android-sdk  — path que instala install_android_sdk_bash_infra
  2. $HOME/Android/Sdk  — default Android Studio Linux
  3. WSL2 Windows path  — $ANDROID_SDK_WIN o /mnt/c/Users/$USER/.../Android/Sdk

Cada candidato se valida con platform-tools/ presente (no solo directorio raiz).

Fix: issue 0076

Co-Authored-By: fn-orquestador <noreply@anthropic.com>
2026-05-15 14:01:36 +02:00
992 changed files with 20815 additions and 109048 deletions
+1 -4
View File
@@ -21,14 +21,12 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
---
@@ -260,7 +258,6 @@ fn check params # Lista funciones sin params_schema
fn doctor # Corre todos los checks
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
fn doctor services # apps tag 'service' + systemctl + puerto
fn doctor services-spec # audita bloque `service:` del app.md (issue 0105)
fn doctor sync # drift pc_locations BD vs disco
fn doctor uses-functions # imports reales vs uses_functions del app.md
fn doctor unused # funciones del registry sin consumidores
+8 -8
View File
@@ -42,10 +42,10 @@ Opcionalmente:
```bash
# Por id
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
# Por dir_path
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
```
Si no hay match → reportar y abortar.
@@ -78,8 +78,8 @@ APP_DB="$APP_DIR/operations.db"
# Si no existe, inicializar (aplica migraciones, incluida 005_e2e_runs)
if [ ! -f "$APP_DB" ]; then
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init "$APP_DIR"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init "$APP_DIR"
fi
# Verificar tabla e2e_runs existe (migracion 005)
@@ -97,7 +97,7 @@ Hay dos caminos:
**Camino A — invocar funcion del registry (preferido):**
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
./fn run e2e_run_checks_go_infra ...
```
@@ -139,15 +139,15 @@ func main() {
Ejecutar con:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
CGO_ENABLED=1 go run -tags fts5 /tmp/run_e2e_<id>.go /tmp/checks.yaml "$APP_DIR"
```
### 5. Eval assertions activas (si la app las tiene)
```bash
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db "$APP_DB"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db "$APP_DB"
```
Capturar fallos como warning checks adicionales.
+38 -38
View File
@@ -15,20 +15,20 @@ Eres el agente constructor del fn_registry. Tu rol es crear funciones, tests y t
```bash
# Buscar si ya existe algo similar (OBLIGATORIO antes de crear)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Buscar tipos existentes
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Ver funciones de un dominio
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
# Ver tipos de un dominio
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
# Verificar que un ID referenciado existe
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
```
Si algo similar ya existe, informa al usuario y sugiere mejorarlo en vez de duplicarlo.
@@ -39,13 +39,13 @@ Antes de implementar logica desde cero, busca funciones del registry que puedas
```bash
# Buscar funciones reutilizables por lo que hacen (ampliar con OR y prefijos)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
# Ver que retorna y que tipos usa una funcion candidata
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
# Buscar funciones puras del mismo dominio (las mas componibles)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
```
**Criterios de reutilizacion:**
@@ -78,38 +78,38 @@ Esto acelera la construccion y fortalece el grafo de dependencias del registry.
| `bash` | `bash/functions/{domain}/{name}.sh` | `bash/functions/pipelines/{name}.sh` | *(no aplica)* |
| `typescript` | `frontend/functions/{domain}/{name}.ts` | *(no aplica)* | `frontend/types/{domain}/{name}.ts` |
**Ruta absoluta donde crear el archivo** = `$HOME/fn_registry/` + `file_path` del .md.
**Ruta absoluta donde crear el archivo** = `/home/lucas/fn_registry/` + `file_path` del .md.
Ejemplo: si `lang: bash` y `domain: infra`, el archivo va en:
- `$HOME/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `$HOME/fn_registry/functions/infra/{name}.sh`
- `/home/lucas/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `/home/lucas/fn_registry/functions/infra/{name}.sh`
### Estructura detallada
**Go** (carpeta raiz: `functions/` y `types/`)
- Funciones: `$HOME/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `$HOME/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `$HOME/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `$HOME/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `$HOME/fn_registry/functions/pipelines/{name}.go` + `.md`
- Funciones: `/home/lucas/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `/home/lucas/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `/home/lucas/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `/home/lucas/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `/home/lucas/fn_registry/functions/pipelines/{name}.go` + `.md`
- Paquete Go = nombre del directorio (core, finance, datascience, cybersecurity, infra, shell, tui, io)
**Python** (carpeta raiz: `python/`)
- Funciones: `$HOME/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `$HOME/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `$HOME/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `$HOME/fn_registry/python/functions/pipelines/{name}.py` + `.md`
- Funciones: `/home/lucas/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `/home/lucas/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `/home/lucas/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `/home/lucas/fn_registry/python/functions/pipelines/{name}.py` + `.md`
**Bash** (carpeta raiz: `bash/`)
- Funciones: `$HOME/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `$HOME/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `$HOME/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Funciones: `/home/lucas/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `/home/lucas/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `/home/lucas/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Tipos: Bash no tiene tipos — usar solo `uses_types` para referenciar tipos de otros lenguajes
**TypeScript** (carpeta raiz: `frontend/`)
- Funciones puras: `$HOME/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `$HOME/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Funciones puras: `/home/lucas/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `/home/lucas/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Tests: junto al archivo, `{name}.test.ts` o `{name}.test.tsx`
- Tipos: `$HOME/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
- Tipos: `/home/lucas/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
---
@@ -591,7 +591,7 @@ Documentar completamente el .md igualmente.
1. **BUSCAR** en registry.db con FTS5 si existe algo similar
2. **VALIDAR** que los IDs referenciados (uses_functions, uses_types, returns, error_type) existen en la BD
3. **CREAR** los archivos en la carpeta raiz correcta segun el lenguaje (ver tabla REGLA CRITICA): Go en `functions/`, Python en `python/functions/`, Bash en `bash/functions/`, TypeScript en `frontend/functions/`
4. **INDEXAR** ejecutando: `cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index`
4. **INDEXAR** ejecutando: `cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index`
5. **VERIFICAR** con: `./fn show {id}` que se indexo correctamente
6. Si hay errores de validacion, corregirlos y re-indexar
@@ -600,10 +600,10 @@ Documentar completamente el .md igualmente.
1. **LEER** la funcion existente (codigo + .md) desde la BD: `sqlite3 registry.db "SELECT code, signature FROM functions WHERE id = '...'"`
2. **CREAR** el archivo de test
3. **EJECUTAR** los tests:
- Go: `cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd $HOME/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- Go: `cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd /home/lucas/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- TypeScript: desde `frontend/`, ejecutar con el test runner configurado
- Bash: `cd $HOME/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
- Bash: `cd /home/lucas/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
4. **ACTUALIZAR** el .md con `tested: true`, `tests: [...]` y `test_file_path`
5. **RE-INDEXAR** y verificar
@@ -620,19 +620,19 @@ Documentar completamente el .md igualmente.
```bash
# Compilar CLI (necesario si se modifico codigo del CLI)
cd $HOME/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
cd /home/lucas/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
# Indexar registry
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
# Tests Go de un dominio
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
# Tests Go de todo el registry
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
# Mostrar funcion indexada
cd $HOME/fn_registry && ./fn show {id}
cd /home/lucas/fn_registry && ./fn show {id}
```
### fn run — Ejecutar funciones y pipelines directamente
@@ -640,7 +640,7 @@ cd $HOME/fn_registry && ./fn show {id}
Despues de crear/indexar, puedes ejecutar directamente con `fn run`:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
@@ -729,7 +729,7 @@ Peticion: "Crea una funcion que calcule la media de un slice de float64"
### Paso 1: Buscar en BD
```bash
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
```
### Paso 2: Crear archivos
@@ -823,6 +823,6 @@ func TestMean(t *testing.T) {
### Paso 3: Indexar y verificar
```bash
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
./fn show mean_go_core
```
+68 -68
View File
@@ -35,22 +35,22 @@ Las apps estan indexadas en registry.db con toda la metadata necesaria para ejec
```bash
# Ver todas las apps disponibles
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
# Ver app completa con dependencias y framework
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
# Buscar apps por FTS (nombre, descripcion, tags, documentacion)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Apps de un dominio
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
# Apps que usan una funcion especifica
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
# Ver documentacion completa de una app
sqlite3 $HOME/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
```
**Campos clave de apps para ejecucion:**
@@ -65,19 +65,19 @@ sqlite3 $HOME/fn_registry/registry.db "SELECT documentation, notes FROM apps WHE
```bash
# Ver pipeline/funcion completa
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
# Ver codigo de la funcion
sqlite3 $HOME/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
# Pipelines disponibles (con tag launcher para TUI)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
# Funciones impuras ejecutables directamente
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
# Buscar por FTS
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
```
### Usar contexto de apps para ejecucion inteligente
@@ -98,10 +98,10 @@ Cuando te pidan ejecutar una app, sigue este flujo:
```bash
# Desde la raiz del registry
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Opcion A: Usar el CLI
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Opcion B: Copiar template directamente
cp fn_operations/project_template/operations.db apps/{app_name}/operations.db
@@ -221,10 +221,10 @@ Las entities representan los datos concretos del proyecto. Las relations documen
### Crear entities (datos que el pipeline consume o produce)
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Entity de entrada
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ticks" \
--type-ref "tick_go_finance" \
@@ -235,7 +235,7 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
--metadata '{"pair":"BTCUSDT","exchange":"binance"}'
# Entity de salida
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ohlcv_5m" \
--type-ref "ohlcv_go_finance" \
@@ -249,7 +249,7 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
### Crear relations (como se conectan entities)
```bash
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation add \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
--db apps/{app_name}/operations.db \
--name "ticks_to_ohlcv" \
--from-entity "{entity_id}" \
@@ -262,13 +262,13 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation add \
```bash
# Listar entities
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
# Listar relations
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
# Ver grafo ASCII
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
@@ -280,7 +280,7 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operation
`fn run` despacha automaticamente segun el lenguaje y tipo:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
@@ -318,13 +318,13 @@ Para apps con su propio main.go/main.py/main.sh:
```bash
# Go app
cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
# Python app
cd $HOME/fn_registry/apps/{app_name} && python3 main.py [args]
cd /home/lucas/fn_registry/apps/{app_name} && python3 main.py [args]
# Bash app
cd $HOME/fn_registry/apps/{app_name} && bash main.sh [args]
cd /home/lucas/fn_registry/apps/{app_name} && bash main.sh [args]
```
### Capturar metricas de ejecucion
@@ -340,7 +340,7 @@ Al ejecutar, siempre captura:
```bash
# Ejemplo: ejecutar con captura de tiempo
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
OUTPUT=$(cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
OUTPUT=$(cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -362,7 +362,7 @@ echo "Status: $STATUS | Start: $START | End: $END"
### Via CLI
```bash
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db apps/{app_name}/operations.db \
--pipeline-id "tick_to_ohlcv_go_finance" \
--relation-id "{relation_id}" \
@@ -396,16 +396,16 @@ sqlite3 apps/{app_name}/operations.db "INSERT INTO executions (id, pipeline_id,
```bash
# Listar todas
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
# Por pipeline
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
# Por status
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
# Detalle de una ejecucion
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
```
---
@@ -441,12 +441,12 @@ Si hay assertions definidas sobre las entities afectadas, evaluarlas para verifi
```bash
# Evaluar assertions de una entity
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID"
# Evaluar Y reaccionar (actualiza status de entities, crea proposals si hay fallos criticos)
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID" \
--react
@@ -467,10 +467,10 @@ Cuando el usuario pide ejecutar algo que aun no tiene app:
```bash
# 1. Crear directorio
mkdir -p $HOME/fn_registry/apps/{app_name}
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: go
@@ -490,7 +490,7 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
@@ -499,7 +499,7 @@ build/
GIEOF
# 4. Inicializar modulo Go
cd $HOME/fn_registry/apps/{app_name}
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# 5. Crear main.go minimo
@@ -523,8 +523,8 @@ func main() {
GOEOF
# 6. Inicializar operations.db
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 7. Indexar en registry.db
./fn index
@@ -534,10 +534,10 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
```bash
# 1. Crear directorio
mkdir -p $HOME/fn_registry/apps/{app_name}
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: py
@@ -557,7 +557,7 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
@@ -565,7 +565,7 @@ __pycache__/
GIEOF
# 4. Crear main.py
cat > $HOME/fn_registry/apps/{app_name}/main.py << 'PYEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/main.py << 'PYEOF'
"""Pipeline executor."""
import sys
import time
@@ -584,8 +584,8 @@ if __name__ == "__main__":
PYEOF
# 5. Inicializar operations.db
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
@@ -595,10 +595,10 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
```bash
# 1. Crear directorio
mkdir -p $HOME/fn_registry/apps/{app_name}
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: bash
@@ -618,14 +618,14 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
GIEOF
# 4. Crear main.sh
cat > $HOME/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
cat > /home/lucas/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
#!/usr/bin/env bash
# Pipeline executor: {app_name}
set -euo pipefail
@@ -650,11 +650,11 @@ main() {
main "$@"
SHEOF
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
# 5. Inicializar operations.db
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
@@ -669,7 +669,7 @@ Este patron captura todo lo necesario para registrar la ejecucion:
### Go
```bash
APP_DIR="$HOME/fn_registry/apps/{app_name}"
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
RELATION_ID="{relation_id}" # vacio si no aplica
@@ -689,8 +689,8 @@ else
fi
# Registrar ejecucion
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
@@ -704,7 +704,7 @@ rm -f "$STDOUT_FILE" "$STDERR_FILE"
### Python
```bash
APP_DIR="$HOME/fn_registry/apps/{app_name}"
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -716,8 +716,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "{pipeline_id}" \
--status "$STATUS" \
@@ -728,7 +728,7 @@ FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
### Bash
```bash
APP_DIR="$HOME/fn_registry/apps/{app_name}"
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
@@ -741,8 +741,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
@@ -758,10 +758,10 @@ Antes de ejecutar, verifica que los snapshots de tipos en operations.db estan al
```bash
# Verificar snapshots
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Actualizar si estan desactualizados
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
```
---
@@ -800,7 +800,7 @@ Crea una proposal cuando detectes:
### Como crear proposals
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Proposal para nueva funcion
./fn proposal add \
@@ -840,7 +840,7 @@ Cuando la proposal viene de un fallo o anomalia en una ejecucion, incluye la evi
```bash
# Obtener el ID de la ejecucion que evidencia el problema
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list \
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list \
--db apps/{app_name}/operations.db --status failure
# Incluir evidencia en la descripcion
@@ -858,19 +858,19 @@ Usa el contexto de la tabla apps para comparar y detectar patrones:
```bash
# Ver que funciones usan las apps — detectar patrones comunes
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
# Ver funciones mas usadas por apps (candidatas a mejora)
sqlite3 $HOME/fn_registry/registry.db "
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT f.value as func_id, COUNT(*) as uso
FROM apps, json_each(apps.uses_functions) f
GROUP BY f.value ORDER BY uso DESC;"
# Ver apps que NO tienen funciones del registry (candidatas a extraccion)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
# Ver si ya existe una proposal para algo similar
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
```
### Flujo de deteccion al ejecutar
+5 -5
View File
@@ -43,12 +43,12 @@ APP_ID="<input>"
RUN_ID="<input>"
# dir_path desde registry
DIR_PATH=$(sqlite3 $HOME/fn_registry/registry.db \
DIR_PATH=$(sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT dir_path FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_ID=$(sqlite3 $HOME/fn_registry/registry.db \
APP_ID=$(sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT id FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_DB="$HOME/fn_registry/$DIR_PATH/operations.db"
APP_DB="/home/lucas/fn_registry/$DIR_PATH/operations.db"
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
# Sanity check
@@ -93,7 +93,7 @@ Por cada fallo:
Antes de crear proposal, verificar que no haya una identica abierta:
```bash
sqlite3 $HOME/fn_registry/registry.db "
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT id FROM proposals
WHERE status = 'pending'
AND target_id = '$APP_ID'
@@ -139,7 +139,7 @@ Sugerencia generica en `description` (NO codigo concreto, solo direccion):
Si la misma assertion/check ha disparado proposal mas de 3 veces en los ultimos 30 dias, marcar `priority` (campo extendido si existe, si no, anotar en `description: '[REINCIDENTE x4]'`).
```bash
sqlite3 $HOME/fn_registry/registry.db "
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT COUNT(*) FROM proposals
WHERE target_id = '$APP_ID'
AND title LIKE '%::$CHECK_ID%'
+11 -21
View File
@@ -30,16 +30,6 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks
6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id.
7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run.
8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas.
9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq_<issue>_<ts>/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `$HOME/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main.
10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `$HOME/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas:
a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix.
b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon.
NO modificar archivos en main directamente.
11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar:
```bash
git -C $HOME/fn_registry status --short
```
Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano.
---
@@ -49,24 +39,24 @@ Antes de arrancar el bucle, comprobar:
```bash
# 1. Migration 006_task_runs.sql existe
ls $HOME/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
ls /home/lucas/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
|| { echo "ABORT: migration 006_task_runs.sql ausente. Aplicar issue 0069 paso 1 antes."; exit 2; }
# 2. Subagentes fn-* presentes
for a in fn-constructor fn-executor fn-recopilador fn-analizador fn-mejorador; do
test -f $HOME/fn_registry/.claude/agents/$a/SKILL.md \
test -f /home/lucas/fn_registry/.claude/agents/$a/SKILL.md \
|| { echo "ABORT: subagente $a ausente"; exit 2; }
done
# 3. master local up-to-date con origin (worktree se creara desde master)
git -C $HOME/fn_registry fetch origin master --quiet
LOCAL=$(git -C $HOME/fn_registry rev-parse master)
REMOTE=$(git -C $HOME/fn_registry rev-parse origin/master)
git -C /home/lucas/fn_registry fetch origin master --quiet
LOCAL=$(git -C /home/lucas/fn_registry rev-parse master)
REMOTE=$(git -C /home/lucas/fn_registry rev-parse origin/master)
test "$LOCAL" = "$REMOTE" \
|| { echo "ABORT: master local desincronizado con origin. git pull antes."; exit 2; }
# 4. Branch auto/<issue> NO existe ya (ni local ni en worktrees)
git -C $HOME/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
git -C /home/lucas/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
&& { echo "ABORT: branch auto/${ISSUE_ID} ya existe. Limpiar antes (git branch -D + worktree remove)."; exit 2; }
# 5. gh CLI autenticado (necesario para PR draft al converger)
@@ -116,7 +106,7 @@ BRANCH="auto/${ISSUE_ID}"
TASK_RUN_ID="task_$(openssl rand -hex 8)"
STARTED_AT=$(date +%s)
WT_ROOT="/tmp/fn_orq_${ISSUE_ID}_${STARTED_AT}"
REPO="$HOME/fn_registry"
REPO="/home/lucas/fn_registry"
# Crear worktree aislado desde master (no toca el principal)
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
@@ -187,13 +177,13 @@ while iter < max_iterations and elapsed < max_minutes:
Usar `Agent` tool con `subagent_type` correcto. Prompt **autocontenido** (paths absolutos, IDs, criterio exito).
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `$HOME/fn_registry/`.
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `/home/lucas/fn_registry/`.
Patron prompt:
```
Working dir: <WT_ROOT> # NO $HOME/fn_registry
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
Branch: auto/<issue_id>
Repo principal (solo lectura para registry.db): $HOME/fn_registry
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
...
```
@@ -346,7 +336,7 @@ Cada `progress_json` entry:
|---|---|---|
| `task_runs` no existe | migration 006 no aplicada | abortar pre-condicion 1 |
| `worktree add` falla con "already exists" | branch o dir previo no limpiado | `git worktree prune` + `git branch -D auto/<id>`, reintentar |
| Subagente toca `$HOME/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
| Subagente toca `/home/lucas/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
| `master` desincronizado con origin | falta `git pull` | abortar pre-condicion 3 |
| Loop infinito (mismo fail siempre) | watchdog ausente o desactivado | watchdog OBLIGATORIO, no skipear |
| Subagente devuelve output ambiguo | prompt insuficiente | rebriefing con paths/IDs explicitos |
+20 -20
View File
@@ -40,10 +40,10 @@ apps/{app_name}/
```bash
# Listar todas las apps
ls -d $HOME/fn_registry/apps/*/
ls -d /home/lucas/fn_registry/apps/*/
# Verificar que cada app tiene app.md
for app in $HOME/fn_registry/apps/*/; do
for app in /home/lucas/fn_registry/apps/*/; do
name=$(basename "$app")
echo "=== $name ==="
[ -f "$app/app.md" ] && echo " app.md: OK" || echo " app.md: FALTA"
@@ -82,8 +82,8 @@ sqlite3 "$APP_DB" "SELECT * FROM schema_migrations ORDER BY version;" 2>/dev/nul
**Si faltan tablas**, aplicar migraciones:
```bash
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### 3. Integridad de Entities
@@ -96,7 +96,7 @@ sqlite3 "$APP_DB" "SELECT id, name, type_ref, status, domain, source FROM entiti
# Validar que type_ref existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT type_ref FROM entities;" | while read ref; do
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
if [ -z "$EXISTS" ]; then
echo "ERROR: type_ref '$ref' no existe en registry.db"
fi
@@ -129,7 +129,7 @@ sqlite3 "$APP_DB" "SELECT r.id, r.name, r.to_entity FROM relations r WHERE r.to_
# Validar que 'via' referencia una funcion/pipeline del registry
sqlite3 "$APP_DB" "SELECT DISTINCT via FROM relations WHERE via != '';" | while read via; do
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
if [ -z "$EXISTS" ]; then
echo "ERROR: relation.via '$via' no existe en registry.db"
fi
@@ -156,7 +156,7 @@ sqlite3 "$APP_DB" "SELECT id, pipeline_id, status, started_at, duration_ms, reco
# Validar que pipeline_id existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT pipeline_id FROM executions;" | while read pid; do
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: pipeline_id '$pid' no existe en registry.db"
fi
@@ -230,7 +230,7 @@ sqlite3 "$APP_DB" "SELECT id, version, lang, algebraic, snapped_at FROM types_sn
# Comparar con registry.db — detectar snapshots desactualizados
sqlite3 "$APP_DB" "SELECT id, version FROM types_snapshot;" | while IFS='|' read id ver; do
REG_VER=$(sqlite3 $HOME/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
REG_VER=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
if [ -z "$REG_VER" ]; then
echo "WARN: snapshot '$id' ya no existe en registry.db"
elif [ "$ver" != "$REG_VER" ]; then
@@ -252,14 +252,14 @@ done
```bash
# Verificar que la app esta en registry.db
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
# Verificar que uses_functions del app.md coincide con lo indexado
sqlite3 $HOME/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
# Verificar que todas las funciones referenciadas existen
sqlite3 $HOME/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
sqlite3 /home/lucas/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: app usa funcion '$fid' que no existe en registry"
fi
@@ -273,7 +273,7 @@ done
Patron para auditar TODAS las apps de una vez:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
echo "========================================="
echo "AUDITORIA DE APPS — fn-recopilador"
@@ -327,7 +327,7 @@ for app_dir in apps/*/; do
[ "$ERROR_LOGS" -gt 0 ] 2>/dev/null && echo " [WARN] $ERROR_LOGS logs de error"
# 9. App indexada en registry.db
INDEXED=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
INDEXED=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
[ -n "$INDEXED" ] && echo " [OK] Indexada en registry.db" || echo " [WARN] NO indexada en registry.db"
done
@@ -393,25 +393,25 @@ echo "========================================="
El recopilador puede sugerir o ejecutar estas reparaciones:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Aplicar migraciones faltantes
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Actualizar snapshot desactualizado
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
# Verificar snapshots
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Evaluar assertions pendientes
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
# Re-indexar para que la app aparezca en registry.db
./fn index
# Ver grafo de la app (util para diagnostico visual)
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
+13 -13
View File
@@ -38,13 +38,13 @@ Antes de crear nada, recopilar contexto:
```bash
# Buscar funciones relevantes por descripcion
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
# Buscar apps similares
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Verificar que el nombre no esta tomado
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
```
4. **Presentar plan al usuario** antes de ejecutar:
@@ -79,7 +79,7 @@ Usar el Agent tool con `subagent_type: "fn-constructor"` pasando:
Despues de que fn-constructor termine, verificar que todo se indexo:
```bash
cd $HOME/fn_registry && ./fn index
cd /home/lucas/fn_registry && ./fn index
# Verificar cada funcion creada
./fn show {id_de_cada_funcion}
```
@@ -91,7 +91,7 @@ cd $HOME/fn_registry && ./fn index
### Estructura base (todos los lenguajes)
```bash
mkdir -p $HOME/fn_registry/apps/{app_name}
mkdir -p /home/lucas/fn_registry/apps/{app_name}
```
### app.md (OBLIGATORIO — siempre primero)
@@ -143,7 +143,7 @@ build/
**Go (CLI/TUI):**
```bash
cd $HOME/fn_registry/apps/{app_name}
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# Crear main.go, app/, config/, views/ segun necesidad
```
@@ -151,7 +151,7 @@ go mod init fn_registry/apps/{app_name}
**Go (Wails — desktop con UI):**
```bash
# Usar scaffold del registry
cd $HOME/fn_registry
cd /home/lucas/fn_registry
./fn run scaffold_wails_app -- --name {app_name} --dir apps/{app_name}
```
@@ -165,20 +165,20 @@ cd $HOME/fn_registry
```bash
# Crear main.sh con source a funciones del registry
# Pattern: source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
```
### Inicializar operations.db
```bash
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### Indexar en registry.db
```bash
cd $HOME/fn_registry && ./fn index
cd /home/lucas/fn_registry && ./fn index
# Verificar
sqlite3 registry.db "SELECT id, name, lang, domain FROM apps WHERE name = '{app_name}';"
```
@@ -241,7 +241,7 @@ Usar el Agent tool con `subagent_type: "gitea"` pasando:
```bash
# 1. Crear repo en Gitea (via API)
# 2. Inicializar git en la app
cd $HOME/fn_registry/apps/{app_name}
cd /home/lucas/fn_registry/apps/{app_name}
git init
git add -A
git commit -m "Initial commit: {app_name} — {descripcion}"
@@ -256,7 +256,7 @@ git push -u origin master
**Despues de publicar**, actualizar el `repo_url` en app.md y re-indexar:
```bash
cd $HOME/fn_registry && ./fn index
cd /home/lucas/fn_registry && ./fn index
```
---
-1
View File
@@ -1 +0,0 @@
../../projects/aurgi/.claude/commands
+108 -23
View File
@@ -1,36 +1,121 @@
---
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
3. `dev/autonomous_protected_paths.json` existe.
4. `master` local up-to-date con `origin/master`.
5. Branch `auto/<issue_id>` NO existe ya.
6. `gh auth status` OK (necesario para PR draft al converger).
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
---
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
## Argumento
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
`$ARGUMENTS``<issue_id>` o `<task_spec_path>` + flags opcionales.
## Por que deprecado
```
/autonomous-task 0070
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
/autonomous-task 0070 --auto-apply-proposals safe
/autonomous-task 0070 --dry-run
/autonomous-task path/to/spec.yaml --branch auto/custom-name
```
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
- Detector issue vs flow.
- Reporte estructurado al humano post-delegate.
- Self-Q&A migrado a fn-orquestador.
Flags:
- `--max-iterations N` tope de iteraciones (default 10)
- `--max-minutes M` timeout total (default 60)
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
- `--dry-run` simula, NO aplica
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
---
## Sustitucion 1:1
## Comportamiento
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
- `issue_id` o `task_spec`
- flags resueltos
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
3. **El subagente:**
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
- Persiste estado en `task_runs` (operations.db del app target o repo root).
- Despacha por fases a los 5 subagentes especializados.
- Aplica proposals filtradas por `--auto-apply-proposals`.
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
4. **Reportar resultado al humano** con:
- `status`, `iterations / max`, `duration / max`
- `branch`, `worktree`, `PR draft url` si converged
- `proposals creadas / aplicadas`
- `last run_id` y status
- Resumen iter-por-iter del `progress_json`
## Modo debug
---
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
## Reglas duras (no negociables)
## Migration deadline
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
- No merge automatico — PR draft siempre.
- No `--no-verify`, no `--force`, no skip hooks.
- Paths protegidos via `dev/autonomous_protected_paths.json`.
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
- Auditoria total en `task_runs.progress_json`.
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
---
Ver `.claude/commands/autopilot.md` para spec completa.
## Integracion con call_monitor (issue 0085)
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
- Loggear sus invocaciones via el hook PostToolUse (automatico).
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
---
## Tipos NO soportados
- Diseño arquitectura nuevo (humano decide).
- Decisiones UX subjetivas.
- Cambios BD productiva.
- Cualquier cosa que toque secrets/credenciales.
- Self-modification del propio orquestador.
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
---
## Output canonico
```
=== /autonomous-task: 0070 ===
status: converged
iterations: 7 / 10
duration: 23 min / 60
branch: auto/0070
worktree: /tmp/fn_orq_0070_1731612345
PR draft: https://github.com/.../pull/123
proposals: 3 creadas, 2 auto-aplicadas
last run_id: e2e_run_abc123 (status: pass)
Iter:
1. construir → ok (2 funciones nuevas)
2. ejecutar → ok
3. analizar → fail (2/8 checks)
4. mejorar → 3 proposals (2 auto-applicadas)
5. construir → ok (re-build tras patches)
6. analizar → pass
7. recopilador → ok (operations.db integra)
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
```
-212
View File
@@ -1,212 +0,0 @@
---
name: autopilot
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
---
# /autopilot — Comando autonomo unificado
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
## Por que solo delegar
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
`fn-orquestador` ahora soporta dos `task_type`:
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
## Sintaxis
```
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
/autopilot issue:<NNNN> # issue explicito
/autopilot i:<NNNN> # alias
/autopilot flow:<NNNN> # flow NNNN
/autopilot f:<NNNN> # alias
/autopilot check <target> # solo audita DoD readiness, no delega
/autopilot <target> --max-iterations N --max-minutes M --dry-run
```
Detector:
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
- `^(issue|i):\d{4}[a-z]?$` → issue.
- `^(flow|f):\d{4}$` → flow.
- Otra cosa → ABORT con error de sintaxis.
## Pre-flight DoD readiness check (OBLIGATORIO)
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
### Issue (`dev/issues/<NNNN>-*.md`)
1. Archivo existe en `dev/issues/` (no en `completed/`).
2. Frontmatter con `status`, `priority`.
3. Al menos UNA de:
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
- `## Acceptance` con checkboxes `[ ]`.
- `## Tests` + `## Tareas` ambas no vacias.
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
### Flow (`dev/flows/<NNNN>-*.md`)
1. Archivo existe en `dev/flows/`.
2. Frontmatter valido.
3. `## Acceptance` con >=1 checkbox.
4. `## Flow` no vacio.
5. Pre-requisitos declarados.
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
Si falla:
```
=== /autopilot check 0125 ===
status: NOT READY
target: issue 0125 (skill-tree-dashboard-panel)
gaps:
- Sin seccion DoD/Acceptance
- "UX intuitiva" linea 47 — no verificable
fix:
- Anadir ## DoD con 3-5 bullets programaticamente verificables
- Reemplazar criterios subjetivos por mediciones concretas
```
Si OK:
```
=== /autopilot check 0107c ===
status: READY
target: issue 0107c (refactor data_table)
dod_items: 5 checkboxes
task_type: refactor_safe
estimated_iter: 3-5
```
## Dispatch a fn-orquestador
Tras pre-flight OK, ejecuta:
```
Agent(
subagent_type="fn-orquestador",
prompt="""
Issue/Flow: <path al .md>
Modo: REAL (o --dry-run)
task_type: <issue|flow>
Pre-condiciones verificadas: 7/7 verde
Master: <sha> sync con origin
Working tree principal: limpio (baseline)
Max iter: N
Max min: M
Auto-apply proposals: safe
Token Gitea: pass gitea/dataforge-git-token
DB task_runs: apps/deploy_server/operations.db (schema task_id)
Reglas duras: autonomous_loop.md (11 reglas)
""",
run_in_background=true
)
```
Cuando termine, reporta al humano con output canonico del orquestador:
```
=== /autopilot 0121b ===
target: issue 0121b (fn doctor e2e-coverage)
delegated_to: fn-orquestador
status: converged
iterations: 1 / 8
duration: 4 min / 30
task_run_id: task_d285372493cce2e6
branch: auto/0121b-orquestador
worktree: /tmp/fn_orq_0121b_1779147778
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
Siguiente: revisar PR, mergear, mover issue a completed/
```
## Reglas duras (autopilot-level)
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
## Flags
| Flag | Default | Que hace |
|---|---|---|
| `--max-iterations N` | 10 | Pasado al orquestador |
| `--max-minutes M` | 60 | Pasado al orquestador |
| `--dry-run` | off | Pasado al orquestador |
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
## Errores canonicos
| Codigo | Significado | Accion |
|---|---|---|
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
## Ejemplos
```bash
# Issue con DoD claro
/autopilot 0107c
# Flow con piezas faltantes — autoriza creacion antes
/autopilot flow:0008 --allow-construct-missing
# Solo audit
/autopilot check 0125
/autopilot check flow:0008
# Dry run
/autopilot 0107c --dry-run
```
## Relacion con otras reglas
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
- [[apps_tbd]] — politica TBD por tipo de cambio.
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
- [[feature_flags]] — codigo incompleto detras de flag OFF.
- [[registry_calls]] — invocaciones canonicas.
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
- [[delegation]] — spawn fn-constructor antes que escribir inline.
## Migracion desde `/autonomous-task`
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
## Historico
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
-86
View File
@@ -1,86 +0,0 @@
---
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
---
# /commands — Catalogo de slash commands del repo
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
## Sintaxis
```
/commands # listado completo agrupado por namespace
/commands <substring> # filtra por substring en nombre o descripcion
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
/commands --json # salida JSON para agentes
```
## Implementacion
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
```bash
#!/usr/bin/env bash
set -euo pipefail
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
CMD_DIR="$ROOT/.claude/commands"
# Recolecta: ns|name|description
collect() {
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
rel="${f#$CMD_DIR/}"
case "$rel" in
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
*) ns="global"; name="${rel%.md}" ;;
esac
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
done | sort
}
collect | awk -F'|' '
{
if ($1 != prev_ns) {
if (prev_ns) print ""
if ($1 == "global") print "## global (/<cmd>)"
else print "## " $1 " (/" $1 ":<cmd>)"
prev_ns = $1
}
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
}'
```
Filtros:
- Substring: `grep -i "<substring>"` sobre stdout.
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
## Salida (formato humano)
```
## global (/<cmd>)
- /app — Crear, configurar y desplegar apps del registry
- /autopilot — Modo full-auto...
- /commands — Catalogo de slash commands del repo
...
## aurgi (/aurgi:<cmd>)
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
```
## Cuando usarlo
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
- Antes de inventar logica inline: comprobar si ya existe un command.
- Auditoria: verificar que los projects exponen sus commands correctamente.
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
## Gotchas
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
+22 -59
View File
@@ -1,74 +1,37 @@
---
description: "Compila app del registry (C++ o Wails Go), copia el .exe a Desktop/apps/<app>/ y relanza en Windows. Wrapper sobre compile_cpp_app o compile_wails_app segun framework declarado en app.md."
---
# /compile — Compila app C++ y la copia al escritorio de Windows
# /compile — Compila app C++ o Wails y la copia al escritorio de Windows
Wrapper sobre 2 pipelines del registry segun el framework:
- **C++ (imgui / cmake)** → `compile_cpp_app_bash_pipelines`. Cross-compile MinGW + assets/enrichers/runtime + taskkill, NO relanza.
- **Wails Go (matrix_client_pc, matrix_admin_panel, etc.)** → `compile_wails_app_bash_pipelines`. `wails build -platform windows/amd64` con `-tags goolm` si E2EE + taskkill + **RELANZA** la app tras copy.
Toda la logica vive en el registry (resolver app desde CWD/arg, build, deploy con preservacion de `local_files/`).
## Dispatch
Wrapper sobre el pipeline `compile_cpp_app_bash_pipelines`. Toda la lógica vive en el registry (resolver app desde CWD/arg, cross-compile MinGW, copiar exe + DLLs + assets/ + enrichers/ + runtime/ a `/mnt/c/Users/lucas/Desktop/apps/<app>/`, taskkill previo, preservar `local_files/`).
```bash
cd $HOME/fn_registry
# Detecta framework via wails.json o CMakeLists.txt en el dir del app
APP="$ARGUMENTS"
RESOLVED=$(bash -c '
source bash/functions/infra/resolve_cpp_app_dir.sh
resolve_cpp_app_dir "'"$APP"'"
' 2>/dev/null) || true
APP_DIR="$(echo "$RESOLVED" | cut -f2)"
if [ -n "$APP_DIR" ] && [ -f "$APP_DIR/wails.json" ]; then
./fn run compile_wails_app "$ARGUMENTS"
elif [ -n "$APP_DIR" ] && [ -f "$APP_DIR/CMakeLists.txt" ]; then
./fn run compile_cpp_app "$ARGUMENTS"
else
echo "ERROR: no se detecto framework (falta wails.json o CMakeLists.txt en $APP_DIR)" >&2
exit 1
fi
cd /home/lucas/fn_registry
./fn run compile_cpp_app "$ARGUMENTS"
```
## Argumento
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`, `matrix_client_pc`).
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
- Sin argumento: deduce desde `pwd` si estas dentro de `cpp/apps/<X>/`, `apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, lista las apps disponibles en stderr y aborta.
- Sin argumento: deduce desde `pwd` si estás dentro de `cpp/apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, el pipeline lista las apps disponibles en stderr y aborta.
## Que hace el pipeline (C++)
## Qué hace el pipeline
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>`.
2. Verifica `CMakeLists.txt`.
3. `build_cpp_windows_bash_infra <app>` — cross-compila con MinGW.
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>` desde arg o CWD.
2. Verifica `CMakeLists.txt` en el dir resuelto.
3. `build_cpp_windows_bash_infra <app>` — cross-compila el target específico con `cpp/build/windows/` (configura toolchain `mingw-w64.cmake` la primera vez).
4. `deploy_cpp_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` + DLLs.
- rsync `assets/`, `enrichers/`, `runtime/` (si aplica).
- Preserva `local_files/`.
- **NO** relanza.
## Que hace el pipeline (Wails)
1. `resolve_cpp_app_dir_bash_infra` (reusado — sirve para Wails apps tambien).
2. Verifica `wails.json` + `go.mod`.
3. Detecta `-tags goolm` automaticamente (grep `matrix_crypto_init` en `app.md` o `build:tags` en `wails.json`).
4. `wails build -platform windows/amd64 [-tags goolm]`.
5. `deploy_wails_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` (+ `appicon.ico` si existe).
- **Relanza** via `cmd.exe /c start "" <app>.exe`.
- Preserva `local_files/`.
- `taskkill.exe /IM <app>.exe /F` (pre-autorizado).
- Copia `<app>.exe` + DLLs al top-level de `Desktop/apps/<app>/`.
- rsync `cpp/build/windows/apps/<app>/assets/` `Desktop/apps/<app>/assets/`.
- rsync `<app_dir>/enrichers/``assets/enrichers/` si existe.
- Si `app.md` declara `python_runtime: true`, regenera `runtime/` con `tools/freeze_python_runtime.sh` y rsync a `assets/runtime/`.
- Copia `gx-cli`/`gx-cli.exe` si existen.
- **NUNCA** toca `local_files/` (estado del usuario).
5. Imprime `ls -lh` del `.exe` final.
## Notas
- Solo target Windows hoy. Linux ya lo da `wails build` / `cpp/build/` nativo.
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
- Variables override-ables: `BUILD_WIN`, `WIN_DESKTOP_APPS`, `FN_REGISTRY_ROOT`.
- Si la app C++ no esta registrada en `cpp/CMakeLists.txt`, el build falla — registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Si la app Wails falla build con `no required module provides package`, correr `go mod tidy` en el dir del app primero.
- Para tocar la logica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,build_cpp_windows,deploy_{cpp,wails}_exe_to_windows,compile_{cpp,wails}_app}.sh`, no este wrapper.
- Si la app no está registrada en `cpp/CMakeLists.txt`, `cmake --build --target <app>` falla. Registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Para tocar la lógica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,deploy_cpp_exe_to_windows,compile_cpp_app}.sh`, no este wrapper.
-274
View File
@@ -1,274 +0,0 @@
# /cpp-app — Crear o modificar app C++ del registry sin olvidar nada
Recopila TODOS los datos necesarios (frontmatter, trio app_hub, panels, AppConfig, service block, e2e_checks, uses_functions) **antes** de tocar el disco. Tras confirmar, ejecuta scaffolder o edits, regenera iconos, refresca app_hub, compila y deploya a Windows.
Sustituye al flujo manual "edito main.cpp + app.md + CMakeLists.txt a mano". Wrapper sobre `init_cpp_app_bash_pipelines` (create) o edits directos sobre `app.md` (modify) + `regenerate_app_icons` + `refresh_app_hub` + `redeploy_cpp_app_windows`.
---
## Uso
```
/cpp-app # interactivo, modo create
/cpp-app <name> # interactivo, modo create con name pre-rellenado
/cpp-app modify <name> # editar app existente
```
---
## Modo CREATE — flujo turno a turno
Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas como default; si no, pregunta name.
### Paso 0 — verificar que no existe
```bash
test -d "$HOME/fn_registry/apps/<name>" \
|| ls $HOME/fn_registry/projects/*/apps/<name> 2>/dev/null
```
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
### Paso 1 — Identidad (AskUserQuestion)
1. **name** (texto libre — valida snake_case + contiene verbo segun `ids_naming.md`). Verbos canonicos: `show, render, view, plot, edit, manage, monitor, browse, explore, run, launch, scan, audit, debug, profile, ...`. Si no trae verbo, sugerir alternativas (`viewer` -> `<name>_viewer`).
2. **project** (select: ninguno / lista de `projects/*/`). Si ninguno -> `apps/<name>/`.
3. **domain** (select: `tools` (default), `gfx`, `tui`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `pipelines`, `browser`).
4. **description** 1 linea (texto libre, max 80 chars). **OBLIGATORIO** — sin esto el hub muestra tarjeta vacia.
### Paso 2 — Trio app_hub OBLIGATORIO
Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE juntos.
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
```bash
ls $HOME/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
```
Sugiere 3-5 candidatos basados en `description`. Default segun domain: `gfx`->`palette`, `tui`->`terminal`, `tools`->`wrench`, `infra`->`gear`, `finance`->`chart-line-up`, `datascience`->`graph`, `cybersecurity`->`shield`.
6. **icon.accent** hex `#rrggbb` (palette select):
- sky `#0ea5e9`, indigo `#4f46e5`, violet `#7c3aed`, pink `#ec4899`, rose `#f43f5e`, red `#dc2626`, orange `#ea580c`, amber `#d97706`, green `#16a34a`, teal `#0d9488`, cyan `#0891b2`, slate `#475569`. Default segun domain.
### Paso 3 — Tags
7. **tags** (multiSelect): `service`, `launcher`, `dashboard`, `viewer`, `editor`, `monitor`, `debug`, `prototype`. Si selecciona `service` -> activar bloque service (Paso 7).
### Paso 4 — Panels iniciales
8. **panels** (texto libre o select):
- Default: 1 panel `Main` (Ctrl+1).
- Opcion lista: hasta 4 paneles. Por cada uno: `{label, shortcut}`. Generara `PanelToggle k_panels[]` en `main.cpp`.
### Paso 5 — AppConfig flags
9. (multiSelect):
- `init_gl_loader` (true si la app llama `gl*` directo, ej. shaders, GPU renderer custom). Default false.
- `viewports` true (default) / false (single-window).
- `auto_dockspace` true (default) / false (solo si gestiona DockSpace propio tipo `shaders_lab`).
- `fps_overlay` activo de inicio? (controla solo el default; el menu Settings lo toggle).
### Paso 6 — Funciones del registry a usar
10. **uses_functions** lista IDs. Antes de preguntar, busca candidatas segun description:
```
mcp__registry__fn_search query="<keyword>" entity="functions"
```
Y muestra capability groups relevantes (`docs/capabilities/INDEX.md`). El usuario puede aceptar lista, anadir IDs, o dejar vacio (se rellena tras codear).
Cada ID que no este en el registry -> ofrecer spawn `fn-constructor` antes de continuar (regla `delegation.md`).
### Paso 7 — Bloque `service:` (solo si tag=service)
11. Si paso 3 marco `service`, recopilar (regla `function_tags.md` + issue 0105):
- `port` int o null
- `health_endpoint` ruta GET o null
- `health_timeout_s` (default 3)
- `runtime` (select: `systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`)
- `systemd_unit` (obligatorio si runtime empieza por `systemd-`)
- `systemd_scope` (`user|system|null`)
- `restart_policy` (select: `always` (Recommended — gotcha: `on-failure` NO reinicia SIGTERM limpio), `on-failure`, `none`)
- `pc_targets` (multiSelect de pc_locations actuales: `aurgi-pc`, `home-wsl`, ...)
- `is_local_only` (true/false default false)
### Paso 8 — Persistencia
12. (multiSelect):
- BD propia SQLite `<name>.db` en `local_files/`? -> recordar usar `fn::local_path("<name>.db")` (cpp_apps.md §7)
- operations.db (para entities/relations)? -> ejecutar `fn ops init` tras crear
- Archivos config en `local_files/`?
### Paso 9 — e2e_checks (issue 0068)
13. Default sugerido (modificable):
```yaml
e2e_checks:
- id: build
cmd: "cmake --build cpp/build --target <name> -j"
timeout_s: 300
- id: self_test
cmd: "./cpp/build/apps/<name>/<name> --self-test"
timeout_s: 30
severity: warning # si todavia no implementa --self-test
```
Pregunta: ¿anadir mas checks (ops_audit, pytest, smoke)?
### Paso 10 — Resumen y confirmacion
Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffolder + post-acciones. Pedir confirmacion antes de ejecutar.
---
## Modo CREATE — ejecucion
Una vez confirmado:
```bash
cd $HOME/fn_registry
# 1. Scaffolder
./fn run init_cpp_app <name> \
[--project <p>] \
[--domain <d>] \
--desc "<description>" \
[--tags "<csv>"]
# 2. Editar app.md generado para anadir:
# - icon: {phosphor, accent}
# - service: {...} (si aplica)
# - uses_functions: [...]
# - e2e_checks: [...]
# (el scaffolder no rellena estos; editarlos con Edit tool)
# 3. Editar main.cpp generado para reflejar:
# - panels[] custom (si != default)
# - cfg.init_gl_loader / cfg.auto_dockspace / cfg.viewports
# - includes de funciones registry usadas
# 4. Editar CMakeLists.txt para anadir paths de funciones del registry:
# ${CMAKE_SOURCE_DIR}/functions/<d>/<f>.cpp
# 5. Si es service -> ofrecer crear systemd unit (skipear si runtime=stdio|manual)
# 6. Si pidio operations.db
./fn ops init apps/<name> # o projects/<p>/apps/<name>
# 7. Generar icono
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# 8. Indexar
./fn index
# 9. Compilar Windows
./fn run redeploy_cpp_app_windows <name> <dir> --build
# 10. Refrescar app_hub
./fn run refresh_app_hub
# 11. Auditoria
./fn doctor cpp-apps
[[ "<tag>" == *service* ]] && ./fn doctor services-spec
```
---
## Modo MODIFY — flujo
`/cpp-app modify <name>`
### Paso 0 — Localizar
```bash
# Buscar apps/<name>/ o projects/*/apps/<name>/
sqlite3 $HOME/fn_registry/registry.db \
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
```
Si no existe: abortar, sugerir `/cpp-app` (sin args) para crear.
### Paso 1 — Mostrar config actual
```bash
mcp__registry__fn_show id="<id>"
cat <dir>/app.md
```
### Paso 2 — Que cambiar (multiSelect)
- `description` (1 linea)
- `icon.phosphor` o `icon.accent`
- `tags` (anadir/quitar; si toca `service` -> Paso 7 del create)
- `uses_functions` (anadir/quitar — recordar editar CMakeLists.txt)
- `panels` (anadir/quitar/renombrar)
- `service:` block (si tag=service)
- `e2e_checks`
- `domain`
- `rename` (cambia name, dir, IDs derivados, repo Gitea — operacion delicada, requiere doble confirmacion)
### Paso 3 — Aplicar cambios
Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write` completo de `app.md` (preserva campos que no toques).
### Paso 4 — Post-acciones (segun lo que toco)
```bash
# Siempre
cd $HOME/fn_registry && ./fn index
# Si toco icon.* -> regenerar appicon
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# Si toco trio o panels o uses_functions o cambia code:
./fn run redeploy_cpp_app_windows <name> <dir> --build
# Si toco description o icon o tags:
./fn run refresh_app_hub
# Si toco service: o tag service
./fn doctor services-spec
# Siempre al final
./fn doctor cpp-apps
```
---
## Reglas duras
- **NUNCA** crear `main.cpp` + `CMakeLists.txt` + `app.md` a mano. Siempre via `init_cpp_app_bash_pipelines` (regla `cpp_apps.md`).
- **NUNCA** poner el codigo en `cpp/apps/<n>/`. Solo `apps/<n>/` o `projects/<p>/apps/<n>/`.
- **NUNCA** dejar `app.md` sin el trio (description + icon.phosphor + icon.accent). Tarjeta del hub queda gris.
- **NUNCA** declarar funciones del registry en `uses_functions` sin listar su `.cpp` en `CMakeLists.txt` (drift detectado por `fn doctor uses-functions`).
- **NUNCA** usar `Restart=on-failure` en systemd unit de un service C++ — gotcha 2026-05-17 (`sqlite_api.service` cayo 20h). Default `Restart=always`.
- Despues de **cualquier** cambio en el trio: `regenerate_app_icons <name>` + `refresh_app_hub`.
---
## Auto-verificacion final
Tras crear o modificar, reportar al usuario:
```
=== app <name> ===
dir: <abs_dir>
domain: <d>
description: "<desc>"
icon: <phosphor> + <accent>
tags: [<csv>]
uses_functions: N funciones (<list_top_5>)
panels: N (<labels>)
e2e_checks: N checks
service: <si/no — port:<p> health:<h>>
Acciones ejecutadas:
[✓] scaffolder / edits
[✓] generate_app_icon
[✓] fn index (registry.db actualizado)
[✓] redeploy_cpp_app_windows (Desktop/apps/<name>/<name>.exe)
[✓] refresh_app_hub (tarjeta visible en hub)
[✓] fn doctor cpp-apps (limpio | N warnings)
Siguiente paso sugerido:
- Abrir app_hub_launcher en Windows y verificar tarjeta
- Anadir tests visuales si la app tiene paneles propios (cpp/PATTERNS.md §11)
```
$ARGUMENTS
+12 -12
View File
@@ -38,19 +38,19 @@ Consultar `registry.db` para encontrar funciones existentes relevantes y evitar
```bash
# Buscar funciones similares por nombre y descripcion (OBLIGATORIO — usar multiples terminos)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
# Buscar tipos relacionados
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Funciones del dominio objetivo
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
# Tipos del dominio objetivo
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
# Funciones que podrian componerse (misma firma de retorno)
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
```
**Clasificar resultados en:**
@@ -103,7 +103,7 @@ Para cada batch del plan, lanzar agentes `fn-constructor` **en paralelo** (un ag
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando un prompt completo con:
```
Crea la siguiente funcion para el registry fn_registry en $HOME/fn_registry:
Crea la siguiente funcion para el registry fn_registry en /home/lucas/fn_registry:
Funcion: {nombre}
Kind: {kind}
@@ -149,7 +149,7 @@ Despues de que TODOS los fn-constructor terminen:
```bash
# Indexar todo de una vez
cd $HOME/fn_registry && ./fn index
cd /home/lucas/fn_registry && ./fn index
```
Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
@@ -166,7 +166,7 @@ Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
```bash
# Verificar cada funcion creada
cd $HOME/fn_registry
cd /home/lucas/fn_registry
./fn show {id_de_cada_funcion}
# Verificar que no hay funciones sin params_schema
@@ -178,7 +178,7 @@ cd $HOME/fn_registry
Para cada funcion con tests, ejecutar:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Go
CGO_ENABLED=1 go test -tags fts5 -v -run TestNombreDelTest ./functions/{domain}/
@@ -197,13 +197,13 @@ bash bash/functions/{domain}/{nombre}_test.sh
```bash
# Verificar que todas las funciones nuevas estan en la BD
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
# Verificar que los tests estan indexados
sqlite3 $HOME/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
# Verificar dependencias
sqlite3 $HOME/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
```
### 6.4 Si algo fallo
+3 -3
View File
@@ -45,7 +45,7 @@ Antes de escribir nada, repasar la conversacion y juntar:
2. **Cambios concretos** desde git:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
git status --short
git diff --stat
git log --since="6 hours ago" --oneline
@@ -70,7 +70,7 @@ Si el material es solo conversacion exploratoria sin artefactos tocados, ir dire
Para cada artefacto identificado, localizar su `.md` consultando `registry.db`:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
# Funcion / tipo
sqlite3 registry.db "SELECT id, file_path FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NAME* OR description:NAME*');"
@@ -180,7 +180,7 @@ Para cada `.md` identificado:
Si los cambios de la sesion incluyen creacion de funciones/tipos/apps/projects/analysis/vaults o modificacion de frontmatter:
```bash
cd $HOME/fn_registry && ./fn index
cd /home/lucas/fn_registry && ./fn index
```
Y verificar:
+1 -1
View File
@@ -17,7 +17,7 @@ Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework:
### 1. Resolver app y directorio
```bash
ROOT=$HOME/fn_registry
ROOT=/home/lucas/fn_registry
ARGS="$ARGUMENTS"
APP_ARG="${ARGS%% *}" # primera palabra
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
+1 -1
View File
@@ -17,7 +17,7 @@ Wrapper sobre `append_diary_entry_bash_infra`. La función del registry maneja t
2. **Llamar la función del registry**:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
source bash/functions/infra/append_diary_entry.sh
append_diary_entry "<TITULO>" "$(cat <<'EOF'
<CUERPO>
-186
View File
@@ -1,186 +0,0 @@
---
name: fix-issue
description: Implementar un issue de dev/issues/ end-to-end. Crea rama, ejecuta tareas, bumpa version si toca modulos/framework/apps (via /version), tests, cierra issue, integra a master.
---
# /fix-issue
Ejecuta el flujo completo de implementacion/cierre de un issue de `dev/issues/`. Adaptado al stack del registry: Go (`-tags fts5 CGO_ENABLED=1`), Python (`python/.venv/bin/python3`), Bash, TypeScript (`pnpm`), C++ (`cmake`+`mingw-w64` toolchain).
## Inputs
```
/fix-issue <NNNN[a|b|c...]>
```
- `NNNN`: numero del issue (ej. `0107`).
- Si es sub-issue, sufijo letra: `0107a`, `0107b`, ...
Si no se proporciona, preguntar.
## Flujo obligatorio
### 1. Resolver el issue
- `dev/issues/<NNNN>-*.md` → si no existe, STOP.
- Si ya en `dev/issues/completed/`, STOP.
- Si es sub-issue, leer tambien el principal para contexto.
### 2. Leer y extraer
- Objetivo, tareas, arquitectura, prerequisitos, riesgos.
- Identificar archivos afectados — anotar si toca:
- `modules/<X>/` o `cpp/framework/` → bumpa version (paso 8).
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` → indexer + `fn index` al cerrar.
- Apps en `apps/<X>/` o `projects/*/apps/<X>/` → requiere rama TBD (regla `apps_tbd.md`) **+ bumpa version per-app (paso 8)**. Si el issue toca multiples apps, una llamada `/version` por app.
- Registry meta (CLAUDE.md, rules, templates) → push directo a master OK.
### 3. Estrategia de rama
**Registry-only changes** (functions/types/docs/rules):
- Push directo a master OK. NO crear rama.
**Apps changes** (apps/, projects/*/apps/):
- Crear rama TBD:
```bash
git checkout master
git pull --rebase
git checkout -b issue/<NNNN>-<slug>
```
La rama es del registry. Si la app es sub-repo, ademas crear rama dentro del sub-repo.
**Modules/framework changes** (`modules/`, `cpp/framework/`):
- Rama TBD obligatoria (afecta a todas las apps que linkean).
### 4. Plan con TaskCreate
- Crear tarea por bloque logico del issue.
- Incluir SIEMPRE:
- Tarea de tests (unit + smoke).
- Tarea de `fn index` si toco metadata.
- Tarea de `/version` si toco `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/` (una llamada por target).
- Tarea de cleanup/docs.
### 5. Implementar
Reglas registry-first (CLAUDE.md):
- ANTES de escribir codigo reutilizable → `mcp__registry__fn_search` para encontrar lo que existe.
- Si falta funcion reutilizable → spawn `fn-constructor` (no escribir inline).
- Si patron se repite >2x → propose nueva funcion.
- NUNCA `sqlite3 registry.db "SELECT ..."` plano — usar MCP.
Convenciones del stack:
| Stack | Build/test |
|---|---|
| Go | `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` y `CGO_ENABLED=1 go test -tags fts5 ./...` |
| Python | `python/.venv/bin/python3 -m pytest <path>` |
| Bash | `bash -n <script>.sh` + tests inline |
| TypeScript | `cd frontend && pnpm build && pnpm test` |
| C++ (Linux) | `cmake --build build --target <app>` |
| C++ (Windows MinGW) | `cmake -B build/windows -DCMAKE_TOOLCHAIN_FILE=cpp/toolchains/mingw-w64.cmake && cmake --build build/windows --target <app>` |
Commits atomicos por bloque logico con prefijos: `feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`. Mensajes en espanol. NO WIP.
### 6. Tests
Stack-dependent (ver arriba). Si tests pasan parcialmente con failures pre-existentes no causadas por la rama, documentar en cuerpo del commit/PR.
### 7. Feature flags (si aplica)
Si el issue forma parte de un feature multi-issue:
- Editar `dev/feature_flags.json` con el flag (desactivado).
- Activar el flag en el ultimo sub-issue del set.
Flag != WIP. Codigo detras de flag debe compilar + testear.
### 8. Version bump (si toco modulos/framework/apps)
**OBLIGATORIO si el issue toco** alguno de:
- `modules/<X>/` → bumpa `modules/<X>/module.md::version`.
- `cpp/framework/` → bumpa `modules/framework/module.md::version`.
- `apps/<X>/` → bumpa `apps/<X>/app.md::version`.
- `projects/<P>/apps/<X>/` → bumpa `projects/<P>/apps/<X>/app.md::version`.
```
/version <path> <major|minor|patch> "<reason>"
```
Reglas (modulos/framework):
- Major: breaking ABI/API publica.
- Minor: additive (nuevo helper, refactor interno sin cambio de API, nuevo miembro).
- Patch: bugfix puro.
Reglas (apps):
- Major: breaking observable (CLI args, schema BBDD propia, formato wire).
- Minor: feature aditiva visible (nuevo panel, endpoint, opcion).
- Patch: bugfix sin cambio observable, refactor interno, mejora perf.
**Una llamada `/version` por target afectado**. Si el issue toca 1 modulo + 2 apps -> 3 llamadas a `/version` (cada una con su `reason` y bump-type apropiado; pueden diferir).
Diff guard: cambios que solo tocan el `app.md` (correccion typo descripcion, anadir tag) NO requieren bump — son metadata, no comportamiento. Detectar con `git diff --name-only | grep -v '\.md$'` para decidir si hay cambio de codigo real.
`/version` solo edita + stage. NO commit. El bump va junto con el codigo correspondiente en el mismo commit (`feat:` o `fix:` o `refactor:`).
Si NO toco modulos/framework/apps, saltar este paso.
### 9. Cerrar el issue
Mover archivo:
```bash
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
```
Actualizar `dev/issues/README.md`:
- Link → `completed/<NNNN>-<slug>.md`
- Estado → `completado`
Si es feature multi-issue y este es el ultimo sub-issue:
- Flip flag en `dev/feature_flags.json` a `enabled: true` con `enabled_at: <YYYY-MM-DD>`.
- Verificar que todos los sub-issues estan en `completed/`.
### 10. Integrar
**Registry-only changes**: push directo a master.
**Apps/modules/framework changes**: `/full-git-push` o `/git-push` (merge --no-ff de la rama a master, push, delete rama).
### 11. Verificar post-cierre
- `fn index` — registry.db al dia.
- `fn doctor` (subcomandos relevantes: `artefacts`, `services`, `cpp-apps`, `uses-functions`).
- Si toco modulos: `fn doctor modules` (post 0107a) — 0 drift.
## Reglas criticas
- **Registry-first**: SIEMPRE buscar antes de escribir; delegar a `fn-constructor` antes que inline.
- **TBD para apps**: NUNCA push directo a master en apps. Rama corta, merge --no-ff.
- **TBD NO para registry**: push directo OK para functions/types/docs/rules.
- **`/version` obligatorio** si tocas modulos, framework o apps (con cambio de codigo real, no solo metadata). Si no, drift entre `version:` y `## Capability growth log` y se pierde trazabilidad.
- **Tests siempre**: no cerrar issue sin tests pasando (salvo failures pre-existentes documentados).
- **Commits atomicos**: 1 commit = 1 bloque logico. No mezclar `feat:` + `test:` en mismo commit.
- **Cerrar siempre**: nunca dejar issue implementado sin mover a `completed/` + actualizar README.
## Referenciado desde
- `.claude/commands/version.md` — bump semver de modulos.
- `.claude/commands/full-git-push.md` — push del registry + sub-repos.
- `.claude/rules/apps_tbd.md` — politica de TBD por tipo de cambio.
## Ejemplo: implementar 0107c (refactor data_table)
```
/fix-issue 0107c
1. Resolver: dev/issues/0107c-split-data-table.md ✓
2. Extraer: refactor 4777 LOC → 6 sub-funciones. Toca modules/ → /version obligatorio.
3. Rama: issue/0107c-split-data-table desde master.
4. Plan: 8 tareas (lectura + 6 sub-funciones + entrypoint thin + version bump).
5. Implementar: spawn fn-constructor en paralelo si hay >1 sub-funcion independiente.
6. Tests: build + smoke + primitives_gallery --capture diff.
7. Flag: parte de modules-v2, NO activar todavia (espera 0107a-f cerrar).
8. /version modules/data_table major "split data_table.cpp into 6 sub-functions"
9. Cerrar: mv → completed/ + README.
10. /git-push.
11. fn index + fn doctor modules → 0 drift en consumidores limpiados.
```
-131
View File
@@ -1,131 +0,0 @@
---
description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2."
---
# /flow — Gestionar flows del registry
Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-<slug>.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria.
**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow.
Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear <id>` en la tabla correspondiente. Nada de inventar nombres.
Diferencia con `dev/issues/`:
- Issues = bugs / features de implementacion.
- Flows = trabajos reutilizables que cruzan varias apps.
## Sintaxis
```
/flow create <slug> # nuevo flow desde template, ID auto
/flow list # tabla resumen
/flow show <NNNN> # imprime contenido + acceptance %
/flow status <NNNN> # status + acceptance % + ultima run
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
```
## Implementacion por subcomando
### `create <slug>`
Pasos:
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.md`. Si existe, error.
3. Calcula siguiente ID libre:
- `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1`
- Suma 1, zero-pad a 4 digitos.
4. Lee `dev/flows/template.md`.
5. Sustituye `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
6. Escribe `dev/flows/NNNN-<slug>.md`.
7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc).
8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance".
### `list`
Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app <name>` filtra por app.
Tambien anade columna `Accept%` calculada desde body:
- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`.
- `% = checked / total * 100` redondeo entero.
### `show <NNNN>`
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
### `status <NNNN>`
Imprime resumen del frontmatter + acceptance %:
```
=== flow 0001 ===
name: hn-top-stories
status: pending
risk: low
priority: high
apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots
acceptance: 2/6 (33%)
updated: 2026-05-16
Pending checks:
- [ ] Recipe creada y validada
- [ ] DAG corre OK 2 veces consecutivas via scheduler
- [ ] data_factory.runs tiene >=2 entries
- [ ] Schema extraido cubre 6/6 fields
```
### `done <NNNN> [--notes "..."]`
Pasos:
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
3. Si `--notes`, append a seccion `## Notas`.
4. `git mv dev/flows/NNNN-<slug>.md dev/flows/completed/`.
5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred).
### `run <NNNN>` — FASE 2 (NO IMPLEMENTADO AUN)
Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.`
Diseño futuro:
- Parsea `## Flow` en pasos.
- Cada paso tipo `function: <id>` -> ejecuta `./fn run <id>`.
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
- Texto libre -> "MANUAL: <text>" + pause user input.
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.jsonl`.
- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.).
## Conventions
- Numeracion 0001+, propia (no comparte con `dev/issues/`).
- Status: `pending | running | done | failed | deferred`.
- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales).
- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra.
- Acceptance es la fuente de verdad del progreso.
## Output style
Caveman. Tablas markdown. Sin emojis. Sin verbosidad.
Errores: 1 linea con el problema + sugerencia.
## Ejemplos
```
/flow create reddit-sentiment-tracker
# crea dev/flows/0008-reddit-sentiment-tracker.md
# anade fila a INDEX
/flow list --pending
# muestra solo flows no cerrados
/flow status 0001
# acceptance 0/6, todos los checks pendientes
# Tras correr el flow manualmente:
# editas el .md, marcas [x] los checks completados
/flow status 0001
# acceptance 6/6
/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta"
# mueve a completed/, marca status=done
```
+1 -15
View File
@@ -50,7 +50,7 @@ Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decid
### 1. AUDIT — ¿estoy siendo registrado?
```bash
ROOT="$HOME/fn_registry"
ROOT="/home/lucas/fn_registry"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Pre-condiciones
@@ -152,20 +152,6 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
```
### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6)
Por cada funcion creada con exito, llama:
```bash
bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "<fn_id>" "<one-line purpose>"
```
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup.
`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo:
- fn_id: `parse_http_log_go_infra`
- purpose: "parsea log Apache/Nginx a struct; pure"
Reporta:
- N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals`
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `full_git_pull_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
./fn run full_git_pull_bash_pipelines
```
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `full_git_push_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd "${FN_REGISTRY_ROOT:-$HOME/fn_registry}"
cd /home/lucas/fn_registry
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
```
-93
View File
@@ -1,93 +0,0 @@
---
description: "Gestiona issues del registry en dev/issues/. Subcomandos: list, show, status, board, dep, roadmap, tag, done, stale, create. Frontmatter YAML canonico (issue 0100)."
---
# /issue — Gestionar issues del registry
Issues viven en `dev/issues/NNNN-<slug>.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags).
Allowlists en `dev/TAXONOMY.md` (no inventar valores).
Diferencia con `dev/flows/`:
- **Issues** = bugs, features, refactors, chores, epics de implementacion.
- **Flows** = casos de uso end-to-end multi-app.
## Sintaxis
```
/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN]
/issue show NNNN
/issue status NNNN # acceptance % + estado deps
/issue board # kanban pendiente/in-progress/bloqueado/done
/issue dep NNNN # arbol bloquea/depende
/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...)
/issue tag NNNN +X -Y # mantenimiento tags/domain
/issue done NNNN # mueve a completed/, valida deps
/issue stale [--days 30]
/issue create <slug> --type T --domain D [--prio P] [--depends NNNN]
```
## Implementacion
**Fase 1 (manual via Claude):**
El agente lee `dev/issues/*.md`, 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"):
if f.name in {"README.md", "template.md"}: continue
txt = f.read_text()
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
if not m: continue
fm = yaml.safe_load(m.group(1)) or {}
fm["_path"] = str(f)
issues.append(fm)
# filter + print
```
**Fase 2 (cuando 0101 dev_console exista):**
Cada subcomando se mapea a `./apps/dev_console/dev_console issue <subcomando> $ARGS`.
## Subcomandos clave
### `list`
Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags.
### `show NNNN`
Read directo del .md + render del frontmatter como tabla + body como markdown.
### `status NNNN`
Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`.
### `board`
Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`.
### `roadmap NNNN`
Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic".
### `done NNNN`
1. Lee frontmatter.
2. Verifica todos `depends` cerrados (sino, error).
3. Cuenta `## Acceptance` 100% (sino, error).
4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`.
5. Actualiza `status: completado` + `updated: today`.
### `create <slug> --type T --domain D`
Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado.
## Reglas
- Domain debe estar en `dev/TAXONOMY.md` allowlist.
- Scope/type/priority idem.
- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`).
- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite).
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `init_cpp_app_bash_pipelines`. Genera la estructura canonica que cumple `cpp/PATTERNS.md` y `.claude/rules/cpp_apps.md` (main.cpp con `cfg.about/log/panels`, sin `app_menubar` manual, dockspace via framework), registra la app en `cpp/CMakeLists.txt`, crea repo Gitea `dataforge/<name>` y ejecuta `fn index`.
```bash
cd $HOME/fn_registry
cd /home/lucas/fn_registry
./fn run init_cpp_app $ARGUMENTS
```
+1 -1
View File
@@ -17,7 +17,7 @@ Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*
### 1. Resolver app objetivo
```bash
ROOT=$HOME/fn_registry
ROOT=/home/lucas/fn_registry
ARG="$ARGUMENTS"
if [ -z "$ARG" ]; then
-170
View File
@@ -1,170 +0,0 @@
---
name: version
description: Bumpear semver de un modulo, framework, paquete o app del registry. Edita <target>.md::version + ## Capability growth log. NO commitea.
---
# /version
Bumpea la version de un **modulo, framework, paquete o app** del registry siguiendo SemVer estricto y mantiene el `## Capability growth log` sincronizado con `<target>.md::version`.
Disenado para usarse desde `/fix-issue` cuando el cambio afecte:
- `modules/<X>/` (cualquier modulo C++) — edita `module.md`
- `cpp/framework/` — edita `modules/framework/module.md`
- `apps/<X>/` o `projects/<P>/apps/<X>/` — edita `app.md`
- Otros paquetes versionados con `<target>.md` y campo `version:`
## Inputs
```
/version <path> <major|minor|patch> "<reason>"
```
- `<path>`: directorio del target (ej. `modules/data_table`, `cpp/framework`, `apps/chart_demo`, `projects/fn_monitoring/apps/registry_dashboard`).
- `<major|minor|patch>`: tipo de bump SemVer.
- `<reason>`: 1-frase humana — lo que cambia. Se inserta en el log.
## Resolucion del archivo target
| Path empieza por | Archivo a editar |
|---|---|
| `modules/` | `<path>/module.md` |
| `cpp/framework` | `modules/framework/module.md` |
| `apps/` | `<path>/app.md` |
| `projects/*/apps/` | `<path>/app.md` |
| `projects/*/analysis/` | `<path>/analysis.md` |
Si no encuentra archivo target -> ERROR.
## Reglas SemVer
### Modulos / framework
| Bump | Cuando |
|---|---|
| `major` | Cambios breaking en API publica: firma de entry function, layout de State struct expuesto, eliminacion de members, cambio incompatible de comportamiento. |
| `minor` | Adiciones backwards-compatible: nuevo evento opt-in, nuevo renderer, nuevo helper, nuevo miembro. |
| `patch` | Bugfix sin cambio de API. |
Refactor interno SIN cambio de API publica -> `minor` (no major).
### Apps
| Bump | Cuando |
|---|---|
| `major` | Breaking observable por usuarios: CLI args incompatibles, schema BBDD propia rompe lectores viejos, formato wire (HTTP/gRPC) incompatible, eliminacion de panel/feature que la gente usaba. |
| `minor` | Feature aditiva: nuevo panel, nuevo endpoint, nueva opcion CLI, nueva tab, mejora visible no rompedora. |
| `patch` | Bugfix sin cambio observable. Refactor interno. Mejoras de perf. |
Bump de **dependencia** (modulo/funcion del registry) que mejora la app pero la app no cambia su API -> `patch` (la app no es responsable de la mejora; el modulo si).
## Flujo
### 1. Validar input
- `<target_file>` existe -> si no, ERROR.
- Bump type en {major, minor, patch} -> si no, ERROR.
- Reason no vacia -> si no, ERROR.
### 2. Leer version actual
Parsear frontmatter. Buscar `version: X.Y.Z`. Si no existe:
- Para `module.md` -> ERROR "module.md sin campo version".
- Para `app.md` -> asumir `0.1.0` (baseline) e insertar el campo despues de `domain:`.
### 3. Calcular proxima version
```
1.4.0 + major = 2.0.0
1.4.0 + minor = 1.5.0
1.4.0 + patch = 1.4.1
```
Major bump -> minor y patch a 0. Minor bump -> patch a 0.
### 4. Editar `<target_file>`
Cambiar linea `version: <old>` por `version: <new>`.
### 5. Anadir entrada a `## Capability growth log`
Insertar al inicio de la lista (lineas posteriores al header `## Capability growth log`):
```markdown
- v<new> (<fecha YYYY-MM-DD>) — <reason>
```
Si la seccion no existe -> crearla al final del archivo antes de `## Notes` (o al final si no hay Notes).
### 6. Verificar drift de members (solo modulos, opcional)
Si la herramienta `fn doctor modules` existe (post 0107a) y el target es modulo:
- Compara `members:` actual vs ultima version registrada en `registry.db::modules_history`.
- Si hay diff en members y bump es `patch` -> WARNING.
- Si hay diff en API publica y bump no es `major` -> ERROR (require `--force`).
No aplica a apps (no tienen `members:`).
### 7. Stage en git
`git add <target_file>`. NO commit. El commit final lo hace el flujo padre.
### 8. Reportar
```
/version apps/chart_demo minor "anade tab radar chart"
apps/chart_demo/app.md
version: 1.2.0 -> 1.3.0
## Capability growth log: + v1.3.0 (2026-05-18) — anade tab radar chart
Staged. NO committed.
Next: terminar el fix-issue y hacer commit con el resto de cambios.
```
## Reglas criticas
- **NUNCA commit**. `/version` solo edita + stage. El commit lo hace el flujo padre (`/fix-issue`, `/git-push`).
- **NUNCA saltar version**. No 1.4.0 -> 1.4.2 directo.
- **NUNCA bajar version**. Si rollback, crea nueva version superior con comportamiento viejo restaurado.
- **fecha = HOY** (`date +%Y-%m-%d`).
- **reason** comprensible sin contexto del PR actual.
## Referenciado desde
- `/fix-issue` — al detectar cambios en `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/`, sugiere ejecutar `/version` antes del commit final.
- `.claude/rules/cpp_apps.md` — politica de bump.
- `dev/issues/0107-modules-standardization.md` — origen del flujo (modulos).
## Ejemplos
```
# Bug fix en data_table (modulo)
/version modules/data_table patch "fix off-by-one en seleccion multi-row con shift+click"
# -> 1.4.0 -> 1.4.1
# Feature opt-in en framework
/version cpp/framework minor "anade cfg.auto_dockspace para overlay de paneles flotantes"
# -> 1.1.0 -> 1.2.0
# Feature en app C++
/version apps/chart_demo minor "anade tab radar chart con datos sinteticos"
# -> 1.2.0 -> 1.3.0
# Bug fix en app de proyecto
/version projects/fn_monitoring/apps/registry_dashboard patch "fix tooltip que mostraba duration_ms en segundos"
# -> 0.4.1 -> 0.4.2
# Breaking en app: cambia schema de su BBDD propia
/version apps/kanban major "cards.assignee_id pasa a ser TEXT[] (era TEXT); requiere migracion 008"
# -> 1.0.0 -> 2.0.0
```
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Editar `version:` a mano sin `## Capability growth log` | Drift entre version y log; nadie sabe que cambio. |
| Bumpear major en app por refactor interno | Confunde al usuario; refactor es patch. |
| Patch para feature visible | Usuario no se entera que esta disponible. |
| Reason "cambios varios" / "mejoras" | Inutil para auditar. Una frase concreta. |
| Bump de app sin tocar codigo de la app (solo dep) | Bump va al modulo, no a la app. |
-67
View File
@@ -1,67 +0,0 @@
---
description: "Vista cross-cutting de issues + flows. Subcomandos: today, weekly, search, dashboard. Mezcla los dos universos en una lista priorizable."
---
# /work — Vista cross-cutting issues + flows
Issues = trabajo de implementacion. Flows = casos de uso multi-app. `/work` los muestra juntos para responder "que hago ahora" sin saltar entre dos sitios.
## Sintaxis
```
/work today # top items prio alta + deps satisfechas (issues + flows)
/work weekly # review semanal: closed vs planeados
/work search "texto" # FTS sobre issues + flows + completed
/work dashboard # JSON consumible por tab Work (issue 0102)
```
## Implementacion
**Fase 1 (manual via Claude):**
El agente lee `dev/issues/*.md` + `dev/flows/*.md`, parsea frontmatter YAML, ordena por:
1. `priority: alta` primero.
2. `status: pendiente` con `depends` todos `completado` (no bloqueados).
3. Items con DoD/Acceptance >=80% (a punto de cerrar).
4. Fecha `updated` mas reciente.
Imprime tabla unificada:
```
KIND | ID | TITLE | PRIO | STATUS | NEXT STEP
issue| 0099 | datahub app launcher | alta | pendiente | revisar deps
flow | 0001 | hn-top-stories | high | pending | cerrar DoD user-facing
issue| 0100 | migrate issue frontmatter | alta | pendiente | ejecutar pipeline
...
```
**Fase 2 (cuando 0101 dev_console exista):**
`./apps/dev_console/dev_console work <subcomando> $ARGS`.
## Subcomandos
### `today`
Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola".
### `weekly`
Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: <esta semana>` -> ratio in/out.
### `search "texto"`
`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`.
### `dashboard`
Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura:
```json
{
"issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]},
"flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}],
"telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N}
}
```
-5
View File
@@ -21,7 +21,6 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
@@ -35,7 +34,3 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
| 34 | [dod_quality.md](dod_quality.md) | DoD Quality Triada: Mecanica + Cobertura (golden + edge + error path con evidencia ejecutable) + Vida util validada (>=7 dias uso real). Cierra anti-criterios contra checkbox vago. Aplica a `dev/flows/` y issues user-facing. |
-74
View File
@@ -1,74 +0,0 @@
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
### El gotcha
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
### El patron correcto al crear apps en worktrees
```bash
# 1. Agente trabaja en worktree del repo padre
cd $HOME/fn_registry/worktrees/<slug>
# 2. Scaffold la app via pipeline canonico
./fn run init_cpp_app <name> # apps C++
# o ./fn run init_jupyter_analysis ... # analysis
# o crear apps/<name>/ a mano (Go service, etc.)
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
cd apps/<name>
git init -b master
git add -A
git -c user.email="agent@fn_registry" -c user.name="agent" \
commit -m "feat: initial scaffold of <name>"
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
```
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
### Que ESTA SI versionado en el repo padre
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
- `docs/capabilities/*.md` si la app aporta a un capability group.
- `dev/feature_flags.json` si introduce flags.
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
### Sintomas de la perdida
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
### Recovery si pasa
1. Re-crear worktree desde master.
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
### Aplica tambien a analysis
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
### Lo que aprende `parallel-fix-issues`
El template del prompt de cada agente DEBE incluir la instruccion:
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
### Relacion con otras reglas
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
-102
View File
@@ -1,102 +0,0 @@
## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069
`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/<issue>`, NUNCA merge a master.
### Cuando se invoca
- Skill `/autonomous-task <issue_id>` (humano lanza explicitamente).
- Cron / dag_engine (`schedule:` en YAML; planificable, no implementado por defecto).
- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita.
### Reglas duras
1. **Sandbox obligatorio**: rama `auto/<issue_id>-<slug>`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master.
2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`.
3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen:
- `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable.
- `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano).
- `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`.
4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`.
5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h.
6. **Idempotencia**: re-ejecutar `/autonomous-task <id>` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`).
7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar.
8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`.
9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C <main_repo> status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT.
10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main.
### Sub-repos vs worktree padre
Cuando el issue toca `app.md` o codigo dentro de `apps/<name>/`, `projects/<p>/apps/<name>/`, `cpp/apps/<name>/`, o `analysis/<a>/` — estos directorios son **sub-repos Gitea independientes** y estan `.gitignore`d en el repo padre `fn_registry` (regla `apps_subrepo.md`). El orquestador:
- **Crea worktree padre** `auto/<issue>` en `/tmp/fn_orq_<issue>_<ts>/` por protocolo, **pero no escribe alli** porque los cambios no se versionan en el padre.
- **Opera DIRECTAMENTE en el sub-repo** de la app/analysis target. Branch `auto/<issue>-<slug>` se crea dentro de `apps/<name>/.git`, NO en el padre.
- **PR draft sale al sub-repo** en `dataforge/<name>` (NO a `dataforge/fn_registry`). Humano revisa+mergea en el sub-repo.
- **Worktree padre queda vacio** y se limpia normal con `git worktree remove` al terminar.
Validado en piloto 0120 (`add_e2e_check` sobre `chart_demo`): PR creado en `dataforge/chart_demo/pulls/1`, sanity check del main repo `fn_registry` confirmo cero contaminacion.
Si el issue toca AMBOS lados (codigo del registry padre + app de sub-repo), el orquestador commitea separado: cambios del padre en `auto/<issue>` (worktree padre), cambios de la app en `auto/<issue>-<slug>` (sub-repo). Dos PRs draft. Humano coordina merge.
### Gitea API vs `gh`
Pre-condicion `gh auth status` es smoke check (target github.com). Mecanismo real de PR es `curl` a Gitea API:
```bash
GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"...","head":"auto/<issue>-<slug>","base":"master","draft":true,"body":"..."}' \
"https://gitea-.../api/v1/repos/dataforge/<repo>/pulls"
```
Validado en pilotos 0076 y 0120.
### Estructura task_run
Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`.
### Fases por iteracion
```
loop:
1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada
2. fn-executor - corre build + tests + smoke
3. fn-recopilador - audita operations.db de la app
4. fn-analizador - corre e2e_checks (registra e2e_runs)
5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit.
6. SI no progreso N iteraciones -> abort. status=stalled.
7. fn-mejorador - crea proposals desde fallos
8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1.
```
### Output al humano
```
=== /autonomous-task 0068 ===
task_run_id: run_e2e_a1b2c3
branch: auto/0068-e2e-validation
iterations: 4
status: done
checks_pass: 8/8
proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003)
proposals_skipped: 1 (refactor — needs human review)
PR: https://gitea.../pulls/42
```
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Mergear `auto/<issue>` a master sin PR + humano | Salta gate, riesgo de regresion |
| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision |
| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review |
| Bucle infinito sin watchdog | Coste descontrolado de tokens |
| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria |
| Override de paths protegidos via env var | Bypass de seguridad |
### Relacion con otras reglas
- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate.
- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD.
- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR.
- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor.
+12 -233
View File
@@ -20,14 +20,14 @@ Razones:
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
### 1. Ubicacion (issue 0096 estandarizada)
### 1. Ubicacion
| Caso | Donde vive |
|---|---|
| App independiente | `apps/<nombre>/` |
| App independiente | `cpp/apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
@@ -84,7 +84,6 @@ Plantilla minima para apps C++:
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
version: 0.1.0 # semver per-app, bumped via /version
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
@@ -103,7 +102,6 @@ Reglas:
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
- `version`: semver per-app. Baseline `0.1.0` para apps nuevas. Bump obligatorio via `/version apps/<name> {major|minor|patch} "<reason>"` cuando `/fix-issue` toque codigo de la app. Trazabilidad humana en seccion `## Capability growth log` al final del `app.md` (una linea por bump). Ver `.claude/commands/version.md`.
### 5. Registro en `cpp/CMakeLists.txt`
@@ -191,105 +189,20 @@ WMs). Activado por defecto, sin opt-in:
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
secundarios (paneles drag-out) que la backend crea dinamicamente.
3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa
`WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada
drag. El subclass se instala en la ventana principal Y en cada HWND
secundario que el backend de ImGui crea cuando un panel se arrastra fuera
del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta
abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` +
`glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag
native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer
existente). El flag `g_in_sizemove` es global a proposito: una sola
sesion de sizemove externo pausa todo el render para que ninguna ventana
compita con el OS.
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
replicando el contrato del title-bar drag native (DefWindowProc bloquea
el hilo, DWM compositor mueve el framebuffer existente).
Estado del subclass:
- `g_subclassed` = `unordered_map<HWND, WNDPROC>`. Chain a la proc
original via `CallWindowProcW`.
- `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa).
- Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada
`pio.Viewports[i]->PlatformHandle` nuevo.
- `uninstall_sizemove_subclass_all()` restaura cada HWND al exit.
#### Iconified main no pierde paneles flotantes (2026-05-16)
El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO
el frame loop. Con multi-viewport activo eso significa que
`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de
refrescar los viewports secundarios — los floating panels aparecen congelados
o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta
viewports secundarios primero; si hay alguno, fall-through al frame normal
(la swap del main HWND minimizado es harmless, los contexts GL secundarios
siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`.
#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16)
WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`):
- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` +
`PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo.
- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/
BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite
`PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo.
Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a
cada viewport flotante porque el subclass per-frame ya cubre todos los HWND.
El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa
render → cero jitter automatico, mismo contrato que el title-bar drag.
**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut
UI. Aceptable porque Alt-modifier en clicks UI es muy raro.
#### Title-bar-only move para ImGui windows (2026-05-16)
`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico
para viewports secundarios: un viewport flotante = OS window borderless con
UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas
arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport
entero, el OS window sigue al cursor sin modifier. Con el flag, floating
panels obedecen el contrato "solo header arrastra" (igual que main que tiene
title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido
antes por el subclass).
#### Test observability — `fn::internal::*` (2026-05-16)
Counters monotonicos para validar el subclass desde tests headless,
zero-cost en prod:
```cpp
namespace fn::internal {
int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE
int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido
int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido
int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN
void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests
}
```
En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan
pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el
modal de Windows. Path real en prod sigue posteandolos.
Tests: `apps/altsnap_jitter_test/` corre seis fases:
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
`vp->Pos` sigue OS dentro de 1px.
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el
HWND principal, asserta que `render()` no se llama durante el bracket.
- `p3.secondary` (Windows): fuerza viewport secundario
(`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket
sobre el. Valida que el subclass per-viewport tambien pausa el render.
- `p4.minimize` (Windows): state machine 4 steps — captura
`IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow +
glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`.
- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` +
`SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta
`alt_rmb_resize_count` incrementa.
- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta
`alt_lmb_move_count` incrementa.
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
que `render()` no se llama durante el bracket.
Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh &&
e2e_run_cpp_windows altsnap_jitter_test`.
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
necesita renderizar incluso durante external move (caso raro: telemetria
@@ -348,137 +261,3 @@ de antes: `imgui.ini` es la unica fuente.
- App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`).
### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16
Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en
`<app_dir>/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro
`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si
`WIN32` + existe `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
`IDI_ICON1 ICON "<path>"` y lo anade a `add_executable`. El compilador RC
(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`)
lo enlaza al `.exe` como recurso `.rsrc`.
Verificar: `x86_64-w64-mingw32-objdump -h <app>.exe | grep rsrc` debe
mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara
`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`).
#### Crear `.ico` para una app nueva
Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de
`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`,
`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor
legibilidad a 16/24px.
Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor
sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una
linea por app:
```python
from infra import generate_app_icon
generate_app_icon(
phosphor_icon_name="chart-bar",
accent_hex="#0ea5e9",
out_ico_path="apps/chart_demo/appicon.ico",
)
```
Mapping vive en el frontmatter de cada `app.md` C++:
```yaml
description: "Frase corta de 1 linea — que hace la app y por que existe."
icon:
phosphor: "chart-bar"
accent: "#0ea5e9"
```
### Trio obligatorio: description + icon.phosphor + icon.accent
**REGLA DURA:** TODA app C++/imgui declara los **3 campos JUNTOS** en su `app.md`:
1. `description:` (string corta, 1 linea) — texto que el `app_hub_launcher` muestra en la tarjeta y que el dashboard usa para tooltips.
2. `icon.phosphor:` (nombre del glyph Phosphor sin sufijo `-fill`) — glyph del icono.
3. `icon.accent:` (hex `#rrggbb`) — color del fondo redondeado del icono **Y** color del boton/border de la tarjeta en `app_hub_launcher`.
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin `icon:` cae al default (`app-window` slate); sin accent el boton del hub aparece blanco. **Documentar uno sin los otros es bug**, no estilo.
### Refrescar el App Hub tras editar el trio
`app_hub_launcher` cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar `description`/`icon.*` en un `app.md` requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
```bash
./fn run refresh_app_hub # icons + manifest + restart hub
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
```
ID: `refresh_app_hub_bash_pipelines`. Compone `export_hub_icons_py_infra` + `export_hub_manifest_py_infra` + `is_cpp_app_running_windows_bash_infra` + `launch_cpp_app_windows_bash_infra`.
Regeneracion batch via pipeline del registry — escanea `app.md`s y compone
`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su
`app.md` y lanzar:
```bash
./fn run regenerate_app_icons # todas
./fn run regenerate_app_icons chart_demo # solo una
```
Convenciones:
- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`).
- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700
funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.).
- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado.
- **Glyph color**: siempre blanco sobre el fondo accent.
Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/`
con `ls | grep <keyword>` antes de inventar — 1512 disponibles.
#### Re-deploy tras cambiar icono
```bash
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
./fn run regenerate_app_icons chart_demo
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc)
./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build
```
Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras
desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.
#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts —
pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el
**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a
menos que adjuntemos el recurso al HWND en runtime.
`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`:
```cpp
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
```
Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado
(`101 ICON "<app_dir>/appicon.ico"`). Si la app no tiene `appicon.ico`, el
`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono
GLFW por defecto (sin error).
Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que
instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
sin codigo extra en la app.
Cache shell: el pipeline `redeploy_cpp_app_windows` llama
`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca
`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer.
-131
View File
@@ -1,131 +0,0 @@
# DoD Quality Triada
**Definition of Done no es un checkbox que se marca a mano. Es un contrato de calidad con 3 capas obligatorias + evidencia ejecutable + uso real >=7 dias.**
Aplica a todos los `dev/flows/` y, por extension, a issues que cierran capabilities user-facing (`dev/issues/`). El registry mismo (funciones puras, tipos) queda exento: su DoD vive en sus tests unitarios.
---
## Por que existe esta regla
El antipatron a eliminar: "tarea hecha porque pase los tests una vez". Despues:
- El flow funciona en `home-wsl` pero falla en `pc-aurgi`.
- El error path declarado nunca se ejercito y cuando ocurre en produccion no esta manejado.
- El dashboard de observabilidad lleva 30 dias sin abrirse.
- El proceso muere cada noche y nadie lo ve hasta que el operador intenta usarlo.
- El approval flow se salta porque "para test es mas comodo".
Resultado: deuda invisible. Cada flow "done" se rompe al primer uso real, el operador pierde confianza en el sistema, y el bucle reactivo no detecta nada porque la telemetria esta verde (los tests sintenticos pasan).
DoD Quality Triada cambia las reglas: cerrar = probar comportamiento + sobrevivir uso real, no = compilar verde.
---
## Las 3 capas
### Capa 1: Mecanica (pre-requisito, NO es DoD por si misma)
Compilar verde, tests verdes, indexado limpio, `fn doctor` verde, `uses_functions` sin drift.
**Regla**: la mecanica NO basta. Es la base para empezar a probar comportamiento. Si te quedas aqui, el flow no esta hecho.
### Capa 2: Cobertura de comportamiento
Cada escenario relevante con prueba ejecutable y assert material. NO smoke "el comando no peto". Minimo:
- **1 golden path** — el caso feliz documentado con assert sobre output concreto.
- **>=2 edge cases** — inputs limite, estados raros, condiciones de borde.
- **>=1 error path** — fallo provocado intencionalmente, manejado y observable (sin crash, sin silent-fail).
Formato canonico (tabla en `## Definition of Done` del flow/issue):
```markdown
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: <desc> | unit / e2e | `<cmd>` | <output concreto> |
| Edge 1: <desc> | unit / e2e | `<cmd>` | <comportamiento concreto> |
| Error 1: <desc> | e2e | `<cmd que rompe>` | <fallo manejado, no crash> |
```
Cuando aplique, cada fila genera un `e2e_check` en el `app.md` correspondiente (issue 0068). `fn-analizador` los corre periodicamente y deja entry en `e2e_runs`.
### Capa 3: Vida util validada
El flow no esta hecho hasta que sobrevive **uso real durante >=7 dias** sin romperse silenciosamente. Cada metrica con umbral medible y dashboard observable.
Formato canonico:
```markdown
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| <metrica 1> | `>=N` | `<dashboard URL / app panel>` | 7 dias |
| crashes | `0` | `journalctl -u <unit>` | 7 dias |
| huecos audit chain | `0` | `cmd: <verify>` | continuo |
```
Reglas:
- Metricas NO se auto-reportan; las lee el operador del dashboard real.
- Si el dashboard no existe o no se ha abierto en 30 dias, el item se invalida.
- Crashes del proceso = 0, huecos en audit = 0, error_rate < umbral declarado.
### Capa transversal: User-facing reforzado
- Surface concreta NO BD ni log (UI app, room Matrix, dashboard, archivo en vault).
- Usage real: humano usa en su PC, su contexto, >=N veces variadas en >=7 dias.
- Variado: >=3 capabilities/casos distintos (no solo "abre dashboard y mira").
- Onboarding: parrafo en `## Notas` que explica como usar la cosa sin leer el flow.
- Latencia medida (no declarada).
---
## Reglas duras para marcar `status: done`
`/flow done` (y por extension cierres de issues user-facing) DEBE rechazar el cierre si:
1. Falta cualquiera de las 3 capas (mecanica + cobertura + vida).
2. Cobertura tiene <1 golden, <2 edge, o <1 error path con evidencia.
3. Vida util tiene tabla vacia o sin dashboard observable real.
4. User-facing usage real <7 dias o <N usos declarados.
5. Cualquier anti-criterio marcado como cierto.
6. `## Notas` sin parrafo onboarding.
7. Algun item de DoD sin comando/URL/log query asociado — solo texto.
Hoy parte de esta validacion es manual (revision humana del operador). La validacion programatica vive en `audit_dod_schema_go_infra` (issue 0114) + `fn doctor dod` y se ampliara hasta cubrir las 3 capas (TBD).
---
## Antipatrones (invalidan la DoD aunque los checkboxes esten verdes)
| Antipatron | Por que es malo | Sustituir por |
|---|---|---|
| Marcar `done` porque pasa una vez | Tarea "hecha" se rompe al primer uso real | Capa 3: >=7 dias de uso real |
| Checkbox sin evidencia ejecutable | DoD se convierte en placebo | Cada item con `cmd:` / URL / log query |
| Test que solo verifica camino feliz | El error path es donde se pierden datos | Capa 2: >=1 error path ejercitado |
| Observabilidad declarada pero dashboard no abierto en 30 dias | Telemetria muerta = ceguera | Capa 3: dashboard real, operador lo abre |
| "Repetible 3 veces consecutivas" con BD efimera | No prueba sobre datos reales acumulados | Capa 3: PC real del operador, datos vivos |
| Approval saltado en algun camino | Security gate roto pero invisible | Anti-criterio explicito: `audit_log` lo prueba |
| Error path manejado solo "en teoria" | Cuando ocurra en produccion el manejo no existe | Capa 2: entry real en `e2e_runs` o audit |
| Solo-en-mi-PC | Falla en otra maquina del operador | Anti-criterio explicito, probar >=2 PCs |
| Self-test que retorna `pass` sin asserts materiales | False positive sistemico | Asserts sobre output concreto, no exit-0 |
| Silent-fail (proceso muere sin alerta) | Operador no se entera hasta intentar usar | Capa 3: crashes=0 + alerta visible |
---
## Relacion con otras reglas
- [[e2e_validation]] — los escenarios de Capa 2 cuando aplican a apps se materializan como `e2e_checks` en `app.md`. `fn-analizador` (fase 4 del bucle reactivo) los corre.
- [[registry_calls]] — la evidencia de uso (`call_monitor.calls`) alimenta los umbrales de Capa 3.
- [[function_growth_and_self_docs]] — cada funcion del registry tiene su propio contrato self-doc (Ejemplo + Cuando usarla + Gotchas). DoD del flow NO sustituye al self-doc de la funcion; lo complementa para el nivel sistema.
- [[autonomous_loop]] — `fn-orquestador` autonomo NO puede marcar `done` sin que se cumplan las 3 capas. Su criterio de convergencia incluye DoD Quality.
- [[apps_tbd]] — TBD garantiza master desplegable; DoD garantiza que lo desplegado funciona en uso real.
---
## TL;DR
1. **Mecanica** = compilar verde (pre-requisito, NO suficiente).
2. **Cobertura** = golden + >=2 edge + >=1 error path con evidencia ejecutable.
3. **Vida util** = >=7 dias de uso real sin romper silenciosamente, dashboard observable abierto.
4. **User-facing reforzado** = humano usa en PC real, >=N veces variadas.
5. **Anti-criterios** invalidan la DoD aunque todo este verde.
6. Sin evidencia ejecutable (cmd/URL/log), NO es DoD: es deseo.
+1 -12
View File
@@ -20,18 +20,10 @@ fn doctor sync # Solo drift pc_locations BD vs disco local
fn doctor uses-functions # Solo audit imports reales vs uses_functions
fn doctor unused # Solo funciones huerfanas del registry
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
# + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio)
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
```
`fn doctor cpp-apps` produce dos secciones:
1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui.
2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline:
- `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion.
- `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK.
- silencio: 0 BeginTable inline (limpio o completamente migrado).
### Mapeo subcomando → funcion del registry
| Subcomando | Funcion |
@@ -41,8 +33,7 @@ fn doctor --json # Salida JSON (cualquier subcomando) — para agentes
| `sync` | `pc_locations_drift_go_infra` |
| `uses-functions` | `audit_uses_functions_go_infra` |
| `unused` | `find_unused_functions_go_infra` |
| `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` |
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
| `cpp-apps` | `audit_cpp_apps_go_infra` |
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
@@ -73,8 +64,6 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes
-23
View File
@@ -28,26 +28,3 @@ Documentar en el `app.md` del service:
- El puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Como comprobar que esta vivo (health check)
### Bloque `service:` obligatorio (issue 0105)
Toda app con `tag: service` declara el bloque `service:` en su frontmatter. El indexer lo persiste en columnas dedicadas de `apps` + tabla `service_targets`. Consumido por `services_api`/`services_monitor` (issue 0106) y por `fn doctor services-spec`.
```yaml
service:
port: 8484 # null si no expone HTTP (stdio, daemon sin API)
health_endpoint: /api/databases # ruta GET, 2xx/3xx = sano; null si no aplica
health_timeout_s: 3
systemd_unit: sqlite_api.service # obligatorio si runtime empieza con `systemd-`
systemd_scope: user # user|system|null (docker-compose)
restart_policy: always # always|on-failure|none
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
pc_targets: # >=1, pc_id de pc_locations
- aurgi-pc
- home-wsl
is_local_only: false # true => no se monitoriza por SSH (siempre local)
```
Validacion: `fn doctor services-spec` (`functions/infra/audit_services_spec.go`). Hoy 11/11 services con bloque completo.
**Gotcha critico:** usar `Restart=always` (no `on-failure`) en el unit systemd. Un `SIGTERM` limpio es exit success → `on-failure` NO reinicia y el service se queda muerto silenciosamente. `sqlite_api.service` cayo 20h asi el 2026-05-17.
+2 -34
View File
@@ -1,35 +1,3 @@
## ids_naming — formato predictible
IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`).
IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087.
### Reglas
1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation.
2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo).
3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md.
4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`).
### Verbos canonicos (allowlist)
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, start, stop, kill, restart, 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`
### Excepciones
- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita.
- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`.
- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `<noun>_<modifier>`. Validator salta el check de verbo si `kind=component`.
### Validator
`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si:
- name no es snake_case.
- name no contiene verbo (excepto component/type).
- domain no esta en lista canonica.
Error tipico:
```
naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md.
naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first.
```
Nombres de funciones en snake_case. Tipos en PascalCase para Go.
-52
View File
@@ -1,52 +0,0 @@
## Slash commands por project (namespaced)
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
### Patron canonico
```
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
```
Resultado:
| cwd | Invocacion |
|---|---|
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
### Como anadir un project nuevo
1. `mkdir -p projects/<p>/.claude/commands/`.
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
### Reglas
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
### Listado actual
| Project | Symlink | Commands disponibles desde fn_registry |
|---|---|---|
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
Anadir filas aqui al introducir un project nuevo con commands.
### Catalogo dinamico
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
### Gotchas
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
+1 -1
View File
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
-53
View File
@@ -1,53 +0,0 @@
#!/usr/bin/env bash
# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor
# creates a new registry function. Idempotent: skips if id already present.
# Used by /fn_claude step 5b (issue 0087, pieza 6).
#
# Usage: append_fn_to_memory.sh <fn_id> "<one-line purpose>"
set -euo pipefail
FN_ID="${1:-}"
PURPOSE="${2:-}"
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&2
exit 2
fi
MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}"
MEM_FILE="$MEM_DIR/MEMORY.md"
[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; }
[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; }
# Per-function reference file slug
SLUG="reference_fn_${FN_ID}.md"
REF_FILE="$MEM_DIR/$SLUG"
# Idempotency: if already linked in MEMORY.md, exit 0
if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then
echo "already in MEMORY.md: $FN_ID"
exit 0
fi
# 1. Create reference memory file
cat > "$REF_FILE" <<EOF
---
name: fn-$FN_ID
description: Registry function $FN_ID — $PURPOSE
metadata:
type: reference
---
Registry function: \`$FN_ID\`
$PURPOSE
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
EOF
# 2. Append index line to MEMORY.md
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
echo "appended: $FN_ID -> $MEM_FILE"
-35
View File
@@ -1,35 +0,0 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_registry_mcp.sh" },
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_fn_match.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Bash|Edit|Write|MultiEdit|mcp__registry__.*",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_call_monitor.sh" }
]
},
{
"matcher": "Edit|Write|MultiEdit|mcp__registry__fn_create_function",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_capability_tag_gate.sh" }
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_capabilities_inject.sh" },
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_registry_first_reminder.sh" }
]
}
]
}
}
@@ -1,121 +0,0 @@
#!/bin/bash
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
#
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Para cada slug:
# 1. git merge --no-ff issue/<slug> a master
# 2. Verificar que master compila después del merge
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
#
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
# NO hace push — eso lo decide el usuario.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Asegurar que estamos en master
echo "=== Cambiando a master ==="
cd "$REPO_ROOT"
git checkout master
MERGED=0
FAILED_AT=""
for slug in "$@"; do
branch="issue/${slug}"
echo ""
echo "=== Integrando: ${branch} ==="
# Verificar que la branch existe
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
echo "FAIL: branch ${branch} no existe"
FAILED_AT="$slug"
break
fi
# Merge --no-ff
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
echo ""
echo "CONFLICT: merge de ${branch} tiene conflictos"
echo "Resolver manualmente y luego continuar con los slugs restantes"
echo ""
echo "Para resolver:"
echo " 1. git status (ver archivos en conflicto)"
echo " 2. Resolver conflictos en cada archivo"
echo " 3. git add <archivos>"
echo " 4. git commit"
echo ""
echo "Slugs pendientes después de ${slug}:"
FOUND=0
for remaining in "$@"; do
if [ "$FOUND" -eq 1 ]; then
echo " - ${remaining}"
fi
if [ "$remaining" = "$slug" ]; then
FOUND=1
fi
done
exit 1
fi
echo "MERGED: ${branch}"
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
if [ -n "${BUILD_CMD:-}" ]; then
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
echo ""
echo "FAIL: master no compila despues de mergear ${branch}"
echo "Revertir con: git reset --hard HEAD~1"
echo "Investigar el problema antes de continuar."
FAILED_AT="$slug"
break
fi
echo "OK: build post-merge exitoso"
else
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
fi
MERGED=$((MERGED + 1))
done
echo ""
echo "=== Resumen de integración ==="
echo "Mergeados: ${MERGED} de $#"
if [ -n "$FAILED_AT" ]; then
echo "Falló en: ${FAILED_AT}"
echo ""
echo "Worktrees NO limpiados (resolver primero el fallo)"
exit 1
fi
# Limpieza de worktrees y branches
echo ""
echo "=== Limpieza ==="
for slug in "$@"; do
path="${REPO_ROOT}/worktrees/${slug}"
branch="issue/${slug}"
if [ -d "$path" ]; then
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
fi
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
done
echo ""
echo "=== Integración completa ==="
echo "Master tiene ${MERGED} merges nuevos."
echo ""
echo "Para publicar: git push"
@@ -1,74 +0,0 @@
#!/bin/bash
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
#
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Cada slug genera:
# worktrees/<slug>/ (worktree completo)
# branch: issue/<slug>
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_DIR="${REPO_ROOT}/worktrees"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug de issue"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
echo "=== Verificando master ==="
CURRENT_BRANCH="$(git branch --show-current)"
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
fi
# NO auto-pull. Usuario decide sync con remote.
mkdir -p "$WORKTREE_DIR"
CREATED=0
SKIPPED=0
FAILED=0
for slug in "$@"; do
branch="issue/${slug}"
path="${WORKTREE_DIR}/${slug}"
if [ -d "$path" ]; then
echo "SKIP: worktree ya existe: ${path}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Verificar que la branch no existe ya
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
git worktree add "$path" "$branch" 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
else
echo "CREATE: worktree ${path} (branch ${branch})"
git worktree add -b "$branch" "$path" master 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
fi
CREATED=$((CREATED + 1))
done
echo ""
echo "=== Resumen ==="
echo "Creados: ${CREATED}"
echo "Existentes: ${SKIPPED}"
echo "Fallidos: ${FAILED}"
echo ""
echo "=== Worktrees activos ==="
git worktree list
@@ -1,165 +0,0 @@
#!/bin/bash
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
#
# Uso:
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
#
# Ejemplos:
# ./verify-worktree.sh worktrees/0026-foo
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
#
# Resolucion de comandos (en orden de prioridad):
# 1. Argumentos posicionales (build-cmd, test-cmd)
# 2. Variables de entorno BUILD_CMD / TEST_CMD
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
# 4. Auto-deteccion segun ficheros del proyecto:
# - go.mod → "go build ./..." + "go test ./..."
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
# - Cargo.toml → "cargo build" + "cargo test"
# - package.json → "npm run build" + "npm test"
# - pyproject.toml → "" + "pytest"
# 5. Si nada se detecta, salta build/test con WARN.
#
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
#
# Exit codes:
# 0 = todo OK
# 1 = error de argumento
# 2 = build fallo
# 3 = tests fallaron
# 4 = issue no cerrado (solo WARN, no falla)
# 5 = sin commits propios
set -euo pipefail
if [ $# -lt 1 ]; then
echo "ERROR: se necesita el path del worktree"
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
exit 1
fi
WORKTREE="$1"
ARG_BUILD_CMD="${2:-}"
ARG_TEST_CMD="${3:-}"
# Resolver path absoluto
if [[ "$WORKTREE" != /* ]]; then
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE="${REPO_ROOT}/${WORKTREE}"
fi
if [ ! -d "$WORKTREE" ]; then
echo "ERROR: worktree no encontrado: ${WORKTREE}"
exit 1
fi
SLUG="$(basename "$WORKTREE")"
echo "=== Verificando: ${SLUG} ==="
# --- Resolver build/test commands ---
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
# Manifest opcional
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
fi
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
fi
# Auto-deteccion
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
AUTO_BUILD=""
AUTO_TEST=""
if [ -f "${WORKTREE}/go.mod" ]; then
# Detectar build tag
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
| sort -u | head -1 || true)
TAG_FLAG=""
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
AUTO_BUILD="go build $TAG_FLAG ./..."
AUTO_TEST="go test $TAG_FLAG ./..."
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
CMAKE_DIR="."
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
AUTO_BUILD="cargo build"
AUTO_TEST="cargo test"
echo "INFO: stack detectado: Rust"
elif [ -f "${WORKTREE}/package.json" ]; then
AUTO_BUILD="npm run build --if-present"
AUTO_TEST="npm test --if-present"
echo "INFO: stack detectado: Node"
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
AUTO_BUILD="" # python normalmente no tiene build step
AUTO_TEST="pytest"
echo "INFO: stack detectado: Python"
else
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
fi
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
fi
# 1. Verificar commits propios
echo ""
echo "--- Commits propios ---"
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
if [ "$COMMIT_COUNT" -eq 0 ]; then
echo "FAIL: sin commits propios en la branch"
exit 5
fi
echo "OK: ${COMMIT_COUNT} commits desde master"
cd "$WORKTREE" && git log master..HEAD --oneline
# 2. Build
echo ""
if [ -n "$BUILD_CMD" ]; then
echo "--- Build ($BUILD_CMD) ---"
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
echo "OK: build exitoso"
else
echo "FAIL: build fallo"
exit 2
fi
else
echo "--- Build SKIPPED (sin comando) ---"
fi
# 3. Tests
echo ""
if [ -n "$TEST_CMD" ]; then
echo "--- Tests ($TEST_CMD) ---"
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
echo "OK: tests pasaron"
else
echo "FAIL: tests fallaron"
exit 3
fi
else
echo "--- Tests SKIPPED (sin comando) ---"
fi
# 4. Issue cerrado
echo ""
echo "--- Cierre de issue ---"
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
if [ "$COMPLETED_FILES" -gt 0 ]; then
echo "OK: issue movido a completed/"
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
else
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
fi
echo ""
echo "=== RESULTADO: ${SLUG} — OK ==="
-7
View File
@@ -81,10 +81,3 @@ broken_paths.txt
imgui.ini
prompts/
kotlin/functions/ui/
# Module versioning auto-generated headers (written by `fn index`, issue 0097)
**/version_generated.h
**/app_modules_generated.h
# Issue migration backups (0100)
dev/issues/.backup_pre_*
-4
View File
@@ -3,10 +3,6 @@
"registry": {
"command": "./apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"]
},
"jupyter": {
"command": "bash",
"args": ["/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh"]
}
}
}
-62
View File
@@ -8,68 +8,6 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased]
## 2026-05-17
### Added
- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.
### Fixed
- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.
### Changed
- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.
## 2026-05-16
### Added
- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.
### Fixed
- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
- Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
- `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.
### Added
- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
- Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
- Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
- Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
- Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
- Convencion documentada en `.claude/rules/cpp_apps.md §11`.
- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.
### Changed
- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.
### Notes
- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.
## 2026-05-14
### Added
-22
View File
@@ -1,22 +0,0 @@
[2026-05-22 23:18:14.872] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:12.811] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:14.628] [INFO] [connect] testing https://agents.organic-machine.com...
[2026-05-22 23:24:14.758] [INFO] [connect] OK
[2026-05-22 23:24:14.765] [INFO] [db] base_url saved
[2026-05-22 23:24:14.765] [INFO] [fetch_agents] starting
[2026-05-22 23:24:14.766] [INFO] [fetch_agents] requesting https://agents.organic-machine.com/agents
[2026-05-22 23:24:14.903] [INFO] [fetch_agents] response status=200 err= body_len=3146
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] parsed 11 rows
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] done
[2026-05-22 23:24:14.910] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:07.469] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:08.242] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:36.670] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:37.446] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:28:07.068] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:03.025] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:38.605] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:48.267] [INFO] app start: Agents Dashboard
[2026-05-22 23:40:58.931] [INFO] app start: Agents Dashboard
[2026-05-22 23:41:16.455] [INFO] app start: Agents Dashboard
[2026-05-22 23:42:35.646] [INFO] app start: Agents Dashboard
-4
View File
@@ -1,4 +0,0 @@
[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test
[2026-05-15 23:51:44.017] [INFO] app exit
[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test
[2026-05-15 23:52:48.135] [INFO] app exit
-6
View File
@@ -16,9 +16,3 @@ vendor/
# OS
.DS_Store
# Runtime DB (datos vivos, no se versiona)
*.db
*.db-shm
*.db-wal
*.bak*
-359
View File
@@ -1,359 +0,0 @@
# dag_engine — Guia de uso
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
---
## 1. Donde viven los DAGs
| Path | Que |
|---|---|
| `apps/dag_engine/dags/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
Por defecto el systemd unit apunta a `apps/dag_engine/dags/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
```ini
ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \
--port 4200 \
--dags-dir /home/lucas/fn_registry/apps/dag_engine/dags \
--db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \
--scheduler
```
Y reload + restart:
```bash
systemctl --user daemon-reload
systemctl --user restart dag_engine.service
```
---
## 2. Anadir un DAG nuevo (workflow)
### Paso a paso
1. **Crear YAML** en `apps/dag_engine/dags/<nombre>.yaml` (ver formato en seccion 3).
2. **Validar** sin ejecutar:
```bash
./apps/dag_engine/dag_engine validate apps/dag_engine/dags/<nombre>.yaml
```
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
3. **Probar ejecucion manual** una vez:
```bash
./apps/dag_engine/dag_engine run apps/dag_engine/dags/<nombre>.yaml
```
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
```bash
systemctl --user restart dag_engine.service
journalctl --user-unit dag_engine.service -n 30 --no-pager
```
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
5. **Verificar en frontend**:
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
- Web: `http://localhost:4200`.
### Disparo manual desde curl o frontend
```bash
curl -X POST http://127.0.0.1:4200/api/dags/<nombre>/run
```
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
---
## 3. Formato YAML
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
### Ejemplo completo
```yaml
name: my_pipeline
description: "Pipeline diario que importa CSV y actualiza Metabase."
group: finanzas # opcional, agrupa DAGs en listados
type: graph # opcional: graph (default) | chain
tags: [daily, csv, metabase] # opcional, filtros en la UI
# Variables de entorno (heredadas por todos los steps).
env:
- DATA_DIR: /home/lucas/data
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
# Cron schedule. Puede ser string o lista.
schedule:
- "0 9 * * *" # 09:00 todos los dias
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
# Working dir + shell por defecto para todos los steps.
working_dir: /home/lucas/fn_registry
shell: /bin/bash
timeout_sec: 1800 # 30 min para todo el DAG
steps:
- name: ingest
description: "Descarga CSV."
command: ./bash/functions/pipelines/ingest_csv.sh
timeout_sec: 300 # 5 min para este step
env:
- SOURCE_URL: https://example.com/data.csv
- name: transform
description: "Limpieza y agregacion."
script: |
#!/usr/bin/env python3
import pandas as pd
df = pd.read_csv("$DATA_DIR/raw.csv")
df.to_parquet("$DATA_DIR/clean.parquet")
depends: [ingest] # debe terminar OK antes
retry_policy:
limit: 2 # reintentos en caso de fallo
interval_sec: 60
- name: load_metabase
command: ./bash/functions/metabase/refresh_dashboard.sh
depends: [transform]
continue_on:
failure: true # no aborta el DAG aunque falle
- name: notify
command: ./bash/functions/io/slack_send.sh "pipeline OK"
depends: [load_metabase]
# Hooks de ciclo de vida.
handler_on:
success: ./bash/functions/io/notify_success.sh
failure: ./bash/functions/io/notify_failure.sh
exit: ./bash/functions/io/cleanup.sh
```
### Campos del DAG (top-level)
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
| `description` | string | "" | Texto libre, aparece en la UI. |
| `group` | string | "" | Agrupa DAGs en listados. |
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
| `shell` | string | `/bin/sh` | Shell para `command:`. |
| `env` | list/map | [] | Variables de entorno DAG-wide. |
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
| `tags` | list[string] | [] | Filtros en la UI. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
### Campos de cada step
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
| `id` | string | "" | Override del id auto-generado. |
| `description` | string | "" | Texto libre. |
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
| `shell` | string | hereda | Override del shell. |
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
| `retry_policy.limit` | int | 0 | Reintentos. |
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
| `tags` | list[string] | [] | Tags por step (UI). |
### Function steps (coherencia con el registry)
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
```yaml
steps:
- name: audit_capabilities
function: audit_capability_groups_go_infra
args: ["--json"]
description: "Audita drift entre tags de capability group y paginas madre"
```
Ventajas vs `command: ./fn run ...`:
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `apps/dag_engine/dags/daily-registry-audit.yaml`.
### Cron schedule
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
| Expresion | Significado |
|---|---|
| `0 9 * * *` | Todos los dias a las 09:00 |
| `*/15 * * * *` | Cada 15 minutos |
| `0 */6 * * *` | Cada 6 horas en punto |
| `0 9 * * 1-5` | 09:00 lunes-viernes |
| `0 21 * * 5` | 21:00 viernes |
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
---
## 4. Comandos CLI
```bash
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
./dag_engine status [dag_name] # historial de ejecuciones
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
./dag_engine server # arranca HTTP + WS hub + frontend embebido
```
Flags del `server`:
| Flag | Default | Que |
|---|---|---|
| `--port` | 4200 | Puerto HTTP. |
| `--dags-dir` | `apps/dag_engine/dags` (via systemd unit) | Dir scaneado para YAMLs. |
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
---
## 5. Que hacer si algo falla
### 5.1. El DAG no aparece en la UI
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
| Causa | Diagnostico | Fix |
|---|---|---|
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags/` | Renombrar a `.yaml`. |
| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. |
| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — |
### 5.2. Validation: FAIL
`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas:
| Mensaje | Causa | Fix |
|---|---|---|
| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. |
| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. |
| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. |
| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. |
| `unknown depends: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
### 5.3. El DAG corre pero un step falla
**Sintoma:** `status: failed` en la UI.
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
3. Errores tipicos:
| stderr | Causa | Fix |
|---|---|---|
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
### 5.4. El scheduler no dispara
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
1. Verifica que el server arranco con `--scheduler`:
```bash
systemctl --user cat dag_engine.service | grep scheduler
```
2. Logs:
```bash
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
```
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
```bash
curl -X POST http://127.0.0.1:4200/api/dags/<name>/run
```
4. Hora del sistema descalibrada:
```bash
timedatectl status
```
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
### 5.5. El frontend C++ no conecta WS
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
| Causa | Fix |
|---|---|
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
| Puerto cambiado | El cliente apunta a `127.0.0.1:4200` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
### 5.6. Cleanup de runs viejos
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
```bash
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
DELETE FROM dag_step_results WHERE run_id IN (
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
);
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
VACUUM;
SQL
```
### 5.7. Restaurar desde backup
Si rompes `dags/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
```bash
cp ~/backups/fn_registry/registry/daily.0/dags/*.yaml \
apps/dag_engine/dags/ 2>/dev/null || \
git checkout HEAD -- apps/dag_engine/dags/
systemctl --user restart dag_engine.service
```
---
## 6. Endpoints HTTP
| Metodo | Path | Que |
|---|---|---|
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
| GET | `/api/dags/{name}` | Detalle + validation. |
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
| GET | `/api/scheduler/status` | Tickers activos. |
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
| POST | `/api/scheduler/stop` | Para scheduler. |
---
## 7. Referencias
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
- WS hub: `apps/dag_engine/events.go`.
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
+1 -9
View File
@@ -6,7 +6,7 @@ import (
)
// RegisterAPI sets up all HTTP routes on the given mux.
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, hub *DagRunHub, frontendFS fs.FS) {
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
// API routes.
mux.HandleFunc("GET /api/dags", handleListDags(executor))
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
@@ -15,18 +15,10 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, h
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
// Function lookup proxy a registry.db (read-only).
mux.HandleFunc("GET /api/functions/{id}", handleGetFunction())
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
// Live updates (WS hub).
if hub != nil {
mux.HandleFunc("GET /api/ws/dagruns", handleDagRunsWS(hub))
}
// Frontend SPA fallback.
if frontendFS != nil {
mux.Handle("/", spaHandler(frontendFS))
+8 -67
View File
@@ -2,8 +2,7 @@
name: dag_engine
lang: go
domain: infra
version: 0.2.0
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
@@ -28,18 +27,6 @@ uses_types:
framework: "net/http + vite + react"
entry_point: "main.go"
dir_path: "apps/dag_engine"
service:
port: 4200
health_endpoint: /api/dags
health_timeout_s: 3
systemd_unit: dag_engine.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- aurgi-pc
- home-wsl
is_local_only: false
---
## Arquitectura
@@ -84,61 +71,15 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```bash
# CLI
./dag-engine run apps/dag_engine/dags/fn_backup.yaml
./dag-engine list apps/dag_engine/dags/
./dag-engine run ~/dagu/dags/example.yaml
./dag-engine list ~/dagu/dags/
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
./dag-engine server --port 4200 --dags-dir apps/dag_engine/dags/ --scheduler
# Browser: http://localhost:4200
# Servidor web
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
# Browser: http://localhost:8090
```
## Notas
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 4200.
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
Fix:
- Borrado `apps/dag_engine/registry.db` stale.
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
### 2026-05-16 — BBDDs canonicas (referencia rapida)
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
## Lo siguiente que pega
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
## Documentacion de usuario
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
**[apps/dag_engine/README.md](README.md)**.
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
- v0.2.0 (2026-06-02) — minor: limpieza de la herencia `dagu` (renombrado `DAGU_ENV``FN_DAG_ENV`, directorio `dags_migrated/``dags/`, eliminado DAGs legacy/ejemplo), historial de ejecuciones reseteado, frontend reescrito con el estilo fn (tema indigo + radius md + `FnProvider` con `@mantine/notifications`, fix de la API `Collapse in``expanded` de Mantine 9.2.1), daemon systemd-user sirviendo React + API en el puerto 4200, y reduccion del binario de ~72MB a ~10MB separando los drivers pesados (duckdb/clickhouse/postgres/matrix/keyring) del paquete `functions/infra` a subpaquetes propios. `go.mod` replace ahora relativo (`../..`).
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
+4 -1
View File
@@ -2,6 +2,8 @@ package main
import (
"flag"
"os"
"path/filepath"
)
// Config holds the runtime configuration for the DAG engine.
@@ -14,9 +16,10 @@ type Config struct {
// DefaultConfig returns sensible defaults.
func DefaultConfig() Config {
home, _ := os.UserHomeDir()
return Config{
Port: 8090,
DagsDir: "dags",
DagsDir: filepath.Join(home, "dagu", "dags"),
DBPath: "dag_engine.db",
}
}
-438
View File
@@ -1,438 +0,0 @@
package main
// WebSocket hub para live updates de dag_runs + dag_step_results.
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
//
// Diseño:
// - Hub global con N subscribers WS.
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
// + recientes finished (ultimos 5s) -> broadcast upsert.
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
// - El cliente trata `runs` y `steps` como upserts por id.
import (
"context"
"database/sql"
"log"
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"dag-engine/store"
)
const (
dagWSTickInterval = 500 * time.Millisecond
dagWSTickIntervalIdle = 2 * time.Second
dagWSIdleThreshold = 30 * time.Second
dagWSSnapshotRuns = 50
dagWSBroadcastTimeout = 2 * time.Second
dagWSRecentFinishedS = 5
)
type wsRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
type wsStep struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
type wsWatermark struct {
Runs int64 `json:"runs"`
Steps int64 `json:"steps"`
}
type wsDagMessage struct {
Type string `json:"type"` // snapshot|delta|ping
Watermark wsWatermark `json:"watermark"`
Dags []DagInfo `json:"dags,omitempty"`
Runs []wsRun `json:"runs,omitempty"`
Steps []wsStep `json:"steps,omitempty"`
ServerTime int64 `json:"server_time"`
}
type wsDagClientCmd struct {
Watermark wsWatermark `json:"watermark,omitempty"`
}
type dagSubscriber struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
out chan wsDagMessage
watermark wsWatermark
}
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
type DagRunHub struct {
db *store.DB
executor *Executor
mu sync.Mutex
subscribers map[*dagSubscriber]struct{}
tickerStop chan struct{}
tickerOn bool
watermark wsWatermark
lastEventAt time.Time
}
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
return &DagRunHub{
db: db,
executor: executor,
subscribers: make(map[*dagSubscriber]struct{}),
}
}
func (h *DagRunHub) register(s *dagSubscriber) {
h.mu.Lock()
h.subscribers[s] = struct{}{}
shouldStart := !h.tickerOn
if shouldStart {
h.tickerStop = make(chan struct{})
h.tickerOn = true
h.lastEventAt = time.Now()
}
h.mu.Unlock()
if shouldStart {
go h.tickerLoop()
}
}
func (h *DagRunHub) unregister(s *dagSubscriber) {
h.mu.Lock()
if _, ok := h.subscribers[s]; !ok {
h.mu.Unlock()
return
}
delete(h.subscribers, s)
close(s.out)
shouldStop := h.tickerOn && len(h.subscribers) == 0
if shouldStop {
close(h.tickerStop)
h.tickerOn = false
}
h.mu.Unlock()
}
func (h *DagRunHub) tickerLoop() {
interval := dagWSTickInterval
t := time.NewTimer(interval)
defer t.Stop()
for {
select {
case <-h.tickerStop:
return
case <-t.C:
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
if err != nil {
log.Printf("[dagws] fetchDelta: %v", err)
} else if len(runs) > 0 || len(steps) > 0 {
h.setWatermark(wm)
h.recordActivity()
h.broadcast(wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
})
}
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
interval = dagWSTickIntervalIdle
} else {
interval = dagWSTickInterval
}
t.Reset(interval)
}
}
}
func (h *DagRunHub) getWatermark() wsWatermark {
h.mu.Lock()
defer h.mu.Unlock()
return h.watermark
}
func (h *DagRunHub) setWatermark(v wsWatermark) {
h.mu.Lock()
if v.Runs > h.watermark.Runs {
h.watermark.Runs = v.Runs
}
if v.Steps > h.watermark.Steps {
h.watermark.Steps = v.Steps
}
h.mu.Unlock()
}
func (h *DagRunHub) recordActivity() {
h.mu.Lock()
h.lastEventAt = time.Now()
h.mu.Unlock()
}
func (h *DagRunHub) lastActivityAt() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
return h.lastEventAt
}
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
// OR (recently finished). Watermark devuelto = max rowid visto.
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
conn := h.db.Conn()
if conn == nil {
return nil, nil, since, nil
}
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Runs, cutoff)
if err != nil {
return nil, nil, since, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Steps, cutoff)
if err != nil {
return runs, nil, since, err
}
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
if out.Runs < since.Runs {
out.Runs = since.Runs
}
if out.Steps < since.Steps {
out.Steps = since.Steps
}
return runs, steps, out, nil
}
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
dags, err := h.executor.ListDAGs()
if err != nil {
log.Printf("[dagws] list dags: %v", err)
dags = nil
}
conn := h.db.Conn()
if conn == nil {
return dags, nil, nil, wsWatermark{}, nil
}
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
ORDER BY started_at DESC
LIMIT ?`, dagWSSnapshotRuns)
if err != nil {
return dags, nil, nil, wsWatermark{}, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
ORDER BY rowid ASC`, dagWSSnapshotRuns)
if err != nil {
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
}
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
}
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsRun
var max int64
for rows.Next() {
var r wsRun
var rowid int64
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, r)
}
return out, max, rows.Err()
}
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsStep
var max int64
for rows.Next() {
var s wsStep
var rowid int64
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
&s.DurationMs, &s.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, s)
}
return out, max, rows.Err()
}
func (h *DagRunHub) broadcast(msg wsDagMessage) {
h.mu.Lock()
subs := make([]*dagSubscriber, 0, len(h.subscribers))
for s := range h.subscribers {
subs = append(subs, s)
}
h.mu.Unlock()
for _, s := range subs {
select {
case s.out <- msg:
default:
log.Printf("[dagws] dropping frame for slow subscriber")
}
}
}
// handleDagRunsWS upgrade WS y gestiona lifecycle.
// Endpoint: GET /api/ws/dagruns
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
log.Printf("[dagws] accept: %v", err)
return
}
defer conn.Close(websocket.StatusInternalError, "closing")
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
sub := &dagSubscriber{
conn: conn,
ctx: ctx,
cancel: cancel,
out: make(chan wsDagMessage, 64),
}
hub.register(sub)
defer hub.unregister(sub)
dags, runs, steps, wm, err := hub.fetchSnapshot()
if err != nil {
log.Printf("[dagws] snapshot: %v", err)
conn.Close(websocket.StatusInternalError, "snapshot failed")
return
}
hub.setWatermark(wm)
initial := wsDagMessage{
Type: "snapshot",
Watermark: wm,
Dags: dags,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}
if err := wsjson.Write(ctx, conn, initial); err != nil {
return
}
readErr := make(chan error, 1)
go func() {
for {
var cmd wsDagClientCmd
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
readErr <- err
return
}
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
if err == nil && (len(runs) > 0 || len(steps) > 0) {
hub.setWatermark(wm)
select {
case sub.out <- wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}:
default:
}
}
}
}
}()
for {
select {
case <-ctx.Done():
return
case err := <-readErr:
if err != nil {
return
}
case msg, ok := <-sub.out:
if !ok {
return
}
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
err := wsjson.Write(wctx, conn, msg)
wcancel()
if err != nil {
return
}
}
}
}
}
+39 -72
View File
@@ -72,15 +72,15 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
return runID, err
}
// Setup FN_DAG_ENV temp file for inter-step communication.
dagEnvFile, err := os.CreateTemp("", "fn_dag_env_*")
// Setup DAGU_ENV temp file for inter-step communication.
daguEnvFile, err := os.CreateTemp("", "dagu_env_*")
if err != nil {
e.failRun(runID, err)
return runID, err
}
dagEnvPath := dagEnvFile.Name()
dagEnvFile.Close()
defer os.Remove(dagEnvPath)
daguEnvPath := daguEnvFile.Name()
daguEnvFile.Close()
defer os.Remove(daguEnvPath)
// Track step outputs for ${step_id.stdout} references.
stepOutputs := make(map[string]string)
@@ -116,7 +116,7 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
}
mu.Unlock()
err := e.executeStep(ctx, runID, dag, step, dagEnvPath, stepOutputs, &mu)
err := e.executeStep(ctx, runID, dag, step, daguEnvPath, stepOutputs, &mu)
if err != nil && !step.ContinueOn.Failure {
mu.Lock()
levelFailed = true
@@ -131,11 +131,11 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
// Run handlers.
if runFailed {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Failure, dagEnvPath, stepOutputs)
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Failure, daguEnvPath, stepOutputs)
} else {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Success, dagEnvPath, stepOutputs)
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Success, daguEnvPath, stepOutputs)
}
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Exit, dagEnvPath, stepOutputs)
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Exit, daguEnvPath, stepOutputs)
// Finalize run.
fin := time.Now()
@@ -153,52 +153,25 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
}
// executeStep runs a single step, recording results in the store.
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, dagEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
stepID := generateID()
now := time.Now()
// Resolve command source: function (registry) takes precedence over command/script.
var command string
var stepFunctionID string
var fnRegistryRoot string
if step.Function != "" {
stepFunctionID = step.Function
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
if fnRegistryRoot == "" {
fnRegistryRoot = "/home/lucas/fn_registry"
}
fnBin := os.Getenv("FN_BIN")
if fnBin == "" {
fnBin = fnRegistryRoot + "/fn"
}
parts := []string{fnBin, "run", step.Function}
parts = append(parts, step.Args...)
command = strings.Join(parts, " ")
} else if step.Command != "" {
command = step.Command
} else if step.Script != "" {
command = step.Script
}
e.store.InsertStepResult(&store.DagStepResult{
ID: stepID,
RunID: runID,
StepName: stepName(step),
FunctionID: stepFunctionID,
Status: "running",
StartedAt: &now,
ID: stepID,
RunID: runID,
StepName: stepName(step),
Status: "running",
StartedAt: &now,
})
// Build environment.
env := buildStepEnv(dag, step, dagEnvPath, outputs)
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
if stepFunctionID != "" {
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
// Determine command.
command := step.Command
if command == "" && step.Script != "" {
command = step.Script
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
@@ -209,15 +182,11 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
// so `fn` resolves registry.db correctly via go.mod walk-up.
// Determine working directory.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
if dir == "" && stepFunctionID != "" {
dir = fnRegistryRoot
}
shell := step.Shell
if shell == "" {
@@ -263,8 +232,8 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
mu.Unlock()
}
// Read FN_DAG_ENV for inter-step env propagation.
readDagEnv(dagEnvPath, outputs)
// Read DAGU_ENV for inter-step env propagation.
readDaguEnv(daguEnvPath, outputs)
if status == "failed" {
return fmt.Errorf("exit code %d", result.ExitCode)
@@ -272,10 +241,10 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
return nil
}
func (e *Executor) runHandlers(ctx context.Context, runID string, dag core.DagDefinition, handlers []core.DagStep, dagEnvPath string, outputs map[string]string) {
func (e *Executor) runHandlers(ctx context.Context, runID string, dag core.DagDefinition, handlers []core.DagStep, daguEnvPath string, outputs map[string]string) {
var mu sync.Mutex
for _, step := range handlers {
e.executeStep(ctx, runID, dag, step, dagEnvPath, outputs, &mu)
e.executeStep(ctx, runID, dag, step, daguEnvPath, outputs, &mu)
}
}
@@ -304,7 +273,7 @@ func stepName(s core.DagStep) string {
return s.ID
}
func buildStepEnv(dag core.DagDefinition, step core.DagStep, dagEnvPath string, outputs map[string]string) []string {
func buildStepEnv(dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string) []string {
env := os.Environ()
// Add DAG-level env.
@@ -317,8 +286,8 @@ func buildStepEnv(dag core.DagDefinition, step core.DagStep, dagEnvPath string,
env = append(env, k+"="+v)
}
// Add FN_DAG_ENV path.
env = append(env, "FN_DAG_ENV="+dagEnvPath)
// Add DAGU_ENV path.
env = append(env, "DAGU_ENV="+daguEnvPath)
return env
}
@@ -331,7 +300,7 @@ func resolveStepRefs(command string, outputs map[string]string) string {
return command
}
func readDagEnv(path string, outputs map[string]string) {
func readDaguEnv(path string, outputs map[string]string) {
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
return
@@ -357,15 +326,14 @@ func generateID() string {
// DagInfo summarizes a DAG file for listing.
type DagInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
}
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
@@ -411,11 +379,10 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
Valid: true,
}
// Attach last 5 runs (most recent first).
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
// Attach last run info.
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
info.LastRuns = runs
}
dags = append(dags, info)
-1
View File
@@ -11,7 +11,6 @@
"dependencies": {
"@mantine/core": "^9.0.2",
"@mantine/hooks": "^9.0.2",
"@mantine/notifications": "^9.0.2",
"@tabler/icons-react": "^3.31.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+8 -21
View File
@@ -1,5 +1,5 @@
import { Routes, Route } from "react-router-dom";
import { AppShell, Container, Title, Group, Text, ThemeIcon, Badge } from "@mantine/core";
import { AppShell, Container, Title, Group, Text } from "@mantine/core";
import { IconTopologyRing } from "@tabler/icons-react";
import { DagList } from "./pages/DagList";
import { DagDetail } from "./pages/DagDetail";
@@ -7,27 +7,14 @@ import { RunDetail } from "./pages/RunDetail";
export function App() {
return (
<AppShell header={{ height: 56 }} padding="lg">
<AppShell header={{ height: 50 }} padding="md">
<AppShell.Header>
<Group h="100%" px="lg" justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<ThemeIcon variant="light" color="indigo" size={34} radius="md">
<IconTopologyRing size={20} />
</ThemeIcon>
<div>
<Group gap={6} align="center">
<Title order={4} fw={600}>
dag_engine
</Title>
<Badge variant="light" color="gray" size="xs" radius="sm">
v0.2.0
</Badge>
</Group>
<Text size="xs" c="dimmed" lh={1}>
fn_registry workflow scheduler
</Text>
</div>
</Group>
<Group h="100%" px="md">
<IconTopologyRing size={24} />
<Title order={4}>DAG Engine</Title>
<Text size="xs" c="dimmed">
fn_registry workflow executor
</Text>
</Group>
</AppShell.Header>
@@ -1,9 +1,17 @@
import { Badge } from "@mantine/core";
import { statusColor } from "../theme";
const colorMap: Record<string, string> = {
success: "green",
failed: "red",
running: "blue",
pending: "gray",
cancelled: "yellow",
skipped: "dimmed",
};
export function StatusBadge({ status }: { status: string }) {
return (
<Badge color={statusColor[status] || "gray"} variant="light" size="sm">
<Badge color={colorMap[status] || "gray"} variant="light" size="sm">
{status}
</Badge>
);
@@ -12,8 +12,8 @@ import type { DagStepResult } from "../types";
const iconMap: Record<string, React.ReactNode> = {
success: <IconCircleCheck size={16} color="var(--mantine-color-green-6)" />,
failed: <IconCircleX size={16} color="var(--mantine-color-red-6)" />,
running: <IconLoader size={16} color="var(--mantine-color-indigo-4)" />,
skipped: <IconCircleMinus size={16} color="var(--mantine-color-gray-6)" />,
running: <IconLoader size={16} color="var(--mantine-color-blue-6)" />,
skipped: <IconCircleMinus size={16} color="var(--mantine-color-dimmed)" />,
pending: <IconClock size={16} color="var(--mantine-color-gray-6)" />,
};
@@ -46,7 +46,7 @@ function StepItem({ step }: { step: DagStepResult }) {
}
>
{hasOutput && (
<Collapse expanded={opened}>
<Collapse in={opened}>
<Box mt="xs">
{step.Stdout && (
<Code block mb="xs" style={{ maxHeight: 200, overflow: "auto" }}>
+9 -3
View File
@@ -1,12 +1,18 @@
import "@mantine/core/styles.css";
import { MantineProvider, createTheme } from "@mantine/core";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { FnProvider } from "./FnProvider";
import { App } from "./App";
const theme = createTheme({
primaryColor: "blue",
fontFamily: "system-ui, -apple-system, sans-serif",
});
createRoot(document.getElementById("root")!).render(
<FnProvider>
<MantineProvider theme={theme} defaultColorScheme="dark">
<BrowserRouter>
<App />
</BrowserRouter>
</FnProvider>
</MantineProvider>
);
@@ -9,63 +9,12 @@ import {
Paper,
Alert,
Loader,
CopyButton,
Tooltip,
ActionIcon,
Code,
} from "@mantine/core";
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
import { IconArrowLeft } from "@tabler/icons-react";
import { getRun } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import { StepTimeline } from "../components/StepTimeline";
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
const lines: string[] = [];
const started = run.StartedAt ? new Date(run.StartedAt) : null;
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
const durationMs =
started && finished ? finished.getTime() - started.getTime() : null;
lines.push(`=== DAG run ${run.ID} ===`);
lines.push(`dag: ${run.DagName}`);
lines.push(`path: ${run.DagPath}`);
lines.push(`status: ${run.Status}`);
lines.push(`trigger: ${run.Trigger}`);
lines.push(`started: ${started ? started.toISOString() : "-"}`);
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
lines.push(
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
);
if (run.Error) {
lines.push("");
lines.push("run error:");
lines.push(run.Error);
}
lines.push("");
lines.push(`--- steps (${steps.length}) ---`);
for (const s of steps) {
lines.push("");
lines.push(
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
);
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
if (s.Error) {
lines.push(" error:");
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stdout) {
lines.push(" stdout:");
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stderr) {
lines.push(" stderr:");
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
}
}
return lines.join("\n");
}
import type { RunDetail as RunDetailType } from "../types";
export function RunDetail() {
const { id } = useParams<{ id: string }>();
@@ -151,41 +100,6 @@ export function RunDetail() {
</Text>
)}
</Paper>
<Paper p="md" withBorder>
<Group justify="space-between" mb="sm">
<Title order={4}>Logs</Title>
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copiado" : "Copiar log completo"}
withArrow
position="left"
>
<ActionIcon
variant={copied ? "filled" : "light"}
color={copied ? "teal" : "blue"}
onClick={copy}
aria-label="Copiar logs"
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code
block
style={{
maxHeight: 480,
overflow: "auto",
whiteSpace: "pre",
fontSize: 12,
}}
>
{buildLogText(run, steps || [])}
</Code>
</Paper>
</Stack>
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default defineConfig({
server: {
port: 5175,
proxy: {
"/api": "http://localhost:4200",
"/api": "http://localhost:8090",
},
},
build: {
+9 -32
View File
@@ -4,28 +4,21 @@ go 1.25.0
require (
fn-registry v0.0.0-00010101000000-000000000000
github.com/mattn/go-sqlite3 v1.14.44
nhooyr.io/websocket v1.8.17
github.com/mattn/go-sqlite3 v1.14.37
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.1.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/flatbuffers v25.1.24+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
@@ -33,39 +26,23 @@ require (
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/rs/zerolog v1.35.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
github.com/zalando/go-keyring v0.2.8 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mau.fi/util v0.9.9 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mautrix v0.28.0 // indirect
)
replace fn-registry => ../..
replace fn-registry => /home/lucas/fn_registry
+14 -69
View File
@@ -1,28 +1,18 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
@@ -31,8 +21,6 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -46,8 +34,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -74,14 +60,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
@@ -90,8 +70,6 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -99,33 +77,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
@@ -134,16 +96,10 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
@@ -155,51 +111,44 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -217,7 +166,3 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ=
maunium.net/go/mautrix v0.28.0/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+4 -4
View File
@@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc {
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
resp := map[string]interface{}{
"info": info,
"dag": dag,
"validation": validation,
"recent_runs": runs,
"info": info,
"dag": dag,
"validation": validation,
"runs": runs,
}
writeJSON(w, http.StatusOK, resp)
}
+2 -3
View File
@@ -68,7 +68,7 @@ Commands:
Server options:
--port <port> HTTP port (default: 8090)
--dags-dir <dir> DAGs directory (default: dags)
--dags-dir <dir> DAGs directory (default: ~/dagu/dags)
--db <path> SQLite database path (default: dag_engine.db)
--scheduler Auto-start cron scheduler`)
}
@@ -286,7 +286,6 @@ func cmdServer(args []string) {
executor := NewExecutor(db, cfg.DagsDir)
scheduler := NewScheduler(executor, cfg.DagsDir)
dagRunHub := NewDagRunHub(db, executor)
// Prepare frontend FS.
var feFS iofs.FS
@@ -304,7 +303,7 @@ func cmdServer(args []string) {
}
mux := http.NewServeMux()
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
RegisterAPI(mux, executor, scheduler, feFS)
handler := corsMiddleware(loggingMiddleware(mux))
+28 -63
View File
@@ -2,43 +2,15 @@ package store
import (
"database/sql"
"embed"
_ "embed"
"fmt"
"io/fs"
"sort"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// applyMigrations executes every embedded migrations/*.sql in order.
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil {
return err
}
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil {
return fmt.Errorf("%s: read: %w", f, err)
}
if _, err := conn.Exec(string(b)); err != nil {
if strings.Contains(err.Error(), "duplicate column") ||
strings.Contains(err.Error(), "already exists") {
continue
}
return fmt.Errorf("%s: %w", f, err)
}
}
return nil
}
//go:embed migrations/001_init.sql
var migrationSQL string
// DB wraps a SQLite connection for DAG run persistence.
type DB struct {
@@ -52,7 +24,7 @@ func Open(path string) (*DB, error) {
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", path, err)
}
if err := applyMigrations(conn); err != nil {
if _, err := conn.Exec(migrationSQL); err != nil {
conn.Close()
return nil, fmt.Errorf("store: migrate: %w", err)
}
@@ -64,24 +36,18 @@ func (db *DB) Close() error {
return db.conn.Close()
}
// Conn exposes the underlying *sql.DB for read-only queries from other
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
func (db *DB) Conn() *sql.DB {
return db.conn
}
// --- DagRun CRUD ---
// DagRun mirrors infra.DagRun for the store layer.
type DagRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
ID string
DagName string
DagPath string
Status string
Trigger string
StartedAt time.Time
FinishedAt *time.Time
Error string
}
// CreateRun inserts a new run record.
@@ -157,18 +123,17 @@ func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error)
// DagStepResult mirrors infra.DagStepResult for the store layer.
type DagStepResult struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
FunctionID string `json:"function_id,omitempty"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
ID string
RunID string
StepName string
Status string
ExitCode int
Stdout string
Stderr string
StartedAt *time.Time
FinishedAt *time.Time
DurationMs int64
Error string
}
// InsertStepResult inserts a new step result.
@@ -183,9 +148,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
finishedAt = &s
}
_, err := db.conn.Exec(
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
startedAt, finishedAt, r.DurationMs, r.Error,
)
return err
@@ -208,7 +173,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s
// ListStepResults returns all step results for a given run.
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
rows, err := db.conn.Query(
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
)
if err != nil {
@@ -220,7 +185,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
for rows.Next() {
var r DagStepResult
var startedAt, finishedAt sql.NullString
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
return nil, err
}
+1 -15
View File
@@ -2,7 +2,6 @@
name: shaders_lab
lang: cpp
domain: gfx
version: 0.1.0
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
@@ -31,11 +30,8 @@ uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "apps/shaders_lab"
dir_path: "cpp/apps/shaders_lab"
repo_url: ""
icon:
phosphor: "palette"
accent: "#ea580c"
---
## Arquitectura
@@ -106,13 +102,3 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
@@ -1,80 +0,0 @@
---
name: chrome_load_extensions
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "chrome_load_extensions [--port N] [--profile DIR] --ext PATH [--ext PATH ...] [--proxy URL] [--url URL]"
description: "Lanza Chrome con extensiones unpacked via --load-extension (WSL2→Windows chrome.exe, paths traducidos, join sin echo, setsid anti-exit-144). OJO: --load-extension SOLO funciona en Chrome for Testing/Chromium/Dev. En Chrome STABLE 138+ esta DESACTIVADO (feature DisableLoadExtensionCommandLineSwitch + bloqueo duro en 148) y carga 0 extensiones aunque el cmdline sea correcto. Para Chrome stable usar install via Web Store (1-clic, persiste en perfil) o enterprise policy ExtensionInstallForcelist (requiere HKLM/HKCU Policies escribible — denegado en maquinas gestionadas)."
tags: [chrome, cdp, browser, extensions, wsl2, navegator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
params:
- name: "--port N"
desc: "Puerto de remote debugging CDP. Default: 9222."
- name: "--profile DIR"
desc: "Chrome user-data-dir. Acepta ruta Windows (C:\\...) o ruta WSL/Linux (se traduce via wslpath -w). Default: C:\\Users\\<USERNAME>\\AppData\\Local\\fn-chrome-cdp-profile (WSL2) o /tmp/fn-chrome-cdp-profile (Linux nativo)."
- name: "--ext PATH"
desc: "Ruta a un directorio de extensión unpacked. Repetible. Acepta ruta Windows (se pasa intacta) o ruta WSL/Linux (se traduce via wslpath -w). Obligatorio al menos uno."
- name: "--proxy URL"
desc: "Proxy opcional, ej. http://127.0.0.1:8889. Agrega --proxy-server=URL a Chrome."
- name: "--url URL"
desc: "URL inicial opcional para abrir con --new-window."
output: "PID del proceso Chrome lanzado (stdout). Mensajes de estado en stderr. CDP listo en 127.0.0.1:<port>."
file_path: "bash/functions/browser/chrome_load_extensions.sh"
---
## Ejemplo
```bash
source bash/functions/browser/chrome_load_extensions.sh
chrome_load_extensions \
--port 9222 \
--profile 'C:\Users\lucas\AppData\Local\fn-chrome-cdp-profile' \
--ext 'C:\Users\lucas\hls-dl-ext' \
--ext 'C:\Users\lucas\ubol' \
--proxy http://127.0.0.1:8889 \
--url https://www.gnularetro.cc/
```
Sin proxy ni URL, sólo extensiones:
```bash
source bash/functions/browser/chrome_load_extensions.sh
pid=$(chrome_load_extensions \
--ext '/home/lucas/dev/hls-dl-ext' \
--ext '/home/lucas/dev/ubol')
# Paths WSL traducidos automáticamente a Windows.
# CDP listo en 127.0.0.1:9222.
echo "Chrome PID: $pid"
```
## Cuando usarla
Cuando necesites Chrome CDP con extensiones unpacked cargadas (HLS downloader, uBlock Origin, extensiones en desarrollo) y `chrome_launch_go_browser` no sirve porque hardcodea `--disable-extensions`. WSL2→Windows. Ideal para sesiones de navegator con proxy + extensión activa.
## Gotchas
- **MUERTO en Chrome STABLE 138+ (validado 2026-05-30, Chrome 148)**: `--load-extension` NO carga nada en el canal stable, ni con `--disable-extensions-except` ni con `--disable-features=DisableLoadExtensionCommandLineSwitch`. `chrome://version` muestra el flag correcto pero `chrome://extensions` sale vacío. Google lo bloqueó duro en stable. La función SOLO sirve en **Chrome for Testing / Chromium / Dev/Canary**, donde el switch sigue activo. Para stable: ver opciones abajo.
- **Instalar en Chrome STABLE (las que SÍ funcionan)**:
1. **Web Store 1-clic** — abre la página del store en el perfil CDP, el humano da "Añadir a Chrome". Persiste en el perfil para siempre (futuros lanzamientos ya con la extensión, sin flags). El popup de confirmación es UI del navegador (no DOM) → NO es CDP-clickable, requiere gesto humano. Único método no-admin que persiste por-perfil.
2. **Enterprise policy** `ExtensionInstallForcelist` (HKCU/HKLM `\Software\Policies\Google\Chrome`) — force-install sin clic desde el store, browser-wide. El key `Policies\Google\Chrome` puede dar "Access denied" al escribir (visto 2026-05-30 incluso en máquina personal vía reg.exe/PowerShell desde WSL — Chrome/Windows protege el subárbol Policies). Si funciona, requiere relanzar Chrome para que descargue del store. Método global (afecta todos los perfiles).
3. Extensiones **unpacked custom** (no en store, ej. un HLS downloader propio) en stable: no hay vía no-admin. Empaquetar a CRX + self-host `update_url` + policy, o usar Chrome for Testing. A menudo innecesario si la lógica vive fuera (ej. `grab_stream.py` descarga sin extensión).
- **Combo flags (solo Chrome for Testing/dev)**: requiere AMBOS `--load-extension=p1,p2` Y `--disable-extensions-except=p1,p2` juntos + `--disable-features=DisableLoadExtensionCommandLineSwitch`. **NUNCA `--disable-extensions`** (desactiva todo).
- **join sin `echo`**: rutas Windows `C:\Users\...` tienen `\U`; el `echo` de zsh (o sh con xpg_echo) lo interpreta como escape unicode y trunca la ruta a `C:`. La función usa acumulador `+=`, no `echo`. Verificable en `chrome://version` (debe verse el path completo, no `--load-extension=C:`).
- **exit 144 en Bash tool**: si el proceso Chrome retiene el pipe stdout, la herramienta devuelve exit 144. Esta función lanza con `setsid ... </dev/null >log 2>&1 &` + `disown` para desacoplar completamente. El log queda en `/tmp/chrome_ext_<port>.log`.
- **WSL2: traducir paths con `wslpath -w`**: los paths de `--ext` y `--profile` que sean rutas Linux se traducen automáticamente. Las rutas Windows (`C:\...`) se pasan intactas. `wslpath` debe estar disponible (estándar en WSL2 desde Windows 10 1903+).
- **Perfil ya abierto**: si Chrome ya tiene ese perfil abierto, relanzar añade una ventana extra a la misma instancia. La función detecta si CDP ya responde en el puerto y avisa por stderr, pero procede igualmente.
- **Web Store vs unpacked**: instalar extensiones desde la Web Store (un clic) persiste en el perfil sin necesidad de flags y sobrevive reinicios. Esta función es para extensiones unpacked en desarrollo o que no están en la Web Store. Si usas ambas, los flags no interfieren con las instaladas del store.
- **zsh globbing**: `--remote-allow-origins=*` está dentro de comillas en la función, no se expande. Si lo pasas desde la línea de comandos, entrecomillarlo.
- **Proxy + extensión**: si usas proxy para captura de tráfico (Burp, mitmproxy, gost), el proxy se aplica a toda la sesión Chrome, incluyendo el tráfico de las extensiones.
@@ -1,161 +0,0 @@
#!/usr/bin/env bash
# chrome_load_extensions — lanza Chrome (WSL2→Windows chrome.exe) con extensiones unpacked cargadas en un perfil CDP.
# Chrome 148+: requiere --load-extension=<paths> Y --disable-extensions-except=<same paths> juntos.
# NUNCA pasar --disable-extensions (desactiva todo, incluyendo las que quieres cargar).
chrome_load_extensions() {
local port=9222
local profile=""
local proxy=""
local url=""
local -a ext_paths=()
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
port="$2"; shift 2 ;;
--profile)
profile="$2"; shift 2 ;;
--ext)
ext_paths+=("$2"); shift 2 ;;
--proxy)
proxy="$2"; shift 2 ;;
--url)
url="$2"; shift 2 ;;
--*)
echo "chrome_load_extensions: flag desconocido: $1" >&2; return 1 ;;
*)
# Positional = extra ext path
ext_paths+=("$1"); shift ;;
esac
done
if [[ ${#ext_paths[@]} -eq 0 ]]; then
echo "chrome_load_extensions: se requiere al menos un --ext PATH de extension unpacked" >&2
return 1
fi
# --- Detectar chrome.exe ---
local chrome_bin=""
if command -v chrome.exe &>/dev/null; then
chrome_bin="chrome.exe"
elif [[ -f "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
elif [[ -f "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
else
echo "chrome_load_extensions: chrome.exe no encontrado en PATH ni en rutas conocidas" >&2
return 1
fi
# --- Detectar WSL2 ---
local wsl2=0
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
wsl2=1
fi
# --- Traducir paths de extensiones a Windows si hace falta ---
local -a win_ext_paths=()
for p in "${ext_paths[@]}"; do
if [[ $wsl2 -eq 1 ]] && [[ "$p" != [A-Za-z]:\\* ]]; then
# Path Linux → traducir a Windows
local win_p
win_p=$(wslpath -w "$p" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$p' falló" >&2
return 1
}
win_ext_paths+=("$win_p")
else
win_ext_paths+=("$p")
fi
done
# --- Resolver perfil ---
if [[ -z "$profile" ]]; then
# Default: perfil canónico fn-chrome-cdp-profile en Windows
local win_user="${USERNAME:-${USER:-lucas}}"
if [[ $wsl2 -eq 1 ]]; then
profile="C:\\Users\\${win_user}\\AppData\\Local\\fn-chrome-cdp-profile"
else
profile="/tmp/fn-chrome-cdp-profile"
fi
elif [[ $wsl2 -eq 1 ]] && [[ "$profile" != [A-Za-z]:\\* ]]; then
# Path Linux del perfil → traducir a Windows
profile=$(wslpath -w "$profile" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$profile' falló" >&2
return 1
}
fi
# --- Construir lista de paths separada por coma (para Chrome) ---
# Chrome usa coma como separador en --load-extension y --disable-extensions-except.
# NO usar `echo` para el join: rutas Windows como C:\Users tienen \U, y el echo de
# zsh (o sh con xpg_echo) interpreta \U como escape unicode y trunca la ruta a "C:".
# Acumulador con += y printf-safe, sin interpretacion de backslashes.
local ext_list=""
local p
for p in "${win_ext_paths[@]}"; do
ext_list+="${ext_list:+,}${p}"
done
# --- Construir args de Chrome ---
local -a args=(
"--remote-debugging-port=${port}"
"--user-data-dir=${profile}"
"--no-first-run"
"--no-default-browser-check"
"--remote-allow-origins=*"
"--load-extension=${ext_list}"
"--disable-extensions-except=${ext_list}"
# Chrome 137+ activa por defecto el feature DisableLoadExtensionCommandLineSwitch,
# que IGNORA silenciosamente --load-extension. Hay que desactivarlo o las
# extensiones unpacked no cargan (chrome://extensions sale vacio).
"--disable-features=DisableLoadExtensionCommandLineSwitch"
)
# WSL2: bind en 0.0.0.0 para que sea accesible desde la red WSL
if [[ $wsl2 -eq 1 ]]; then
args+=("--remote-debugging-address=0.0.0.0")
fi
if [[ -n "$proxy" ]]; then
args+=("--proxy-server=${proxy}")
fi
if [[ -n "$url" ]]; then
args+=("--new-window" "$url")
fi
# --- Revisar si CDP ya responde en el puerto ---
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
echo "chrome_load_extensions: CDP ya activo en puerto ${port}; lanzando ventana extra" >&2
fi
# --- Lanzar Chrome desacoplado del proceso padre ---
# setsid + redirección evita el exit 144 en el Bash tool (el pipe no queda retenido).
setsid "$chrome_bin" "${args[@]}" </dev/null >"/tmp/chrome_ext_${port}.log" 2>&1 &
local chrome_pid=$!
disown "$chrome_pid"
echo "chrome_load_extensions: Chrome lanzado PID=${chrome_pid} puerto=${port}" >&2
# --- Esperar a que CDP esté listo (hasta 15 segundos) ---
local deadline=$(( $(date +%s) + 15 ))
local ready=0
while [[ $(date +%s) -lt $deadline ]]; do
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
ready=1
break
fi
sleep 0.5
done
if [[ $ready -eq 1 ]]; then
echo "chrome_load_extensions: CDP listo en 127.0.0.1:${port}"
else
echo "chrome_load_extensions: advertencia — CDP no respondió en 15s en puerto ${port}; Chrome puede estar iniciando lentamente" >&2
fi
echo "$chrome_pid"
}
@@ -1,81 +0,0 @@
---
name: install_chromium_proxy_extension
kind: function
lang: bash
domain: browser
version: 1.0.0
purity: impure
signature: install_chromium_proxy_extension --ext-dir DIR [--name NAME] [--stable-dir DIR] [--uninstall]
description: "Instala una extension desempaquetada de Chromium en todos los perfiles del usuario de forma persistente, escribiendo un fragmento en /etc/chromium.d/ que el wrapper de Chromium carga en cada arranque. Pensado para distribuir la extension de toggle de proxy de web_proxy sin Web Store, pero sirve para cualquier extension desempaquetada."
tags: [web-proxy, chromium, extension, browser, proxy, install]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
params:
- name: --ext-dir
desc: "Directorio de la extension desempaquetada de origen (debe contener manifest.json). Obligatorio salvo en --uninstall."
- name: --name
desc: "Nombre del fragmento en /etc/chromium.d/ (default web_proxy_ext). Identifica esta instalacion para poder desinstalarla."
- name: --stable-dir
desc: "Ruta estable donde se copia la extension, independiente del repo (default ~/.web_proxy/extension). --load-extension apunta aqui."
- name: --uninstall
desc: "Elimina el fragmento de /etc/chromium.d/ y la copia estable. No requiere --ext-dir."
output: "JSON en stdout: {installed|uninstalled, name, stable_dir, chromiumd, ext_id}. Requiere sudo para escribir en /etc/chromium.d/."
file_path: bash/functions/browser/install_chromium_proxy_extension.sh
---
# install_chromium_proxy_extension
Instala una extension desempaquetada de Chromium en **todos los perfiles** del
usuario, de forma persistente, sin pasar por la Chrome Web Store.
## Ejemplo
```bash
# Instalar la extension de toggle de proxy de web_proxy en todos los perfiles
install_chromium_proxy_extension --ext-dir /home/enmanuel/fn_registry/apps/web_proxy/extension
# Desinstalarla
install_chromium_proxy_extension --uninstall
# Otra extension, con nombre y ruta estable propios
install_chromium_proxy_extension --ext-dir ~/mis-extensiones/foo --name foo_ext --stable-dir ~/.local/share/foo_ext
```
Tras instalar, cierra y vuelve a abrir Chromium: la extension aparece en todos
los perfiles, incluidos los que se creen despues.
## Cuando usarla
Cuando necesitas que una extension desempaquetada este presente en todos los
perfiles de Chromium de una maquina (por ejemplo, un toggle de proxy de captura
preconfigurado) y no quieres publicarla en la Web Store ni cargarla a mano en
cada perfil. Es la pieza que hace que `web_proxy` quede "a un clic" en cualquier
ventana de Chromium.
## Gotchas
- **Requiere sudo** para escribir en `/etc/chromium.d/`. Ten las credenciales
cacheadas (`sudo -v`) antes de invocarla de forma no interactiva.
- **Solo para el wrapper de Chromium de Debian/Ubuntu** (paquete `chromium`,
no snap ni Google Chrome). El wrapper hace `source /etc/chromium.d/*` en cada
arranque. Comprueba con `head -1 $(command -v chromium)` que es un script.
- **`--enable-remote-extensions` es imprescindible** en estos builds: sin el,
el wrapper anade `--disable-extensions-except` y `--disable-background-networking`,
que deshabilitan toda extension que no venga por `--load-extension`. El
fragmento generado lo incluye; por eso las demas extensiones del usuario
siguen funcionando.
- La extension se carga **desempaquetada** (`--load-extension`), no como `.crx`
firmado. Chromium puede mostrar un aviso de "extensiones en modo desarrollador".
El force-install via managed policy con `.crx` local + `update_url file://`
no funciona con este wrapper (lo bloquea `--disable-extensions-except`).
- El ID de la extension depende de `--stable-dir` (se deriva del path). Si
cambias la ruta estable, el ID cambia.
- No reinicia Chromium: los cambios aplican en el siguiente arranque del
navegador.
## Capability growth log
- v1.0.0 (2026-06-02) — version inicial. Instala/desinstala extension global via /etc/chromium.d con --enable-remote-extensions + --load-extension.
@@ -1,103 +0,0 @@
#!/usr/bin/env bash
# install_chromium_proxy_extension — instala una extension desempaquetada de
# Chromium en TODOS los perfiles del usuario, de forma persistente, escribiendo
# un fragmento en /etc/chromium.d/ que el wrapper de Chromium carga en cada
# arranque.
#
# Por que /etc/chromium.d en vez de managed policy con .crx force_installed:
# el wrapper de Chromium de Debian/Ubuntu (xtradeb y derivados), cuando NO se
# pasa --enable-remote-extensions, anade --disable-extensions-except=<solo las
# de --load-extension> y --disable-background-networking. Eso deshabilita
# cualquier extension force_installed por policy y bloquea su update check. La
# via fiable es habilitar --enable-remote-extensions y cargar la extension
# desempaquetada con --load-extension, ambos inyectados de forma global desde
# /etc/chromium.d/, que el wrapper hace `source` en cada lanzamiento.
install_chromium_proxy_extension() {
local ext_dir=""
local name="web_proxy_ext"
local stable_dir="$HOME/.web_proxy/extension"
local chromiumd="/etc/chromium.d"
local uninstall="no"
# Permite sudo no interactivo via SUDO_ASKPASS (sudo -A) cuando se ejecuta
# sin terminal (agentes, CI). Con terminal interactivo usa sudo normal.
local SUDO="sudo"
[[ -n "${SUDO_ASKPASS:-}" ]] && SUDO="sudo -A"
while [[ $# -gt 0 ]]; do
case "$1" in
--ext-dir) ext_dir="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--stable-dir) stable_dir="$2"; shift 2 ;;
--uninstall) uninstall="yes"; shift ;;
*) echo "ERROR: argumento desconocido: $1" >&2; return 1 ;;
esac
done
if [[ ! -d "$chromiumd" ]]; then
echo "ERROR: $chromiumd no existe. Este Chromium no usa el wrapper con /etc/chromium.d." >&2
echo " Comprueba 'head -1 \$(command -v chromium)'; si no es un wrapper shell, usa otra via." >&2
return 1
fi
# Desinstalacion: quitar el fragmento global y la copia estable.
if [[ "$uninstall" == "yes" ]]; then
$SUDO rm -f "${chromiumd}/${name}" || {
echo "ERROR: no se pudo eliminar ${chromiumd}/${name} (requiere sudo)." >&2
return 1
}
rm -rf "$stable_dir"
printf '{"uninstalled": true, "name": "%s"}\n' "$name"
return 0
fi
# Instalacion: validar la extension de origen.
if [[ -z "$ext_dir" || ! -f "${ext_dir}/manifest.json" ]]; then
echo "ERROR: --ext-dir debe apuntar a un directorio con manifest.json." >&2
return 1
fi
# Copiar la extension a una ubicacion estable, independiente del repo, para
# que --load-extension no se rompa si el repo se mueve o se limpia.
mkdir -p "$stable_dir" || {
echo "ERROR: no se pudo crear $stable_dir." >&2
return 1
}
# Vaciar destino y copiar el contenido del origen.
rm -rf "${stable_dir:?}/"* 2>/dev/null
cp -r "${ext_dir}/." "$stable_dir/" || {
echo "ERROR: no se pudo copiar la extension a $stable_dir." >&2
return 1
}
# Escribir el fragmento que el wrapper carga en cada arranque. Se hace via
# archivo temporal + sudo cp para no exponer el contenido por una tuberia.
local tmp
tmp="$(mktemp)"
printf 'export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --enable-remote-extensions --load-extension=%s"\n' "$stable_dir" > "$tmp"
if ! $SUDO cp "$tmp" "${chromiumd}/${name}"; then
rm -f "$tmp"
echo "ERROR: no se pudo escribir ${chromiumd}/${name} (requiere sudo)." >&2
return 1
fi
$SUDO chmod 0644 "${chromiumd}/${name}" 2>/dev/null
rm -f "$tmp"
# ID de extension desempaquetada (deterministico: sha256 del path estable).
local ext_id
ext_id="$(python3 - "$stable_dir" <<'PY' 2>/dev/null
import hashlib, sys
h = hashlib.sha256(sys.argv[1].encode()).hexdigest()[:32]
print(''.join(chr(ord('a') + int(c, 16)) for c in h))
PY
)"
printf '{"installed": true, "name": "%s", "stable_dir": "%s", "chromiumd": "%s/%s", "ext_id": "%s"}\n' \
"$name" "$stable_dir" "$chromiumd" "$name" "$ext_id"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
install_chromium_proxy_extension "$@"
fi
@@ -1,74 +0,0 @@
---
name: launch_chromium_proxy
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "launch_chromium_proxy [--proxy URL] [--profile DIR] [--url URL] [--ca-cert PATH] [--extra \"ARGS\"]"
description: "Lanza Chromium (o Chrome) apuntando a un proxy HTTP/HTTPS local con un perfil completamente aislado del perfil real del usuario. Pensado para capturar trafico con un proxy de interceptacion (mitmproxy, Burp Suite) sin contaminar la sesion normal de navegacion. Emite un JSON con el PID del proceso lanzado."
tags: [chromium, chrome, proxy, mitmproxy, burp, browser, web-proxy, intercept, tls]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--proxy URL"
desc: "URL del proxy HTTP/HTTPS local, ej. http://127.0.0.1:8080. Se pasa a Chromium como --proxy-server=URL. Default: http://127.0.0.1:8080."
- name: "--profile DIR"
desc: "Directorio de perfil aislado para Chromium (--user-data-dir). Se crea automaticamente si no existe. Default: /tmp/chromium-proxy. Usar un path distinto por sesion si se quiere aislamiento total entre corridas."
- name: "--url URL"
desc: "URL inicial a abrir al arrancar el navegador. Opcional. Si se omite, Chromium abre su pagina de nueva pestana."
- name: "--ca-cert PATH"
desc: "Ruta a un CA cert PEM del proxy (ej. ~/.mitmproxy/mitmproxy-ca-cert.pem). Si se pasa, la funcion NO agrega --ignore-certificate-errors y asume que el usuario ya importo el CA en el perfil o en el sistema. Si se omite, se agrega --ignore-certificate-errors automaticamente para que el proxy MITM no rompa HTTPS (menos seguro)."
- name: "--extra \"ARGS\""
desc: "Flags extra que se pasan directamente a chromium, ej. --extra \"--disable-gpu --window-size=1280,800\". El valor completo debe ir entre comillas."
output: "JSON en stdout: {\"pid\": <pid>, \"browser\": \"<binario>\", \"proxy\": \"<url>\", \"profile\": \"<dir>\", \"log\": \"<ruta_log>\"}. Mensajes de estado e informacion de CA en stderr. El navegador corre en background desacoplado de la sesion. Exit codes: 0=lanzado correctamente, 1=binario no encontrado o argumento invalido o error al crear el directorio de perfil."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/launch_chromium_proxy.sh"
---
## Ejemplo
```bash
source bash/functions/browser/launch_chromium_proxy.sh
# Caso mas comun: interceptar trafico con mitmproxy corriendo en 8080
# (sin CA instalado: --ignore-certificate-errors se aplica automaticamente)
result=$(launch_chromium_proxy --proxy http://127.0.0.1:8080 --url https://httpbin.org/get)
echo "$result"
# {"pid":12345,"browser":"chromium","proxy":"http://127.0.0.1:8080","profile":"/tmp/chromium-proxy","log":"/tmp/chromium-proxy-12345.log"}
# Con CA cert instalado (mitmproxy): sin --ignore-certificate-errors
launch_chromium_proxy \
--proxy http://127.0.0.1:8080 \
--ca-cert ~/.mitmproxy/mitmproxy-ca-cert.pem \
--profile /tmp/mitm-session \
--url https://api.ejemplo.com/v1/test
# Con Burp Suite en puerto 8081, perfil aislado y ventana de tamano fijo
launch_chromium_proxy \
--proxy http://127.0.0.1:8081 \
--profile /tmp/burp-session \
--extra "--window-size=1440,900" \
--url https://app.objetivo.com
```
## Cuando usarla
Cuando necesitas capturar y analizar trafico HTTPS de un navegador con mitmproxy, Burp Suite u otro proxy de interceptacion, sin tocar el perfil real del usuario ni sus cookies/credenciales guardadas. Ideal antes de hacer analisis de trafico de una app web o API, o al reproducir un flujo autenticado desde una sesion limpia.
## Gotchas
- **Deteccion de binario en orden**: la funcion prueba `chromium`, `chromium-browser`, `google-chrome-stable`, `google-chrome`. En sistemas donde solo existe `google-chrome`, ese sera el binario usado. Si ninguno esta en el PATH, retorna exit 1. Instalar con `sudo apt install chromium` o `chromium-browser`.
- **`--ignore-certificate-errors` sin `--ca-cert`**: este flag desactiva toda la validacion TLS del navegador. Es conveniente para empezar rapido, pero reduce la seguridad de la sesion. Para produccion o analisis de seguridad serio, instalar el CA del proxy en el sistema (`sudo cp mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt && sudo update-ca-certificates`) o en el perfil de Chromium (chrome://settings/certificates), y pasar `--ca-cert` para que la funcion omita el flag inseguro.
- **`--proxy-bypass-list="<-loopback>"`**: fuerza que el trafico loopback (127.0.0.1, localhost) TAMBIEN pase por el proxy. Sin esto, Chromium excluye loopback del proxy por defecto y no veras esas peticiones en mitmproxy. Si quieres el comportamiento estandar (excluir loopback), elimina este flag via `--extra`.
- **Perfil persistente entre sesiones**: el perfil en `/tmp/chromium-proxy` (o el directorio que elijas) persiste entre ejecuciones. Si quieres una sesion 100% limpia cada vez, pasa `--profile /tmp/chromium-proxy-$$` (usa el PID del shell como sufijo) o borra el directorio antes de llamar a la funcion.
- **`setsid` + `disown`**: el navegador se lanza desacoplado de la sesion del agente. Si la shell/sesion que llamo a la funcion termina, el proceso Chromium sigue vivo. Para matarlo, usar `kill <pid>` con el PID del JSON de salida.
- **Log del navegador**: stdout y stderr de Chromium se redirigen a `/tmp/chromium-proxy-<pid>.log`. Si el navegador no arranca, revisar ese archivo para ver el error.
- **Chrome STABLE 138+**: al igual que `chrome_load_extensions`, algunos flags de automatizacion estan bloqueados en Chrome STABLE. Para interceptacion de trafico `--proxy-server` y `--user-data-dir` siguen funcionando en todas las versiones. Esta funcion es compatible con Chrome/Chromium en cualquier canal.
- **Multiples instancias**: si ya hay una instancia de Chromium corriendo con el mismo `--user-data-dir`, Chromium puede reusar esa instancia en lugar de abrir una nueva. Usar un directorio de perfil distinto por sesion concurrente.
@@ -1,118 +0,0 @@
#!/usr/bin/env bash
# launch_chromium_proxy — Lanza Chromium apuntando a un proxy HTTP/HTTPS local con perfil aislado.
launch_chromium_proxy() {
local proxy_url="http://127.0.0.1:8080"
local profile_dir="/tmp/chromium-proxy"
local start_url=""
local ca_cert=""
local extra_args=""
local ext_dir=""
# Parsear argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--proxy)
proxy_url="$2"; shift 2 ;;
--profile)
profile_dir="$2"; shift 2 ;;
--url)
start_url="$2"; shift 2 ;;
--ca-cert)
ca_cert="$2"; shift 2 ;;
--ext)
ext_dir="$2"; shift 2 ;;
--extra)
extra_args="$2"; shift 2 ;;
*)
echo "ERROR: argumento desconocido: $1" >&2
return 1 ;;
esac
done
# Detectar binario del navegador
local browser_bin=""
for candidate in chromium chromium-browser google-chrome-stable google-chrome; do
if command -v "$candidate" &>/dev/null; then
browser_bin="$candidate"
break
fi
done
if [[ -z "$browser_bin" ]]; then
echo "ERROR: no se encontro ningun binario Chromium/Chrome en el PATH." >&2
echo " Probados: chromium, chromium-browser, google-chrome-stable, google-chrome." >&2
return 1
fi
# Crear directorio de perfil si no existe
if [[ ! -d "$profile_dir" ]]; then
mkdir -p "$profile_dir" || {
echo "ERROR: no se pudo crear el directorio de perfil: $profile_dir" >&2
return 1
}
fi
# Construir argumentos del navegador
local args=(
"--user-data-dir=${profile_dir}"
"--no-first-run"
"--no-default-browser-check"
)
# Proxy fijo opcional. Con "--proxy none" (o vacio) no se fija proxy en el
# cmdline: util cuando una extension de proxy gestiona la conexion (toggle).
if [[ -n "$proxy_url" && "$proxy_url" != "none" ]]; then
args+=("--proxy-server=${proxy_url}" "--proxy-bypass-list=<-loopback>")
fi
# Cargar una extension desempaquetada (--load-extension). Funciona en
# Chromium (no en Chrome stable 138+). Para persistencia en todos los
# perfiles se usa managed policy en su lugar.
if [[ -n "$ext_dir" ]]; then
args+=("--load-extension=${ext_dir}")
fi
# Manejo de certificados TLS
if [[ -n "$ca_cert" ]]; then
# El usuario instalo el CA en el perfil; no ignorar errores de certificado.
# (El CA se instala en el sistema o en el perfil antes de lanzar.)
echo "INFO: CA cert declarado: $ca_cert" >&2
echo "INFO: Asegurate de haber importado el CA en el perfil o en el sistema antes de navegar HTTPS." >&2
else
# Sin CA cert: ignorar errores de certificado para que mitmproxy/Burp funcionen sin configuracion extra.
# ADVERTENCIA: esto desactiva la validacion TLS completa del navegador.
args+=("--ignore-certificate-errors")
echo "WARN: --ignore-certificate-errors activo. Usa --ca-cert si instalaste el CA del proxy." >&2
fi
# URL inicial opcional
if [[ -n "$start_url" ]]; then
args+=("$start_url")
fi
# Argumentos extra pasados por el usuario
# shellcheck disable=SC2206
local extra_arr=()
if [[ -n "$extra_args" ]]; then
read -r -a extra_arr <<< "$extra_args"
args+=("${extra_arr[@]}")
fi
# Log temporal para stderr/stdout del navegador
local log_file="/tmp/chromium-proxy-$$.log"
# Lanzar en background desacoplado de la sesion del agente
setsid "$browser_bin" "${args[@]}" </dev/null >"$log_file" 2>&1 &
local browser_pid=$!
disown "$browser_pid"
# Emitir JSON con informacion del proceso lanzado
printf '{"pid":%d,"browser":"%s","proxy":"%s","profile":"%s","log":"%s"}\n' \
"$browser_pid" "$browser_bin" "$proxy_url" "$profile_dir" "$log_file"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
launch_chromium_proxy "$@"
fi
@@ -1,88 +0,0 @@
---
name: query_mitm_flows
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "query_mitm_flows(file_or_glob: string, filter?: string, har?: string, mitmdump?: string) -> void"
description: "Consulta capturas .mitm guardadas con mitmproxy: vuelca los flujos que coinciden con un filtro de mitmproxy a stdout, o exporta la captura a HAR. Acepta uno o varios archivos .mitm (incluyendo globs expandidos por el shell). Autodetecta mitmdump en PATH y $HOME/.local/bin."
tags: [bash, cybersecurity, mitmproxy, web-proxy, query, har, capture, traffic, proxy, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_or_glob
desc: "Ruta a un archivo .mitm o glob expandido por el shell (ej. ~/captures/traffic-*.mitm). Acepta múltiples archivos como argumentos posicionales."
- name: filter
desc: "Expresión de filtro mitmproxy (ej. '~m POST & ~u /api', '~c 500', '~d example.com'). Si se omite, vuelca todos los flujos."
- name: har
desc: "Ruta de salida para exportar en formato HAR (--set hardump=OUT). Si se omite, el volcado va a stdout."
- name: mitmdump
desc: "Ruta al binario mitmdump. Si se omite, autodetecta en PATH y luego en $HOME/.local/bin/mitmdump."
output: "Vuelca los flujos capturados a stdout (modo default) o exporta a un archivo HAR (con --har); informa la ruta de exportación en stderr al terminar. Exit code de mitmdump propagado."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/query_mitm_flows.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/query_mitm_flows.sh
# Volcar todos los flujos de una captura
query_mitm_flows ~/captures/traffic-20260602.mitm
# Filtrar solo peticiones POST a /api (glob expandido por el shell)
query_mitm_flows ~/captures/traffic-20260602-*.mitm --filter "~m POST & ~u /api"
# Ver solo respuestas con código 500
query_mitm_flows session.mitm --filter "~c 500"
# Exportar a HAR para abrir en el Network tab del browser DevTools
query_mitm_flows ~/captures/traffic-20260602.mitm --har salida.har
# Exportar a HAR con filtro aplicado
query_mitm_flows session.mitm --filter "~d example.com" --har example_flows.har
# Especificar mitmdump manualmente (ej. en un venv de Python)
query_mitm_flows session.mitm --mitmdump ~/.venv/bin/mitmdump --filter "~m POST"
```
## Cuando usarla
Cuando hayas capturado tráfico HTTP/HTTPS con mitmproxy y necesites consultarlo después sin abrir una interfaz interactiva: filtrar flujos específicos por método, dominio, URL o código de respuesta, o exportar la captura a HAR para análisis posterior en herramientas externas (browser DevTools, Insomnia, Postman). Úsala desde scripts de análisis automatizados o pipelines de revisión de seguridad.
## Gotchas
**Sintaxis de filtros mitmproxy** — los filtros se pasan como expresión entre comillas:
| Operador | Significado | Ejemplo |
|---|---|---|
| `~u <regex>` | URL (path + query) | `~u /api/login` |
| `~d <regex>` | Dominio del host | `~d example.com` |
| `~m <método>` | Método HTTP | `~m POST` |
| `~c <código>` | Código de respuesta | `~c 500` |
| `~bq <regex>` | Body del request | `~bq password` |
| `~bs <regex>` | Body del response | `~bs token` |
| `~t <regex>` | Content-Type | `~t application/json` |
| `~s` | Solo respuestas | `~s` |
| `~q` | Solo requests | `~q` |
Combinar con `&` (AND), `|` (OR), `!` (NOT). Ejemplos:
- `"~m POST & ~u /api"` — POST a rutas /api
- `"~c 500 | ~c 503"` — errores de servidor
- `"~d example.com & !~u /static"` — todo de example.com excepto estáticos
**Globs:** el shell expande el glob *antes* de pasar los argumentos a la función. Pasar `~/captures/traffic-*.mitm` sin comillas para que el shell expanda; con comillas el glob llega literalmente y fallará si el archivo no existe.
**Inspección interactiva:** para navegar los flujos con UI, usar `mitmweb -r <file>` (web) o `mitmproxy -r <file>` (TUI), no esta función. Esta función es para consulta programática y scripting.
**Múltiples archivos con `-r`:** mitmdump acepta varios flags `-r` y concatena los flujos en orden. El filtro se aplica sobre el conjunto completo.
**HAR y filtros juntos:** al usar `--har` con `--filter`, solo los flujos que pasen el filtro se exportan al HAR.
@@ -1,101 +0,0 @@
#!/usr/bin/env bash
# query_mitm_flows — Consulta capturas .mitm con mitmdump: vuelca flujos a stdout
# o exporta a HAR. Acepta uno o varios archivos (también globs expandidos por el shell).
query_mitm_flows() {
local -a files=()
local filter=""
local har_out=""
local mitmdump_bin=""
# ── Parseo de argumentos ────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--filter)
[[ -z "${2:-}" ]] && { echo "ERROR: --filter requiere un valor" >&2; return 1; }
filter="$2"; shift 2 ;;
--har)
[[ -z "${2:-}" ]] && { echo "ERROR: --har requiere una ruta de salida" >&2; return 1; }
har_out="$2"; shift 2 ;;
--mitmdump)
[[ -z "${2:-}" ]] && { echo "ERROR: --mitmdump requiere la ruta al binario" >&2; return 1; }
mitmdump_bin="$2"; shift 2 ;;
--*)
echo "ERROR: opcion desconocida: $1" >&2; return 1 ;;
*)
files+=("$1"); shift ;;
esac
done
# ── Validar que se paso al menos un archivo ─────────────────────────────────
if [[ ${#files[@]} -eq 0 ]]; then
echo "ERROR: se requiere al menos un archivo .mitm como argumento" >&2
echo "Uso: query_mitm_flows <file_or_glob> [--filter EXPR] [--har OUT] [--mitmdump BIN]" >&2
return 1
fi
# ── Verificar que cada archivo existe ──────────────────────────────────────
local -a valid_files=()
for f in "${files[@]}"; do
if [[ -f "$f" ]]; then
valid_files+=("$f")
else
echo "ERROR: archivo no encontrado: $f" >&2
fi
done
if [[ ${#valid_files[@]} -eq 0 ]]; then
echo "ERROR: ningun archivo valido encontrado (el patron no matcheo nada)" >&2
return 1
fi
# ── Autodetectar mitmdump ───────────────────────────────────────────────────
if [[ -z "$mitmdump_bin" ]]; then
if command -v mitmdump &>/dev/null; then
mitmdump_bin="mitmdump"
elif [[ -x "$HOME/.local/bin/mitmdump" ]]; then
mitmdump_bin="$HOME/.local/bin/mitmdump"
else
echo "ERROR: mitmdump no encontrado. Instala mitmproxy:" >&2
echo " pip install mitmproxy o pip install --user mitmproxy" >&2
return 1
fi
fi
if [[ ! -x "$(command -v "$mitmdump_bin" 2>/dev/null || echo "$mitmdump_bin")" ]]; then
echo "ERROR: binario mitmdump no ejecutable: $mitmdump_bin" >&2
return 1
fi
# ── Construir y ejecutar el comando ────────────────────────────────────────
local -a cmd=("$mitmdump_bin" -n)
# Añadir todos los archivos de entrada con -r
for f in "${valid_files[@]}"; do
cmd+=(-r "$f")
done
# Modo HAR
if [[ -n "$har_out" ]]; then
cmd+=(--set "hardump=${har_out}")
fi
# Filtro de flujos (argumento posicional al final)
if [[ -n "$filter" ]]; then
cmd+=("$filter")
fi
"${cmd[@]}"
local exit_code=$?
if [[ -n "$har_out" && $exit_code -eq 0 ]]; then
echo "HAR exportado a ${har_out}" >&2
fi
return $exit_code
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
query_mitm_flows "$@"
fi
@@ -38,7 +38,7 @@ if [[ -n "$matches" ]]; then
fi
# Escanear repo especifico
scan_secrets_in_dirty $HOME/fn_registry
scan_secrets_in_dirty /home/lucas/fn_registry
```
## Patrones detectados
@@ -6,21 +6,16 @@
scan_secrets_in_dirty() {
local repo_dir="${1:-.}"
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
if [[ ! -d "$repo_dir/.git" ]]; then
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
return 1
fi
# Listar archivos modificados o nuevos (excluyendo borrados)
# y filtrar por patron de secret en el nombre del archivo.
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
# propio scanner ni docs que hablen de "secret"/"token".
# y filtrar por patron de secret en el nombre del archivo
git -C "$repo_dir" status --porcelain \
| awk '{print $NF}' \
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
| grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \
|| true
}
@@ -1,80 +0,0 @@
---
name: start_mitm_capture
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "start_mitm_capture([--port N] [--out DIR] [--rotate-min N] [--addon PATH] [--mitmdump BIN] [--log PATH]) -> string"
description: "Arranca mitmdump en modo headless en segundo plano como proxy de interceptación liviano, con rotación de capturas cada N minutos vía el addon rotate_capture_flows.py del registry. El proceso sobrevive al cierre de la shell (setsid). Emite JSON con PID, puerto, directorio de salida y ruta del log."
tags: [bash, cybersecurity, mitmproxy, proxy, capture, web-proxy, network, interception, mitmdump]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--port N"
desc: "Puerto TCP donde mitmdump escucha conexiones del cliente proxy. Default: 8080."
- name: "--out DIR"
desc: "Directorio donde se guardan los archivos .mitm rotados. Se crea automáticamente si no existe. Default: $HOME/captures."
- name: "--rotate-min N"
desc: "Minutos de duración de cada archivo de captura antes de rotar. Default: 20."
- name: "--addon PATH"
desc: "Ruta al addon Python de rotación (rotate_capture_flows.py). Default: derivado desde FN_REGISTRY_ROOT o desde la ubicación del propio script (3 niveles arriba)."
- name: "--mitmdump BIN"
desc: "Ruta al binario mitmdump. Default: autodetectado con command -v mitmdump, luego $HOME/.local/bin/mitmdump. Si no se encuentra, falla con instrucción de instalación."
- name: "--log PATH"
desc: "Archivo de log del proceso mitmdump. Default: <out>/mitmdump.log."
output: "JSON en stdout: {\"pid\": <pid>, \"port\": <port>, \"out_dir\": \"<dir>\", \"rotate_min\": <n>, \"log\": \"<log>\"}. Exit 1 en error (binario no encontrado, addon ausente, proceso muerto al arrancar)."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/start_mitm_capture.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/start_mitm_capture.sh
# Arranque con defaults (puerto 8080, capturas en ~/captures, rotación cada 20 min)
start_mitm_capture --port 8080 --out /home/enmanuel/captures --rotate-min 20
# Salida esperada:
# {"pid": 123456, "port": 8080, "out_dir": "/home/enmanuel/captures", "rotate_min": 20, "log": "/home/enmanuel/captures/mitmdump.log"}
# Puerto alternativo y rotación más frecuente
start_mitm_capture --port 9090 --out /tmp/mitm_session --rotate-min 5
# Pasando addon y binario explícitos
start_mitm_capture \
--port 8080 \
--out /home/enmanuel/captures \
--addon /home/enmanuel/fn_registry/python/functions/cybersecurity/rotate_capture_flows.py \
--mitmdump /home/enmanuel/.local/bin/mitmdump
# Leer el PID para poder parar el proceso más adelante
info=$(start_mitm_capture --port 8080 --out /home/enmanuel/captures)
pid=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)['pid'])")
echo "Proxy corriendo con PID $pid"
# Para parar: kill "$pid"
```
## Cuando usarla
Cuando necesites un proxy de interceptación pasivo siempre activo en segundo plano que capture y rote el tráfico HTTP/HTTPS de forma continua sin supervisión manual. Úsala antes de lanzar pruebas de integración, sesiones de auditoría web o scraping supervisado donde quieras replay o análisis posterior de las capturas `.mitm`.
## Gotchas
- **HTTPS requiere instalar el CA de mitmproxy en el cliente.** El certificado raíz está en `~/.mitmproxy/mitmproxy-ca-cert.pem` tras el primer arranque. En navegadores: importar como CA de confianza. En curl/httpx: `--cacert ~/.mitmproxy/mitmproxy-ca-cert.pem`. En Chrome/Chromium headless: `--ignore-certificate-errors` (solo para pruebas).
- **El proceso queda en background.** Para pararlo: `kill <pid>` (el PID se devuelve en el JSON) o `port_kill 8080` si tienes la función del registry disponible.
- **El addon rotate_capture_flows.py debe existir.** La función lo busca en la raíz del registry derivada automáticamente; si el registry está en una ubicación no estándar, pasa `--addon` explícitamente o setea `FN_REGISTRY_ROOT`.
- **setsid desacopla el proceso de la shell.** Si cierras la terminal, mitmdump sigue corriendo. Necesitas matar el PID manualmente o via systemd/supervisor si quieres ciclo de vida gestionado.
- **El log crece sin límite.** El log de mitmdump no rota; solo rotan los archivos `.mitm` de capturas. Monitoriza el tamaño de `<out>/mitmdump.log` en sesiones largas.
- **Solo funciona con mitmdump >= 9.x** (API de addons `--set` con parámetros por nombre). Versiones anteriores usan sintaxis distinta.
## Notas
La derivación de la raíz del registry cuando `FN_REGISTRY_ROOT` no está seteado asciende 3 niveles desde `bash/functions/cybersecurity/` hasta la raíz usando `dirname "${BASH_SOURCE[0]}"`. Esto funciona siempre que el script se sourcee desde su ubicación original en el registry.
@@ -1,114 +0,0 @@
#!/usr/bin/env bash
# start_mitm_capture — Arranca mitmdump en segundo plano como proxy de interceptación
# con rotación de capturas cada N minutos vía addon Python del registry.
start_mitm_capture() {
local port=8080
local out_dir="$HOME/captures"
local rotate_min=20
local addon_path=""
local mitmdump_bin=""
local log_path=""
# Parseo de argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--port) port="$2"; shift 2 ;;
--out) out_dir="$2"; shift 2 ;;
--rotate-min) rotate_min="$2"; shift 2 ;;
--addon) addon_path="$2"; shift 2 ;;
--mitmdump) mitmdump_bin="$2"; shift 2 ;;
--log) log_path="$2"; shift 2 ;;
*)
echo "ERROR: argumento desconocido: $1" >&2
echo "Uso: start_mitm_capture [--port N] [--out DIR] [--rotate-min N] [--addon PATH] [--mitmdump BIN] [--log PATH]" >&2
return 1
;;
esac
done
# Derivar raíz del registry cuando FN_REGISTRY_ROOT no está seteado
local registry_root
if [[ -n "${FN_REGISTRY_ROOT:-}" ]]; then
registry_root="$FN_REGISTRY_ROOT"
else
# bash/functions/cybersecurity/ -> 3 niveles arriba = raíz del registry
registry_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
fi
# Default addon usando la raíz derivada
if [[ -z "$addon_path" ]]; then
addon_path="${registry_root}/python/functions/cybersecurity/rotate_capture_flows.py"
fi
# Default log path (depende de out_dir, se resuelve después de crear el dir)
# Se asigna más abajo si sigue vacío
# Crear directorio de capturas si no existe
if [[ ! -d "$out_dir" ]]; then
mkdir -p "$out_dir" || {
echo "ERROR: no se pudo crear el directorio de capturas: $out_dir" >&2
return 1
}
fi
# Asignar log por defecto ahora que out_dir existe
if [[ -z "$log_path" ]]; then
log_path="${out_dir}/mitmdump.log"
fi
# Resolver binario mitmdump
if [[ -z "$mitmdump_bin" ]]; then
if command -v mitmdump &>/dev/null; then
mitmdump_bin="$(command -v mitmdump)"
elif [[ -x "$HOME/.local/bin/mitmdump" ]]; then
mitmdump_bin="$HOME/.local/bin/mitmdump"
else
echo "ERROR: mitmdump no encontrado; instala con: uv tool install mitmproxy" >&2
return 1
fi
else
if [[ ! -x "$mitmdump_bin" ]]; then
echo "ERROR: el binario indicado no existe o no es ejecutable: $mitmdump_bin" >&2
return 1
fi
fi
# Verificar que el addon existe
if [[ ! -f "$addon_path" ]]; then
echo "ERROR: addon no encontrado: $addon_path" >&2
echo " Asegúrate de que FN_REGISTRY_ROOT apunta a la raíz del registry" >&2
echo " o pasa --addon con la ruta correcta a rotate_capture_flows.py" >&2
return 1
fi
# Arrancar mitmdump en background con setsid (sobrevive al cierre de la shell).
# El redirect </dev/null desacopla stdin: sin él, el proceso retiene el pipe
# heredado y la shell sale con exit 144 (SIGTERM) al cerrarse. disown lo saca
# de la tabla de jobs para que la shell no le envíe señales al terminar.
setsid "$mitmdump_bin" \
-s "$addon_path" \
--set "rotate_min=${rotate_min}" \
--set "capture_dir=${out_dir}" \
--listen-port "$port" \
</dev/null >> "$log_path" 2>&1 &
local pid=$!
disown "$pid" 2>/dev/null || true
# Esperar ~1s y verificar que el proceso sigue vivo
sleep 1
if ! kill -0 "$pid" 2>/dev/null; then
echo "ERROR: mitmdump murió al arrancar. Últimas líneas del log:" >&2
tail -20 "$log_path" >&2
return 1
fi
# Emitir JSON con información del proceso arrancado
printf '{"pid": %d, "port": %d, "out_dir": "%s", "rotate_min": %d, "log": "%s"}\n' \
"$pid" "$port" "$out_dir" "$rotate_min" "$log_path"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
start_mitm_capture "$@"
fi
@@ -3,7 +3,7 @@ name: deploy_cpp_exe_to_windows
kind: function
lang: bash
domain: infra
version: "1.1.0"
version: "1.0.0"
purity: impure
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
@@ -22,14 +22,14 @@ params:
- name: app_name
desc: "Nombre de la app (ej: chart_demo). Se usa para localizar cpp/build/windows/apps/<app>/<app>.exe y el directorio destino Desktop/apps/<app>/."
- name: app_dir
desc: "Ruta absoluta al directorio fuente de la app (ej: $HOME/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
desc: "Ruta absoluta al directorio fuente de la app (ej: /home/lucas/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
output: "Copia archivos al escritorio de Windows. Imprime 'OK: <app> -> <dest>' en stdout. Si local_files/ existe, imprime su tamanio. Errores fatales a stderr con exit 1."
---
## Ejemplo
```bash
deploy_cpp_exe_to_windows "chart_demo" "$HOME/fn_registry/cpp/apps/chart_demo"
deploy_cpp_exe_to_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_demo"
# OK: chart_demo -> /mnt/c/Users/lucas/Desktop/apps/chart_demo
# Con rutas custom via env vars
@@ -55,7 +55,7 @@ Desktop/apps/<APP>/
- `BUILD_WIN` — directorio de build Windows; default `$FN_REGISTRY_ROOT/cpp/build/windows`
- `WIN_DESKTOP_APPS` — directorio destino; default `/mnt/c/Users/lucas/Desktop/apps`
- `FN_REGISTRY_ROOT` — raiz del registry; default `$HOME/fn_registry`
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
## Notas
@@ -63,8 +63,3 @@ Desktop/apps/<APP>/
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
## Capability growth log
v1.1.0 (2026-05-17) — Bugfix: el `cp` del .exe no chequeaba exit status y la funcion reportaba OK aunque fallase por "Permission denied" (proceso aun vivo). Ahora: (1) tras `taskkill.exe`, poll de hasta 3s sobre `tasklist.exe` esperando muerte real del proceso; (2) `cp` envuelto en retry 5 veces con backoff 0.5s y re-taskkill entre intentos; (3) si los 5 intentos fallan, `return 1` (antes: silently continued).
v1.0.0 — Initial.
@@ -12,7 +12,7 @@ deploy_cpp_exe_to_windows() {
return 1
fi
local root="${FN_REGISTRY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
@@ -30,38 +30,12 @@ deploy_cpp_exe_to_windows() {
mkdir -p "$dest" "$assets"
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
# Windows libera el file handle async tras taskkill. Hacemos poll hasta que
# el proceso desaparezca de tasklist o se agote el timeout.
if command -v taskkill.exe >/dev/null 2>&1; then
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
if ! tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null \
| grep -qi "^${app}.exe"; then
break
fi
sleep 0.3
done
fi
# --- 4. Copiar .exe al top level con retry ---
# Windows puede tener el archivo aun bloqueado momentaneamente; reintentar.
local cp_ok=0
local attempt
for attempt in 1 2 3 4 5; do
if cp -v "$exe_src" "$dest/"; then
cp_ok=1
break
fi
echo "deploy_cpp_exe_to_windows: cp intento $attempt fallo, reintentando..." >&2
# Reintentar taskkill por si el proceso resucito o quedo zombie.
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
sleep 0.5
done
if [ "$cp_ok" -ne 1 ]; then
echo "ERROR: cp del .exe fallo tras 5 intentos. $exe_src -> $dest/" >&2
return 1
fi
# --- 4. Copiar .exe al top level ---
cp -v "$exe_src" "$dest/"
# --- 5. DLLs al top level (Windows DLL search convention) ---
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
@@ -1,61 +0,0 @@
---
name: deploy_wails_exe_to_windows
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "deploy_wails_exe_to_windows <app_name> <app_dir>"
description: "Copia el .exe de una app Wails desde <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso anterior (taskkill /F) y relanza la app via cmd.exe. Single-binary: no copia DLLs (Webview2 nativo en SO). Preserva local_files/ si existe."
tags: ["wails", "windows", "deploy", "cross-compile", "mingw", "infra", "launch", "matrix-mas"]
params:
- name: app_name
desc: "Nombre del binario sin extension (ej. matrix_client_pc). Debe coincidir con el nombre del .exe generado por wails build."
- name: app_dir
desc: "Ruta absoluta al directorio raiz de la app, donde vive build/bin/<app>.exe. Puede estar en projects/<project>/apps/<app>/ o apps/<app>/."
output: "Imprime pasos en stderr. En stdout: ls -lh del .exe desplegado. Exit 0 si ok, exit 1 si build/bin/<app>.exe no existe o los args estan vacios."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "args vacios devuelven error con mensaje de uso"
- "app_dir inexistente devuelve exit 1"
- "build/bin exe inexistente devuelve exit 1"
test_file_path: "bash/functions/infra/deploy_wails_exe_to_windows_test.sh"
file_path: "bash/functions/infra/deploy_wails_exe_to_windows.sh"
---
## Ejemplo
```bash
source bash/functions/infra/deploy_wails_exe_to_windows.sh
# Desplegar matrix_client_pc tras wails build -platform windows/amd64
deploy_wails_exe_to_windows matrix_client_pc \
$HOME/fn_registry/projects/element_agents/apps/matrix_client_pc
```
Con override de destino:
```bash
WIN_DESKTOP_APPS=/mnt/c/Users/lucas/Desktop/apps \
deploy_wails_exe_to_windows matrix_admin_panel \
$HOME/fn_registry/projects/element_agents/apps/matrix_admin_panel
```
## Cuando usarla
Tras un `wails build -platform windows/amd64` exitoso, para desplegar el binario compilado en Windows y relanzarlo en el mismo paso. Ideal en el ciclo de iteracion rapida: compilar → desplegar → ver cambios. Equivalente a `deploy_cpp_exe_to_windows_bash_infra` pero para apps Wails (single-binary sin DLLs extras).
## Gotchas
- **taskkill /F fuerza muerte** sin permitir guardado en disco. Las apps Wails persisten estado en keyring de Windows y AppData — este kill es seguro para ellas. Si la app tuviera autosave en progreso, se perderia (aceptable en ciclos de dev).
- **UNC paths prohibidos en cmd.exe**: `cmd.exe /c start` debe ejecutarse con `cd` previo al directorio Windows (`/mnt/c/...`). Intentar lanzar con path `\\wsl.localhost\...` falla con "UNC paths are not supported as the current directory".
- **cmd.exe start no bloquea**: la funcion espera 3s y verifica via `tasklist.exe`. Si la app cierra sola tras el arranque (error de inicio), el warn final lo indica pero no causa exit 1. Revisar logs en `%APPDATA%\<app>\` o `%LOCALAPPDATA%\<app>\`.
- **Single-binary Wails**: no copiar DLLs. Webview2 es nativo del SO (Windows 10+ ya lo incluye). Si una version vieja de Windows no tuviera Webview2, la app falla al arrancar — solucion: instalar Webview2 Runtime en esa maquina.
- **Build previo es responsabilidad del caller**: esta funcion NO compila. Para matrix_client_pc usa `-tags goolm` por el crypto de Matrix: `wails build -platform windows/amd64 -tags goolm`.
- **WIN_DESKTOP_APPS override**: variable de entorno para cambiar el destino. Util en CI o maquinas con escritorio en otra ruta.
@@ -1,92 +0,0 @@
#!/usr/bin/env bash
# deploy_wails_exe_to_windows — Copia el .exe de una app Wails compilado en
# <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso
# anterior y relanza la app. Single-binary: no copia DLLs (Webview2 nativo SO).
# Pre-authorized: taskkill.exe /F — idempotente, sin prompt.
set -euo pipefail
deploy_wails_exe_to_windows() {
local app="${1:-}"
local app_dir="${2:-}"
if [ -z "$app" ] || [ -z "$app_dir" ]; then
echo "ERROR: uso: deploy_wails_exe_to_windows <app_name> <app_dir>" >&2
echo " app_name: nombre del binario sin extension (ej. matrix_client_pc)" >&2
echo " app_dir: ruta absoluta al directorio de la app (donde vive build/bin/)" >&2
return 1
fi
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
# --- 1. Validar que el .exe existe ---
local exe_src="${app_dir}/build/bin/${app}.exe"
if [ ! -f "$exe_src" ]; then
echo "ERROR: no se encontro $exe_src" >&2
echo "Compila primero con: wails build -platform windows/amd64" >&2
return 1
fi
# --- 2. Crear directorio destino (preserva local_files/ si existe) ---
local dest="${win_desktop_apps}/${app}"
mkdir -p "$dest"
echo "[deploy_wails] dest: $dest" >&2
# --- 3. Matar proceso si esta corriendo en Windows ---
# Pre-authorized. Wails apps usan AppData+keyring para estado, kill /F es seguro.
if command -v taskkill.exe >/dev/null 2>&1; then
echo "[deploy_wails] matando ${app}.exe si corre..." >&2
taskkill.exe /IM "${app}.exe" /F 2>/dev/null || true
fi
# --- 4. Esperar a que Windows libere el file handle ---
sleep 1
# --- 5. Copiar .exe (cp -f: overwrite sin borrar el directorio) ---
echo "[deploy_wails] copiando ${app}.exe..." >&2
cp -f "$exe_src" "$dest/${app}.exe"
# --- 6. Copiar appicon.ico si existe (opcional, algunos hubs lo leen) ---
local icon_src="${app_dir}/appicon.ico"
if [ -f "$icon_src" ]; then
echo "[deploy_wails] copiando appicon.ico..." >&2
cp -f "$icon_src" "$dest/appicon.ico"
fi
# --- 7. Relanzar la app desde su dir Windows ---
# Usar cmd.exe /c start desde el dir destino (no UNC paths — falla en cmd.exe).
echo "[deploy_wails] lanzando ${app}.exe..." >&2
(
cd "$dest"
cmd.exe /c start "" "${app}.exe"
)
# --- 8. Dar tiempo a que el proceso arranque ---
sleep 3
# --- 9. Verificar que el proceso esta corriendo ---
if command -v tasklist.exe >/dev/null 2>&1; then
local tasklist_out
tasklist_out=$(tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null || true)
if echo "$tasklist_out" | grep -qi "^${app}.exe"; then
local pid
pid=$(echo "$tasklist_out" | grep -i "^${app}.exe" | awk '{print $2}' | head -n1)
echo "[deploy_wails] ${app}.exe corriendo con PID $pid" >&2
else
echo "WARN: ${app}.exe no aparece en tasklist tras el lanzamiento." >&2
echo " Puede que la app cerro con error. Revisar AppData para logs." >&2
fi
fi
# --- 10. Resumen final en stdout ---
ls -lh "$dest/${app}.exe"
echo "[deploy_wails] OK: ${app} deployado en $dest" >&2
if [ -d "$dest/local_files" ]; then
echo "[deploy_wails] local_files/ preservado: $(du -sh "$dest/local_files" | cut -f1)" >&2
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
deploy_wails_exe_to_windows "$@"
fi
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Tests para deploy_wails_exe_to_windows
# Solo prueba validacion de argumentos y rutas — no ejecuta taskkill/cmd.exe reales.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/deploy_wails_exe_to_windows.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $test_name"
PASS=$((PASS + 1))
else
echo "FAIL: $test_name — expected '$expected', got '$got'"
FAIL=$((FAIL + 1))
fi
}
# --- Test 1: args vacios devuelven error con mensaje de uso ---
actual_exit=0
deploy_wails_exe_to_windows >/dev/null 2>&1 || actual_exit=$?
assert_eq "args vacios devuelven error con mensaje de uso" "1" "$actual_exit"
# --- Test 2: app_dir inexistente devuelve exit 1 ---
actual_exit=0
deploy_wails_exe_to_windows "myapp" "/tmp/nonexistent_dir_$(date +%s)" >/dev/null 2>&1 || actual_exit=$?
assert_eq "app_dir inexistente devuelve exit 1" "1" "$actual_exit"
# --- Test 3: build/bin exe inexistente devuelve exit 1 ---
TMPDIR_APP=$(mktemp -d)
# Crear estructura de dir de app pero SIN el exe
mkdir -p "$TMPDIR_APP/build/bin"
actual_exit=0
deploy_wails_exe_to_windows "myapp" "$TMPDIR_APP" >/dev/null 2>&1 || actual_exit=$?
rm -rf "$TMPDIR_APP"
assert_eq "build/bin exe inexistente devuelve exit 1" "1" "$actual_exit"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
+3 -3
View File
@@ -30,15 +30,15 @@ file_path: "bash/functions/infra/discover_git_repos.sh"
source bash/functions/infra/discover_git_repos.sh
# Listar todos los repos bajo fn_registry
discover_git_repos $HOME/fn_registry
discover_git_repos /home/lucas/fn_registry
# Contar repos
discover_git_repos $HOME/fn_registry | wc -l
discover_git_repos /home/lucas/fn_registry | wc -l
# Iterar
while IFS= read -r repo; do
echo "Repo: $repo"
done < <(discover_git_repos $HOME/fn_registry)
done < <(discover_git_repos /home/lucas/fn_registry)
```
## Notas
+1 -1
View File
@@ -33,7 +33,7 @@ file_path: "bash/functions/infra/docker_cp_file.sh"
```bash
source functions/infra/docker_cp_file.sh
result=$(docker_cp_file $HOME/fn_registry/registry.db metabase /registry.db)
result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
echo "$result"
# {"local_size":524288,"remote_size":524288}
@@ -32,7 +32,7 @@ file_path: "bash/functions/infra/git_auto_commit_dirty.sh"
source bash/functions/infra/git_auto_commit_dirty.sh
# Commitear con mensaje automatico
subject=$(git_auto_commit_dirty $HOME/fn_registry)
subject=$(git_auto_commit_dirty /home/lucas/fn_registry)
echo "Commit: $subject"
# Commitear con mensaje fijo
@@ -17,7 +17,7 @@ error_type: "error_go_core"
imports: []
example: |
# Manual check
bash bash/functions/infra/git_hook_audit_app_drift.sh $HOME/fn_registry/apps/kanban
bash bash/functions/infra/git_hook_audit_app_drift.sh /home/lucas/fn_registry/apps/kanban
# Used by pre_commit_hook_install_bash_infra (v2 hook chain)
file_path: bash/functions/infra/git_hook_audit_app_drift.sh
@@ -17,9 +17,7 @@ git_hook_audit_app_drift() {
echo "ERROR: repo_dir required" >&2
return 2
fi
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
if [[ ! -d "$repo_dir/.git" ]]; then
echo "ERROR: $repo_dir is not a git repo" >&2
return 2
fi
+2 -2
View File
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_pull_with_stash.sh"
source bash/functions/infra/git_pull_with_stash.sh
# Pullear repo con auto-stash
status=$(git_pull_with_stash $HOME/fn_registry)
status=$(git_pull_with_stash /home/lucas/fn_registry)
echo "$status"
# [pulled] fn_registry
# o:
@@ -46,7 +46,7 @@ while IFS= read -r repo; do
if [[ "$result" == "[diverged]"* || "$result" == "[stash-conflict]"* ]]; then
diverged+=("$result")
fi
done < <(discover_git_repos $HOME/fn_registry)
done < <(discover_git_repos /home/lucas/fn_registry)
if [[ ${#diverged[@]} -gt 0 ]]; then
echo "ATENCION: repos que requieren intervencion manual:"
+2 -2
View File
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_push_if_ahead.sh"
source bash/functions/infra/git_push_if_ahead.sh
# Pushear si hay commits locales
status=$(git_push_if_ahead $HOME/fn_registry)
status=$(git_push_if_ahead /home/lucas/fn_registry)
echo "$status"
# [push] fn_registry (master, 3 commits ahead)
# o:
@@ -39,7 +39,7 @@ echo "$status"
# Iterar sobre multiples repos
while IFS= read -r repo; do
git_push_if_ahead "$repo"
done < <(discover_git_repos $HOME/fn_registry)
done < <(discover_git_repos /home/lucas/fn_registry)
```
## Estados de salida
+1 -8
View File
@@ -51,14 +51,7 @@ git_push_if_ahead() {
echo "[push] $repo_name ($branch, $ahead commits ahead)" >&2
local push_out
push_out=$(git -C "$abs_repo" push origin "$branch" 2>&1) || {
# Preservar las lineas con los keywords que el orquestador usa para
# decidir el auto-recover (rejected / fast-forward / non-fast-forward).
# Un `tail -1` plano se quedaba con la linea final de `hint:` y perdia
# "[rejected]" + "Updates were rejected", impidiendo el recover.
local reason
reason=$(echo "$push_out" | grep -iE 'rejected|fast-forward|denied|permission|error:' | head -3 | tr '\n' ' ')
[[ -z "$reason" ]] && reason=$(echo "$push_out" | tail -1)
echo "[error] $repo_name: $reason"
echo "[error] $repo_name: $(echo "$push_out" | tail -1)"
return 0
}
echo "$push_out" | tail -3 >&2
+12 -6
View File
@@ -3,7 +3,7 @@ name: gradle_run
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "gradle_run(project_dir: string, task...: string) -> int"
description: "Wrapper canonico para invocar gradlew Android en WSL2 con JDK 17 + ANDROID_HOME validados."
@@ -24,7 +24,7 @@ tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gradle_run.sh"
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre tanto SDK Linux (~/Android/Sdk via install_android_sdk) como SDK Windows (/mnt/c/...) montado en WSL."
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre SDK Linux en $HOME/android-sdk (install_android_sdk_bash_infra), $HOME/Android/Sdk (Android Studio), y SDK Windows (/mnt/c/...) montado en WSL."
---
## Ejemplo
@@ -50,10 +50,12 @@ Si no esta fijado en el entorno, busca en orden:
Si ninguno existe → error en stderr y `return 1`.
### ANDROID_HOME
Si no esta fijado:
1. Intenta `$HOME/Android/Sdk` (SDK Linux via `install_android_sdk_bash_infra`)
2. Si no existe, intenta `$ANDROID_SDK_WIN` (SDK Windows montado en `/mnt/c/...`)
3. Si ninguno, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
Si no esta fijado, busca en orden (requiere que el directorio tenga `platform-tools/`):
1. `$HOME/android-sdk` — default de `install_android_sdk_bash_infra` (lowercase)
2. `$HOME/Android/Sdk` — default de Android Studio en Linux
3. `$ANDROID_SDK_WIN` (o `/mnt/c/Users/$USER/AppData/Local/Android/Sdk`) — SDK Windows montado en WSL2
Si ninguno existe con `platform-tools/`, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
## Exit codes
@@ -69,4 +71,8 @@ Si no esta fijado:
Source-able y ejecutable directo. Al sourcear, el caller importa la funcion `gradle_run` sin ejecutarla. Al ejecutar directamente, delega `"$@"` a `gradle_run`.
No exporta `JAVA_HOME`/`ANDROID_HOME` al entorno del shell padre — los variables se pasan solo al subshell de gradlew para evitar contaminar el entorno.
## Capability growth log
- v1.1.0 (2026-05-15) — ANDROID_HOME detection order: prioriza `$HOME/android-sdk` (install_android_sdk default) sobre `$HOME/Android/Sdk`; requiere platform-tools/ presente; anade WSL2 Windows path como fallback explicito (issue 0076)
---
+17 -8
View File
@@ -44,16 +44,25 @@ gradle_run() {
fi
# ---- Resolver ANDROID_HOME ---------------------------------------------
# Orden de busqueda (de mas probable a menos para entorno Linux/WSL2):
# 1. $HOME/android-sdk — instalado por install_android_sdk_bash_infra (default)
# 2. $HOME/Android/Sdk — ruta de Android Studio en Linux
# 3. $ANDROID_SDK_WIN — SDK Windows montado en WSL2 via /mnt/c/...
# Solo se acepta un candidato si tiene platform-tools/, no solo el directorio raiz.
local android_home="${ANDROID_HOME:-}"
if [[ -z "$android_home" ]]; then
local _default_linux="$HOME/Android/Sdk"
if [[ -d "$_default_linux" ]]; then
android_home="$_default_linux"
elif [[ -n "${ANDROID_SDK_WIN:-}" && -d "${ANDROID_SDK_WIN}" ]]; then
# SDK Windows montado en WSL via /mnt/c/...
android_home="${ANDROID_SDK_WIN}"
fi
unset _default_linux
local _sdk_candidates=(
"$HOME/android-sdk"
"$HOME/Android/Sdk"
"${ANDROID_SDK_WIN:-/mnt/c/Users/$USER/AppData/Local/Android/Sdk}"
)
for _candidate in "${_sdk_candidates[@]}"; do
if [[ -d "$_candidate" && -d "$_candidate/platform-tools" ]]; then
android_home="$_candidate"
break
fi
done
unset _sdk_candidates _candidate
fi
# ANDROID_HOME puede quedar vacio si no hay SDK instalado; gradle mostrara
-76
View File
@@ -1,76 +0,0 @@
---
name: jupyter_mcp_serve
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
error_type: "error_go_core"
signature: "jupyter_mcp_serve.sh [--dry-run]"
description: "Arranca (o reusa) un Jupyter Lab colaborativo en un puerto propio y lanza el Jupyter MCP server enganchado por stdio. Entrypoint robusto para la entrada 'jupyter' de .mcp.json: garantiza que el MCP SIEMPRE tiene servidor al que conectarse, sin depender de que haya un jupyter en 8888."
tags: [notebook, jupyter, mcp, infra, launcher-glue]
uses_functions: []
uses_types: []
params:
- name: "--dry-run"
desc: "Opcional. Arranca/verifica jupyter pero NO hace exec del MCP; loguea el comando elegido. Para tests."
output: "Proceso jupyter-mcp-server enganchado por stdio a un Jupyter Lab colaborativo local (127.0.0.1, puerto JUPYTER_MCP_PORT, default 8899). Logs en ~/.fn_jupyter_mcp/. stdout reservado al protocolo MCP."
---
## Que hace
El MCP de Jupyter (datalayer `jupyter-mcp-server`) **no arranca jupyter**, solo se
conecta a uno existente. Si la URL configurada no tiene jupyter detras, el MCP
nunca conecta. En esta maquina `localhost:8888` es el **proxy HTTP del contenedor
VPN gluetun**, no un jupyter — por eso el MCP fallaba siempre.
Este wrapper resuelve la cadena entera:
1. Localiza el venv (`python/.venv`) y los binarios `jupyter` + `jupyter-mcp-server`.
2. Si ya hay un jupyter gestionado vivo en `127.0.0.1:$PORT` (`/api/status` = 200) lo reusa.
3. Si no, arranca `jupyter lab` colaborativo detached (RTC via `jupyter-collaboration`),
en `JUPYTER_MCP_ROOT` (default = raiz del repo, asi cualquier notebook del arbol es lanzable).
4. Detecta el dialecto de CLI del MCP (`--document-url` nuevo / `--jupyter-url` viejo / env vars)
y hace `exec` del MCP por `--transport stdio`.
Self-adapting: funciona aunque cambie la version de `jupyter-mcp-server`.
## Ejemplo
```bash
# Como lo usa Claude Code (entrada en .mcp.json):
# "jupyter": { "command": "bash", "args": ["bash/functions/infra/jupyter_mcp_serve.sh"] }
# Test manual (arranca jupyter en 8899, no lanza el MCP):
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
curl -s http://127.0.0.1:8899/api/status # {"started":..., "version":...}
# Cambiar puerto / raiz de notebooks:
JUPYTER_MCP_PORT=8900 JUPYTER_MCP_ROOT=/home/enmanuel/fn_registry/analysis \
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
```
## Cuando usarla
Cuando quieras que el MCP de Jupyter de Claude Code **siempre** tenga servidor:
es el `command` de la entrada `jupyter` en `.mcp.json`. No la invoques a mano salvo
para depurar (`--dry-run`) o para levantar el jupyter colaborativo sin el MCP.
## Gotchas
- **stdout reservado**: el MCP habla por stdout (protocolo stdio). El wrapper jamas
escribe a stdout — todo log va a stderr y `~/.fn_jupyter_mcp/wrapper.log`. No metas
`echo` a stdout aqui o rompes el handshake del MCP.
- **Puerto 8888 ocupado por gluetun** en esta maquina. Por eso el default es **8899**.
Si 8899 tambien se ocupa, exporta `JUPYTER_MCP_PORT`.
- **Token vacio**: solo escucha en `127.0.0.1` con `disable_check_xsrf` + `allow_origin '*'`.
Aceptable en local; NO exponer el puerto a la red.
- **venv requerido**: necesita `python/.venv` con `jupyterlab`, `jupyter-collaboration`
y `jupyter-mcp-server`. Reconstruir: `cd python && uv sync --extra jupyter`.
- El jupyter arrancado queda **detached** (nohup): persiste entre invocaciones del MCP.
Para pararlo: `python/.venv/bin/jupyter server stop 8899` o `pkill -f 'jupyter-lab.*8899'`.
## Capability growth log
v1.0.0 (2026-06-01) — version inicial. Wrapper auto-start: reusa/levanta jupyter
colaborativo en puerto propio (8899) y autodetecta el dialecto de CLI del MCP.
-109
View File
@@ -1,109 +0,0 @@
#!/usr/bin/env bash
# jupyter_mcp_serve — arranca (o reusa) un Jupyter Lab colaborativo y lanza el
# Jupyter MCP server enganchado a el por stdio. Pensado para ser el `command` de
# la entrada "jupyter" en .mcp.json: garantiza que el MCP SIEMPRE tiene servidor.
#
# Por que existe: el MCP datalayer NO arranca jupyter, solo se conecta. Si la URL
# apunta a un puerto sin jupyter (en esta maquina 8888 = proxy VPN gluetun), el
# MCP nunca conecta. Este wrapper levanta su propio jupyter en un puerto propio.
#
# Env overrides:
# JUPYTER_MCP_ROOT raiz de notebooks (default: raiz del repo)
# JUPYTER_MCP_PORT puerto del jupyter gestionado (default: 8899)
# JUPYTER_MCP_VENV venv (default: <repo>/python/.venv)
# JUPYTER_MCP_TOKEN token (default: "" — solo escucha en 127.0.0.1)
#
# stdout esta RESERVADO al protocolo stdio del MCP. Todo log va a stderr + LOGFILE.
# Nunca hacer echo a stdout aqui.
#
# Uso directo / test:
# bash jupyter_mcp_serve.sh --dry-run # arranca jupyter, NO exec del MCP, loguea args
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# raiz del repo = tres niveles arriba de bash/functions/infra/
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
VENV="${JUPYTER_MCP_VENV:-$REPO_ROOT/python/.venv}"
ROOT_DIR="${JUPYTER_MCP_ROOT:-$REPO_ROOT}"
PORT="${JUPYTER_MCP_PORT:-8899}"
HOST=127.0.0.1
TOKEN="${JUPYTER_MCP_TOKEN:-}"
LOGDIR="${HOME}/.fn_jupyter_mcp"
mkdir -p "$LOGDIR"
LOGFILE="$LOGDIR/wrapper.log"
JLOG="$LOGDIR/jupyterlab.log"
log(){ printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGFILE"; printf '%s\n' "$*" >&2; }
JUPYTER="$VENV/bin/jupyter"
MCP="$VENV/bin/jupyter-mcp-server"
if [ ! -x "$JUPYTER" ]; then
log "FATAL: $JUPYTER no existe. Instala: cd $REPO_ROOT/python && uv pip install --python .venv/bin/python3 jupyterlab jupyter-collaboration jupyter-mcp-server"
exit 1
fi
if [ ! -x "$MCP" ]; then
log "FATAL: $MCP no existe. Instala jupyter-mcp-server en el venv."
exit 1
fi
server_up(){
local code
code="$(curl -s -m 3 -o /dev/null -w '%{http_code}' "http://$HOST:$PORT/api/status?token=$TOKEN" 2>/dev/null || true)"
[ "$code" = "200" ]
}
if server_up; then
log "reuso jupyter existente en $HOST:$PORT"
else
log "arranco jupyter colaborativo en $HOST:$PORT (root=$ROOT_DIR)"
nohup "$JUPYTER" lab \
--no-browser \
--ServerApp.ip="$HOST" \
--ServerApp.port="$PORT" \
--ServerApp.root_dir="$ROOT_DIR" \
--IdentityProvider.token="$TOKEN" \
--ServerApp.disable_check_xsrf=True \
--ServerApp.allow_origin='*' \
>>"$JLOG" 2>&1 &
disown 2>/dev/null || true
# esperar hasta ~30s a que levante
for _ in $(seq 1 60); do
server_up && break
sleep 0.5
done
if ! server_up; then
log "FATAL: jupyter no levanto en 30s. Ver $JLOG"
exit 1
fi
log "jupyter arriba"
fi
BASE="http://$HOST:$PORT"
# Detectar el dialecto de CLI del MCP (cambia entre versiones de jupyter-mcp-server)
HELP="$("$MCP" --help 2>&1 || true)"
ARGS=(--transport stdio)
if printf '%s' "$HELP" | grep -q -- '--document-url'; then
ARGS+=(--document-url "$BASE" --runtime-url "$BASE")
printf '%s' "$HELP" | grep -q -- '--document-token' && ARGS+=(--document-token "$TOKEN" --runtime-token "$TOKEN")
elif printf '%s' "$HELP" | grep -q -- '--jupyter-url'; then
ARGS+=(--jupyter-url "$BASE" --jupyter-token "$TOKEN")
else
# fallback: variables de entorno que las distintas versiones reconocen
export DOCUMENT_URL="$BASE" RUNTIME_URL="$BASE" DOCUMENT_TOKEN="$TOKEN" RUNTIME_TOKEN="$TOKEN"
export JUPYTER_URL="$BASE" JUPYTER_TOKEN="$TOKEN"
fi
log "MCP cmd: $MCP ${ARGS[*]}"
if [ "$DRY" = "1" ]; then
log "--dry-run: no ejecuto el MCP. Jupyter sigue corriendo en $BASE"
exit 0
fi
exec "$MCP" "${ARGS[@]}"
+2 -12
View File
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
kind: function
lang: bash
domain: infra
version: "1.1.0"
version: "1.0.0"
purity: impure
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
@@ -61,20 +61,10 @@ Mitad complementaria de `deploy_cpp_exe_to_windows_bash_infra`. El flujo complet
build_cpp_windows "registry_dashboard"
# 2. Copiar al escritorio (mata proceso si corre, copia DLLs+assets)
deploy_cpp_exe_to_windows "registry_dashboard" "$HOME/fn_registry/apps/registry_dashboard"
deploy_cpp_exe_to_windows "registry_dashboard" "/home/lucas/fn_registry/apps/registry_dashboard"
# 3. Lanzar
launch_cpp_app_windows "registry_dashboard"
```
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
## Gotchas
- Si `FN_REGISTRY_ROOT_WSL` no es tu ruta default de fn_registry (`/home/<user>/fn_registry`), setea la variable antes de invocar esta función: `FN_REGISTRY_ROOT_WSL=/ruta/custom launch_cpp_app_windows <app>`.
- El proceso hijo hereda `FN_REGISTRY_ROOT` como path Windows (backslashes) y `FN_REGISTRY_ROOT_WSL` como path Linux. En el exe C++, `py_resolve_interpreter()` usa `FN_REGISTRY_ROOT_WSL` para construir el invocation `wsl.exe -- /path/python3`.
- PowerShell escapa `$` con `\$` para evitar expansión de variables en el string del comando.
## Capability growth log
- v1.1.0 (2026-05-16) — auto-propaga `FN_REGISTRY_ROOT` (Windows path) + `FN_REGISTRY_ROOT_WSL` (Linux path) al proceso hijo para que pueda invocar WSL python via `wsl.exe`.
+2 -12
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env bash
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
# v1.1.0: propaga FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# al proceso hijo para que pueda invocar WSL python via wsl.exe.
launch_cpp_app_windows() {
local app="${1:-}"
@@ -28,18 +26,10 @@ launch_cpp_app_windows() {
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
win_exe="$win_app_dir\\$app.exe"
# Deducir raiz del registry en Linux (WSL) y traducir a Windows path.
# FN_REGISTRY_ROOT_WSL puede sobreescribirse en el entorno del llamante.
local linux_root win_root
linux_root="${FN_REGISTRY_ROOT_WSL:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
win_root=$(wslpath -w "$linux_root")
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
# Se inyectan FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# para que el exe pueda localizar el venv WSL y hacer: wsl.exe -- python3 ...
powershell.exe -NoProfile -Command \
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
>/dev/null 2>&1
local ts
@@ -1,89 +0,0 @@
---
name: mas_client_register
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "mas_client_register(ssh_host: string, container: string, config_file: string, dry_run: bool) -> json"
description: "Registra y sincroniza clientes OAuth en Matrix Authentication Service (MAS) ejecutando mas-cli config sync dentro del container Docker remoto via SSH. Verifica sintaxis YAML, soporte dry-run para ver diff antes de aplicar, y emite JSON estructurado con resultado. Idempotente: re-ejecucion con misma config no genera cambios."
tags: [matrix, mas, oauth, oidc, migration, mas-migration, infra, docker, ssh, matrix-mas]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: ssh_host
desc: "alias SSH del VPS donde corre MAS (ej. organic-machine.com). Debe estar en ~/.ssh/config con key auth."
- name: container
desc: "nombre del container Docker con MAS (ej. element_matrix_chat-mas-1). El config dentro del container se espera en /data/config.yaml."
- name: config_file
desc: "ruta absoluta en el VPS al archivo mas/config.yaml (ej. /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml). MAS lo monta como /data/config.yaml."
- name: dry_run
desc: "flag opcional --dry-run: ejecuta mas-cli config dump y devuelve el estado sin aplicar cambios. Util para verificar antes de activar MSC3861."
output: "JSON con: status ('ok'|'dry-run'|'error'), applied (bool), clients_total (int), clients_diff (array de lineas del output de mas-cli), stderr (string con logs de error si aplica)."
tested: true
tests:
- "help flag emite JSON parseable"
- "args faltantes retornan JSON de error sin ssh"
- "jq disponible en host local"
test_file_path: "bash/functions/infra/mas_client_register_test.sh"
file_path: "bash/functions/infra/mas_client_register.sh"
---
## Ejemplo
```bash
# Dry-run: verificar que clients se aplicarian correctamente
source bash/functions/infra/mas_client_register.sh
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml \
--dry-run
# Aplicar sync real (con --prune para eliminar clients viejos)
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml
```
Salida esperada (sync OK):
```json
{
"status": "ok",
"applied": true,
"clients_total": 6,
"clients_diff": ["synced client element-web", "synced client synapse-admin", "..."],
"stderr": ""
}
```
Salida dry-run:
```json
{
"status": "dry-run",
"applied": false,
"clients_total": 42,
"clients_diff": ["clients:", " - client_id: element-web", " ..."],
"stderr": ""
}
```
## Cuando usarla
Usar despues de editar `mas/config.yaml` localmente y antes de hacer restart a Synapse con `msc3861` habilitado en `homeserver.yaml`. Ejecutar primero con `--dry-run` para verificar que los 6 clients OAuth (Element Web, Synapse-Admin, matrix_client_pc, matrix_client_android, matrix_admin_panel, Synapse-internal) estan correctamente definidos, luego sin `--dry-run` para aplicar el sync.
## Gotchas
- **`--prune` elimina clients no declarados en config**: el sync real usa `--prune`, lo que borra cualquier client OAuth que exista en MAS pero no este en el `config.yaml`. Verificar con `--dry-run` antes de aplicar en produccion.
- **Requiere `jq` en el host local**: el JSON output se construye con `jq`. Si no esta instalado, la funcion falla con error claro antes de conectar al VPS.
- **`mas-cli` debe estar en el container**: la funcion asume que `mas-cli` esta en el PATH dentro del container MAS. Si el container usa una imagen diferente, verificar con `docker exec <container> mas-cli --version`.
- **Config dentro del container siempre en `/data/config.yaml`**: el `--config-file` apunta a la ruta en el VPS (para que el operador sepa que archivo editar), pero el comando dentro del container usa `/data/config.yaml` (el mount point estandar de MAS). Si el compose monta el archivo en otro path, ajustar la constante `container_config` en el script.
- **SSH key debe estar en agent o `~/.ssh/config`**: la funcion usa `ssh <alias>` directamente. Si la key requiere passphrase, ejecutar `ssh-add` antes.
- **Si `config.yaml` es invalido, sync aborta sin tocar estado**: el paso 1 (`mas-cli config check`) detecta errores de sintaxis YAML antes de intentar sync. El estado de MAS no se modifica si la config tiene errores.
- **Idempotente**: re-ejecutar con la misma config no genera cambios en MAS (mas-cli detecta que el estado ya coincide).

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