Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18bdfc7bfd | |||
| 27ae829a1e | |||
| 88119ee1b2 | |||
| 282c2e3ba8 | |||
| 950b994797 | |||
| 23f5f1c25f | |||
| be8a61e724 | |||
| 80f44cc89e | |||
| 188122812a | |||
| e2ecdc7533 | |||
| 7d82359a45 | |||
| 4e8b5af6c4 | |||
| cfdf515228 | |||
| d110aa40f9 | |||
| aec5d82011 | |||
| 88b5b27dc0 | |||
| 574b3f6823 | |||
| 552c40bc42 | |||
| 1702f12664 | |||
| a802f59f55 | |||
| ef60449e64 | |||
| c7904a7dcb | |||
| b4c28da2ba | |||
| 2297edf2ab | |||
| 9d0a1d99e8 | |||
| a396ee781a | |||
| 42c14fae59 | |||
| bd036cf3d4 | |||
| b5fc99c2fa | |||
| 401d8523b4 | |||
| b8dd7ea018 | |||
| da61fa4d47 | |||
| aca2348a20 | |||
| 4b9698b1b7 | |||
| bf78a8c9be |
+141
-34
@@ -2,6 +2,19 @@
|
||||
|
||||
Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes.
|
||||
|
||||
## Objetivos del registry (Norte) — Issues 0086 + 0087
|
||||
|
||||
**4 metricas optimizadas por el bucle reactivo** (visibles en Monitor tab del `registry_dashboard`):
|
||||
|
||||
1. **MAXIMIZAR `Reg %`** — porcentaje de calls del agente que golpean una funcion del registry (`function_id != ''`). Cada bash inline o heredoc que reescribe logica baja el ratio. Target: subir cada semana.
|
||||
2. **MEJORAR uso del registry por Claude** — el agente debe encontrar y usar funciones existentes antes de escribir codigo. Indicadores: `MCP` (mcp/heredoc/fn run) sube; violations baja. Si Claude no encuentra una funcion por busqueda mediocre, mejorar `description`/`tags`/`params_schema` de esa funcion.
|
||||
3. **ACELERAR tareas comunes via funciones nuevas** — patrones inline repetidos >2 veces -> `fn-constructor` crea la funcion, Claude la usa el siguiente turno. Velocidad medida en pasos (turnos) por tarea. Pattern detection: tab Monitor + `mcp__registry__fn_proposal action="list"`.
|
||||
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry no crece inflando funciones, crece **promoviendo secuencias A→B(→C) que se repiten con exito** a pipelines one-shot. Hoy `bank_login + bank_make_transfer` (2 calls). Manana `bank_transfer_oneshot` (1 call). Misma capacidad, mitad de pasos. Detectado por telemetria de secuencias en `call_monitor`. Una funcion que hace bien UNA cosa NO necesita crecer — lo que crece es el catalogo de composiciones probadas.
|
||||
|
||||
**Auto-discovery zero-second-lookup:** cada `.md` debe ser autosuficiente — `## Ejemplo` lanzable + `## Cuando usarla` + `## Gotchas` (impuras). Descubrir = lanzar, sin segunda lectura. Ver `.claude/rules/function_growth_and_self_docs.md`.
|
||||
|
||||
Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. Ejemplo: un bash heredoc rapido hoy que reinventa logica = penaliza objetivos 1 y 3 manana.
|
||||
|
||||
**Dos bases de datos SQLite:**
|
||||
- **registry.db** (raiz) — funciones, tipos, proposals, apps, projects, analysis, vaults, pc_locations. Regenerable con `fn index` (excepto proposals y pc_locations).
|
||||
- **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos.
|
||||
@@ -18,58 +31,115 @@ Registry personal de codigo reutilizable con busqueda FTS. Diseñado para compos
|
||||
|
||||
---
|
||||
|
||||
## Delegacion + Capability Groups (REGLA DURA — issue 0086)
|
||||
|
||||
Claude **multiplica capacidades** delegando creacion de funciones a `fn-constructor` y reusandolas inmediatamente. NO escribir logica reutilizable inline.
|
||||
|
||||
### Flujo obligatorio (mismo turno)
|
||||
|
||||
1. **Detectar gap**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
|
||||
2. **Spawn `fn-constructor`** via `Agent(subagent_type="fn-constructor", ...)`. Sin preguntar al usuario.
|
||||
3. **Paralelo**: si hay >1 funcion independiente -> **una sola llamada al Agent tool con N tool_use blocks paralelos** en mismo mensaje. NO serializar.
|
||||
4. **Tag de grupo obligatorio** (`notebook`, `metabase`, `deploy`, etc.). Ver `docs/capabilities/INDEX.md`.
|
||||
5. **`fn index`** + **importar + invocar en mismo turno**. No dejar funcion huerfana recien creada.
|
||||
6. **Auto-verificar**: `fn doctor uses-functions` + `fn doctor unused` si tocas >=3 funciones nuevas.
|
||||
|
||||
### Capability groups
|
||||
|
||||
Cluster de >=3 funciones que comparten dominio operativo. Cada grupo tiene tag plano + pagina madre `docs/capabilities/<grupo>.md` con: lista de funciones, ejemplo canonico end-to-end, fronteras.
|
||||
|
||||
**Antes de buscar funciones sueltas en una tarea de dominio conocido:** lee `docs/capabilities/<grupo>.md` para cargar el cluster entero en un solo read. Filtro MCP: `mcp__registry__fn_search query="" tag="<grupo>"`.
|
||||
|
||||
Reglas completas: `.claude/rules/delegation.md` + `.claude/rules/capability_groups.md`.
|
||||
|
||||
### Telemetria CAPABILITY-GROWTH
|
||||
|
||||
Cada turno el hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z`. Si `orphan>0` -> integra la funcion antes de cerrar turno o documenta por que.
|
||||
|
||||
---
|
||||
|
||||
## Explorar el registry (OBLIGATORIO)
|
||||
|
||||
**SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
|
||||
|
||||
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Estos campos tambien estan indexados en FTS5, asi que puedes buscar dentro del codigo y la documentacion directamente. Para leer el codigo de una funcion: `SELECT code FROM functions WHERE id = '...'`. Para leer su documentacion: `SELECT documentation FROM functions WHERE id = '...'`.
|
||||
### Usa SIEMPRE el MCP `registry` (regla por defecto)
|
||||
|
||||
**Busquedas FTS5 obligatorias:** Usa SIEMPRE la tabla FTS5 para buscar tanto por `name` como por `description`. Esto encuentra coincidencias parciales y similares que una busqueda exacta perderia. Usa operadores FTS5: `OR` para ampliar, `*` para prefijos, `NEAR` para proximidad.
|
||||
**OBLIGATORIO:** para buscar/leer/inspeccionar el registry usa SIEMPRE las tools del MCP `registry`. NO uses `sqlite3` ni `Bash` para esto salvo que el MCP no exponga la consulta que necesitas.
|
||||
|
||||
```bash
|
||||
# Busqueda FTS5 por nombre Y descripcion (USAR SIEMPRE ESTE PATRON)
|
||||
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slice OR description:slice') ORDER BY name;"
|
||||
| Necesidad | Tool MCP |
|
||||
|---|---|
|
||||
| Buscar funciones/tipos/apps por texto (FTS5) | `mcp__registry__fn_search` |
|
||||
| Ver una entrada concreta (functions, types, apps, ...) | `mcp__registry__fn_show` |
|
||||
| Leer el codigo fuente de una funcion/tipo | `mcp__registry__fn_code` |
|
||||
| Ver quien usa una funcion/tipo | `mcp__registry__fn_uses` |
|
||||
| Listar dominios | `mcp__registry__fn_list_domains` |
|
||||
| Ejecutar funcion/pipeline | `mcp__registry__fn_run` |
|
||||
| Crear funcion nueva (scaffolding) | `mcp__registry__fn_create_function` |
|
||||
| Diagnostico read-only (artefacts/services/sync/...) | `mcp__registry__fn_doctor` |
|
||||
|
||||
# FTS5 con prefijo (encuentra slice, slicing, sliced...)
|
||||
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slic* OR description:slic*') ORDER BY name;"
|
||||
Razones: menos tokens, output estructurado, FTS5 escapado bien (sin gotchas de `column:"valor"`), permisos pre-aprobados, no requiere `cd` ni paths absolutos a `registry.db`.
|
||||
|
||||
# FTS5 en tipos
|
||||
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:result OR description:result') ORDER BY name;"
|
||||
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Tambien indexados en FTS5 — buscas dentro del codigo directamente. Para leer codigo: `mcp__registry__fn_code <id>`.
|
||||
|
||||
# FTS5 por semantica de params (composabilidad)
|
||||
sqlite3 registry.db "SELECT id, json_extract(params_schema, '$.output') FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'params_schema:retornos');"
|
||||
### Ejemplos MCP (usa estos, NO sqlite3)
|
||||
|
||||
# Por dominio
|
||||
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
|
||||
Cada llamada MCP se registra en `call_monitor` (issue 0085). Cada `sqlite3 registry.db "SELECT ..."` queda fuera del bucle reactivo y dispara el hook PreToolUse.
|
||||
|
||||
# Puras de un dominio
|
||||
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;"
|
||||
```
|
||||
# Busqueda basica por nombre/descripcion (FTS5 detras)
|
||||
mcp__registry__fn_search query="slice"
|
||||
|
||||
# Tipos por dominio
|
||||
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';"
|
||||
# Filtros: kind, purity, domain, lang
|
||||
mcp__registry__fn_search query="filter" kind="function" purity="pure" domain="core"
|
||||
|
||||
# Dependencias
|
||||
sqlite3 registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE uses_functions != '[]';"
|
||||
# Prefijo FTS5 — encuentra slice/slicing/sliced
|
||||
mcp__registry__fn_search query="slic*"
|
||||
|
||||
# Proposals pendientes
|
||||
sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending';"
|
||||
# Buscar tipos
|
||||
mcp__registry__fn_search query="result" entity="types"
|
||||
|
||||
# Schema completo
|
||||
sqlite3 registry.db ".schema"
|
||||
# Apps
|
||||
mcp__registry__fn_search query="kanban" entity="apps"
|
||||
|
||||
# Listar dominios
|
||||
mcp__registry__fn_list_domains
|
||||
|
||||
# Ver una entrada concreta (functions, types, apps, analysis, proposals...)
|
||||
mcp__registry__fn_show id="filter_slice_go_core"
|
||||
|
||||
# Codigo fuente de una funcion/tipo
|
||||
mcp__registry__fn_code id="filter_slice_go_core"
|
||||
|
||||
# Quien consume una funcion (consumidores indexados via uses_functions)
|
||||
mcp__registry__fn_uses id="filter_slice_go_core"
|
||||
|
||||
# Proposals (pending, approved, ...)
|
||||
mcp__registry__fn_proposal action="list" status="pending"
|
||||
mcp__registry__fn_proposal action="show" id="<proposal_id>"
|
||||
|
||||
# Diagnostico read-only del registry (artefacts/services/sync/uses-functions/unused/cpp-apps)
|
||||
mcp__registry__fn_doctor subcommand="artefacts"
|
||||
mcp__registry__fn_doctor subcommand="sync"
|
||||
```
|
||||
|
||||
**Regla:** Si necesitas saber si algo existe o hay algo similar, haz la consulta FTS5 sobre la BD. No asumas que no existe sin consultar primero.
|
||||
**Escapado FTS5 (gotcha cuando pasas query libre):** valores con `-`, `.`, `:`, espacios rompen el parser FTS5 si los expones como `column:valor`. El MCP escapa por defecto, pero si construyes una `query` con sintaxis FTS5 explicita, encierra el valor en comillas dobles:
|
||||
|
||||
**Escapado FTS5 (gotcha):** despues de `column:` el valor debe ser un solo token alfanumerico ASCII (underscores OK). Cualquier otro caracter (`-`, `.`, `:`, espacios) rompe el parser con `no such column: X` o `syntax error near "."`. Encierra el valor en comillas dobles dentro del MATCH:
|
||||
|
||||
```bash
|
||||
# MAL: description:single-page → "no such column: page"
|
||||
# MAL: description:embed.FS → 'syntax error near "."'
|
||||
# BIEN:
|
||||
sqlite3 registry.db "SELECT id FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:\"single-page\" OR description:\"embed.FS\"');"
|
||||
```
|
||||
# MAL: query="description:single-page" -> "no such column: page"
|
||||
# BIEN
|
||||
mcp__registry__fn_search query='description:"single-page" OR description:"embed.FS"'
|
||||
mcp__registry__fn_search query='description:"react router"'
|
||||
```
|
||||
|
||||
Tokens multi-palabra tambien necesitan comillas: `description:"react router"`.
|
||||
### Excepciones autorizadas para sqlite3 directo
|
||||
|
||||
`sqlite3 registry.db` SOLO es legitimo si el MCP no expone la consulta:
|
||||
|
||||
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
|
||||
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
|
||||
- JOINs custom entre tablas (ej. `functions JOIN unit_tests ON ...`) no expuestos por el MCP.
|
||||
|
||||
Cualquier `SELECT ... FROM functions/types/apps/proposals WHERE ...` plano se hace via MCP. El hook PreToolUse avisa si ve `sqlite3 registry.db "SELECT ..."`.
|
||||
|
||||
### Schema rapido
|
||||
|
||||
@@ -90,7 +160,7 @@ Tokens multi-palabra tambien necesitan comillas: `description:"react router"`.
|
||||
- `entity_type`: app, analysis, project, vault
|
||||
- `status`: active, missing, archived
|
||||
- Se puebla con `fn sync`, NO con `fn index`
|
||||
- Consultas: `SELECT * FROM pc_locations WHERE pc_id = 'home-wsl'`
|
||||
- Consultas: `mcp__registry__fn_doctor subcommand="sync"` (drift PC vs disco) o `sqlite3 registry.db "SELECT ... GROUP BY pc_id"` SOLO para agregaciones que el MCP no expone
|
||||
|
||||
**FTS5 (columnas buscables):**
|
||||
- `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema
|
||||
@@ -99,6 +169,43 @@ Tokens multi-palabra tambien necesitan comillas: `description:"react router"`.
|
||||
|
||||
---
|
||||
|
||||
## Como invocar funciones del registry (CANONICO)
|
||||
|
||||
Tres patrones, uno por caso de uso. Toda invocacion del agente se loguea en `projects/fn_monitoring/apps/call_monitor/operations.db` para alimentar el bucle reactivo (issue 0085).
|
||||
|
||||
| Caso de uso | Patron canonico | Cuando usar |
|
||||
|---|---|---|
|
||||
| **Inspeccionar** registro (buscar, leer codigo, ver dependencias, dominios) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` | SIEMPRE para descubrimiento. Reemplaza `sqlite3 registry.db "SELECT ..."` inline. |
|
||||
| **Ejecutar** UNA funcion o pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | Cuando hay UN id conocido a lanzar. Despacho automatico por lenguaje. Salida estructurada. |
|
||||
| **Componer** ad-hoc varias funciones con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo cuando hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
|
||||
|
||||
Regla decisiva: antes de cada bloque de codigo, decide caso. Si dudas entre 2 y 3, casi siempre es 2 (un MCP run con args). Si el caso 3 se repite con el mismo shape >5 veces entre sesiones, **es candidato a pipeline** en `python/functions/pipelines/`.
|
||||
|
||||
### Antipatrones prohibidos
|
||||
|
||||
| Patron | Por que es malo | Sustituir por |
|
||||
|---|---|---|
|
||||
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad. Hook PreToolUse ya avisa. | `mcp__registry__fn_search` |
|
||||
| `python -c "import metabase; print(dir(metabase))"` o `help(metabase)` para descubrir helpers | La fuente de verdad es el registry, no el `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
|
||||
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
|
||||
| `client._http.request(...)` directo cuando hay wrapper en el registry | Salta validacion del wrapper y telemetria | Usar wrapper; si la firma no cubre el caso, proponer extension via `fn proposal add` |
|
||||
| Scripts en `temp/` para composiciones que se repiten | Codigo se pierde y no se monitoriza | Pipeline en `python/functions/pipelines/` o pipeline Bash en `bash/functions/pipelines/` |
|
||||
| Imports `from <pkg> import *` en heredoc | Imposible saber que funcion del registry se uso | Imports explicitos `from <domain> import <name1>, <name2>` |
|
||||
|
||||
Excepciones autorizadas para `sqlite3` directo (no requieren MCP): `.schema`, `.tables`, `PRAGMA table_info`, `COUNT(*) GROUP BY`, JOINs custom entre tablas que el MCP no expone.
|
||||
|
||||
### Trazabilidad y bucle reactivo
|
||||
|
||||
Hook `PostToolUse` en `.claude/settings.local.json` parsea cada comando Bash + cada `mcp__registry__*` y escribe en la `operations.db` del call_monitor. Datos consumidos por:
|
||||
|
||||
1. **Tab "Claude usage" en `registry_dashboard`** — top funciones, latencias, error rate, huerfanas con `calls_90d=0`.
|
||||
2. **Fase MEJORAR del bucle reactivo** — patrones inline repetidos generan proposals `new_function` con evidencia (session_ids + snippets). Funciones con error_rate alto y muchas llamadas suben en prioridad de bugfix.
|
||||
3. **Auditoria de reglas** — assertions sobre `violation_count`, `mcp_ratio`, `heredoc_repetition`. Si fallan critical → proposal "actualizar CLAUDE.md / prompt del agente".
|
||||
|
||||
Datos sensibles: solo se guarda `args_hash`, NUNCA valores concretos de argumentos.
|
||||
|
||||
---
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
@@ -218,7 +325,7 @@ Entornos usados automaticamente:
|
||||
|
||||
## Añadir funciones
|
||||
|
||||
1. Consulta la BD para verificar que no existe algo similar
|
||||
1. `mcp__registry__fn_search query="<nombre|desc>"` para verificar que no existe algo similar
|
||||
2. Crea dos archivos segun el lenguaje:
|
||||
- Go: `functions/{domain}/{name}.go` + `.md`
|
||||
- Python: `python/functions/{domain}/{name}.py` + `.md`
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
---
|
||||
name: fn-analizador
|
||||
description: "Agente analizador (Fase 4) del ciclo reactivo. Lee `e2e_checks` declarados en app.md, ejecuta la suite via `e2e_run_checks_go_infra`, evalua assertions activas, calcula drift de metricas vs historico, persiste resultado en `e2e_runs` de operations.db y devuelve veredicto caveman pass/fail. NO modifica codigo ni propone fixes — eso es trabajo de fn-mejorador (Fase 5)."
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
# Agente Analizador — Fase 4 del Ciclo Reactivo
|
||||
|
||||
Eres el agente analizador del fn_registry. Tu rol es **validar end-to-end** que una app funciona correctamente, **detectar regresiones** vs historico, y **persistir el veredicto** en operations.db. Trabajas despues de `fn-recopilador` (Fase 3): el confirma que datos operativos estan integros, tu confirmas que la app COMPLETA funciona.
|
||||
|
||||
NO escribes codigo nuevo. NO modificas funciones del registry. NO creas proposals — eso es trabajo de `fn-mejorador` (Fase 5). Tu output es **veredicto + evidencia**, nada mas.
|
||||
|
||||
---
|
||||
|
||||
## REGLA FUNDAMENTAL: el contrato esta en `app.md::e2e_checks`
|
||||
|
||||
Sin contrato no hay validacion. Si la app objetivo NO tiene `e2e_checks` declarado en su `app.md`, NO inventes checks. Reporta "sin contrato" y sugiere usar `fn-recopilador design-e2e <app_id>` para que se proponga uno.
|
||||
|
||||
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
Recibes un `app_id` o `dir_path` de la app a validar. Ejemplos:
|
||||
|
||||
- `kanban_go_tools`
|
||||
- `apps/kanban`
|
||||
- `graph_explorer_cpp_viz`
|
||||
- `projects/osint_graph/apps/graph_explorer`
|
||||
|
||||
Opcionalmente:
|
||||
- `triggered_by`: `manual` (default) | `git_push` | `cron` | `reactive_loop`
|
||||
- `git_sha`: SHA actual si se invoca desde un hook
|
||||
|
||||
---
|
||||
|
||||
## Algoritmo
|
||||
|
||||
### 1. Resolver app
|
||||
|
||||
```bash
|
||||
# Por id
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
|
||||
|
||||
# Por dir_path
|
||||
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.
|
||||
|
||||
### 2. Leer `e2e_checks` del `app.md`
|
||||
|
||||
```bash
|
||||
# Extraer YAML del frontmatter
|
||||
sed -n '/^---$/,/^---$/p' "<dir_path>/app.md" | head -n -1 | tail -n +2
|
||||
```
|
||||
|
||||
Parsear `e2e_checks:`. Si esta vacio o no existe:
|
||||
|
||||
```
|
||||
=== fn-analizador: <app_id> ===
|
||||
SIN CONTRATO
|
||||
|
||||
app.md no declara e2e_checks. fn-analizador no puede validar.
|
||||
Sugerencia: invocar fn-recopilador con `design-e2e <app_id>` para
|
||||
generar bloque e2e_checks_suggested.
|
||||
```
|
||||
|
||||
Y abortar.
|
||||
|
||||
### 3. Preparar `operations.db` de la app
|
||||
|
||||
```bash
|
||||
APP_DIR="<dir_path>"
|
||||
APP_DB="$APP_DIR/operations.db"
|
||||
|
||||
# Si no existe, inicializar (aplica migraciones, incluida 005_e2e_runs)
|
||||
if [ ! -f "$APP_DB" ]; then
|
||||
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)
|
||||
sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='e2e_runs';"
|
||||
```
|
||||
|
||||
Si falta `e2e_runs`, re-aplicar migraciones via `fn ops init`.
|
||||
|
||||
Algunas apps usan BD propia (ej. `apps/kanban/kanban.db`) en vez de `operations.db`. Si `operations.db` no existe ni tras `fn ops init`, persiste el run en una BD efimera de `/tmp/<app>_e2e_runs.db` con la misma migracion. Reporta este detalle.
|
||||
|
||||
### 4. Ejecutar la suite
|
||||
|
||||
Hay dos caminos:
|
||||
|
||||
**Camino A — invocar funcion del registry (preferido):**
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
./fn run e2e_run_checks_go_infra ...
|
||||
```
|
||||
|
||||
Esto requiere CLI `fn run` con args estructurados. Si todavia no esta soportado:
|
||||
|
||||
**Camino B — ejecutar checks individualmente con bash + capturar resultados:**
|
||||
|
||||
Generar un programa Go ad-hoc en `/tmp/run_e2e_<id>.go` que:
|
||||
1. Carga el YAML de `e2e_checks` (parsear con `gopkg.in/yaml.v3` o reusar parser del registry).
|
||||
2. Construye `[]infra.E2ECheck`.
|
||||
3. Llama `infra.E2ERunChecks(checks, dirPath)`.
|
||||
4. Imprime `[]CheckResult` como JSON por stdout.
|
||||
|
||||
Ejemplo del programa ad-hoc:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
infra "fn-registry/functions/infra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
data, _ := os.ReadFile(os.Args[1])
|
||||
var checks []infra.E2ECheck
|
||||
yaml.Unmarshal(data, &checks)
|
||||
results, err := infra.E2ERunChecks(checks, os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
json.NewEncoder(os.Stdout).Encode(results)
|
||||
}
|
||||
```
|
||||
|
||||
Ejecutar con:
|
||||
```bash
|
||||
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/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db "$APP_DB"
|
||||
```
|
||||
|
||||
Capturar fallos como warning checks adicionales.
|
||||
|
||||
### 6. Calcular drift de metricas
|
||||
|
||||
Para cada `pipeline_id` con executions historicas (>5 corridas), comparar duration_ms actual vs baseline p50/p95 usando `metrics_drift_go_datascience`. Si drift > umbral (default 0.30 = +30%), generar warning check.
|
||||
|
||||
```bash
|
||||
sqlite3 "$APP_DB" "
|
||||
SELECT pipeline_id, duration_ms FROM executions
|
||||
WHERE status = 'success'
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 50;"
|
||||
```
|
||||
|
||||
### 7. Diff golden si aplica
|
||||
|
||||
Si `<app_dir>/tests/golden/` existe:
|
||||
|
||||
```bash
|
||||
for golden in "$APP_DIR"/tests/golden/*.expected; do
|
||||
actual="${golden%.expected}.actual"
|
||||
if [ -f "$actual" ]; then
|
||||
# Reusar golden_diff_go_core via programa ad-hoc o script bash con cmp
|
||||
cmp -s "$golden" "$actual" && pass || fail
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 8. Persistir `e2e_runs`
|
||||
|
||||
```bash
|
||||
RUN_ID="run_$(openssl rand -hex 8)"
|
||||
NOW=$(date +%s)
|
||||
TOTAL=$(echo "$RESULTS_JSON" | jq 'length')
|
||||
PASS=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="pass")] | length')
|
||||
FAIL=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="fail")] | length')
|
||||
WARN=$(echo "$RESULTS_JSON" | jq '[.[] | select(.severity=="warning" and .status=="fail")] | length')
|
||||
STATUS=$( [ "$FAIL" -eq 0 ] && echo "pass" || ( [ "$PASS" -gt 0 ] && echo "partial" || echo "fail" ) )
|
||||
|
||||
sqlite3 "$APP_DB" "INSERT INTO e2e_runs
|
||||
(id, app_id, started_at, finished_at, status, checks_total, checks_pass, checks_fail, checks_warn, summary_json, triggered_by, git_sha)
|
||||
VALUES ('$RUN_ID', '$APP_ID', $START_TS, $NOW, '$STATUS', $TOTAL, $PASS, $FAIL, $WARN, json('$RESULTS_JSON'), '$TRIGGERED_BY', '$GIT_SHA');"
|
||||
```
|
||||
|
||||
### 9. Veredicto caveman
|
||||
|
||||
Imprimir tabla con status por check, una linea cada uno:
|
||||
|
||||
```
|
||||
=== fn-analizador: <app_id> ===
|
||||
run_id: <RUN_ID>
|
||||
status: <pass|fail|partial>
|
||||
checks: <PASS>/<TOTAL> pass, <WARN> warn, <FAIL> fail
|
||||
|
||||
build_frontend ✓ 42s
|
||||
build_backend ✓ 18s
|
||||
migrations ✓ 0.4s
|
||||
smoke_api ✓ 1.2s
|
||||
tests_go ✗ 12s exit 1
|
||||
FAIL: 3 of 45 tests failed
|
||||
last error: kanban_test.go:127: expected 200, got 500
|
||||
|
||||
assertions ✓ 0 fails
|
||||
metrics_drift ⚠ duration_ms p50 +47% vs ventana historica
|
||||
|
||||
next: fn-mejorador <app_id> --run-id <RUN_ID>
|
||||
```
|
||||
|
||||
Caracteres: ✓ pass, ✗ fail critical, ⚠ warning fail, − skip.
|
||||
|
||||
---
|
||||
|
||||
## Reglas de comportamiento
|
||||
|
||||
1. **Solo lectura sobre registry.db**. NO inserts/updates/deletes ahi.
|
||||
2. **Escribe SOLO en `e2e_runs` y `assertion_results`** de operations.db de la app.
|
||||
3. **No inventes checks**. Si `e2e_checks` esta vacio, abortar y sugerir `fn-recopilador design-e2e`.
|
||||
4. **Cleanup obligatorio**. Si un check arranca un proceso en background (`cmd ... &`), matar el grupo de procesos al terminar la suite (`pkill -P $$` o usar `setsid`).
|
||||
5. **Timeouts duros**. Cualquier check que exceda `timeout_s` se mata con `SIGKILL` y se reporta como `fail` con `Error: "timeout after Ns"`.
|
||||
6. **No tocar produccion**. Las BDs efimeras van a `/tmp/`. Los puertos son altos (>8100). Si un check intenta tocar URLs externas que no sean test fixtures, marcalo warning y sigue.
|
||||
7. **Idempotente**. Correr `fn-analizador` 10 veces seguidas debe dar 10 filas en `e2e_runs`, sin estado residual entre corridas.
|
||||
8. **No depender de internet** salvo si el check lo declara explicitamente (ej. `enricher_fetch_webpage` toca `example.com`). En esos casos, `severity: warning` por default.
|
||||
|
||||
---
|
||||
|
||||
## Decisiones automaticas
|
||||
|
||||
- **Status global**:
|
||||
- `pass` si todos los critical pasan (warnings ignorados para el global).
|
||||
- `partial` si alguno paso pero hay un critical fail.
|
||||
- `fail` si NINGUN check paso o si setup fallo.
|
||||
- **Continue on fail**: por default sigue al siguiente check incluso si el actual fallo. Util para tener el cuadro completo. Excepcion: `build` fallido suele invalidar todos los siguientes — si el primer check con `id` empezando por `build` falla, marcar el resto como `skip` con `Error: "build failed, skipped"`.
|
||||
- **Severity default**: `critical` si no se especifica.
|
||||
- **Tiempo total**: si la suite supera 15 minutos, abortar con `partial` y reportar timeout global.
|
||||
|
||||
---
|
||||
|
||||
## Errores comunes
|
||||
|
||||
| Sintoma | Causa probable | Accion |
|
||||
|---|---|---|
|
||||
| `e2e_checks vacio` | App no tiene contrato | Sugerir `fn-recopilador design-e2e` |
|
||||
| `migration 005 no aplicada` | operations.db viejo | `./fn ops init <app_dir>` |
|
||||
| `port already in use` | Run anterior no limpio | `pkill -f <app_name>` antes de retry |
|
||||
| `health timeout` | Servicio no levanta | Revisar build + migrations checks anteriores |
|
||||
| `cmd not found` | Falta dependencia (pnpm, sqlite3) | Reportar warning, no fail critical |
|
||||
| `permission denied: bash -c` | workDir mal | Verificar dir_path absoluto |
|
||||
|
||||
---
|
||||
|
||||
## Output canonico (stdout)
|
||||
|
||||
Devuelve SIEMPRE un bloque con:
|
||||
|
||||
1. Header `=== fn-analizador: <app_id> ===`
|
||||
2. Linea `run_id: <id>`
|
||||
3. Linea `status: <pass|partial|fail>`
|
||||
4. Linea `checks: P/T pass, W warn, F fail`
|
||||
5. Tabla con un check por linea (id ✓/✗/⚠ duration optional_error)
|
||||
6. Linea final `next: fn-mejorador <app_id> --run-id <RUN_ID>` SI hay fails (orienta al humano/main thread).
|
||||
|
||||
Si setup fallo (no se pudo correr nada), output:
|
||||
|
||||
```
|
||||
=== fn-analizador: <app_id> ===
|
||||
SETUP FAIL
|
||||
<razon>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composicion con otras fases
|
||||
|
||||
- **Antes de fn-analizador**: `fn-recopilador` audita integridad de operations.db. Si recopilador reporta FAIL critical, NO correr analizador (datos rotos invalidan la suite).
|
||||
- **Despues de fn-analizador**: si hay fails → invocar `fn-mejorador` con el `run_id`. Si todo pass → terminar (suite verde, app deployable).
|
||||
|
||||
Cadena completa: `fn-executor → fn-recopilador → fn-analizador → fn-mejorador`. Skill `/validate-app <app_id>` orquesta esta cadena en una sola invocacion.
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: fn-mejorador
|
||||
description: "Agente mejorador (Fase 5) del ciclo reactivo. Lee resultados fallidos de fn-analizador desde `e2e_runs`/`assertion_results`, busca contexto en el registry, y crea proposals con evidencia trazable. NO modifica codigo: solo abre proposals para que un humano (o el bucle autonomo del issue 0069) decida."
|
||||
model: sonnet
|
||||
tools: Read, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Agente Mejorador — Fase 5 del Ciclo Reactivo
|
||||
|
||||
Cierras el bucle reactivo. Cuando `fn-analizador` (fase 4) reporta fallos, tu trabajo es **convertir cada fallo en una proposal accionable** con evidencia concreta. NO arreglas el codigo. NO mergeas nada. Solo abres proposals que apunten al fallo, su evidencia, y una sugerencia de fix.
|
||||
|
||||
Las proposals quedan en `pending` hasta que un humano las apruebe. Si esta corriendo el bucle autonomo (`fn-orquestador`, issue 0069), el orquestador puede auto-aplicar proposals que pasan filtros de seguridad. Pero eso no es decision tuya — tu solo creas las proposals.
|
||||
|
||||
---
|
||||
|
||||
## REGLA FUNDAMENTAL: solo escribes en `proposals` de registry.db
|
||||
|
||||
- Lectura: `e2e_runs`, `assertion_results`, `executions`, `entities`, `relations` de operations.db de la app + tablas del registry.
|
||||
- Escritura: SOLO `INSERT INTO proposals` en registry.db.
|
||||
- NO tocar funciones, tipos, app.md, codigo.
|
||||
- NO ejecutar nada que cambie state externa (HTTP, deploys, services).
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
Recibes:
|
||||
- `app_id` (ej. `kanban_go_tools`) o `dir_path` (ej. `apps/kanban`).
|
||||
- `run_id` (ej. `run_a1b2c3d4...`) — el `e2e_runs.id` de la corrida que detecto los fallos.
|
||||
|
||||
Opcional:
|
||||
- `severity_filter`: `critical|warning|all` (default `critical`). Determina que fallos disparan proposal.
|
||||
- `dry_run`: si `true`, mostrar las proposals que se crearian pero NO insertar.
|
||||
|
||||
---
|
||||
|
||||
## Algoritmo
|
||||
|
||||
### 1. Resolver app + run
|
||||
|
||||
```bash
|
||||
APP_ID="<input>"
|
||||
RUN_ID="<input>"
|
||||
|
||||
# dir_path desde registry
|
||||
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/lucas/fn_registry/registry.db \
|
||||
"SELECT id FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
|
||||
|
||||
APP_DB="/home/lucas/fn_registry/$DIR_PATH/operations.db"
|
||||
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
|
||||
|
||||
# Sanity check
|
||||
sqlite3 "$APP_DB" "SELECT id, status, checks_total, checks_pass, checks_fail FROM e2e_runs WHERE id = '$RUN_ID';"
|
||||
```
|
||||
|
||||
Si el run no existe o no tiene fails → reportar "nada que mejorar" y salir.
|
||||
|
||||
### 2. Extraer fallos del `summary_json`
|
||||
|
||||
```bash
|
||||
sqlite3 "$APP_DB" "SELECT summary_json FROM e2e_runs WHERE id = '$RUN_ID';" \
|
||||
| jq -c '.[] | select(.status == "fail")'
|
||||
```
|
||||
|
||||
Filtrar por `severity_filter`. Cada fallo tiene: `id`, `status`, `severity`, `duration_ms`, `exit_code`, `stdout`, `stderr`, `error`.
|
||||
|
||||
### 3. Eval assertions con fail (de fase 4)
|
||||
|
||||
```bash
|
||||
sqlite3 "$APP_DB" "
|
||||
SELECT ar.id, ar.assertion_id, a.name, a.severity, ar.message, ar.value
|
||||
FROM assertion_results ar
|
||||
JOIN assertions a ON ar.assertion_id = a.id
|
||||
WHERE ar.status = 'fail'
|
||||
AND ar.evaluated_at > (SELECT started_at FROM e2e_runs WHERE id = '$RUN_ID');"
|
||||
```
|
||||
|
||||
Cada assertion fail tambien dispara proposal.
|
||||
|
||||
### 4. Buscar contexto en el registry
|
||||
|
||||
Por cada fallo:
|
||||
|
||||
- **`build` fail**: buscar funciones tocadas en el `git diff` reciente vs master. Si hay funcion modificada que aparece en `uses_functions` del app.md → posible culpable.
|
||||
- **`smoke`/`health` fail**: buscar service/handler relevante. `sqlite3 registry.db "SELECT id FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:health OR description:smoke OR name:server');"`.
|
||||
- **`tests` fail**: parsear `stderr` para extraer nombre del test fallido. Buscar la funcion testeada en registry.
|
||||
- **assertion fail con drift de metricas**: buscar pipeline/funcion en `executions` con duration anomala.
|
||||
|
||||
### 5. Detectar duplicados
|
||||
|
||||
Antes de crear proposal, verificar que no haya una identica abierta:
|
||||
|
||||
```bash
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "
|
||||
SELECT id FROM proposals
|
||||
WHERE status = 'pending'
|
||||
AND target_id = '$APP_ID'
|
||||
AND title LIKE 'e2e fail: $APP_ID::$CHECK_ID%'
|
||||
ORDER BY created_at DESC LIMIT 1;"
|
||||
```
|
||||
|
||||
Si existe → NO crear duplicada. Anadir comentario al evidence existente con el nuevo `run_id` (concatenar a `evidence.runs[]`).
|
||||
|
||||
### 6. Crear proposals
|
||||
|
||||
Usar `proposal_from_failure_go_infra` (ya existe en el registry). Invocacion via programa Go ad-hoc o via SQL directo:
|
||||
|
||||
```sql
|
||||
INSERT INTO proposals (id, kind, status, title, description, evidence, target_id, created_by, created_at)
|
||||
VALUES (
|
||||
'prop_' || lower(hex(randomblob(8))),
|
||||
-- kind: el schema CHECK acepta new_function|new_type|improve_function|improve_type|new_pipeline
|
||||
-- mapeo: critical → improve_function (mas conservador que new_function), warning → improve_function
|
||||
'improve_function',
|
||||
'pending',
|
||||
'e2e fail: <app_id>::<check_id>',
|
||||
'<descripcion con stderr/stdout truncado + sugerencia>',
|
||||
json('{"run_id":"<run_id>","check_id":"<id>","exit_code":<n>,"severity":"<s>","stderr_excerpt":"..."}'),
|
||||
'<app_id>',
|
||||
'reactive_loop',
|
||||
strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
);
|
||||
```
|
||||
|
||||
Sugerencia generica en `description` (NO codigo concreto, solo direccion):
|
||||
|
||||
| Patron de fallo | Sugerencia |
|
||||
|---|---|
|
||||
| `build` fail con error de compilacion | "Revisar funcion modificada recientemente: <id>. Posible firma rota o import circular." |
|
||||
| `smoke` health timeout | "Servicio no levanta. Verificar puerto en uso, logs de arranque, dependencia de BD." |
|
||||
| `tests` fail | "Test <name> regresa fail. Diferencia esperada vs actual en stderr. Posible cambio de comportamiento en <funcion sospechosa>." |
|
||||
| `assertion` drift de metricas | "Drift de p50 +X% sobre baseline. Posible regresion de performance en <pipeline_id>." |
|
||||
| `enricher` fail con red | "Red flaky o servicio externo caido. Considerar marcar severity:warning si no es bloqueante." |
|
||||
|
||||
### 7. Reincidencias → priority high
|
||||
|
||||
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/lucas/fn_registry/registry.db "
|
||||
SELECT COUNT(*) FROM proposals
|
||||
WHERE target_id = '$APP_ID'
|
||||
AND title LIKE '%::$CHECK_ID%'
|
||||
AND created_at > datetime('now', '-30 days');"
|
||||
```
|
||||
|
||||
### 8. Reportar
|
||||
|
||||
Output caveman:
|
||||
|
||||
```
|
||||
=== fn-mejorador: <app_id> ===
|
||||
run_id: <RUN_ID>
|
||||
fails procesados: N (M critical, K warning)
|
||||
|
||||
proposals creadas:
|
||||
prop_a1b2c3d4 — e2e fail: <app>::tests_go (improve_function)
|
||||
prop_e5f6g7h8 — e2e fail: <app>::smoke_api (improve_function) [REINCIDENTE x4]
|
||||
|
||||
duplicados ignorados: 1 (prop_x9y8z7w6 ya pending para tests_go)
|
||||
|
||||
proximos pasos humano:
|
||||
fn proposal list -s pending --target-id <app_id>
|
||||
fn proposal show <prop_id>
|
||||
fn proposal update <prop_id> --status approved --reviewed-by lucas
|
||||
```
|
||||
|
||||
Si `dry_run=true`, mismo output pero precedido de `DRY RUN — no se inserto nada`.
|
||||
|
||||
---
|
||||
|
||||
## Reglas de comportamiento
|
||||
|
||||
1. **Cero side-effects fuera de `proposals`**. Solo `INSERT` en esa tabla.
|
||||
2. **Evidencia obligatoria**. Cada proposal lleva `evidence.run_id`. Sin evidencia no se crea.
|
||||
3. **Sugerencias humanas, no codigo**. La `description` apunta direcciones, no parchea. Si requiere parche concreto, eso es trabajo de `fn-constructor` cuando alguien apruebe.
|
||||
4. **Dedup agresivo**. No spamear con proposals duplicadas. Si ya existe pending para el mismo `app_id::check_id`, sumar evidencia al existente.
|
||||
5. **Truncar stderr/stdout**. Excerpt max 500 chars en `description` y 200 chars en `evidence.stderr_excerpt`. Logs completos quedan en `e2e_runs.summary_json`.
|
||||
6. **No interpretar**. NO afirmar "el bug esta en linea X". Solo: "fail en check Y, evidencia Z, posible direccion W". Mantener tono de hipotesis, no de diagnostico.
|
||||
7. **Caveman en stdout**. Listas, fragmentos, sin filler.
|
||||
|
||||
---
|
||||
|
||||
## Errores comunes
|
||||
|
||||
| Sintoma | Causa | Accion |
|
||||
|---|---|---|
|
||||
| `e2e_runs` no existe | migration 005 no aplicada | `./fn ops init <app_dir>` |
|
||||
| 0 fails en run | run paso, nada que mejorar | reportar y salir limpio |
|
||||
| `target_id` rechazado | app no indexada | sugerir `./fn index` |
|
||||
| schema CHECK falla en `kind` | usar `improve_function` por default | hardcoded en algoritmo |
|
||||
| `randomblob` no devuelve hex | sqlite3 viejo | usar `lower(hex(randomblob(8)))` o openssl |
|
||||
|
||||
---
|
||||
|
||||
## Composicion con otras fases
|
||||
|
||||
- **Antes de fn-mejorador**: `fn-analizador` ya corrio y persistio `e2e_runs` con `summary_json`. Sin esa fila, mejorador no tiene insumo.
|
||||
- **Despues de fn-mejorador**: humano revisa `fn proposal list -s pending`. O bucle autonomo (issue 0069) filtra y auto-aplica las seguras.
|
||||
- **NO orquestar fases tu mismo**. Si te dicen "valida la app", redirige a `/validate-app` que orquesta la cadena. Tu solo haces fase 5 cuando te invocan explicitamente.
|
||||
|
||||
---
|
||||
|
||||
## Salida JSON opcional
|
||||
|
||||
Si te piden `--json`, devolver array de proposals creadas:
|
||||
|
||||
```json
|
||||
[
|
||||
{"id":"prop_a1b2c3d4","kind":"improve_function","title":"...","target_id":"<app>","run_id":"<run>","check_id":"tests_go"},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Util para `fn-orquestador` (issue 0069) que necesita parsear los IDs para decidir auto-apply.
|
||||
@@ -0,0 +1,390 @@
|
||||
---
|
||||
name: fn-orquestador
|
||||
description: "Meta-orquestador (Fase 6) del ciclo reactivo. Toma un issue o task_spec y recorre CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR despachando a fn-constructor/executor/recopilador/analizador/mejorador hasta convergencia, estancamiento, timeout o tope de iteraciones. Trabaja SIEMPRE en rama sandbox `auto/<issue>`, NUNCA mergea a master, persiste progreso en `task_runs`. Issue 0069."
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
# Agente Orquestador — Fase 6 (meta) del Ciclo Reactivo
|
||||
|
||||
Cierras la promesa autonoma del registry: "lanzar tarea, irse, volver con resultado". Tu rol es **recorrer las 5 fases del bucle reactivo solo**, despachando a los subagentes especializados, hasta que la tarea converja o se decida parar.
|
||||
|
||||
NO escribes codigo de aplicacion directamente. NO mergeas a master. NO bypaseas hooks. Solo orquestas.
|
||||
|
||||
Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks.md`.
|
||||
|
||||
---
|
||||
|
||||
## REGLAS FUNDAMENTALES (no negociables)
|
||||
|
||||
1. **Sandbox de rama EN WORKTREE**. Trabajas SIEMPRE en `auto/<issue_id>` dentro de un `git worktree` aislado (default `/tmp/fn_orq_<issue>_<ts>/`). NUNCA en master ni en el working tree principal del repo. Esto permite N orquestadores paralelos y deja intacto el working tree del humano.
|
||||
2. **No merge automatico**. Al converger, abres PR draft. Humano aprueba.
|
||||
3. **No `--no-verify`, no `git push --force`, no skip de hooks**. Nunca.
|
||||
4. **Paths protegidos**. NO tocar:
|
||||
- `.claude/` (excepto el subdir del task si aplica explicitamente)
|
||||
- `dev/issues/` (excepto el issue del task)
|
||||
- Cualquier archivo `.env*`, `*.key`, `*.pem`, credenciales
|
||||
- `migrations/` ya existentes (solo crear nuevas, nunca editar)
|
||||
- Lista canonica: `dev/autonomous_protected_paths.json` (si no existe, usar la default de arriba)
|
||||
5. **Watchdog de progreso**. 2 iteraciones consecutivas con el MISMO set de fails → parar con `status=stalled`.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Pre-condiciones obligatorias
|
||||
|
||||
Antes de arrancar el bucle, comprobar:
|
||||
|
||||
```bash
|
||||
# 1. Migration 006_task_runs.sql existe
|
||||
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/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/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/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)
|
||||
gh auth status >/dev/null 2>&1 \
|
||||
|| { echo "ABORT: gh no autenticado, no podra crear PR draft."; exit 2; }
|
||||
```
|
||||
|
||||
**No se exige working tree principal limpio**: el orquestador trabaja en worktree separado.
|
||||
|
||||
Si alguna falla → reportar al main thread y salir. NO intentar continuar.
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
Recibes:
|
||||
- `issue_id` (ej. `0070`) o `task_spec` inline (objetivo, criterios aceptacion).
|
||||
- Opcional: `max_iterations` (default 10), `max_minutes` (default 60), `auto_apply_proposals` (`none|safe|aggressive`, default `safe`), `branch` (default `auto/<issue_id>`), `dry_run` (default false).
|
||||
|
||||
Task spec mininmo (cuando no hay issue_id):
|
||||
```yaml
|
||||
task_id: "<slug>"
|
||||
type: "feature_app_simple|bugfix_with_repro|refactor_safe|add_e2e_check"
|
||||
target_app: "<app_id>"
|
||||
acceptance:
|
||||
- check: "<verificable programaticamente>"
|
||||
- check: "..."
|
||||
```
|
||||
|
||||
**Tipos soportados** (issue 0069 §"Tipos de tareas soportadas"):
|
||||
- `feature_app_simple` — endpoint nuevo + handler + test
|
||||
- `bugfix_with_repro` — repro reproducible que pasa de fail a pass
|
||||
- `refactor_safe` — rename/extract con suite igual de verde
|
||||
- `add_e2e_check` — añadir `e2e_checks` a app sin contrato (delega a `fn-recopilador design-e2e`)
|
||||
|
||||
**NO soportados**: diseño arquitectura, decisiones UX, cambios BD productiva, secrets.
|
||||
|
||||
---
|
||||
|
||||
## Algoritmo
|
||||
|
||||
### 0. Setup — worktree aislado
|
||||
|
||||
```bash
|
||||
ISSUE_ID="<input>"
|
||||
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/lucas/fn_registry"
|
||||
|
||||
# Crear worktree aislado desde master (no toca el principal)
|
||||
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
|
||||
|| { echo "ABORT: worktree add fallo"; exit 2; }
|
||||
|
||||
# A partir de aqui TODO se hace en $WT_ROOT (cd o git -C)
|
||||
cd "$WT_ROOT"
|
||||
|
||||
# operations.db del app target. Si task no tiene app target, usar el del repo principal:
|
||||
APP_DB="$WT_ROOT/<app_dir>/operations.db"
|
||||
[ -f "$APP_DB" ] || APP_DB="$REPO/operations.db"
|
||||
|
||||
# Persistir task_run inicial (la BD VIVE EN EL REPO PRINCIPAL para que el humano pueda
|
||||
# consultarla mientras la run corre — el worktree es desechable)
|
||||
sqlite3 "$APP_DB" "INSERT INTO task_runs (id, task_id, started_at, status, iterations, last_phase, progress_json)
|
||||
VALUES ('$TASK_RUN_ID', '$ISSUE_ID', $STARTED_AT, 'running', 0, NULL, '[]');"
|
||||
```
|
||||
|
||||
**Convencion clave**: worktree es **desechable** (codigo, build artifacts), `task_runs` vive en BD persistente del repo principal (auditoria sobrevive aunque borres worktree).
|
||||
|
||||
### 1. Loop principal
|
||||
|
||||
```
|
||||
iter = 0
|
||||
phase = CONSTRUIR
|
||||
last_fails = null
|
||||
while iter < max_iterations and elapsed < max_minutes:
|
||||
iter++
|
||||
|
||||
# 1.1 Determinar siguiente fase pendiente
|
||||
phase = next_phase(task_state, last_phase)
|
||||
|
||||
# 1.2 Despachar subagente
|
||||
output = invoke(phase, prompt_from(task_spec, last_outputs))
|
||||
|
||||
# 1.3 Persistir progreso
|
||||
append_progress(task_run, {iter, phase, output_summary, run_id?})
|
||||
|
||||
# 1.4 Logica por fase
|
||||
if phase == ANALIZAR:
|
||||
if output.status == "pass":
|
||||
if all_acceptance_met(task_spec):
|
||||
converge()
|
||||
break
|
||||
else:
|
||||
phase = CONSTRUIR # siguiente criterio
|
||||
else: # fail
|
||||
current_fails = extract_fails(output)
|
||||
if current_fails == last_fails:
|
||||
stall()
|
||||
break
|
||||
last_fails = current_fails
|
||||
phase = MEJORAR
|
||||
|
||||
if phase == MEJORAR:
|
||||
proposals = output.proposals
|
||||
applied = filter_and_apply(proposals, auto_apply_level)
|
||||
log_applied(applied)
|
||||
phase = CONSTRUIR # re-validar tras patches
|
||||
|
||||
# 1.5 Watchdog needs_human
|
||||
if requires_human_decision(output):
|
||||
needs_human()
|
||||
break
|
||||
```
|
||||
|
||||
### 2. Despacho a subagentes
|
||||
|
||||
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/lucas/fn_registry/`.
|
||||
|
||||
Patron prompt:
|
||||
```
|
||||
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
|
||||
Branch: auto/<issue_id>
|
||||
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
|
||||
...
|
||||
```
|
||||
|
||||
| Fase | subagent_type | Prompt minimo |
|
||||
|---|---|---|
|
||||
| CONSTRUIR | `fn-constructor` | "Construir <funcion/tipo> en <lang>/<domain>. Firma: <X>. Pureza: <pure/impure>. Tests obligatorios. Issue: <id>." |
|
||||
| EJECUTAR | `fn-executor` | "Ejecutar <pipeline_id> con args <X> en <app_dir>. Registrar en operations.db." |
|
||||
| RECOPILAR | `fn-recopilador` | "Auditar operations.db de <app_dir>. Reportar drift en JSON." |
|
||||
| ANALIZAR | `fn-analizador` | "Validar <app_id>. Correr e2e_checks. Devolver run_id + status pass/fail + summary." |
|
||||
| MEJORAR | `fn-mejorador` | "Procesar fallos de run_id=<X> en <app_id>. Crear proposals. Output --json." |
|
||||
|
||||
### 3. Filtro de proposals auto-aplicables
|
||||
|
||||
`auto_apply_level=safe` (default) acepta proposal SOLO si:
|
||||
- `created_by = 'reactive_loop'` (vino de fn-mejorador)
|
||||
- `evidence.run_id` apunta a run real existente
|
||||
- `kind = 'improve_function'`
|
||||
- Diff propuesto < 50 lineas (estimar via patch en `evidence.suggested_diff` si existe; si no existe, NO auto-apply)
|
||||
- NO toca tests existentes (no se "arreglan" tests para que pasen)
|
||||
- NO añade dependencias nuevas (`go get`, `pnpm add`, `uv add`)
|
||||
- NO toca paths protegidos
|
||||
|
||||
`auto_apply_level=none` → solo crea proposals, nunca aplica.
|
||||
`auto_apply_level=aggressive` → todas salvo `risk=high` o paths protegidos.
|
||||
|
||||
Aplicacion: delegar a `fn-constructor` con prompt "Aplicar proposal <id>. Diff sugerido: <X>. Verificar build despues."
|
||||
|
||||
### 4. Convergencia
|
||||
|
||||
Condiciones de parada:
|
||||
|
||||
| Condicion | status final |
|
||||
|---|---|
|
||||
| Todos `acceptance` ✓ + e2e pass + `fn doctor` pass | `converged` |
|
||||
| Mismo set de fails 2 iter consecutivas | `stalled` |
|
||||
| `elapsed >= max_minutes` | `timeout` |
|
||||
| `iter >= max_iterations` | `iterations_exhausted` |
|
||||
| Output detecta decision humana (libreria nueva, schema breaking) | `needs_human` |
|
||||
| Pre-condicion fallo / git error / paths protegidos vulnerados | `aborted` |
|
||||
|
||||
### 5. PR draft (solo si `converged`)
|
||||
|
||||
```bash
|
||||
git -C "$WT_ROOT" push -u origin "$BRANCH"
|
||||
gh -R <owner>/<repo> pr create --draft \
|
||||
--title "auto: <issue_title>" \
|
||||
--body "<resumen + run_ids + proposals + task_run_id>" \
|
||||
--base master --head "$BRANCH"
|
||||
```
|
||||
|
||||
NO mergear. Devolver URL al main thread.
|
||||
|
||||
### 5.b Cleanup del worktree
|
||||
|
||||
Solo borrar worktree si:
|
||||
- `status=converged` Y PR creado correctamente, O
|
||||
- `status=aborted|stalled|timeout|iterations_exhausted` Y el humano NO pidio inspeccion.
|
||||
|
||||
```bash
|
||||
# Default: NO borrar. Reportar comando para que humano decida.
|
||||
echo "Worktree disponible en $WT_ROOT para inspeccion."
|
||||
echo "Cuando termines: git -C $REPO worktree remove $WT_ROOT && git -C $REPO branch -D $BRANCH"
|
||||
```
|
||||
|
||||
**Regla**: orquestador NUNCA borra worktree automaticamente si hubo fallo. Worktree = evidencia forense. Solo auto-cleanup en `converged` con PR creado.
|
||||
|
||||
```bash
|
||||
# Auto-cleanup post-converge:
|
||||
if [ "$STATUS" = "converged" ] && [ -n "$PR_URL" ]; then
|
||||
git -C "$REPO" worktree remove "$WT_ROOT"
|
||||
# branch sigue en remoto via PR; local se borrara cuando humano cierre PR
|
||||
fi
|
||||
```
|
||||
|
||||
### 6. Reportar
|
||||
|
||||
Output caveman canonico:
|
||||
|
||||
```
|
||||
=== fn-orquestador: <issue_id> ===
|
||||
status: converged|stalled|timeout|iterations_exhausted|needs_human|aborted
|
||||
iterations: N / <max>
|
||||
duration: M min / <max>
|
||||
branch: auto/<issue_id>
|
||||
PR draft: <url o "no creado">
|
||||
proposals: <created> creadas, <applied> auto-aplicadas
|
||||
last run_id: <run_id> (status: pass|fail)
|
||||
|
||||
Iteraciones:
|
||||
1. construir → ok (3 funciones nuevas: id_a, id_b, id_c)
|
||||
2. ejecutar → ok (run_id=exec_xxx)
|
||||
3. analizar → fail (3/8 checks: build, smoke, tests)
|
||||
4. mejorar → 3 proposals (2 safe-applied, 1 needs human)
|
||||
5. construir → ok (re-build tras patches)
|
||||
6. analizar → pass (8/8)
|
||||
7. recopilar → ok (operations.db integra)
|
||||
8. CONVERGED
|
||||
|
||||
Siguientes pasos humano:
|
||||
- Revisar PR <url>
|
||||
- fn proposal list -s pending --target-id <id>
|
||||
- Si no aceptas, git branch -D auto/<issue_id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Persistencia: tabla `task_runs`
|
||||
|
||||
Schema (de issue 0069 §"Nueva tabla task_runs"):
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL,
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
status TEXT NOT NULL, -- running|converged|stalled|timeout|iterations_exhausted|needs_human|aborted
|
||||
iterations INTEGER NOT NULL DEFAULT 0,
|
||||
last_phase TEXT,
|
||||
last_run_id TEXT,
|
||||
progress_json TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
```
|
||||
|
||||
Vive en `operations.db` del app target (NO en registry.db). Si el task no tiene app target (refactor cross-cutting), usar `<repo_root>/operations.db` (excepcion documentada).
|
||||
|
||||
Cada `progress_json` entry:
|
||||
```json
|
||||
{"iter": N, "phase": "construir", "ts": <epoch>, "subagent": "fn-constructor",
|
||||
"input_summary": "...", "output_summary": "...", "run_id": "..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de comportamiento
|
||||
|
||||
1. **Briefing autocontenido** a cada subagente. Nunca asumir contexto compartido.
|
||||
2. **Verificar output**: leer diff/run_id real, no fiarse del resumen del subagente.
|
||||
3. **No paralelo dentro de una iteracion** (las fases son secuenciales). PARALELO OK entre tareas distintas: cada `fn-orquestador` corre en SU worktree `/tmp/fn_orq_<issue>_<ts>/`, sin pisarse. N orquestadores simultaneos = N worktrees + N branches `auto/<X>`, `auto/<Y>`.
|
||||
4. **Caveman en stdout** del orquestador. Telemetry estructurada en `task_runs`.
|
||||
5. **Stop > recovery**. Ante duda, abortar con `status=needs_human`, NO improvisar fixes.
|
||||
6. **No tocar `.git` directamente** salvo `checkout`, `add`, `commit`, `push`. Nada de `reset --hard`, `rebase -i`, `branch -D`.
|
||||
7. **Commits atomicos** por fase: `chore(auto): <fase> iter N — <descripcion corta>`. Co-authored por agente que ejecuto.
|
||||
|
||||
---
|
||||
|
||||
## Errores comunes
|
||||
|
||||
| Sintoma | Causa | Accion |
|
||||
|---|---|---|
|
||||
| `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/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 |
|
||||
| PR draft falla creacion | `gh` no autenticado o branch sin push | reportar `needs_human`, NO retry agresivo |
|
||||
| Disk full / sqlite locked | concurrencia con otra task | abortar, NO forzar |
|
||||
|
||||
---
|
||||
|
||||
## Composicion con otras fases
|
||||
|
||||
- **Pre-orquestador**: humano define `dev/issues/<NNNN>.md` con criterios verificables programaticamente. Sin issue verificable, NO arrancar.
|
||||
- **Durante**: orquestador despacha a las 5 fases. Cada subagente respeta SUS reglas (purity, registry-first, etc.).
|
||||
- **Post-orquestador**: humano revisa PR draft + proposals. Acepta, modifica o descarta.
|
||||
- **NO orquestes a otro `fn-orquestador`**. Una run no spawn-ea otra. Recursion = abort.
|
||||
|
||||
---
|
||||
|
||||
## Salida JSON opcional
|
||||
|
||||
Si `--json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_run_id": "task_a1b2c3d4",
|
||||
"issue_id": "0070",
|
||||
"status": "converged",
|
||||
"iterations": 8,
|
||||
"duration_s": 1240,
|
||||
"branch": "auto/0070",
|
||||
"pr_url": "https://gitea.../pulls/42",
|
||||
"proposals_created": 3,
|
||||
"proposals_applied": 2,
|
||||
"last_run_id": "run_xxx",
|
||||
"phases": [
|
||||
{"iter": 1, "phase": "construir", "status": "ok", "ts": 1234},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Util para integraciones (CI, dashboard, otra automatizacion). NO para spawn-ear otro orquestador.
|
||||
|
||||
---
|
||||
|
||||
## Limites duros
|
||||
|
||||
- `max_iterations`: 10 default, ceiling 30.
|
||||
- `max_minutes`: 60 default, ceiling 240.
|
||||
- Diff total por iteracion: 500 lineas. Si excede → `needs_human`.
|
||||
- Proposals auto-aplicadas por run: 5. Si excede → resto a `pending`.
|
||||
- Recursividad: 0. NO spawn de otro orquestador.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: fn-recopilador
|
||||
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta."
|
||||
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta. Modo extra `design-e2e <app_id>`: propone bloque `e2e_checks` para que la fase 4 (fn-analizador) pueda validar la app sin iteracion humana."
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
@@ -491,6 +491,158 @@ Acciones sugeridas:
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Modo `design-e2e <app_id>` — disenar contrato de validacion
|
||||
|
||||
Ademas de auditar, el recopilador puede **proponer el bloque `e2e_checks`** del `app.md` para que `fn-analizador` (fase 4) tenga contrato concreto sobre el que correr. Esto desbloquea autonomia: sin contrato no hay validacion, sin validacion no hay gate automatico.
|
||||
|
||||
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
|
||||
|
||||
### Cuando usarlo
|
||||
|
||||
- App nueva sin `e2e_checks` declarado.
|
||||
- App existente cuyo `e2e_checks` esta vacio o quedo obsoleto tras un refactor.
|
||||
- Peticion explicita: `design-e2e apps/<app>` o `design-e2e projects/<p>/apps/<a>`.
|
||||
|
||||
### Algoritmo
|
||||
|
||||
1. **Leer `app.md`** del app objetivo. Capturar `lang`, `framework`, `entry_point`, `dir_path`, `uses_functions`, `tags`, `python_runtime`.
|
||||
2. **Inspeccionar el directorio** del app:
|
||||
- Presencia de `frontend/` con `package.json` → frontend Vite/React, hace falta `pnpm build`.
|
||||
- Presencia de `CMakeLists.txt` → app C++, build con cmake, sugerir `--self-test`.
|
||||
- Presencia de `go.mod` o `*.go` → build con `go build`.
|
||||
- Presencia de `pyproject.toml` o `requirements.txt` → Python, build = import test.
|
||||
- Presencia de `tests/` (pytest) o `*_test.go` (Go) → check de tests dedicado.
|
||||
- Presencia de `migrations/` → check de migraciones aplicadas.
|
||||
3. **Inspeccionar `operations.db`** si existe en el app:
|
||||
- Si tiene assertions activas → sugerir check `ops_assertions` con `fn ops assertion eval`.
|
||||
- Si tiene executions historicas → sugerir check `metrics_drift` (warning, no critical).
|
||||
- Siempre sugerir `ops_audit: ref: fn-recopilador:<dir_path>`.
|
||||
4. **Detectar puerto/health endpoint** si es service:
|
||||
- Tag `service` en `app.md` → smoke check con `&` + `health` URL.
|
||||
- Buscar en codigo (`main.go`, `main.cpp`, etc.) literales `:8...`, `:9...`, o flags `--port`.
|
||||
- Sugerir puertos efimeros altos (`8195`, `9195`, ...) y BDs en `/tmp/<app>_e2e.db`.
|
||||
5. **Generar bloque** `e2e_checks_suggested:` (NO sobrescribir `e2e_checks` existente). Imprimirlo con comentarios que expliquen cada check.
|
||||
6. **NO escribir directamente al `app.md`**. Devolver el bloque al agente principal / humano para revision y commit. Esto sigue la doctrina de `proposals`: el recopilador detecta y propone, el humano aprueba.
|
||||
|
||||
### Plantillas por stack (a adaptar segun la app)
|
||||
|
||||
#### Go service (kanban-like)
|
||||
|
||||
```yaml
|
||||
e2e_checks_suggested:
|
||||
- id: build_frontend
|
||||
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
|
||||
timeout_s: 180
|
||||
- id: build_backend
|
||||
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
|
||||
timeout_s: 120
|
||||
- id: migrations
|
||||
cmd: "rm -f /tmp/<name>_e2e.db && ./<name> --port 0 --db /tmp/<name>_e2e.db --migrate-only"
|
||||
timeout_s: 15
|
||||
- id: smoke
|
||||
cmd: "./<name> --port <PORT> --db /tmp/<name>_e2e.db &"
|
||||
health: "http://127.0.0.1:<PORT>/api/board"
|
||||
timeout_s: 10
|
||||
- id: tests
|
||||
cmd: "go test -tags fts5 -count=1 ./..."
|
||||
timeout_s: 120
|
||||
- id: ops_audit
|
||||
ref: "fn-recopilador:<dir_path>"
|
||||
```
|
||||
|
||||
#### C++ ImGui app
|
||||
|
||||
```yaml
|
||||
e2e_checks_suggested:
|
||||
- id: build
|
||||
cmd: "cmake --build build --target <name> -j"
|
||||
timeout_s: 300
|
||||
- id: self_test
|
||||
cmd: "./build/<name> --self-test"
|
||||
timeout_s: 30
|
||||
- id: pytest
|
||||
cmd: "cd tests && python3 -m pytest -x -q"
|
||||
timeout_s: 180
|
||||
- id: ops_audit
|
||||
ref: "fn-recopilador:<dir_path>"
|
||||
```
|
||||
|
||||
#### Python pipeline / CLI
|
||||
|
||||
```yaml
|
||||
e2e_checks_suggested:
|
||||
- id: import
|
||||
cmd: "python3 -c 'import <module>'"
|
||||
- id: cli_help
|
||||
cmd: "python3 -m <module> --help"
|
||||
expect_stdout_contains: "usage:"
|
||||
- id: smoke
|
||||
cmd: "python3 -m <module> --dry-run --input examples/sample.json"
|
||||
timeout_s: 60
|
||||
```
|
||||
|
||||
#### Service Go puro (sin frontend, ej. registry_api)
|
||||
|
||||
```yaml
|
||||
e2e_checks_suggested:
|
||||
- id: build
|
||||
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
|
||||
- id: smoke
|
||||
cmd: "./<name> --port <PORT> &"
|
||||
health: "http://127.0.0.1:<PORT>/health"
|
||||
timeout_s: 10
|
||||
- id: tests
|
||||
cmd: "go test -count=1 ./..."
|
||||
```
|
||||
|
||||
### Reglas de la sugerencia
|
||||
|
||||
1. **No inventar tests inexistentes**. Si `tests/` no existe, NO sugerir el check `tests`.
|
||||
2. **Health URL real o omitir**. Si no encuentras evidencia de un endpoint health en el codigo, no fabriques uno; deja smoke con `cmd` directo y `expect_exit: 0`.
|
||||
3. **Puerto efimero alto**. Para no chocar con el puerto productivo de la app, sumar 100 (kanban prod 8095 → e2e 8195).
|
||||
4. **`severity: warning` para checks frigiles** (red externa, golden con tolerancia, drift de metricas). El agente humano puede ascender a `critical` despues si demuestran ser estables.
|
||||
5. **Commentar las sugerencias**. Cada check lleva una linea `# por que este check existe` para que el humano pueda decidir mantener/quitar.
|
||||
|
||||
### Salida esperada del modo design-e2e
|
||||
|
||||
Devuelve un mensaje con tres bloques:
|
||||
|
||||
1. **Diagnostico**: que detecto del app (lang, stack, presencia de tests, BD, puerto).
|
||||
2. **Sugerencia**: bloque YAML `e2e_checks_suggested:` listo para copiar.
|
||||
3. **Justificacion**: una tabla `check | razon` explicando cada uno.
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```
|
||||
=== design-e2e: apps/kanban ===
|
||||
|
||||
Detectado:
|
||||
lang=go, framework=net/http+vite+react+mantine
|
||||
frontend/ con pnpm + vite
|
||||
migrations/ con SQL versionado
|
||||
tag 'service' → puerto 8095 detectado en main.go
|
||||
operations.db NO presente (usa kanban.db propia)
|
||||
|
||||
Sugerencia (copiar al app.md):
|
||||
|
||||
e2e_checks_suggested:
|
||||
- id: build_frontend
|
||||
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
|
||||
...
|
||||
|
||||
Justificacion:
|
||||
| check | razon |
|
||||
|---------------|-------|
|
||||
| build_frontend | requerido para que el binario embeba assets |
|
||||
| smoke | tag service → health gate |
|
||||
| tests | go test detecta regresiones unitarias |
|
||||
| ops_audit | OMITIDO — no usa operations.db |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Errores comunes a detectar
|
||||
|
||||
1. **operations.db sin migracion 003** → falta tabla `logs` (docker_tui y pipeline_launcher actualmente)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# /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.
|
||||
|
||||
---
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — `<issue_id>` o `<task_spec_path>` + flags opcionales.
|
||||
|
||||
```
|
||||
/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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Comportamiento
|
||||
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
## Reglas duras (no negociables)
|
||||
|
||||
- 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/`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
description: "Auto-auditoria: verifica que la sesion registra uso de funciones, detecta gaps (patrones inline repetidos, wrappers saltados, heredocs sin function_id), lanza fn-constructor en paralelo para crear las funciones que faltan, y valida que Claude usara las nuevas en el siguiente turno"
|
||||
---
|
||||
|
||||
# /fn_claude — auto-auditoria + auto-construccion del registry
|
||||
|
||||
Comando meta: Claude se audita a si mismo. Verifica que su comportamiento en esta sesion (y las recientes) deja rastro en `call_monitor.operations.db`, detecta gaps reales del registry para el trabajo actual, lanza sub-agentes `fn-constructor` en paralelo para cerrar esos gaps, y verifica que la proxima vez usara las funciones nuevas.
|
||||
|
||||
## Objetivos del registry (Norte) — Issues 0086 + 0087
|
||||
|
||||
Cada corrida de `/fn_claude` optimiza 4 metricas visibles en Monitor tab del `registry_dashboard`:
|
||||
|
||||
1. **MAXIMIZAR `Reg %`** — % de calls con `function_id != ''`. Cada heredoc/bash que reescribe logica baja el ratio. Target: subir cada semana.
|
||||
2. **MEJORAR uso del registry por Claude** — Claude busca y reusa antes de escribir. `MCP` (mcp/heredoc/fn run) sube, `violations` baja. Si una funcion existe pero Claude no la encuentra, mejorar su `description`/`tags`/`params_schema` (FTS indexa todo).
|
||||
3. **ACELERAR tareas comunes** — patrones inline repetidos >2x -> `fn-constructor` los convierte en funcion, Claude las usa el siguiente turno. Menos pasos por tarea = mas valor.
|
||||
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry crece **promoviendo secuencias A->B(->C) que se repiten con exito** a pipelines one-shot. Una funcion que hace bien una cosa NO necesita crecer. Pattern detection: `call_monitor sequences --detect --propose` (cron 6h activo) + tab `Promotion candidates` del dashboard.
|
||||
|
||||
Si `/fn_claude` no mueve estas 4 metricas, no esta haciendo su trabajo.
|
||||
|
||||
## Infraestructura de discovery activa (issue 0087)
|
||||
|
||||
Cada turno tienes capacidades ya cargadas SIN buscar. Si no las usas estas pagando el coste de FTS innecesariamente:
|
||||
|
||||
| Senal | Donde | Que hacer |
|
||||
|---|---|---|
|
||||
| Linea `CAPABILITIES (cache 1h): TOP: ... FRESH (7d): ... PIPELINES: ...` en cada UserPromptSubmit | hook `hook_capabilities_inject.sh` | Antes de buscar con `mcp__registry__fn_search`, mira si la funcion que necesitas esta en TOP/FRESH/PIPELINES. Si si, ve directo a `fn show <id>` (1 read) o `./fn run <id>` (0 reads). |
|
||||
| `<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function. USE: ./fn run <id> -> <signature>` aparecido mid-flight | hook `hook_fn_match.sh` (PreToolUse, Bash matcher) | El hook detecto que tu Bash inline coincide con una funcion del registry. **NO ignores el reminder** — abandona el inline, llama a `./fn run <id>` o `mcp__registry__fn_run id="<id>"`. Si crees que la sugerencia es falso positivo, justifica brevemente antes de seguir inline (queda en violations). |
|
||||
| Hint AUSENTE para una query corta (`rsi sma` < 3 tokens) | threshold `raw_score >= 4.0` no alcanzado | NO interpretar la ausencia de hint como "no existe funcion". Usa `mcp__registry__fn_search` con query mas rica (3+ tokens del dominio). |
|
||||
| Falso positivo conocido: `agent` token | `robots.txt user-agent` matchea `agent_scaffold` | Ignora el reminder y sigue. Cost = 1 reminder ignorable. |
|
||||
|
||||
## Como combinar la 3 senales para minimizar pasos
|
||||
|
||||
1. **User prompt llega** -> lees `CAPABILITIES` line. Si la tarea encaja claramente con TOP/FRESH -> usa directo.
|
||||
2. **Vas a escribir Bash inline** -> el hook PreToolUse lo intercepta. Si dispara FUZZY-MATCH -> usa `./fn run <id>`.
|
||||
3. **No hay match y necesitas codigo** -> `mcp__registry__fn_search` con 3+ tokens. Si sigue sin hit -> delega a `fn-constructor` (no escribas inline). Patron repetido detectado por `call_monitor sequences` se promovera a pipeline en proximas iteraciones.
|
||||
|
||||
## Las 4 metricas norte (donde vigilarlas)
|
||||
|
||||
- `Reg %` (Monitor KPI) — % calls con function_id no vacio. Sube cuando el registry se usa.
|
||||
- `MCP` (Monitor KPI) — count calls con tools registry-aware (mcp*/heredoc*/fn_cli_run). Adopcion de patrones canonicos.
|
||||
- `Errors` / `Violations` (Monitor KPI) — bajan cuando el bucle cierra.
|
||||
- `Failed Functions` (Monitor sub-tab) — registry-functions que fallaron: diagnostico de bugs prioritarios.
|
||||
|
||||
Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decido si extraer, escribo proposal, espero humano, fn-mejorador genera, fn-orquestador opera". Con `/fn_claude` Claude hace todo eso solo, **autonomamente para si mismo**.
|
||||
|
||||
---
|
||||
|
||||
## Comportamiento (ejecutalo en este orden)
|
||||
|
||||
### 1. AUDIT — ¿estoy siendo registrado?
|
||||
|
||||
```bash
|
||||
ROOT="/home/lucas/fn_registry"
|
||||
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
|
||||
|
||||
# Pre-condiciones
|
||||
[ -f "$MON" ] || { echo "call_monitor.operations.db NO existe — issue 0085a no aplicado"; exit 1; }
|
||||
[ "$FN_TELEMETRY" = "1" ] || echo "WARNING: FN_TELEMETRY != 1 — wrappers Python/Bash inactivos"
|
||||
|
||||
# Metricas de la sesion actual + ultimas 24h
|
||||
sqlite3 "$MON" <<SQL
|
||||
SELECT 'calls_session', COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}'
|
||||
UNION ALL SELECT 'calls_24h', COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
|
||||
UNION ALL SELECT 'violations_24h', COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
|
||||
UNION ALL SELECT 'tool_used_distribution_24h', NULL;
|
||||
SELECT tool_used, COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER) GROUP BY tool_used ORDER BY 2 DESC;
|
||||
SQL
|
||||
```
|
||||
|
||||
Si `calls_session = 0` → algo esta mal (hook PostToolUse no fire o BD no escribible). Reporta y para.
|
||||
|
||||
Si `mcp_*` / total < 0.4 → estas usando demasiado heredoc/sqlite directo. Reporta como warning.
|
||||
|
||||
### 2. GAP — ¿que funciones faltan?
|
||||
|
||||
Dos fuentes:
|
||||
|
||||
#### 2a. Patrones repetidos en heredocs/Edit
|
||||
|
||||
```sql
|
||||
-- En call_monitor.operations.db
|
||||
SELECT tool_used, COUNT(*) AS hits
|
||||
FROM calls
|
||||
WHERE function_id = ''
|
||||
AND ts >= CAST(strftime('%s','now','-7 days') AS INTEGER)
|
||||
AND tool_used IN ('heredoc_py', 'heredoc_bash', 'sqlite_direct')
|
||||
GROUP BY tool_used;
|
||||
```
|
||||
|
||||
Si `heredoc_py > 5` sin function_id → Claude esta componiendo logica que probablemente debe ser pipeline. Investigar el ultimo heredoc del transcript: si reescribe algo que ya es funcion del registry → violation candidate. Si no, es candidato a pipeline nuevo.
|
||||
|
||||
#### 2b. Trabajo actual de la sesion — gap inferido del contexto
|
||||
|
||||
Lee el ultimo prompt del usuario y los ultimos 10 turnos. Lista funciones que:
|
||||
|
||||
- Has llamado inline (sed/awk/jq custom, transformaciones de datos, parsing).
|
||||
- Has reinventado (HTTP client raw, SQLite open con flags, FS walks).
|
||||
- Has compuesto >2 veces con el mismo shape.
|
||||
|
||||
Para cada candidato:
|
||||
|
||||
```bash
|
||||
# Verifica si ya existe algo similar en el registry
|
||||
mcp__registry__fn_search "<keyword del candidato>"
|
||||
```
|
||||
|
||||
Si NO existe match relevante → candidato a `fn-constructor`.
|
||||
Si existe pero firma incompleta → candidato a `improve_function` (proposal, NO auto-construccion).
|
||||
|
||||
### 3. PROPOSE — lista candidatos
|
||||
|
||||
Genera tabla:
|
||||
|
||||
```
|
||||
| Candidato | Razon | Lenguaje | Dominio | Evidencia (snippet) |
|
||||
|---|---|---|---|---|
|
||||
| <name> | inline_repeated/wrapper_skip/new | go/py/bash | core/infra/... | <heredoc fragment> |
|
||||
```
|
||||
|
||||
Si lista vacia → "no gaps detected, sesion saludable" + reporta metricas. Para.
|
||||
|
||||
### 4. CONSTRUCT — lanza fn-constructor en paralelo
|
||||
|
||||
Para cada candidato, dispara un sub-agente `fn-constructor` con prompt autocontenido:
|
||||
|
||||
```
|
||||
Agent(subagent_type="fn-constructor", prompt=...)
|
||||
```
|
||||
|
||||
Prompts en PARALELO en un mismo mensaje (varios Agent calls). Pasar:
|
||||
- nombre propuesto, lang, domain
|
||||
- firma esperada (params + return)
|
||||
- pureza
|
||||
- descripcion + ejemplo de uso (heredoc real detectado)
|
||||
- nota: "esta funcion la necesita Claude para auto-uso futuro"
|
||||
|
||||
### 5. VALIDATE — ¿la proxima sesion la usara?
|
||||
|
||||
Despues de que fn-constructor termine:
|
||||
|
||||
```bash
|
||||
./fn index 2>&1 | tail -2
|
||||
# Verifica que las nuevas funciones existen
|
||||
for fn in <lista>; do
|
||||
mcp__registry__fn_show "$fn" >/dev/null && echo "OK: $fn" || echo "FAIL: $fn"
|
||||
done
|
||||
```
|
||||
|
||||
Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
|
||||
|
||||
```bash
|
||||
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
|
||||
```
|
||||
|
||||
Reporta:
|
||||
- N funciones nuevas creadas (con IDs)
|
||||
- N proposals nuevas en `registry.db.proposals`
|
||||
- Recomendacion al usuario: "proximo turno mencionar/usar `<fn_id>` para validar que el wrapper se invoca correctamente"
|
||||
|
||||
### 6. SELF-TEST — telemetria del propio /fn_claude
|
||||
|
||||
`/fn_claude` mismo debe quedar registrado. Tras ejecutar, query final:
|
||||
|
||||
```bash
|
||||
sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}' AND ts >= <inicio_comando>"
|
||||
```
|
||||
|
||||
Si la cuenta no aumento → el comando esta operando fuera de la telemetria (bug). Reportar.
|
||||
|
||||
---
|
||||
|
||||
## Reglas duras
|
||||
|
||||
1. **NO ejecutar fn-constructor para algo que ya existe.** Buscar primero via `mcp__registry__fn_search`. Si match relevante → NO crear duplicado.
|
||||
2. **NO crear funciones especulativas.** Cada candidato debe tener evidencia real (snippet de heredoc o llamada inline detectada en esta sesion o en `call_monitor.calls` reciente).
|
||||
3. **PARALELO**: si hay >1 candidato, lanza todos los `fn-constructor` en un solo mensaje con multiples `Agent` calls. NO secuencial.
|
||||
4. **No autonomous merge**: las funciones nuevas viven en el branch local. NO push automatico. Humano revisa y push manual.
|
||||
5. **Limites duros**: max 5 funciones nuevas por invocacion. Si detectas mas, prioriza por evidence weight (`occurrences * recency`) y reporta el resto como pending.
|
||||
6. **Si la sesion no esta siendo registrada (`calls_session = 0`)**: ABORT antes de fase 2. No tiene sentido auto-construir sin telemetria.
|
||||
|
||||
---
|
||||
|
||||
## Output canonico
|
||||
|
||||
```
|
||||
=== /fn_claude — auto-auditoria ===
|
||||
session_id: <id>
|
||||
calls_session: N
|
||||
calls_24h: M (mcp_ratio: 0.XX)
|
||||
violations_24h: K
|
||||
pending_proposals: P (existentes en registry.db)
|
||||
|
||||
GAPS DETECTADOS:
|
||||
1. <name>_<lang>_<domain> — razon — evidencia
|
||||
2. ...
|
||||
|
||||
LANZADOS (en paralelo):
|
||||
fn-constructor #1: <name1> → en progreso
|
||||
fn-constructor #2: <name2> → en progreso
|
||||
...
|
||||
|
||||
VALIDADAS tras ./fn index:
|
||||
✓ <name1>_<lang>_<domain>
|
||||
✓ <name2>_<lang>_<domain>
|
||||
|
||||
PROPOSALS NUEVAS: <count>
|
||||
|
||||
PROXIMO TURNO: menciona `<name1>` para validar wrapper.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cuando usar
|
||||
|
||||
- Al inicio de una sesion larga, para verificar telemetria activa.
|
||||
- A media sesion, cuando notes que estas reescribiendo el mismo bloque.
|
||||
- Antes de cerrar sesion, para capitalizar lo aprendido como funciones reutilizables.
|
||||
- Tras `/autonomous-task` para validar que el orquestador no genero ruido (proposals/funciones huerfanas).
|
||||
|
||||
---
|
||||
|
||||
## Cuando NO usar
|
||||
|
||||
- En sesiones cortas (<5 turnos) — no hay datos suficientes.
|
||||
- Si `call_monitor.operations.db` no esta inicializado (`call_monitor init` primero).
|
||||
- Si el usuario quiere control manual del proceso de extraccion. Este comando es agresivo.
|
||||
@@ -109,6 +109,177 @@ metabase_update_dashboard(client, dash["id"], dashcards=[
|
||||
|
||||
**Filtros de list_dashboards:** `all`, `mine`, `archived`
|
||||
|
||||
### Dashboards — helpers compositivos (añadir KPIs a dashboard existente)
|
||||
|
||||
Helpers para el flujo tipico "anadir N cards (KPI) al final de un tab existente reusando los mismos filtros que otro card vecino". Evitan los gotchas: replicar `parameter_mappings`, calcular `row` libre, escapado raro de `column_settings`, generacion de `lib/uuid` en MBQL.
|
||||
|
||||
```python
|
||||
from metabase import (
|
||||
metabase_mbql_from_source_card,
|
||||
metabase_copy_dashcard_mappings,
|
||||
metabase_dashboard_next_row,
|
||||
metabase_dashboard_append_row,
|
||||
metabase_viz_column_format,
|
||||
metabase_smartscalar_anothercolumn_viz,
|
||||
)
|
||||
```
|
||||
|
||||
#### `metabase_mbql_from_source_card`
|
||||
|
||||
Construye `dataset_query` MBQL sobre una saved-card (`source-card`), con aggregations + joins + filters + breakouts + segunda stage de expressions. Genera `lib/uuid` automatico en cada nodo.
|
||||
|
||||
```python
|
||||
dq = metabase_mbql_from_source_card(
|
||||
database_id=6,
|
||||
source_card_id=5305,
|
||||
aggregations=[
|
||||
{"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"},
|
||||
{"op": "sum", "field": "PrecioCompra", "base_type": "type/Decimal"},
|
||||
{"op": "sum", "field": "PrecioTasas", "base_type": "type/Float"},
|
||||
],
|
||||
joins=[
|
||||
{"alias": "Centros - idCentro", "source_card_id": 4076,
|
||||
"fields": "none", "local_field": "idCentro", "local_base_type": "type/Text",
|
||||
"foreign_field_id": 17316, "foreign_base_type": "type/Text"},
|
||||
],
|
||||
filters=[["not-empty", {}, ["field", {"base-type": "type/Text"},
|
||||
"Centros - idCentro__Companies__name"]]],
|
||||
expressions=[
|
||||
{"name": "MasadeMargen", "expr":
|
||||
{"op": "-", "args": [{"field": "sum"},
|
||||
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]}},
|
||||
{"name": "Margen", "expr":
|
||||
{"op": "coalesce", "args": [
|
||||
{"op": "/", "args": [
|
||||
{"op": "-", "args": [{"field": "sum"},
|
||||
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]},
|
||||
{"field": "sum"}]},
|
||||
0]}},
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Ops soportadas en expressions: `+`, `-`, `*`, `/`, `coalesce`, `case`. Referencia a otra expresion en la misma stage: `{"ref": "Margen"}`. Aliases de aggregations son posicionales: `sum`, `sum_2`, `sum_3`... (orden = declaracion).
|
||||
|
||||
#### `metabase_copy_dashcard_mappings`
|
||||
|
||||
Copia los `parameter_mappings` de un dashcard "donante" a un card nuevo. Devuelve lista lista para pegar en `dashcards_add`.
|
||||
|
||||
```python
|
||||
mappings = metabase_copy_dashcard_mappings(
|
||||
client,
|
||||
dashboard_id=734,
|
||||
source_card_id=9918, # card donante con 18 filtros mapeados
|
||||
dest_card_id=9947, # card destino nueva
|
||||
)
|
||||
# Devuelve [{"parameter_id","card_id","target"}, ...] con card_id=9947
|
||||
```
|
||||
|
||||
#### `metabase_dashboard_next_row`
|
||||
|
||||
Calcula el primer `row` libre al final de un tab.
|
||||
|
||||
```python
|
||||
row = metabase_dashboard_next_row(client, dashboard_id=734, tab_id=191)
|
||||
# row=12 si el ultimo card termina en row+size_y=12
|
||||
# tab_id=0 → dashboards sin tabs
|
||||
```
|
||||
|
||||
#### `metabase_dashboard_append_row`
|
||||
|
||||
Combo: append N cards en una fila horizontal al final del tab, copiando mappings de un donante. Una sola llamada hace `next_row` + grid math + `copy_mappings` + `update_dashboard_safe`.
|
||||
|
||||
```python
|
||||
metabase_dashboard_append_row(
|
||||
client,
|
||||
dashboard_id=734,
|
||||
tab_id=191,
|
||||
card_ids=[9947, 9948, 9949],
|
||||
height=4,
|
||||
donor_card_id=9918, # mismos 18 filtros del dashboard
|
||||
grid_width=24, # default Metabase v0.59
|
||||
)
|
||||
# Coloca 3 cards de size_x=8 en row=next, cols 0/8/16, con mappings copiados
|
||||
```
|
||||
|
||||
#### `metabase_viz_column_format`
|
||||
|
||||
Construye una entrada de `column_settings` con la clave JSON-escaped (`'["name","Margen"]'`) sin tener que recordar el formato exacto.
|
||||
|
||||
```python
|
||||
metabase_viz_column_format("Margen", number_style="percent", decimals=2)
|
||||
# {'["name","Margen"]': {"number_style": "percent", "decimals": 2}}
|
||||
|
||||
metabase_viz_column_format("MasadeMargen", number_style="currency",
|
||||
currency="EUR", decimals=0, currency_in_header=False)
|
||||
# {'["name","MasadeMargen"]': {...}}
|
||||
```
|
||||
|
||||
Mergea varios resultados en `column_settings` de las visualization_settings.
|
||||
|
||||
#### `metabase_smartscalar_anothercolumn_viz`
|
||||
|
||||
Construye `visualization_settings` completo para `display=smartscalar` con comparativa tipo `anotherColumn` (compara dos columnas de la misma fila — no requiere breakout temporal).
|
||||
|
||||
```python
|
||||
viz = metabase_smartscalar_anothercolumn_viz(
|
||||
main_column="Margen",
|
||||
compare_column="Margen_N1",
|
||||
label="vs N-1",
|
||||
number_style="percent",
|
||||
decimals=2,
|
||||
)
|
||||
# Setear en /api/card via PUT visualization_settings=viz
|
||||
```
|
||||
|
||||
**⚠ Gotcha smartscalar Metabase v0.59:** el visualization solo acepta `type: "anotherColumn"` cuando la query NO produce filas multiples. Si Metabase muestra el error *"Agrupa solo por un campo de tiempo para ver como ha cambiado con el tiempo"*, hace falta un **breakout temporal** en la MBQL (ej. `breakouts=[{"field":"fecha","base_type":"type/Date","temporal_unit":"month"}]`) y usar el comparison `previousValue` en lugar de `anotherColumn`. Alternativa: `metabase_smartscalar_kpi_sql` + `metabase_smartscalar_kpi_payload` (patron 2-row nativo) si la card es SQL nativo.
|
||||
|
||||
#### Patron canonico — anadir 3 KPI cards a tab existente
|
||||
|
||||
```python
|
||||
import os, sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from metabase import (
|
||||
MetabaseClient, metabase_create_card, metabase_mbql_from_source_card,
|
||||
metabase_dashboard_append_row, metabase_viz_column_format,
|
||||
metabase_smartscalar_anothercolumn_viz,
|
||||
)
|
||||
|
||||
c = MetabaseClient("https://reports.autingo.es", os.environ["MB_API_KEY"])
|
||||
|
||||
# 1) MBQL reusando una saved-card como source
|
||||
def query():
|
||||
return metabase_mbql_from_source_card(
|
||||
database_id=6, source_card_id=5305,
|
||||
aggregations=[
|
||||
{"op":"sum","field":"PrecioVenta","base_type":"type/Decimal"},
|
||||
{"op":"sum","field":"PrecioCompra","base_type":"type/Decimal"},
|
||||
{"op":"sum","field":"PrecioTasas","base_type":"type/Float"},
|
||||
],
|
||||
# joins/filters/expressions ...
|
||||
)
|
||||
|
||||
# 2) Crear cards
|
||||
card1 = metabase_create_card(c, "Masa de Margen", query(),
|
||||
display="scalar", collection_id=500)
|
||||
viz1 = {"scalar.field": "MasadeMargen",
|
||||
"column_settings": metabase_viz_column_format(
|
||||
"MasadeMargen", number_style="currency", currency="EUR", decimals=0)}
|
||||
c._http.request("PUT", f"/api/card/{card1['id']}", json={"visualization_settings": viz1})
|
||||
|
||||
card2 = metabase_create_card(c, "Margen", query(), display="smartscalar", collection_id=500)
|
||||
viz2 = metabase_smartscalar_anothercolumn_viz(
|
||||
main_column="Margen", compare_column="Margen_N1", number_style="percent", decimals=2)
|
||||
c._http.request("PUT", f"/api/card/{card2['id']}", json={"visualization_settings": viz2})
|
||||
|
||||
# 3) Append fila al tab con mappings copiados del donante
|
||||
metabase_dashboard_append_row(
|
||||
c, dashboard_id=734, tab_id=191,
|
||||
card_ids=[card1["id"], card2["id"]],
|
||||
height=4, donor_card_id=9918,
|
||||
)
|
||||
```
|
||||
|
||||
### Documents (ProseMirror)
|
||||
|
||||
Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# /new-cpp-app — Crear app C++ nueva con scaffolder estandar
|
||||
|
||||
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/lucas/fn_registry
|
||||
./fn run init_cpp_app $ARGUMENTS
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
```
|
||||
/new-cpp-app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
|
||||
```
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```bash
|
||||
# App suelta en cpp/apps/<name>/
|
||||
/new-cpp-app my_tool --desc "Herramienta para X"
|
||||
|
||||
# App dentro de un proyecto
|
||||
/new-cpp-app finance_panel --project budget --desc "Panel de finanzas" --tags "finance,dashboard"
|
||||
```
|
||||
|
||||
## Que genera
|
||||
|
||||
```
|
||||
<dir>/
|
||||
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render)
|
||||
CMakeLists.txt # add_imgui_app(<name> main.cpp)
|
||||
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url)
|
||||
```
|
||||
|
||||
Mas registro en `cpp/CMakeLists.txt`, repo Gitea con commit inicial, y `fn index` para que aparezca en `registry.db`.
|
||||
|
||||
## Despues de crear
|
||||
|
||||
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
|
||||
2. Anadir las funciones al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
|
||||
3. Build: `/compile <name>` o `cd cpp && cmake --build build --target <name> -j`.
|
||||
|
||||
## Cuando NO usar
|
||||
|
||||
NUNCA — esta es la unica via para crear apps C++ nuevas. Si el scaffolder no cubre un caso, modificar la plantilla en `bash/functions/pipelines/init_cpp_app.sh`. Escribir `main.cpp + CMakeLists.txt + app.md` a mano esta prohibido por `.claude/rules/cpp_apps.md`.
|
||||
|
||||
## Auditoria post-creacion
|
||||
|
||||
```
|
||||
fn doctor cpp-apps
|
||||
```
|
||||
|
||||
Lista apps que se desvian del estandar (sin `cfg.about`, con `app_menubar` manual, dockspace duplicado, etc.).
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
description: "Recordatorio operativo para usar subagentes fn (constructor/executor/recopilador/analizador/mejorador) y paralelizar trabajo independiente"
|
||||
---
|
||||
|
||||
# /subagentes — usa subagentes fn y paraleliza
|
||||
|
||||
Recuerda: antes de escribir codigo nuevo o ejecutar pipelines en serie, **delega a subagentes** y **paraleliza** llamadas independientes (un mensaje, varios `Agent` calls).
|
||||
|
||||
---
|
||||
|
||||
## Mapa de subagentes fn (ciclo reactivo)
|
||||
|
||||
| Fase | Agente | Cuando dispararlo |
|
||||
|---|---|---|
|
||||
| 1 CONSTRUIR | `fn-constructor` | Falta funcion/tipo/test reutilizable. NUNCA escribir inline en `apps/` si es reutilizable |
|
||||
| 2 EJECUTAR | `fn-executor` | Correr pipeline/funcion del registry + registrar ejecucion en `operations.db` |
|
||||
| 3 RECOPILAR | `fn-recopilador` | Auditar integridad de `operations.db`. Modo `design-e2e <app>` propone bloque `e2e_checks` |
|
||||
| 4 ANALIZAR | `fn-analizador` | Ejecutar `e2e_checks` de `app.md`, veredicto pass/fail, persistir en `e2e_runs` |
|
||||
| 5 MEJORAR | `fn-mejorador` | Convertir fallos de `e2e_runs` en `proposals` con evidencia trazable |
|
||||
| 6 META | `fn-orquestador` | Recorrer fases 1-5 solo hasta convergencia. Sandbox `auto/<issue>`. Issue 0069 |
|
||||
|
||||
**Pre-condiciones de `fn-orquestador`** (abortara si no se cumplen):
|
||||
- Migration `fn_operations/migrations/006_task_runs.sql` aplicada
|
||||
- Issue con criterios de aceptacion **verificables programaticamente** (no "funciona bien")
|
||||
- `master` local up-to-date con `origin/master`
|
||||
- Branch `auto/<issue>` NO existe ya (limpiar previo si hace falta)
|
||||
- `gh` autenticado (PR draft al converger)
|
||||
- Tipo soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`
|
||||
|
||||
**Aislamiento por worktree**: cada run crea `/tmp/fn_orq_<issue>_<ts>/` via `git worktree add`. Working tree principal del usuario queda intacto. N orquestadores paralelos = N worktrees independientes. `task_runs` persiste en BD del repo principal (auditoria sobrevive aunque borres worktree).
|
||||
|
||||
## Otros subagentes utiles
|
||||
|
||||
- `Explore` — busquedas amplias en codebase (>3 queries) sin contaminar contexto principal
|
||||
- `general-purpose` — research multi-step open-ended
|
||||
|
||||
## Reglas duras
|
||||
|
||||
1. **Paralelo real**: tareas independientes → un mensaje con varios `Agent` calls. NO en serie.
|
||||
2. **Briefing autocontenido**: subagente no ve historial. Pasar paths absolutos, IDs, criterio exito.
|
||||
3. **No delegar comprension**: nada de "haz lo que veas". Especificar que cambiar, donde, por que.
|
||||
4. **Verificar output**: leer diff/resultado, no confiar en resumen del subagente.
|
||||
5. **No duplicar**: si delegas research, no lo repitas tu.
|
||||
|
||||
## Patrones canonicos de paralelismo
|
||||
|
||||
- 3 funciones de registry independientes → 3 `fn-constructor` en paralelo
|
||||
- Auditar N apps → N `fn-recopilador` en paralelo
|
||||
- Validar varias apps → N `fn-analizador` en paralelo
|
||||
- Build cpp + tests py + audit operations.db → 3 calls paralelos
|
||||
- Tras `fn-analizador` con fallos → `fn-mejorador` por cada `run_id`
|
||||
- Tarea multi-fase autonoma (issue con criterios verificables) → `fn-orquestador` (1 sola run, NO recursivo)
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
- Escribir funcion reutilizable inline en `apps/` (debe ir a `functions/` via `fn-constructor`)
|
||||
- Lanzar subagentes en serie cuando son independientes
|
||||
- Prompt de 1 linea sin contexto ("arregla esto")
|
||||
- Invocar subagente y luego hacer tu mismo el trabajo
|
||||
- Spawn `fn-orquestador` sin migration 006 o sin issue verificable (abortara)
|
||||
- `fn-orquestador` recursivo (un orquestador no spawn-ea otro)
|
||||
|
||||
## Checklist pre-respuesta
|
||||
|
||||
- ¿>1 tarea independiente? → paralelizar
|
||||
- ¿Hace falta funcion/tipo nuevo? → `fn-constructor`, NO inline
|
||||
- ¿Hay que ejecutar/auditar/validar? → fase 2/3/4 segun toque
|
||||
- ¿`e2e_runs` con fallos? → `fn-mejorador`
|
||||
- ¿Issue con criterios verificables + tipo soportado? → `fn-orquestador` (chequear pre-condiciones)
|
||||
- ¿Research amplio (>3 queries)? → `Explore`
|
||||
|
||||
## Plantilla minima de prompt para subagente
|
||||
|
||||
```
|
||||
Contexto: <que repo, que app, que objetivo>
|
||||
Input: <paths absolutos, IDs registry, run_id si aplica>
|
||||
Tarea: <accion concreta y acotada>
|
||||
Criterio exito: <como sabe que termino>
|
||||
Limites: <que NO debe tocar>
|
||||
Telemetria: tus tool calls quedan registradas en projects/fn_monitoring/apps/call_monitor/operations.db
|
||||
via hook PostToolUse heredado de settings.local.json. Sigue patrones canonicos
|
||||
(mcp__registry__fn_*, ./fn run, heredoc importando) — los antipatrones se loguean
|
||||
como violations.
|
||||
```
|
||||
|
||||
## Telemetria heredada (issue 0085 hardening 5)
|
||||
|
||||
Los hooks de `.claude/settings.local.json` se heredan automaticamente por cada sub-agente que Claude Code lance via la tool `Agent`. Eso significa:
|
||||
|
||||
- Cada Bash, Edit, Write, MultiEdit, `mcp__registry__*` del sub-agente dispara `hook_call_monitor.sh` exactamente igual que en la sesion principal.
|
||||
- El `session_id` del JSON de input del hook viene del sub-agente, distinto al de la sesion padre. Util para auditar comportamiento por agente.
|
||||
- Las violations detectadas (sqlite3 directo, heredoc reinventando, etc) cuentan tambien para sub-agentes — un `fn-constructor` que reescribe inline en lugar de delegar a otro `fn-constructor` queda registrado.
|
||||
- `FN_TELEMETRY=1` esta en el `env` block de settings.local.json — los heredocs Python/Bash de sub-agentes ya tienen wrappers activos automaticamente.
|
||||
|
||||
Implicacion: NO necesitas pasar flags `--telemetry` a sub-agentes. Solo asegurate de que el prompt sigue patrones canonicos. La regla `.claude/rules/registry_calls.md` se aplica igual.
|
||||
|
||||
Si un sub-agente abre un proceso hijo que escapa al hook (ej. `nohup ... &`, daemons), ese subproceso queda fuera de la telemetria — documentalo en el prompt si es un caso valido.
|
||||
@@ -0,0 +1,135 @@
|
||||
# /validate-app — Validar end-to-end una app del registry
|
||||
|
||||
Orquesta la cadena `fn-executor → fn-recopilador → fn-analizador → fn-mejorador` (fases 2-5 del bucle reactivo) sobre una app concreta. Devuelve veredicto pass/fail + IDs de proposals creadas si hay fallos.
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — `<app_id>` o `<dir_path>`. Ejemplos:
|
||||
- `kanban_go_tools`
|
||||
- `apps/kanban`
|
||||
- `graph_explorer_cpp_viz`
|
||||
- `projects/osint_graph/apps/graph_explorer`
|
||||
|
||||
Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps con `e2e_checks` declarado y pedir.
|
||||
|
||||
## Pasos
|
||||
|
||||
### 1. Resolver app objetivo
|
||||
|
||||
```bash
|
||||
ROOT=/home/lucas/fn_registry
|
||||
ARG="$ARGUMENTS"
|
||||
|
||||
if [ -z "$ARG" ]; then
|
||||
CWD="$(pwd)"
|
||||
case "$CWD" in
|
||||
"$ROOT"/apps/*|"$ROOT"/projects/*/apps/*)
|
||||
ARG="$(realpath --relative-to="$ROOT" "$CWD")"
|
||||
;;
|
||||
*)
|
||||
sqlite3 "$ROOT/registry.db" "SELECT id, dir_path FROM apps ORDER BY id;"
|
||||
echo "Especifica app_id o dir_path"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Resolver a (id, dir_path)
|
||||
if echo "$ARG" | grep -q "^apps/\|^projects/"; then
|
||||
APP_DIR="$ARG"
|
||||
APP_ID=$(sqlite3 "$ROOT/registry.db" "SELECT id FROM apps WHERE dir_path = '$ARG';")
|
||||
else
|
||||
APP_ID="$ARG"
|
||||
APP_DIR=$(sqlite3 "$ROOT/registry.db" "SELECT dir_path FROM apps WHERE id = '$ARG';")
|
||||
fi
|
||||
|
||||
[ -z "$APP_ID" ] || [ -z "$APP_DIR" ] && { echo "App no encontrada: $ARG"; exit 1; }
|
||||
```
|
||||
|
||||
### 2. Verificar contrato `e2e_checks`
|
||||
|
||||
```bash
|
||||
HAS_CHECKS=$(awk '/^e2e_checks:/,/^[a-z_]+:|^---$/' "$ROOT/$APP_DIR/app.md" | grep -c "^ - id:")
|
||||
|
||||
if [ "$HAS_CHECKS" -eq 0 ]; then
|
||||
echo "App $APP_ID no tiene e2e_checks declarados."
|
||||
echo "Invocar fn-recopilador design-e2e para generar contrato:"
|
||||
echo ""
|
||||
echo " Agent(subagent_type=fn-recopilador, prompt=\"design-e2e $APP_DIR\")"
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
### 3. Fase 3 — RECOPILAR (auditar operations.db)
|
||||
|
||||
Invocar `fn-recopilador` para confirmar que los datos operativos estan integros antes de validar. Si recopilador reporta FAIL critical, NO continuar.
|
||||
|
||||
```
|
||||
Agent(subagent_type=fn-recopilador,
|
||||
prompt="Auditar app $APP_DIR. Reportar OK/WARN/FAIL en formato corto.
|
||||
Si hay FAIL critical, advertirlo claramente. Solo lectura.")
|
||||
```
|
||||
|
||||
Si reporta FAIL critical → abortar con mensaje y no llegar a fn-analizador.
|
||||
|
||||
### 4. Fase 4 — ANALIZAR (correr e2e_checks)
|
||||
|
||||
```
|
||||
Agent(subagent_type=fn-analizador,
|
||||
prompt="Validar end-to-end la app $APP_ID (dir_path: $APP_DIR).
|
||||
Leer e2e_checks del app.md, ejecutar via e2e_run_checks_go_infra,
|
||||
evaluar assertions, calcular drift, persistir en e2e_runs.
|
||||
triggered_by: manual.
|
||||
git_sha: $(git rev-parse --short HEAD 2>/dev/null || echo '')
|
||||
|
||||
Devolver veredicto caveman + run_id.")
|
||||
```
|
||||
|
||||
Capturar `RUN_ID` del output. Capturar `STATUS` (`pass`|`partial`|`fail`).
|
||||
|
||||
### 5. Fase 5 — MEJORAR (proposals si hay fallos)
|
||||
|
||||
Solo si `STATUS != pass`:
|
||||
|
||||
```
|
||||
Agent(subagent_type=fn-mejorador,
|
||||
prompt="App $APP_ID tuvo fallos en run_id $RUN_ID.
|
||||
Leer e2e_runs y summary_json de $APP_DIR/operations.db.
|
||||
Por cada fail critical: crear proposal kind=new_function|improve_function
|
||||
en registry.db con created_by=reactive_loop, evidence con run_id+check_id.
|
||||
Sugerir fix concreto en description.
|
||||
Devolver lista de proposal_ids creados.")
|
||||
```
|
||||
|
||||
Capturar `PROPOSAL_IDS`.
|
||||
|
||||
### 6. Reporte final al usuario
|
||||
|
||||
Tabla resumen:
|
||||
|
||||
```
|
||||
=== /validate-app: $APP_ID ===
|
||||
|
||||
Fase 3 RECOPILAR: ✓ datos operativos integros
|
||||
Fase 4 ANALIZAR: <STATUS> (run_id: <RUN_ID>)
|
||||
<P>/<T> checks pass, <W> warn, <F> fail
|
||||
Fase 5 MEJORAR: <N> proposals creadas: <PROPOSAL_IDS>
|
||||
|
||||
Detalle por check:
|
||||
build_frontend ✓ 42s
|
||||
build_backend ✓ 18s
|
||||
smoke_api ✓ 1.2s
|
||||
tests_go ✗ 12s — 3/45 fails
|
||||
|
||||
Siguientes pasos:
|
||||
- Revisar proposals: fn proposal list -s pending
|
||||
- Ver run completo: sqlite3 $APP_DIR/operations.db "SELECT * FROM e2e_runs WHERE id='<RUN_ID>'"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- **fn-mejorador no existe todavia** (paso 6 del issue 0068). Mientras tanto, si STATUS != pass, solo imprime el detalle del fallo y sugerir crear proposal manual.
|
||||
- Si un agente subagente devuelve respuesta ambigua (no extrae RUN_ID claramente), pedir clarificacion al usuario antes de continuar.
|
||||
- Para apps sin `operations.db` (ej. kanban usa `kanban.db`), `e2e_runs` se persiste en `/tmp/<app>_e2e_runs.db` con la misma migracion 005.
|
||||
- Caveman OK en stdout salvo en mensajes de error donde claridad supera brevedad.
|
||||
- Tras correr la cadena, NO commitear nada automaticamente. La decision de mergear es del humano.
|
||||
@@ -29,3 +29,8 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 23 | [fn_doctor.md](fn_doctor.md) | `fn doctor`: diagnostico read-only de artefactos, services, sync drift, uses_functions, unused — wrappers de funciones del registry |
|
||||
| 24 | [feature_flags.md](feature_flags.md) | TBD: feature flags para mergear codigo incompleto sin romper master. Patrones por stack (Go/TS/Bash/Py), branch-by-abstraction, anti-patrones |
|
||||
| 25 | [db_migrations.md](db_migrations.md) | Migraciones SQLite obligatorias para cualquier cambio de schema. Aditivas, idempotentes, archivos numerados. Nunca borrar .db ni modificar migraciones existentes |
|
||||
| 26 | [e2e_validation.md](e2e_validation.md) | Contrato `e2e_checks` en `app.md` consumido por fn-analizador (fase 4 del bucle reactivo). Issue 0068 |
|
||||
| 27 | [registry_calls.md](registry_calls.md) | Patrones canonicos para invocar funciones del registry (MCP inspect / MCP run / heredoc compose), antipatrones, excepciones, telemetria. Issue 0085 |
|
||||
| 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 |
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## Capability groups: tags + paginas madre en docs/capabilities/
|
||||
|
||||
Un **capability group** es un cluster de >=3 funciones del registry que comparten un dominio operativo (ej. `notebook`, `metabase`, `deploy`). Cada grupo tiene un **tag plano** (sin prefijo) y una **pagina madre** en `docs/capabilities/<grupo>.md`. La pagina madre desbloquea el conjunto entero en un solo read.
|
||||
|
||||
### Para que existen
|
||||
|
||||
Sin grupos, Claude redescubre funciones via FTS5 una a una cada sesion ("¿como interactuo con Jupyter? ¿como subo deploy?"). Con grupos, Claude lee `docs/capabilities/<grupo>.md` y carga las 5-10 funciones del cluster con su ejemplo canonico — menos turnos perdidos en discovery.
|
||||
|
||||
### Convencion de tag
|
||||
|
||||
- **Slug del grupo** = tag plano. Ej: `notebook`, `metabase`, `android-emu`.
|
||||
- **No prefijos** (`cap:`, `group:`). Ya hay namespacing implicito porque convivirian con tags semanticos sueltos.
|
||||
- **Una funcion puede llevar varios tags de grupo** si pertenece a dos clusters (raro pero valido).
|
||||
- Filtro MCP: `mcp__registry__fn_search query="" tag="notebook"` lista el grupo.
|
||||
|
||||
### Cuando crear grupo nuevo
|
||||
|
||||
- **Minimo 3 funciones** afines. Con 2 no compensa pagina madre — quedan tags sueltos.
|
||||
- **Dominio operativo claro**: el grupo debe ser describible en 1 frase ("operar Jupyter colaborativo", "deploy via SSH+systemd").
|
||||
- **Frontera neta** con grupos existentes. Si solapa con otro -> reorganizar, no duplicar.
|
||||
|
||||
### Como crear grupo
|
||||
|
||||
1. Anadir el tag al frontmatter `.md` de >=3 funciones afines. `fn index` lo registra.
|
||||
2. Crear `docs/capabilities/<grupo>.md` con plantilla:
|
||||
- **Lista de funciones**: tabla `ID | firma corta | que hace`.
|
||||
- **Ejemplo canonico**: 1-2 bloques de codigo end-to-end con los IDs reales.
|
||||
- **Fronteras**: que NO cubre el grupo.
|
||||
- **Prerequisitos** y **notas** si aplica.
|
||||
3. Anadir fila al `docs/capabilities/INDEX.md`.
|
||||
4. Correr `fn doctor capabilities` para auditar drift.
|
||||
|
||||
### Auto-generacion
|
||||
|
||||
`fn doctor capabilities --update` (TBD) reescribe la tabla de funciones de cada pagina madre preservando bloques curated (`Ejemplo canonico`, `Fronteras`, `Notas`). Las secciones curated nunca se sobrescriben.
|
||||
|
||||
### Como Claude usa los grupos
|
||||
|
||||
Cuando una tarea cae en un dominio conocido:
|
||||
|
||||
1. `Read docs/capabilities/INDEX.md` para localizar grupo.
|
||||
2. `Read docs/capabilities/<grupo>.md` para cargar funciones + ejemplo.
|
||||
3. Solo si el grupo no cubre lo necesario, `mcp__registry__fn_search` para funciones sueltas.
|
||||
4. Si el grupo deberia cubrir pero falta funcion -> `fn-constructor` + tagear con el grupo en el frontmatter.
|
||||
|
||||
### Auditoria
|
||||
|
||||
```bash
|
||||
fn doctor capabilities # lista grupos + drift
|
||||
fn doctor capabilities --json # para agentes
|
||||
```
|
||||
|
||||
Comprueba:
|
||||
- Tag con N >=3 funciones pero sin pagina madre -> "tag huerfano".
|
||||
- Pagina madre sin tag respaldo -> "grupo fantasma".
|
||||
- Funcion con tag de grupo pero la pagina madre no la lista (autogen desfasada) -> "drift".
|
||||
|
||||
### Relacion con dominios
|
||||
|
||||
Los **dominios** del registry (`core`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `tui`, `pipelines`, `browser`) son taxonomia ortogonal — un grupo puede atravesar varios dominios (ej. `deploy` toca `infra` y `shell`). NO renombrar dominio a grupo ni viceversa.
|
||||
@@ -6,6 +6,20 @@
|
||||
|
||||
Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli.
|
||||
|
||||
### Scaffolder canonico — OBLIGATORIO
|
||||
|
||||
**REGLA DURA:** crear apps C++ nuevas SIEMPRE con `fn run init_cpp_app <name> [--project <p>] [--desc "..."]`. NUNCA escribir `main.cpp` + `CMakeLists.txt` + `app.md` desde cero a mano en `cpp/apps/` ni `projects/*/apps/`. Tampoco copiar otra app y renombrar — la deriva entre patrones es lo que estamos eliminando.
|
||||
|
||||
Si el scaffolder no cubre un caso (ej. necesitas plantilla diferente, layout custom desde el primer dia), **modificas el scaffolder**, no escribes la app a mano. La plantilla canonica es codigo, no decoracion.
|
||||
|
||||
Razones:
|
||||
- Garantiza `cfg.about` + `cfg.log` + `cfg.panels` + framework defaults aplicados.
|
||||
- Genera frontmatter `app.md` valido (framework, dir_path, repo_url) para `fn index`.
|
||||
- Registra `add_subdirectory` en `cpp/CMakeLists.txt` (raiz o bloque `_DIR` para projects).
|
||||
- Crea repo Gitea `dataforge/<name>` con master + commit inicial.
|
||||
|
||||
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
|
||||
|
||||
### 1. Ubicacion
|
||||
|
||||
| Caso | Donde vive |
|
||||
@@ -165,6 +179,37 @@ Beneficios:
|
||||
- Reset trivial (basta borrar `local_files/`).
|
||||
- Separacion clara para backup/sync (solo `local_files/` es propio del PC).
|
||||
|
||||
### 7.1 Anti-jitter automatico (AltSnap, tiling WMs)
|
||||
|
||||
`fn::run_app` aplica tres capas de proteccion contra jitter al mover la
|
||||
ventana con herramientas externas (AltSnap en Windows, snap-assist, tiling
|
||||
WMs). Activado por defecto, sin opt-in:
|
||||
|
||||
1. **GLFW pos/size callbacks** — `vp->Pos/Size` se sincronizan al instante
|
||||
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** (`#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).
|
||||
|
||||
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`, asserta
|
||||
que `render()` no se llama durante el bracket.
|
||||
|
||||
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
|
||||
en vivo, video stream), tendria que evitar el bypass — actualmente no hay
|
||||
flag para desactivarlo (anadir `cfg.pause_on_external_sizemove = true` por
|
||||
default si surge necesidad).
|
||||
|
||||
### 8. Convenciones de runtime
|
||||
|
||||
Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app:
|
||||
@@ -192,3 +237,27 @@ Si la app tiene componentes que se quieren proteger contra regresiones visuales,
|
||||
- `about` info: nombre, version (semver), descripcion 1 frase.
|
||||
- Persistencia: `<app>.db` SQLite junto al exe; nunca tocar `registry.db` ni `operations.db` salvo lectura.
|
||||
- Modo CLI: si la app acepta args, documentarlos en el `app.md` con ejemplos.
|
||||
|
||||
### 10. Layouts persistentes (default)
|
||||
|
||||
`fn::run_app` provee menu Layouts (Save current as.../Apply/Delete/Reset) sin
|
||||
codigo. Crea `<exe_dir>/local_files/layouts.db` (tabla `imgui_layouts` +
|
||||
`layout_meta`) y persiste el `imgui.ini` serializado por nombre.
|
||||
|
||||
**Restore-on-open / save-on-close (1.1.0+):** al cerrar la app, el slot del
|
||||
layout activo se reescribe con el `imgui.ini` actual (los retoques de
|
||||
docking sobreviven). Al abrir, si habia un layout activo persistido en
|
||||
`layout_meta.last_active`, se carga en el primer frame. Si la app no usa
|
||||
named layouts (nunca clico Save/Apply), el comportamiento sigue siendo el
|
||||
de antes: `imgui.ini` es la unica fuente.
|
||||
|
||||
- App nueva: nada que tocar — Layouts viene activo.
|
||||
- App quiere personalizar `on_reset` (ej. re-mostrar paneles especificos como
|
||||
`shaders_lab`): abre su propio `LayoutStorage`, llama
|
||||
`layout_storage_make_callbacks`, override `on_reset`, y pasa
|
||||
`cfg.layouts_cb = &cb`. Cuando se pasa `layouts_cb`, el auto-storage se
|
||||
desactiva y la app es responsable de `layout_storage_apply_pending` al
|
||||
inicio de su `render`.
|
||||
- App headless / capture mode: `cfg.auto_layouts = false`.
|
||||
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
|
||||
`local_files/`).
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
## Delegacion: spawn fn-constructor en vez de escribir inline
|
||||
|
||||
**REGLA DURA.** Si vas a escribir logica reutilizable inline en un artefacto (app, analysis, playground) o heredoc, STOP y delega a `fn-constructor`. La misma sesion debe crear + usar la funcion. No acumular huerfanas.
|
||||
|
||||
### Cuando un patron es candidato a funcion
|
||||
|
||||
- Aparece >=2 veces en esta sesion o en heredocs recientes.
|
||||
- Firma generica (no depende de tipos internos del artefacto).
|
||||
- 1 responsabilidad clara (CRUD, parse, transform, http call, formato fijo, etc.).
|
||||
- No es one-liner idiomatico de stdlib (`time.Now().UTC().Format(...)` queda fuera).
|
||||
|
||||
### Flujo obligatorio (mismo turno)
|
||||
|
||||
1. **Detectar**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
|
||||
2. **Spawn `fn-constructor` inmediato** via `Agent(subagent_type="fn-constructor", ...)`:
|
||||
- **Sin preguntar al usuario** (autorizado por defecto).
|
||||
- Si hay >1 funcion independiente -> una sola llamada al Agent tool con **N tool_use blocks paralelos** en el mismo mensaje. NO serializar.
|
||||
3. **Tagear con grupo de capacidad** al menos UN tag de grupo (`notebook`, `metabase`, `deploy`, etc.). Ver `capability_groups.md`.
|
||||
4. **`fn index`** para registrar.
|
||||
5. **Importar + invocar en el mismo turno** — no dejar funcion huerfana recien creada.
|
||||
6. **Auto-verificar** con `fn doctor uses-functions` y `fn doctor unused` si tocas >=3 funciones nuevas.
|
||||
|
||||
### Anti-patrones auditables
|
||||
|
||||
| Anti-patron | Consecuencia | Sustituir por |
|
||||
|---|---|---|
|
||||
| Escribir helper inline en artefacto en vez de delegar | Reinvento por sesion | Spawn fn-constructor |
|
||||
| Crear N funciones serialmente | Latencia x N | Multiples `Agent()` en mismo mensaje |
|
||||
| Crear funcion y no usarla en el turno | Huerfana desde dia 1 (`calls_90d=0`) | Importar + invocar antes de cerrar turno |
|
||||
| Crear funcion sin tag de grupo | Imposible descubrir en bloque proxima sesion | Anadir tag de grupo (capability group) |
|
||||
| Reescribir en heredoc logica que ya existe | Capitalizacion perdida | `mcp__registry__fn_search` antes de escribir |
|
||||
|
||||
### Excepciones
|
||||
|
||||
- **Logica de dominio especifica del artefacto** (CRUD de tabla concreta, layout de UI, flujo unico de la app) -> queda en el artefacto. Solo lo reutilizable se delega.
|
||||
- **Stub temporal con `not implemented`**: aceptable si la dependencia externa no esta disponible. Documentar en `.md` (ver `stubs.md`).
|
||||
|
||||
### Telemetria
|
||||
|
||||
Cada `code_writes` + `calls` se registra en `call_monitor/operations.db` (issue 0085). Vista `session_capability_growth` mide ratio creadas vs usadas por sesion. Hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z` en cada turno.
|
||||
|
||||
Si `orphan>0` al cerrar la sesion -> revisar: o la funcion era especulativa (no debio crearse) o falta integrarla en el codigo del artefacto.
|
||||
@@ -0,0 +1,162 @@
|
||||
## Validacion end-to-end de apps (bucle reactivo, fase 4)
|
||||
|
||||
**Contrato obligatorio para apps que vayan a master con gate automatico**: declarar `e2e_checks` en su `app.md`. Sin contrato, `fn-analizador` no puede validar y la app cae al modo "manual": el humano sigue iterando.
|
||||
|
||||
Ver tambien: `apps_tbd.md`, `feature_flags.md`, issue 0068.
|
||||
|
||||
### Por que
|
||||
|
||||
El bucle reactivo del registry tiene 5 fases. Las 3 primeras (`fn-constructor`, `fn-executor`, `fn-recopilador`) cubren CONSTRUIR/EJECUTAR/RECOPILAR. La fase 4 (ANALIZAR) y la 5 (MEJORAR) no funcionan sin un contrato explicito de "como sabe el agente que esta app esta sana". Ese contrato es `e2e_checks`.
|
||||
|
||||
### Donde vive
|
||||
|
||||
En el frontmatter de cada `app.md`, lista `e2e_checks`. Convencion: `id` unico por check, ejecucion en orden declarado, falla = stop o continue segun severidad (TBD por implementar).
|
||||
|
||||
### Tipos de check
|
||||
|
||||
| Campo | Que hace |
|
||||
|---|---|
|
||||
| `id` | Identificador unico del check dentro de la app (`build`, `smoke`, `tests_unit`, ...) |
|
||||
| `cmd` | Comando shell. Exit 0 = pass salvo override de `expect_exit`. |
|
||||
| `health` | URL HTTP. Hace GET, espera 200, util tras un `cmd` que arranca un servicio en background (con `&`). |
|
||||
| `ref` | Referencia a otro agente / funcion del registry (ej. `fn-recopilador:apps/X`, `fn-doctor:artefacts`). |
|
||||
| `timeout_s` | Timeout en segundos. Default 60. |
|
||||
| `expect_exit` | Codigo de salida esperado (default 0). |
|
||||
| `expect_stdout_contains` | Substring que debe aparecer en stdout. |
|
||||
| `expect_stdout_json` | JSONPath o key=value que debe satisfacer la salida. |
|
||||
| `severity` | `critical` (default) o `warning`. Critical = bloquea merge; warning = registra y sigue. |
|
||||
|
||||
### Patrones por stack
|
||||
|
||||
#### Go service con frontend embebido
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build_frontend
|
||||
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
|
||||
timeout_s: 180
|
||||
- id: build_backend
|
||||
cmd: "CGO_ENABLED=1 go build -tags fts5 -o myapp ."
|
||||
- id: smoke
|
||||
cmd: "./myapp --port 8200 --db /tmp/myapp_e2e.db &"
|
||||
health: "http://127.0.0.1:8200/api/health"
|
||||
- id: tests
|
||||
cmd: "go test -tags fts5 -count=1 ./..."
|
||||
```
|
||||
|
||||
#### C++ ImGui app
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "cmake --build build --target myapp -j"
|
||||
timeout_s: 300
|
||||
- id: self_test
|
||||
cmd: "./build/myapp --self-test"
|
||||
timeout_s: 30
|
||||
- id: pytest
|
||||
cmd: "cd tests && python3 -m pytest -x -q"
|
||||
```
|
||||
|
||||
Apps C++ deben implementar `--self-test` que arranca, verifica subsistemas (GL loader, fonts, DBs locales), y sale con codigo 0/1.
|
||||
|
||||
#### Python pipeline / CLI
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: import
|
||||
cmd: "python3 -c 'import myapp'"
|
||||
- id: cli_help
|
||||
cmd: "python3 -m myapp --help"
|
||||
expect_stdout_contains: "usage:"
|
||||
- id: dry_run
|
||||
cmd: "python3 -m myapp --dry-run --input examples/sample.json"
|
||||
```
|
||||
|
||||
#### App con operations.db
|
||||
|
||||
Anadir siempre:
|
||||
|
||||
```yaml
|
||||
- id: ops_audit
|
||||
ref: "fn-recopilador:apps/myapp"
|
||||
```
|
||||
|
||||
Esto invoca al recopilador en modo audit sobre `apps/myapp/operations.db`.
|
||||
|
||||
### Reglas
|
||||
|
||||
1. **Idempotente**: cada check debe poderse correr N veces sin efectos secundarios. Usar BDs en `/tmp/`, puertos altos, `--port 0` cuando se pueda.
|
||||
2. **Sin credenciales reales**: ningun check toca produccion ni servicios externos sensibles. Si necesita HTTP de prueba, usar `httpbin.org` o un mock local.
|
||||
3. **Tiempo acotado**: cada check declara `timeout_s`. Suma total de la app < 10 min como objetivo razonable.
|
||||
4. **Determinista**: si el check depende de red flaky, marcalo `severity: warning` o usalo solo como diagnostico, no como gate.
|
||||
5. **Cleanup implicito**: si el check arranca un proceso en background (`&`), debe morir al final. `fn-analizador` mata el grupo de procesos al terminar la suite.
|
||||
|
||||
### Como diseñar `e2e_checks` para una app existente
|
||||
|
||||
`fn-recopilador` tiene un modo `design-e2e <app_id>` que:
|
||||
|
||||
1. Inspecciona `app.md` (lang, framework, entry_point, uses_functions).
|
||||
2. Revisa estructura del directorio (presencia de `tests/`, `frontend/`, `Makefile`, `CMakeLists.txt`, etc.).
|
||||
3. Audita `operations.db` (si existe) para sugerir `ops_audit`.
|
||||
4. Devuelve bloque `e2e_checks_suggested:` listo para copiar al `app.md` tras revision humana.
|
||||
|
||||
Comando indicativo:
|
||||
```
|
||||
Agent(subagent_type="fn-recopilador",
|
||||
prompt="design-e2e apps/<app>")
|
||||
```
|
||||
|
||||
El recopilador NO escribe directo al `app.md`; deja la propuesta para que el humano apruebe (similar a `proposals`).
|
||||
|
||||
### Adopcion gradual
|
||||
|
||||
- Apps SIN `e2e_checks` declarado: `fn doctor` muestra warning, no bloquea nada.
|
||||
- Apps CON `e2e_checks`: `fn-analizador` corre la suite. Si critical falla → `fn-mejorador` crea proposal. Gate opcional en `/git-push`.
|
||||
- Pilotos iniciales: `apps/kanban`, `projects/osint_graph/apps/graph_explorer`. Resto de apps van migrando segun necesidad.
|
||||
|
||||
### Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| `cmd: "make test"` con make-target opaco | Ilegible. El check debe ser ejecutable directo y auditable. |
|
||||
| Check que tarda > 5 min sin razon (smoke pesado) | Bloquea iteracion. Mover a CI nocturno con tag `slow`. |
|
||||
| Smoke que toca produccion | Riesgo. Smoke usa BD efimera, puertos altos, mocks. |
|
||||
| `expect_stdout_contains: ""` | Vacio = siempre pass. No es un check. |
|
||||
| Anidar checks (uno depende de side-effects de otro sin declararlo) | Frigil. Cada check arranca lo que necesita. |
|
||||
| Usar `e2e_checks` como sustituto de tests unitarios | Son cosas distintas. Unit tests viven en `*_test.go`/`pytest`. e2e valida que el sistema arranque y haga su trabajo. |
|
||||
|
||||
### Tabla `e2e_runs` en operations.db
|
||||
|
||||
Cada corrida de `fn-analizador` se persiste:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS e2e_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
app_id TEXT NOT NULL,
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
status TEXT NOT NULL, -- pass|fail|partial
|
||||
checks_total INTEGER NOT NULL,
|
||||
checks_pass INTEGER NOT NULL,
|
||||
checks_fail INTEGER NOT NULL,
|
||||
summary_json TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Migracion: `fn_operations/migrations/006_e2e_runs.sql` (issue 0068, paso 3).
|
||||
|
||||
### Output canonico de fn-analizador
|
||||
|
||||
Tabla caveman, una linea por check:
|
||||
|
||||
```
|
||||
build ✓ 42s
|
||||
smoke ✓ 0.8s
|
||||
ops_audit ✓
|
||||
tests ✗ 12s exit 1, 3/45 failures
|
||||
assertion:R1 ✗ warning duration drift +47% vs p50
|
||||
golden:home ✓
|
||||
```
|
||||
|
||||
Rojo cuando `severity: critical` y status fail. Esto es lo que el agente principal lee y reenvia al humano.
|
||||
@@ -13,12 +13,13 @@
|
||||
### Comandos
|
||||
|
||||
```bash
|
||||
fn doctor # Corre TODOS los checks (artefacts + services + sync + uses-functions + unused)
|
||||
fn doctor # Corre TODOS los checks (artefacts + services + sync + uses-functions + unused + cpp-apps)
|
||||
fn doctor artefacts # Solo artefactos: git/venv/app.md/upstream
|
||||
fn doctor services # Solo apps con tag 'service' + systemctl + puerto
|
||||
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)
|
||||
|
||||
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
|
||||
```
|
||||
@@ -32,6 +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` | `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.
|
||||
|
||||
@@ -58,6 +60,10 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
|
||||
| `port not listening` | `port_kill_bash_infra <port>` (si zombie) y relanzar |
|
||||
| `missing_in_app_md` | Editar `app.md` y añadir el ID a `uses_functions` |
|
||||
| `unused` (funcion huerfana) | Decidir: usar, deprecar (tag), o borrar |
|
||||
| `manual_app_menubar_call` | Borrar `fn_ui::app_menubar(...)` del render — el framework ya lo dibuja |
|
||||
| `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 |
|
||||
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
|
||||
|
||||
### Para agentes
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
## Function growth + self-documenting capability
|
||||
|
||||
Dos doctrinas hermanas. Una define **como deben ser** las funciones (auto-descubribles y lanzables sin segunda lectura). La otra define **como crece** el registry (no inflando funciones — promoviendo composiciones a pipelines).
|
||||
|
||||
Issue 0087.
|
||||
|
||||
---
|
||||
|
||||
### Parte A — `.md` autosuficiente (contrato OBLIGATORIO)
|
||||
|
||||
Cuando Claude (o un humano) encuentra una funcion via FTS / fuzzy match / capability page / TOP block, el `.md` debe bastar para **lanzarla sin abrir el codigo**. Esto es lo que hace que descubrir = lanzar y elimina el coste del second lookup.
|
||||
|
||||
**Secciones obligatorias** en cada `.md` del registry (functions + pipelines + types con uso practico):
|
||||
|
||||
| Seccion | Contenido | Tamaño |
|
||||
|---|---|---|
|
||||
| Frontmatter | `name`, `signature`, `params` (con `desc` por param), `output`, `tags`, `uses_functions`, etc. Lo de hoy. | — |
|
||||
| `## Ejemplo` | Bloque de codigo lanzable con args **concretos**. Copiar+pegar produce ejecucion real. NO placeholders abstractos. | 3-10 lineas |
|
||||
| `## Cuando usarla` | 1-2 frases con triggers: "cuando hagas X / antes de Y / si necesitas Z". Verbos imperativos. Ayuda al fuzzy match y a Claude a saber sin leer el codigo. | 1-3 lineas |
|
||||
| `## Gotchas` | Problemas conocidos / no-go cases. Obligatoria para funciones impuras o con efectos (Windows-side, red, FS write, GPU). Omisible para funciones puras triviales. | 0-5 puntos |
|
||||
| `## Capability growth log` | Solo SI la funcion ha crecido. Una linea por version: `v1.1.0 (YYYY-MM-DD) — anade --build flag para skip build`. No se rellena en v1.0.0. | crece con el tiempo |
|
||||
|
||||
**Anti-patrones del .md:**
|
||||
|
||||
- Ejemplo con `<arg1>`, `<arg2>` placeholders abstractos — NO. Ejemplos con valores reales (`registry_dashboard`, `/home/lucas/...`).
|
||||
- "Cuando usarla" vacio o "ver descripcion arriba" — NO. Frase nueva con trigger explicito.
|
||||
- `notes` lleno + `## Gotchas` vacio cuando la funcion tiene efectos — mover de `notes` a `## Gotchas`.
|
||||
- Capability growth log inventado (sin que la funcion haya cambiado) — NO. Solo se rellena cuando hay version bump real.
|
||||
|
||||
**Verificacion** (TBD: convertir a check de `fn doctor`): cada .md de `functions/`/`pipelines/` debe tener `## Ejemplo` y `## Cuando usarla`. `## Gotchas` obligatoria solo si `purity: impure`. `## Capability growth log` libre.
|
||||
|
||||
---
|
||||
|
||||
### Parte B — Crecimiento por composicion (no por inflado)
|
||||
|
||||
**Principio:** una funcion que hace bien UNA cosa NO necesita crecer. Anadir params "por si acaso" la hace peor (Inner Platform Effect). Lo que crece es el **registry**: pipelines nuevos que componen funciones existentes.
|
||||
|
||||
#### Ejemplo del principio
|
||||
|
||||
- **Hoy:** Claude para hacer una transferencia bancaria llama `bank_login` -> `bank_list_accounts` -> `bank_make_transfer`. 3 calls, 3 decisiones, 3 puntos de fallo.
|
||||
- **Manana:** pipeline `bank_transfer_oneshot(account, amount, target)` que compone las 3 internamente. 1 call, 1 decision.
|
||||
|
||||
Misma capacidad, 3x menos pasos. **Esto es lo que multiplica la velocidad de Claude**, no anadir flags a `bank_login`.
|
||||
|
||||
#### Como se promueve una composicion
|
||||
|
||||
Senal detectable en `call_monitor.operations.db`: secuencia A→B(→C) con
|
||||
|
||||
- **Mismo session_id**.
|
||||
- **Intervalo entre calls < N segundos** (default 30s).
|
||||
- **Occurrences > K** (default 5) en ventana de **D dias** (default 30).
|
||||
- **Success rate > S** (default 0.9 — falla < 10%).
|
||||
- **No existe ya un pipeline** que la cubra (validar con FTS sobre `uses_functions`).
|
||||
|
||||
Cuando se cumple → **proposal `new_pipeline`** con evidencia (sequence_ids, session_ids, occurrence count). Humano (o `fn-orquestador` autonomo) decide promover.
|
||||
|
||||
#### Implementacion (issue 0087 tanda A)
|
||||
|
||||
- `call_monitor sequences --detect` subcomando: escanea `calls` table, agrupa por session+window, computa secuencias, upserta en tabla `function_sequences`.
|
||||
- Cron diario que ejecuta el detector + genera proposals automaticas.
|
||||
- Visible en Monitor tab del `registry_dashboard`: sub-tab "Promotion candidates".
|
||||
|
||||
#### Cuando SI inflar una funcion
|
||||
|
||||
Casos legitimos para anadir feature a una funcion existente:
|
||||
|
||||
1. **Generalizar firma** sin romper consumidores (anadir param opcional con default sensato).
|
||||
2. **Mejor manejo de error** (mensajes mas claros, retry sensible).
|
||||
3. **Default mas inteligente** (autodetectar lo que antes era arg obligatorio).
|
||||
4. **Eliminar gotcha conocido** (fix de bug que estaba en `## Gotchas`).
|
||||
|
||||
NO infles para casos hipoteticos. NO anadas params "por flexibilidad". Si dudas, separa la responsabilidad en una funcion nueva o un pipeline.
|
||||
|
||||
#### Capability growth log — cuando se rellena
|
||||
|
||||
- Se rellena **solo cuando la funcion crece** (alguno de los 4 casos arriba).
|
||||
- Cada bump de `version` -> 1 linea en `## Capability growth log` con fecha y resumen 1-frase.
|
||||
- Una funcion estable de hace 6 meses puede seguir en v1.0.0 sin log: indica madurez, no abandono.
|
||||
- Telemetria (call_monitor) decide si una funcion estable es huerfana (`calls_90d=0`) o usada-y-buena (`calls_30d>10, error_rate<0.05`). Las primeras se deprecan; las segundas se respetan.
|
||||
|
||||
---
|
||||
|
||||
### Parte C — Output de discovery
|
||||
|
||||
Cuando un mecanismo de discovery (fuzzy match / FRESH hook / TOP block / capability page) surfacea una funcion, el payload **minimo** es:
|
||||
|
||||
```
|
||||
<id> → <signature> → <ejemplo de 1 linea>
|
||||
```
|
||||
|
||||
Ejemplo concreto:
|
||||
```
|
||||
redeploy_cpp_app_windows_bash_pipelines
|
||||
./fn run redeploy_cpp_app_windows registry_dashboard /path/to/app [--build]
|
||||
use: tras compilar cpp/build/windows, antes de smoke test manual
|
||||
```
|
||||
|
||||
Si Claude necesita mas (gotchas, params completos, codigo), un `mcp__registry__fn_show <id>` adicional. Pero el primer hit ya basta para el 80% de casos.
|
||||
|
||||
---
|
||||
|
||||
### Parte D — Relacion con otras reglas
|
||||
|
||||
- [[registry_first]] dice CUANDO buscar/usar/delegar. Esta regla dice **COMO** debe ser la funcion para que esa busqueda valga.
|
||||
- [[ids_naming]] hace ID predictible. Esta regla hace metadata predictible.
|
||||
- [[delegation]] dice cuando spawnar fn-constructor. Esta regla es lo que fn-constructor debe producir.
|
||||
- [[capability_groups]] agrupa funciones afines. Las paginas madre de cada grupo deben respetar el mismo contrato self-doc (mejor con su propio ejemplo end-to-end por grupo).
|
||||
|
||||
### Resumen TL;DR
|
||||
|
||||
1. Cada `.md` autosuficiente: Ejemplo + Cuando usarla + Gotchas (si impura) + Growth log (si crecio).
|
||||
2. Las funciones que hacen bien una cosa NO necesitan crecer.
|
||||
3. El registry crece **promoviendo composiciones repetidas a pipelines**, no inflando funciones.
|
||||
4. Telemetria de `call_monitor` detecta secuencias candidatas y abre proposals automaticas.
|
||||
5. Discovery devuelve siempre: `id + signature + 1-line example`. Resto on-demand.
|
||||
@@ -0,0 +1,147 @@
|
||||
## Como invocar funciones del registry — patrones canonicos
|
||||
|
||||
Toda invocacion del agente al registry sigue uno de **tres patrones**. Cualquier otro patron es antipatron auditable. Las invocaciones se loguean en `projects/fn_monitoring/apps/call_monitor/operations.db` (issue 0085) para alimentar el bucle reactivo.
|
||||
|
||||
### Patrones canonicos
|
||||
|
||||
| Caso | Patron | Cuando |
|
||||
|---|---|---|
|
||||
| **Inspeccionar** (buscar, leer codigo, ver dependencias, listar dominios, leer proposals) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` / `fn_proposal` | SIEMPRE para descubrimiento, lectura de codigo, exploracion. |
|
||||
| **Ejecutar** UNA funcion/pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | ID conocido + args planos. Despacho automatico por lenguaje. |
|
||||
| **Componer** ad-hoc multi-funcion con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo si hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
|
||||
|
||||
### Antipatrones prohibidos (audit-targeted)
|
||||
|
||||
| Patron | Razon | Sustituir por |
|
||||
|---|---|---|
|
||||
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad | `mcp__registry__fn_search` |
|
||||
| `sqlite3 registry.db "SELECT ... FROM proposals"` | Mismo problema | `mcp__registry__fn_proposal` |
|
||||
| `python -c "import metabase; dir(metabase)"` para descubrir helpers | Fuente de verdad = registry, no `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
|
||||
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
|
||||
| `client._http.request(...)` saltando wrapper del registry | Salta validacion del wrapper y telemetria | Usar wrapper; si firma incompleta, `fn proposal add --kind improve_function` |
|
||||
| Scripts en `temp/` para composiciones que se repiten >2 veces | Codigo perdido + sin monitoreo | Pipeline en `python/functions/pipelines/` o `bash/functions/pipelines/` |
|
||||
| `from <pkg> import *` en heredoc | Imposible identificar funciones usadas | Imports explicitos `from <domain> import <name1>, <name2>` |
|
||||
|
||||
### Excepciones autorizadas para `sqlite3` directo
|
||||
|
||||
Casos donde el MCP no aplica y `sqlite3 registry.db` es legitimo:
|
||||
|
||||
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
|
||||
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
|
||||
- JOINs custom entre tablas que el MCP no expone (`functions JOIN unit_tests ON ...`).
|
||||
- Columnas que el MCP no devuelve (rare; preferir proponer ampliacion del MCP).
|
||||
|
||||
El hook `PreToolUse` (`.claude/scripts/hook_registry_mcp.sh`) ya deja pasar estas excepciones y solo avisa cuando ve `sqlite3 registry.db "SELECT ..."` plano.
|
||||
|
||||
### Excepcion: hooks e infraestructura de telemetria (issue 0087)
|
||||
|
||||
Los **hooks** (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, etc.) y los **binarios de infraestructura** que sirven al agente (`fn_match`, `fn doctor`, `call_monitor`) **pueden leer `registry.db` directo** via `sqlite3` o `database/sql` con conexion read-only. NO estan sujetos a la regla MCP-first porque:
|
||||
|
||||
- No son acciones del agente — son inspeccion automatizada del entorno.
|
||||
- El MCP requiere tool invocation por Claude; un hook no puede invocar tools.
|
||||
- Latencia objetivo (50-200ms) incompatible con round-trip MCP.
|
||||
|
||||
**Restricciones:**
|
||||
- SOLO lectura. Conexion debe abrirse con `?mode=ro` o `?_query_only=1`.
|
||||
- NUNCA escritura a `registry.db` desde hooks.
|
||||
- Si un hook necesita escribir (cache, telemetria propia), usa su propia DB (`operations.db` del app de hooks, o `~/.fn_hooks/cache.db`).
|
||||
|
||||
Esta excepcion es **explicita y acotada** — no aplica al agente, que sigue regido por la regla MCP-first.
|
||||
|
||||
### Verificacion previa — `fn doctor`
|
||||
|
||||
Antes de empezar trabajo no trivial sobre el registry, ejecutar `fn doctor` para confirmar que el ecosistema esta sano:
|
||||
|
||||
- Artefactos OK (sin `git_not_initialized`, `venv_broken_path`, etc.).
|
||||
- Services activos cuando se necesiten (`sqlite_api`, `registry_api`, `registry_mcp`).
|
||||
- Sin drift `pc_locations` vs disco.
|
||||
- Sin drift `uses_functions` vs imports reales.
|
||||
|
||||
Si `fn doctor` reporta `service inactive` para `registry_mcp.service`, el MCP estara siendo invocado en modo stdio por Claude Code (normal); el systemd unit solo aplica al modo HTTP. Si el binario no responde, rebuild: `cd apps/registry_mcp && CGO_ENABLED=1 go build -tags fts5 -o registry_mcp .`.
|
||||
|
||||
### Tools MCP disponibles
|
||||
|
||||
| Tool | Lectura/escritura | Gating |
|
||||
|---|---|---|
|
||||
| `fn_search` | read | siempre on |
|
||||
| `fn_show` | read | siempre on |
|
||||
| `fn_code` | read | siempre on |
|
||||
| `fn_uses` | read | siempre on |
|
||||
| `fn_list_domains` | read | siempre on |
|
||||
| `fn_proposal` | read | siempre on |
|
||||
| `fn_doctor` | read | siempre on |
|
||||
| `fn_run` | execute (mutating side-effects) | requiere `--enable-run` |
|
||||
| `fn_create_function` | write | requiere `--enable-write` |
|
||||
|
||||
### Heredoc Python — convenciones obligatorias
|
||||
|
||||
Cuando el caso 3 (composicion) sea inevitable:
|
||||
|
||||
1. **Imports explicitos** desde paquetes del registry. Nunca `import *`.
|
||||
2. **No reescribir** la firma de una funcion del registry — importarla.
|
||||
3. **Args via env vars o stdin JSON**, nunca interpolacion shell directa (inyeccion).
|
||||
4. **Output a stdout JSON** cuando vaya a ser consumido por el siguiente paso.
|
||||
5. **Si el heredoc supera ~30 lineas**, extraer a `python/functions/pipelines/`. El monitor avisara automaticamente cuando un patron similar se repita >5 veces.
|
||||
|
||||
### Trazabilidad — bucle reactivo
|
||||
|
||||
Cada evento alimenta a `call_monitor.db` (event-log append-only) y se rollupea en una vista `function_stats` con contadores por funcion del registry. Tablas event-log:
|
||||
|
||||
| Tabla | Captura |
|
||||
|---|---|
|
||||
| `calls` | Cada invocacion (heredoc/mcp/fn_run): function_id, tool_used, duration_ms, success, error_class, args_hash |
|
||||
| `code_writes` | Cada Edit/Write sobre archivo del registry: function_id, session_id, lines_added/removed |
|
||||
| `test_runs` | Cada `go test`/`pytest` que toca codigo del registry: function_id, test_id, passed, duration_ms |
|
||||
| `e2e_runs_fn` | Cada check `e2e_checks` de app que usa la funcion: function_id, app_id, check_id, passed |
|
||||
| `violations` | Antipatron detectado: rule_id, session_id, command_snippet, severity |
|
||||
| `patterns` | Heredocs clusterizados: pattern_hash, session_ids[], occurrences, representative_snippet |
|
||||
| `sessions` | session_id, cwd, started_at, ended_at, health_score, mcp_ratio |
|
||||
|
||||
Vista agregada `function_stats` por `function_id`:
|
||||
|
||||
- **Uso:** `calls_total`, `calls_24h/7d/30d/90d`, `last_used_at`
|
||||
- **Errores:** `errors_total`, `error_rate`, `last_error_class`, `last_error_ts`
|
||||
- **Performance:** `mean_duration_ms`, `p95_duration_ms`
|
||||
- **Codigo:** `writes_count`, `last_write_at`
|
||||
- **Tests:** `tests_total`, `tests_failed`, `test_fail_rate`, `last_test_failed_at`
|
||||
- **E2E:** `e2e_total`, `e2e_failed`, `e2e_fail_rate`, `consumer_apps_count`
|
||||
- **Salud:** `violations_caused`
|
||||
|
||||
Assertions derivadas → proposals automaticas:
|
||||
|
||||
| Regla | Threshold | Proposal |
|
||||
|---|---|---|
|
||||
| Huerfana absoluta | `calls_90d=0 AND writes_count=0` | `deprecate_function` |
|
||||
| Bug prioritario | `error_rate>0.1 AND calls_7d>5` | `improve_function` (bug) |
|
||||
| Regresion performance | `p95_24h > 1.5 * p95_30d` | `improve_function` (perf) |
|
||||
| Test flaky | `test_fail_rate>0.1 AND tests_total>10` | `improve_function` (flaky) |
|
||||
| Wrapper saltado | `violations_caused>3` | `improve_function` (API gap) |
|
||||
| Patron inline sin funcion | `patterns.occurrences>5 AND no match FTS` | `new_function` con snippet |
|
||||
| Blast radius alto | `e2e_fail_rate>0 AND consumer_apps_count>=3` | `improve_function` (critical) |
|
||||
|
||||
Datos sensibles: solo `args_hash`, NUNCA valores concretos. Snippets de error redactados via allowlist.
|
||||
|
||||
### Capas de monitorizacion (issue 0085)
|
||||
|
||||
Cobertura por capa, no todas activas a la vez:
|
||||
|
||||
| # | Capa | Activacion | Cobertura |
|
||||
|---|---|---|---|
|
||||
| 1 | Hook PostToolUse Bash | siempre (settings.local.json) | mcp, fn_cli_run, edit_registry, violations |
|
||||
| 2 | Wrapper Python `registry_telemetry` | `FN_TELEMETRY=1` env var | heredocs + notebooks Jupyter |
|
||||
| 3 | Wrapper Bash `telemetry_prelude.sh` | `source` explicito o `FN_TELEMETRY=1` | heredoc bash + apps bash |
|
||||
| 4 | Interceptor en `fn run` | siempre (binario Go) | duration/error real de invocacion CLI |
|
||||
| 5 | `fn doctor copied-code` | comando manual / cron | drift estatico: codigo copiado en apps |
|
||||
| 6 | `function_versions` + snapshot | poblado por `fn index` + edit-hook | historial de versiones |
|
||||
| 7-8 | Build-tag Go / macro C++ | opt-in por app | runtime de app (futuro) |
|
||||
|
||||
**Boundary:** monitorizamos al **agente** y a **invocaciones canonicas**. Runtime de apps Go/C++ compiladas queda fuera. Compensar con tests + `e2e_checks` (issue 0068).
|
||||
|
||||
### Que NO se monitoriza
|
||||
|
||||
- Funcion Go/C++ llamada internamente por app ya compilada.
|
||||
- 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.
|
||||
|
||||
**Implicacion:** una funcion con `calls_90d=0` puede ser huerfana real O usada en runtime invisible. Antes de proponer `deprecate_function`, cruzar con `consumer_apps_count > 0` (e2e) o con `fn doctor uses-functions` (declaraciones estaticas).
|
||||
Executable
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook: registra cada invocacion del agente en
|
||||
# projects/fn_monitoring/apps/call_monitor/operations.db (issue 0085b).
|
||||
#
|
||||
# Identifica tool, extrae function_id cuando es posible, clasifica el patron
|
||||
# (mcp_*, fn_cli_run, heredoc_py, sqlite_direct, edit_registry, ...) y
|
||||
# detecta antipatrones para registrar violations.
|
||||
#
|
||||
# NUNCA bloquea la herramienta. Falla silenciosamente si la BD no esta lista.
|
||||
# Solo guarda args_hash, jamas valores concretos.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- Resolve registry root (walks up from cwd looking for registry.db) ----
|
||||
resolve_root() {
|
||||
local d="${PWD}"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ]; then
|
||||
printf '%s' "$d"
|
||||
return 0
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ROOT=$(resolve_root) || exit 0
|
||||
DB="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
|
||||
|
||||
# Si la BD aun no existe, el hook no hace nada (esperando init).
|
||||
[ -f "$DB" ] || exit 0
|
||||
|
||||
# ---- Read stdin JSON ----
|
||||
INPUT=$(cat)
|
||||
if [ -z "$INPUT" ]; then exit 0; fi
|
||||
|
||||
# Required jq presence
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
command -v sqlite3 >/dev/null 2>&1 || exit 0
|
||||
|
||||
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
|
||||
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""')
|
||||
TS=$(date -u +%s)
|
||||
|
||||
# Tool response success/error
|
||||
SUCCESS=1
|
||||
ERROR_CLASS=""
|
||||
ERROR_SNIPPET=""
|
||||
RESP_IS_ERROR=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.is_error // false) else false end')
|
||||
if [ "$RESP_IS_ERROR" = "true" ]; then
|
||||
SUCCESS=0
|
||||
ERROR_SNIPPET=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.error // .tool_response.content // "") else "" end' | head -c 240 | tr '\n' ' ')
|
||||
fi
|
||||
|
||||
# args_hash: sha256 truncado del tool_input (sin valores)
|
||||
ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -c1-16)
|
||||
|
||||
# Helpers SQL
|
||||
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
|
||||
|
||||
insert_call() {
|
||||
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
|
||||
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
|
||||
# Politica issue 0087: command_snippet solo se rellena cuando function_id
|
||||
# esta vacio. Si la call golpea una funcion del registry, su ID y
|
||||
# tool_used bastan; no duplicamos el comando.
|
||||
if [ -n "$fn_id" ]; then snippet=""; fi
|
||||
# Redact common secrets antes de persistir
|
||||
snippet=$(printf '%s' "$snippet" \
|
||||
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
|
||||
| head -c 200)
|
||||
fn_esc=$(sql_escape "$fn_id")
|
||||
tu_esc=$(sql_escape "$tool_used")
|
||||
ec_esc=$(sql_escape "$ERROR_CLASS")
|
||||
es_esc=$(sql_escape "$ERROR_SNIPPET")
|
||||
sid_esc=$(sql_escape "$SESSION_ID")
|
||||
ah_esc=$(sql_escape "$ARGS_HASH")
|
||||
snip_esc=$(sql_escape "$snippet")
|
||||
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
|
||||
}
|
||||
|
||||
insert_code_write() {
|
||||
local fn_id="$1" file_path="$2" added="${3:-0}" removed="${4:-0}"
|
||||
local fn_esc fp_esc sid_esc
|
||||
fn_esc=$(sql_escape "$fn_id")
|
||||
fp_esc=$(sql_escape "$file_path")
|
||||
sid_esc=$(sql_escape "$SESSION_ID")
|
||||
sqlite3 "$DB" "INSERT INTO code_writes (session_id, function_id, file_path, lines_added, lines_removed, ts) VALUES ('$sid_esc','$fn_esc','$fp_esc',$added,$removed,$TS);" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Snapshot a function version row when an edit lands on a registry file.
|
||||
# Uses sha256 of file bytes as content_hash (separate namespace from index source).
|
||||
insert_edit_version() {
|
||||
local fn_id="$1" abs_path="$2"
|
||||
[ -f "$abs_path" ] || return 0
|
||||
command -v sha256sum >/dev/null 2>&1 || return 0
|
||||
local hash
|
||||
hash=$(sha256sum "$abs_path" 2>/dev/null | awk '{print $1}')
|
||||
[ -z "$hash" ] && return 0
|
||||
local fn_esc h_esc
|
||||
fn_esc=$(sql_escape "$fn_id")
|
||||
h_esc=$(sql_escape "$hash")
|
||||
sqlite3 "$DB" "INSERT OR IGNORE INTO function_versions (function_id, content_hash, version, snapped_at, source, lines_added, lines_removed) VALUES ('$fn_esc','$h_esc','',$TS,'edit_hook',0,0);" 2>/dev/null || true
|
||||
}
|
||||
|
||||
insert_violation() {
|
||||
local rule_id="$1" fn_id="$2" snippet="$3" severity="${4:-warning}"
|
||||
local r_esc fn_esc sn_esc sev_esc sid_esc
|
||||
r_esc=$(sql_escape "$rule_id")
|
||||
fn_esc=$(sql_escape "$fn_id")
|
||||
sn_esc=$(sql_escape "$(printf '%s' "$snippet" | head -c 240 | tr '\n' ' ')")
|
||||
sev_esc=$(sql_escape "$severity")
|
||||
sid_esc=$(sql_escape "$SESSION_ID")
|
||||
sqlite3 "$DB" "INSERT INTO violations (session_id, rule_id, function_id, command_snippet, severity, ts) VALUES ('$sid_esc','$r_esc','$fn_esc','$sn_esc','$sev_esc',$TS);" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ---- Derive function_id from registry file path ----
|
||||
# Matches paths under functions/<domain>/<name>.<ext>, python/functions/<domain>/<name>.py,
|
||||
# bash/functions/<domain>/<name>.sh, frontend/functions/<domain>/<name>.ts(x)
|
||||
derive_fn_id_from_path() {
|
||||
local p="$1"
|
||||
[ -z "$p" ] && return 1
|
||||
case "$p" in
|
||||
functions/*/*.go|*/functions/*/*.go)
|
||||
local dom name
|
||||
dom=$(printf '%s' "$p" | sed -E 's|.*functions/([^/]+)/.*|\1|')
|
||||
name=$(printf '%s' "$p" | sed -E 's|.*functions/[^/]+/([^/.]+)\..*|\1|')
|
||||
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_go_%s' "$name" "$dom" && return 0 ;;
|
||||
python/functions/*/*.py)
|
||||
local dom name
|
||||
dom=$(printf '%s' "$p" | sed -E 's|python/functions/([^/]+)/.*|\1|')
|
||||
name=$(printf '%s' "$p" | sed -E 's|python/functions/[^/]+/([^/.]+)\..*|\1|')
|
||||
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_py_%s' "$name" "$dom" && return 0 ;;
|
||||
bash/functions/*/*.sh)
|
||||
local dom name
|
||||
dom=$(printf '%s' "$p" | sed -E 's|bash/functions/([^/]+)/.*|\1|')
|
||||
name=$(printf '%s' "$p" | sed -E 's|bash/functions/[^/]+/([^/.]+)\..*|\1|')
|
||||
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_bash_%s' "$name" "$dom" && return 0 ;;
|
||||
frontend/functions/*/*.ts|frontend/functions/*/*.tsx)
|
||||
local dom name
|
||||
dom=$(printf '%s' "$p" | sed -E 's|frontend/functions/([^/]+)/.*|\1|')
|
||||
name=$(printf '%s' "$p" | sed -E 's|frontend/functions/[^/]+/([^/.]+)\..*|\1|')
|
||||
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_ts_%s' "$name" "$dom" && return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- Dispatch by tool ----
|
||||
case "$TOOL_NAME" in
|
||||
mcp__registry__fn_search)
|
||||
insert_call "" "mcp_fn_search"
|
||||
;;
|
||||
mcp__registry__fn_show)
|
||||
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
||||
insert_call "$ID" "mcp_fn_show"
|
||||
;;
|
||||
mcp__registry__fn_code)
|
||||
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
||||
insert_call "$ID" "mcp_fn_code"
|
||||
;;
|
||||
mcp__registry__fn_uses)
|
||||
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
||||
insert_call "$ID" "mcp_fn_uses"
|
||||
;;
|
||||
mcp__registry__fn_run)
|
||||
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
||||
insert_call "$ID" "mcp_fn_run"
|
||||
;;
|
||||
mcp__registry__fn_list_domains)
|
||||
insert_call "" "mcp_fn_list_domains"
|
||||
;;
|
||||
mcp__registry__fn_proposal)
|
||||
insert_call "" "mcp_fn_proposal"
|
||||
;;
|
||||
mcp__registry__fn_doctor)
|
||||
insert_call "" "mcp_fn_doctor"
|
||||
;;
|
||||
mcp__registry__fn_create_function)
|
||||
insert_call "" "mcp_fn_create_function"
|
||||
;;
|
||||
|
||||
Edit|Write|MultiEdit)
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||||
ABS_PATH="$FILE_PATH"
|
||||
# Make path relative to root if absolute and inside root
|
||||
case "$FILE_PATH" in
|
||||
"$ROOT"/*) FILE_PATH="${FILE_PATH#$ROOT/}" ;;
|
||||
/*) ABS_PATH="$FILE_PATH" ;;
|
||||
*) ABS_PATH="$ROOT/$FILE_PATH" ;;
|
||||
esac
|
||||
FN_ID=$(derive_fn_id_from_path "$FILE_PATH" || true)
|
||||
if [ -n "$FN_ID" ]; then
|
||||
insert_code_write "$FN_ID" "$FILE_PATH" 0 0
|
||||
insert_call "$FN_ID" "edit_registry"
|
||||
insert_edit_version "$FN_ID" "$ABS_PATH"
|
||||
fi
|
||||
;;
|
||||
|
||||
Bash)
|
||||
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
|
||||
CMD_HEAD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
|
||||
|
||||
# Classify
|
||||
TOOL_USED="bash_other"
|
||||
FN_ID=""
|
||||
|
||||
if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])\./fn[[:space:]]+run[[:space:]]+'; then
|
||||
TOOL_USED="fn_cli_run"
|
||||
FN_ID=$(printf '%s' "$CMD" | sed -nE 's/.*\.\/fn[[:space:]]+run[[:space:]]+([A-Za-z0-9_]+).*/\1/p' | head -n1)
|
||||
elif printf '%s' "$CMD" | grep -qE 'python/\.venv/bin/python3[[:space:]]+-[[:space:]]+<<'; then
|
||||
TOOL_USED="heredoc_py"
|
||||
elif printf '%s' "$CMD" | grep -qE 'sqlite3[[:space:]][^|]*\bregistry\.db\b'; then
|
||||
TOOL_USED="sqlite_direct"
|
||||
fi
|
||||
|
||||
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
|
||||
|
||||
# ---- Violation rules ----
|
||||
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
|
||||
if [ "$TOOL_USED" = "sqlite_direct" ]; then
|
||||
if ! printf '%s' "$CMD" | grep -qiE '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list)|COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
|
||||
insert_violation "sqlite3_registry_select" "" "$CMD_HEAD" "warning"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. python -c "import X; dir(X)"
|
||||
if printf '%s' "$CMD" | grep -qE 'python[3]?[[:space:]]+-c[[:space:]]+["'\''].*import.*(dir|help)\('; then
|
||||
insert_violation "python_dir_inspect" "" "$CMD_HEAD" "info"
|
||||
fi
|
||||
|
||||
# 3. from <pkg> import * (en heredoc python)
|
||||
if [ "$TOOL_USED" = "heredoc_py" ]; then
|
||||
if printf '%s' "$CMD" | grep -qE 'from[[:space:]]+[A-Za-z0-9_.]+[[:space:]]+import[[:space:]]+\*'; then
|
||||
insert_violation "import_star_in_heredoc" "" "$CMD_HEAD" "warning"
|
||||
fi
|
||||
if printf '%s' "$CMD" | grep -qE 'client\._http\.request\('; then
|
||||
insert_violation "client_http_request_direct" "" "$CMD_HEAD" "warning"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES)
|
||||
# del registry como additionalContext en cada turno del usuario.
|
||||
#
|
||||
# Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h).
|
||||
# Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo.
|
||||
#
|
||||
# NUNCA bloquea: si algo falla, emite contexto vacio y sale 0.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CACHE_DIR="${HOME}/.cache/fn_registry"
|
||||
CACHE_FILE="${CACHE_DIR}/capabilities.txt"
|
||||
TTL_SECONDS=3600
|
||||
|
||||
# Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR)
|
||||
resolve_root() {
|
||||
local d="${PWD}"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then
|
||||
printf '%s' "$d"
|
||||
return 0
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then
|
||||
printf '%s' "${CLAUDE_PROJECT_DIR}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean
|
||||
cat >/dev/null 2>&1 || true
|
||||
|
||||
ROOT=$(resolve_root) || exit 0
|
||||
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
|
||||
|
||||
# Cache freshness check
|
||||
need_refresh=1
|
||||
if [ -f "$CACHE_FILE" ]; then
|
||||
now=$(date +%s)
|
||||
mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
|
||||
age=$((now - mtime))
|
||||
if [ "$age" -lt "$TTL_SECONDS" ]; then
|
||||
need_refresh=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$need_refresh" -eq 1 ]; then
|
||||
# Regenerate: call fn doctor capabilities --emit-claude-md and process
|
||||
raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true)
|
||||
if [ -z "$raw" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract top 5 from each section using awk.
|
||||
# Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines".
|
||||
line=$(printf '%s\n' "$raw" | awk '
|
||||
BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; }
|
||||
/^## .*Top 20/ { sec="TOP"; next }
|
||||
/^## .*Fresh/ { sec="FRESH"; next }
|
||||
/^## .*Pipelines/ { sec="PIPE"; next }
|
||||
/^## / { sec=""; next }
|
||||
/^- `/ {
|
||||
# extract first backticked token
|
||||
s = $0
|
||||
sub(/^- `/, "", s)
|
||||
i = index(s, "`")
|
||||
if (i == 0) next
|
||||
id = substr(s, 1, i-1)
|
||||
if (sec == "TOP" && n_top < 5) { tops[n_top++] = id }
|
||||
if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id }
|
||||
if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id }
|
||||
}
|
||||
END {
|
||||
out = "CAPABILITIES (cache 1h):"
|
||||
if (n_top > 0) {
|
||||
line = " TOP: " tops[0]
|
||||
for (i=1; i<n_top; i++) line = line ", " tops[i]
|
||||
out = out "\n" line
|
||||
}
|
||||
if (n_fresh > 0) {
|
||||
line = " FRESH (7d): " fresh[0]
|
||||
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
|
||||
out = out "\n" line
|
||||
}
|
||||
if (n_pipe > 0) {
|
||||
line = " PIPELINES: " pipes[0]
|
||||
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
|
||||
out = out "\n" line
|
||||
}
|
||||
print out
|
||||
}
|
||||
')
|
||||
|
||||
if [ -z "$line" ]; then
|
||||
exit 0
|
||||
fi
|
||||
printf '%s\n' "$line" >"$CACHE_FILE" 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# Emit cached content as additionalContext
|
||||
if [ ! -s "$CACHE_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ctx=$(cat "$CACHE_FILE")
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -n --arg ctx "$ctx" '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "UserPromptSubmit",
|
||||
additionalContext: $ctx
|
||||
}
|
||||
}'
|
||||
else
|
||||
# Fallback: print raw text (Claude Code prints stdout as context too)
|
||||
printf '%s\n' "$ctx"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook: gate "tag de capability group obligatorio" tras crear/modificar
|
||||
# funciones del registry. Issue 0086 paso 9/gate.
|
||||
#
|
||||
# Comportamiento:
|
||||
# - Detecta .md de funciones (functions/, python/functions/, bash/functions/,
|
||||
# frontend/functions/, cpp/functions/) modificados en los ultimos 60s.
|
||||
# - Lee frontmatter `tags:` y verifica si al menos uno coincide con un capability
|
||||
# group declarado en docs/capabilities/INDEX.md.
|
||||
# - Si NO hay match -> emite additionalContext con la lista de funciones afectadas.
|
||||
# - NUNCA bloquea. Solo warning visible.
|
||||
#
|
||||
# Salida JSON consumida por Claude Code:
|
||||
# { "hookSpecificOutput": { "hookEventName": "PostToolUse",
|
||||
# "additionalContext": "..." } }
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
resolve_root() {
|
||||
local d="${PWD}"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ]; then
|
||||
printf '%s' "$d"
|
||||
return 0
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ROOT=$(resolve_root) || exit 0
|
||||
INDEX="$ROOT/docs/capabilities/INDEX.md"
|
||||
|
||||
# Si no existe el INDEX aun, no hay grupos definidos -> nada que verificar.
|
||||
[ -f "$INDEX" ] || exit 0
|
||||
|
||||
# Consume stdin (sin parsear — no necesitamos session_id para este gate)
|
||||
cat >/dev/null
|
||||
|
||||
# Solo correr si hay jq disponible
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# 1. Cargar lista de capability groups desde el INDEX.
|
||||
# Formato esperado en INDEX.md: | [name](name.md) | N | descripcion |
|
||||
CAP_GROUPS=$(grep -oE '\[[a-z][a-z0-9_-]*\]\([a-z][a-z0-9_-]*\.md\)' "$INDEX" \
|
||||
| sed -E 's/^\[([^]]+)\].*/\1/' \
|
||||
| sort -u)
|
||||
|
||||
[ -z "$CAP_GROUPS" ] && exit 0
|
||||
|
||||
# 2. Encontrar .md de funciones modificados en ultimos 60s.
|
||||
RECENT=$(find "$ROOT/functions" "$ROOT/python/functions" "$ROOT/bash/functions" \
|
||||
"$ROOT/frontend/functions" "$ROOT/cpp/functions" \
|
||||
-maxdepth 4 -type f -name '*.md' -mmin -1 2>/dev/null || true)
|
||||
|
||||
[ -z "$RECENT" ] && exit 0
|
||||
|
||||
# 3. Para cada .md reciente: extraer tags del frontmatter, comparar con groups.
|
||||
MISSING=""
|
||||
while IFS= read -r mdfile; do
|
||||
[ -z "$mdfile" ] && continue
|
||||
# Extrae el bloque entre los dos `---` del inicio
|
||||
front=$(awk '/^---$/{c++; next} c==1 {print} c>=2 {exit}' "$mdfile" 2>/dev/null || true)
|
||||
[ -z "$front" ] && continue
|
||||
|
||||
# tags: [a, b, c] o tags:\n - a\n - b
|
||||
tags_inline=$( { printf '%s\n' "$front" | grep -E '^tags:[[:space:]]*\[' | head -1 \
|
||||
| sed -E 's/^tags:[[:space:]]*\[(.*)\].*$/\1/' \
|
||||
| tr ',' '\n' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
|
||||
|
||||
tags_block=$( { printf '%s\n' "$front" | awk '
|
||||
/^tags:[[:space:]]*$/ {intag=1; next}
|
||||
intag && /^[[:space:]]*-[[:space:]]/ {sub(/^[[:space:]]*-[[:space:]]*/, ""); print; next}
|
||||
intag && !/^[[:space:]]/ {intag=0}
|
||||
' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
|
||||
|
||||
tags=$( { printf '%s\n%s\n' "$tags_inline" "$tags_block" | grep -v '^$'; } || true )
|
||||
|
||||
matched=0
|
||||
while IFS= read -r g; do
|
||||
[ -z "$g" ] && continue
|
||||
if printf '%s\n' "$tags" | grep -qx "$g"; then
|
||||
matched=1
|
||||
break
|
||||
fi
|
||||
done <<< "$CAP_GROUPS"
|
||||
|
||||
if [ "$matched" -eq 0 ]; then
|
||||
rel="${mdfile#$ROOT/}"
|
||||
MISSING="${MISSING}${rel}\n"
|
||||
fi
|
||||
done <<< "$RECENT"
|
||||
|
||||
# 4. Si hay funciones sin tag de grupo, emitir aviso.
|
||||
if [ -n "$MISSING" ]; then
|
||||
CAP_GROUPS_CSV=$(printf '%s' "$CAP_GROUPS" | tr '\n' ',' | sed 's/,$//')
|
||||
WARN="CAPABILITY-GAP (issue 0086): funcion(es) recien tocada(s) sin tag de capability group: $(printf '%b' "$MISSING" | tr '\n' ' ')"
|
||||
WARN+="| Grupos disponibles: ${CAP_GROUPS_CSV}. Anade al menos uno al frontmatter \`tags:\` y corre \`./fn index\`. Si la funcion no encaja en ningun grupo existente, considera crear grupo nuevo (>=3 funciones) o dejarla con tag plano (no de grupo)."
|
||||
jq -n --arg ctx "$WARN" '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PostToolUse",
|
||||
additionalContext: $ctx
|
||||
}
|
||||
}'
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse hook: sugiere funciones del registry cuando un comando Bash
|
||||
# inline probablemente reinventa una funcion existente (issue 0087).
|
||||
#
|
||||
# Llama a `./fn match "<cmd>"` con timeout 200ms. Si encaja con alta
|
||||
# confianza, imprime un <system-reminder> a stderr para que Claude Code
|
||||
# lo lea como recordatorio. NUNCA bloquea la tool — exit 0 siempre.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- Always exit 0, no matter what ----
|
||||
trap 'exit 0' ERR
|
||||
|
||||
# ---- Resolve registry root (walks up from cwd) ----
|
||||
resolve_root() {
|
||||
local d="${PWD}"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ]; then
|
||||
printf '%s' "$d"
|
||||
return 0
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ROOT=$(resolve_root) || exit 0
|
||||
FN_BIN="$ROOT/fn"
|
||||
[ -x "$FN_BIN" ] || exit 0
|
||||
|
||||
# ---- Read stdin JSON ----
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
INPUT=$(cat)
|
||||
[ -z "$INPUT" ] && exit 0
|
||||
|
||||
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
||||
[ "$TOOL_NAME" = "Bash" ] || exit 0
|
||||
|
||||
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
||||
[ -z "$CMD" ] && exit 0
|
||||
|
||||
# Single-line for matching against denylist patterns
|
||||
CMD_FLAT=$(printf '%s' "$CMD" | tr '\n' ' ')
|
||||
|
||||
# ---- Denylist (skip antes de llamar fn match para ahorrar el invoke) ----
|
||||
|
||||
# Comandos demasiado cortos -> trivial
|
||||
CMD_LEN=${#CMD_FLAT}
|
||||
[ "$CMD_LEN" -lt 20 ] && exit 0
|
||||
|
||||
# Trivial single-utility commands
|
||||
case "$CMD_FLAT" in
|
||||
"ls"|"ls "*|"cd"|"cd "*|"pwd"|"pwd "*|"cat"|"cat "*|"echo"|"echo "*)
|
||||
exit 0 ;;
|
||||
"grep"|"grep "*|"head"|"head "*|"tail"|"tail "*|"wc"|"wc "*)
|
||||
exit 0 ;;
|
||||
"mkdir"|"mkdir "*|"rm"|"rm "*|"mv"|"mv "*|"cp"|"cp "*)
|
||||
exit 0 ;;
|
||||
"git"|"git "*)
|
||||
exit 0 ;;
|
||||
"go"|"go "*)
|
||||
# go build / go test corrientes — el agente ya los maneja
|
||||
exit 0 ;;
|
||||
esac
|
||||
|
||||
# Comandos que ya usan el registry: ./fn ..., fn run ..., mcp__registry__*
|
||||
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])\./fn([[:space:]]|$)'; then
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])fn[[:space:]]+(run|search|show|code|uses|doctor|index|match|list|add|proposal|sync|ops|check)'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pure-cd (movement only, no logic)
|
||||
if printf '%s' "$CMD_FLAT" | grep -qE '^[[:space:]]*cd[[:space:]]+[^&|;]+$'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- Llamar fn match con timeout 200ms ----
|
||||
command -v timeout >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Truncar el comando a algo razonable para fn match (evitar args huge)
|
||||
CMD_TRUNC=$(printf '%s' "$CMD_FLAT" | head -c 500)
|
||||
|
||||
MATCH_JSON=$(timeout 0.2 "$FN_BIN" match "$CMD_TRUNC" --format json --top 3 2>/dev/null) || exit 0
|
||||
[ -z "$MATCH_JSON" ] && exit 0
|
||||
|
||||
# ---- Parsear JSON ----
|
||||
HIGH_CONF=$(printf '%s' "$MATCH_JSON" | jq -r '.high_confidence // false' 2>/dev/null || echo "false")
|
||||
TOP_ID=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].id // ""' 2>/dev/null || echo "")
|
||||
TOP_SCORE=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].score // 0' 2>/dev/null || echo "0")
|
||||
TOP_SIG=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].signature // ""' 2>/dev/null || echo "")
|
||||
TOP_SNIP=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].snippet // ""' 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$TOP_ID" ] && exit 0
|
||||
|
||||
# Trigger condition: (high_confidence==true OR score>=0.85) AND score>=0.6
|
||||
# - high_confidence requires top1/top2 gap > 1.5 (set por fn match)
|
||||
# - score>=0.85 cubre matches muy fuertes donde el gap es modesto
|
||||
SCORE_HI=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.85) ? "1" : "0" }')
|
||||
SCORE_MIN=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.6) ? "1" : "0" }')
|
||||
|
||||
[ "$SCORE_MIN" = "1" ] || exit 0
|
||||
if [ "$HIGH_CONF" != "true" ] && [ "$SCORE_HI" != "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Truncar snippet a 100 chars y limpiar saltos de linea
|
||||
SNIP_SHORT=$(printf '%s' "$TOP_SNIP" | tr '\n' ' ' | head -c 100)
|
||||
|
||||
# Formatear score con 2 decimales
|
||||
SCORE_FMT=$(awk -v s="$TOP_SCORE" 'BEGIN{ printf "%.2f", s+0 }')
|
||||
|
||||
# ---- Emitir <system-reminder> a stderr ----
|
||||
cat >&2 <<EOF
|
||||
<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function.
|
||||
USE: ./fn run $TOP_ID -> $TOP_SIG
|
||||
SNIPPET: $SNIP_SHORT
|
||||
Confidence: $SCORE_FMT. If you proceed inline, the violation will be logged.
|
||||
</system-reminder>
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
|
||||
# Test manual:
|
||||
# echo '{"tool_name":"Bash","tool_input":{"command":"taskkill.exe /IM registry_dashboard.exe /F"},"session_id":"test"}' \
|
||||
# | bash .claude/scripts/hook_fn_match.sh
|
||||
#
|
||||
# Casos silenciosos:
|
||||
# echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' \
|
||||
# | bash .claude/scripts/hook_fn_match.sh
|
||||
# echo '{"tool_name":"Bash","tool_input":{"command":"./fn run filter_slice_go_core 1 2 3"},"session_id":"test"}' \
|
||||
# | bash .claude/scripts/hook_fn_match.sh
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# UserPromptSubmit hook: recordatorio compacto de patrones canonicos del registry.
|
||||
# Inyectado como additionalContext en cada turno del usuario.
|
||||
# Issue 0085 (hardening 2).
|
||||
#
|
||||
# NUNCA bloquea. Solo printf de additionalContext.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve registry root (walks up from cwd)
|
||||
resolve_root() {
|
||||
local d="${PWD}"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ]; then
|
||||
printf '%s' "$d"
|
||||
return 0
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ROOT=$(resolve_root) || exit 0
|
||||
|
||||
# Read input, extract session_id (UserPromptSubmit payload includes it)
|
||||
INPUT=$(cat)
|
||||
SESSION_ID=""
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Count current pending proposals + recent violations for situational awareness
|
||||
PROPOSALS_PENDING="?"
|
||||
VIOLATIONS_24H="?"
|
||||
CALLS_24H="?"
|
||||
CAP_CREATED=0
|
||||
CAP_USED=0
|
||||
CAP_ORPHAN=0
|
||||
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
REG="$ROOT/registry.db"
|
||||
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
|
||||
[ -f "$REG" ] && PROPOSALS_PENDING=$(sqlite3 "$REG" "SELECT COUNT(*) FROM proposals WHERE status='pending'" 2>/dev/null || echo "?")
|
||||
if [ -f "$MON" ]; then
|
||||
VIOLATIONS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
|
||||
CALLS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
|
||||
if [ -n "$SESSION_ID" ]; then
|
||||
sid_esc=$(printf '%s' "$SESSION_ID" | sed "s/'/''/g")
|
||||
CAP_CREATED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc'" 2>/dev/null || echo 0)
|
||||
CAP_USED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session>0" 2>/dev/null || echo 0)
|
||||
CAP_ORPHAN=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session=0" 2>/dev/null || echo 0)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
REMINDER="REGISTRY-FIRST (issue 0085 telemetry active): "
|
||||
REMINDER+="Inspect → mcp__registry__fn_search/show/code/uses/proposal. "
|
||||
REMINDER+="Execute one fn → mcp__registry__fn_run or ./fn run. "
|
||||
REMINDER+="Compose multi-fn → heredoc python IMPORTANDO del registry. "
|
||||
REMINDER+="NUNCA sqlite3 registry.db directo (salvo schema/PRAGMA/COUNT/JOIN). "
|
||||
REMINDER+="NUNCA reescribir inline logica que ya es funcion. "
|
||||
REMINDER+="Si patron se repite >2x → propose nueva funcion via fn-constructor. "
|
||||
REMINDER+="Estado: pending_proposals=${PROPOSALS_PENDING} violations_24h=${VIOLATIONS_24H} calls_24h=${CALLS_24H}. "
|
||||
REMINDER+="CAPABILITY-GROWTH (issue 0086): created_this_session=${CAP_CREATED} used=${CAP_USED} orphan=${CAP_ORPHAN}. Si orphan>0 -> integra la funcion en el codigo o documenta por que se quedo huerfana. "
|
||||
REMINDER+="Comando autocheck: /fn_claude."
|
||||
|
||||
jq -n --arg ctx "$REMINDER" '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "UserPromptSubmit",
|
||||
additionalContext: $ctx
|
||||
}
|
||||
}'
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse hook: NO bloquea. Inyecta recordatorio cuando ve sqlite3 sobre registry.db
|
||||
# para que el modelo prefiera el MCP `registry` la proxima vez.
|
||||
|
||||
input="$(cat)"
|
||||
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""')"
|
||||
|
||||
# Solo nos importa registry.db (NO operations.db, NO otros .db).
|
||||
if ! printf '%s' "$cmd" | grep -Eq 'sqlite3[^|]*\bregistry\.db\b'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Casos legitimos donde el MCP no aplica: introspeccion de schema, agregaciones, JOINs.
|
||||
if printf '%s' "$cmd" | grep -Eq '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list))'; then
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$cmd" | grep -Eqi '(COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Caso a redirigir: emitir nota como additionalContext y dejar pasar el comando.
|
||||
jq -n '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: "Aviso: sqlite3 directo sobre registry.db detectado. Para futuras consultas usa el MCP registry (mcp__registry__fn_search / fn_show / fn_code / fn_uses / fn_list_domains). Fallback a sqlite3 SOLO para .schema, PRAGMA, COUNT/GROUP BY, JOINs custom."
|
||||
}
|
||||
}'
|
||||
exit 0
|
||||
@@ -80,3 +80,4 @@ Thumbs.db
|
||||
broken_paths.txt
|
||||
imgui.ini
|
||||
prompts/
|
||||
kotlin/functions/ui/
|
||||
|
||||
@@ -14,3 +14,9 @@
|
||||
[submodule "cpp/vendor/implot3d"]
|
||||
path = cpp/vendor/implot3d
|
||||
url = https://github.com/brenocq/implot3d.git
|
||||
[submodule "cpp/vendor/sdl3"]
|
||||
path = cpp/vendor/sdl3
|
||||
url = https://github.com/libsdl-org/SDL.git
|
||||
[submodule "emsdk"]
|
||||
path = emsdk
|
||||
url = https://github.com/emscripten-core/emsdk.git
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"registry": {
|
||||
"command": "/home/lucas/fn_registry/apps/registry_mcp/registry_mcp",
|
||||
"args": ["--enable-run", "--enable-write"],
|
||||
"env": {
|
||||
"FN_REGISTRY_ROOT": "/home/lucas/fn_registry"
|
||||
}
|
||||
"command": "./apps/registry_mcp/registry_mcp",
|
||||
"args": ["--enable-run", "--enable-write"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,44 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 2026-05-14
|
||||
|
||||
### Added
|
||||
|
||||
- **Issue 0086 — Monitor tab del `registry_dashboard`** (sub-repo `dataforge/registry_dashboard`). Pestaña `Monitor` primera y por defecto del TabBar, landing del bucle reactivo construir->ejecutar->recopilar->analizar->mejorar.
|
||||
- 7 KPIs (Calls / MCP / Reg % / Errors / Violations / Copies / Versions) filtradas por ventana temporal (1h/24h/7d/30d/All).
|
||||
- Sub-tab `Recent Executions` con columnas When/Function/Tool/ms/OK/Error. Columna Function muestra `$ <snippet>` en gris cuando `function_id` vacio, hover tooltip con comando completo. Checkbox `Only registry functions` filtra por `function_id != ''`.
|
||||
- Sub-tab `Failed Functions` (5a) — subset filtrado a registry-functions fallidas, columnas When/Function/Tool/Error class/Error snippet, function_id en rojo.
|
||||
- Live scatter `duracion (ms)` vs `time`: eje X auto-scroll a `now`, ventana configurable (1m/5m/15m/1h/6h) independiente del filtro de KPIs, eje Y dinamico `0..max(visible)+500ms`. Hora local (`UseLocalTime`). Series ok/error en verde/rojo. Hover sobre punto = tooltip Function/Tool/Duration/Error.
|
||||
- Indicador `live`/`offline` con timestamp del ultimo evento WS.
|
||||
- **WebSocket live stream sqlite_api -> registry_dashboard** (sub-repo `dataforge/sqlite_api`). Endpoint `GET /api/events/call_monitor`. Hub global con subscribers; ticker arranca solo con >=1 subscriber (cero overhead si nadie mira). Cliente recibe snapshot inicial (KPIs + 100 ultimas filas + watermark) y luego deltas `id > watermark`. Cliente puede mandar `{watermark: N}` para resumir tras reconexion.
|
||||
- **WS client C++** hand-rolled RFC6455 en `ws_client.{h,cpp}` (~330 LOC) en el dashboard. Localhost-only (no TLS). Thread propio, reconnect exponencial 0.5s->8s, FIN/text/ping/pong/close handling, queue thread-safe drenada cada frame.
|
||||
- **Migration 007 `command_snippet` en `calls`** (`projects/fn_monitoring/apps/call_monitor/migrations/007_calls_command_snippet.sql`). Aditiva, idempotente. Llena por hook `hook_call_monitor.sh` solo cuando `function_id == ''`. Redactado de `password=`/`token=`/`secret=`/`api_key=`/`bearer=`. Truncado 200 chars.
|
||||
- **Issue 0087 — Capability Discovery Acceleration**. Modelo 5 capas + 7 piezas (ver `dev/issues/0087-*.md`).
|
||||
- **`fn match`** (`cmd/fn/match.go`) — subcommand fuzzy-FTS5 que dado un comando devuelve top-N funciones del registry candidates. Latencia 6-7ms. Output JSON con `score` (normalizado top=1.0) + `raw_score` (absoluto pre-normalizacion) + `high_confidence` gate (`raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`).
|
||||
- **`fn doctor capabilities --emit-claude-md`** (`cmd/fn/doctor.go` + `functions/infra/emit_capabilities_md.go`) — emite bloque markdown con secciones TOP 20 (por `calls_total`), Fresh 7d, Pipelines top 5. Fallback si `call_monitor.operations.db` ausente.
|
||||
- **`call_monitor sequences --detect [--propose]`** (`projects/fn_monitoring/apps/call_monitor/sequences.go` + `migrations/006_function_sequences.sql`). Detecta secuencias A->B(->C) en `calls` (same session, gap < 30s, occ >= 5, sess >= 2, success_rate >= 0.9) y abre proposals `new_pipeline` automaticamente.
|
||||
- **Hook `PreToolUse` `hook_fn_match.sh`** — denylist + `fn match` con timeout 0.2s. Inyecta `<system-reminder>FUZZY-MATCH: USE ./fn run <id>` cuando confidence alta. Latencia 113ms trigger / 32ms denylist. Registrado en `.claude/settings.local.json` (Bash matcher).
|
||||
- **Hook `UserPromptSubmit` `hook_capabilities_inject.sh`** — cache 1h en `~/.cache/fn_registry/capabilities.txt`. Emite JSON `hookSpecificOutput.additionalContext` con linea compacta `CAPABILITIES: TOP / FRESH / PIPELINES`. Latencia cold 33ms / warm 18ms.
|
||||
- **Timer systemd user** `call_monitor_sequences.timer` (OnCalendar 0/6h) + `.service` oneshot ejecutando `call_monitor sequences --detect --propose --report`. Versionado en `projects/fn_monitoring/apps/call_monitor/systemd/`.
|
||||
- **3 funciones nuevas grupo `cpp-windows`** + pagina madre `docs/capabilities/cpp-windows.md`:
|
||||
- `launch_cpp_app_windows_bash_infra` — `cmd.exe`/`PowerShell Start-Process` para lanzar exe en Windows desde WSL2.
|
||||
- `is_cpp_app_running_windows_bash_infra` — `tasklist.exe /FI` con exit code 0/1 + stdout `RUNNING: PID=N MEM=K` o `NOT_RUNNING`.
|
||||
- `redeploy_cpp_app_windows_bash_pipelines` — pipeline build? + deploy + launch + verify en 1 invocacion. Reemplaza ~6 commands manuales.
|
||||
- **ADR 0004 `docs/adr/0004-telemetry-driven-capability-growth.md`** — formaliza el bucle telemetria -> proposal -> capability group -> discovery acceleration como motor de crecimiento del registry.
|
||||
- **Regla `.claude/rules/function_growth_and_self_docs.md`** (entry #30 en `INDEX.md`) — contrato `.md` autosuficiente (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por promocion de composiciones, NO por inflado de funciones individuales.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`.claude/CLAUDE.md` Norte ampliado** — 4o objetivo `PROMOVER COMPOSICIONES A PIPELINES` (el registry crece por composicion, no por inflado). Linea sobre auto-discovery zero-second-lookup.
|
||||
- **`.claude/rules/registry_calls.md`** — clausula nueva: hooks e infraestructura de telemetria (`fn_match`, `fn doctor`, `call_monitor`) pueden leer `registry.db` directo con conexion read-only. NO sujeto a regla MCP-first (no son acciones del agente).
|
||||
- **`/fn_claude` command** mejorado con objetivos del Monitor + interpretacion de `FUZZY-MATCH` hint + `CAPABILITIES` line + threshold semantica.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`launch_cpp_app_windows` quoting bug** — `cmd.exe /c "cd /d \"$dir\" && start ..."` rompia con paths Windows (el `\"` final se interpretaba como escape de comilla -> string sin cerrar -> "Windows cannot find \\"). Fix: reescribir a `powershell.exe -Command "Start-Process -FilePath ... -WorkingDirectory ..."` (single-quote PowerShell es literal, sin procesar `\` ni `$`).
|
||||
- **`fn match high_confidence` siempre true** — debido a normalizacion `top=1.0`. Fix: añadir `raw_score` preservado pre-normalizacion + gate dual `raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`. Threshold 4.0 tuneado contra 14 patrones del analysis `domain_coverage_gaps` (~93% precision).
|
||||
|
||||
## 2026-05-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "analyze_dns(domain: string, mode: string) -> void"
|
||||
description: "Análisis DNS completo de un dominio: registros A/AAAA/MX/NS/TXT/CNAME/SOA, consulta whois y verificación contra listas negras DNSBL (spamhaus, spamcop, sorbs, barracuda)."
|
||||
tags: [bash, cybersecurity, dns, network, whois, dnsbl, reconnaissance]
|
||||
tags: [bash, cybersecurity, dns, network, whois, dnsbl, reconnaissance, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "audit_http_headers(url: string) -> void"
|
||||
description: "Audita las cabeceras HTTP de seguridad de una URL: verifica la presencia de HSTS (con validación de max-age mínimo de 6 meses), Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy y cabeceras CORS. También detecta cabeceras que exponen información del servidor."
|
||||
tags: [bash, cybersecurity, web, http, headers, security, hsts, csp, hardening]
|
||||
tags: [bash, cybersecurity, web, http, headers, security, hsts, csp, hardening, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "audit_ssh_config(config_path: string) -> void"
|
||||
description: "Audita la configuración de sshd_config evaluando parámetros de seguridad críticos (PermitRootLogin, PasswordAuthentication, Port, MaxAuthTries, X11Forwarding, AllowUsers). También revisa intentos de login fallidos en los logs y lista las claves autorizadas del usuario actual."
|
||||
tags: [bash, cybersecurity, ssh, audit, security, hardening, linux]
|
||||
tags: [bash, cybersecurity, ssh, audit, security, hardening, linux, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "check_firewall() -> void"
|
||||
description: "Detecta el firewall activo del sistema (ufw, firewalld o iptables) y muestra su estado, reglas activas y puertos en escucha para cruzar con las reglas. Si no se detecta ningún firewall, emite una advertencia de exposición."
|
||||
tags: [bash, cybersecurity, firewall, ufw, iptables, network, hardening, linux]
|
||||
tags: [bash, cybersecurity, firewall, ufw, iptables, network, hardening, linux, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "detect_suspicious_users() -> void"
|
||||
description: "Revisa el sistema en busca de indicadores de compromiso en cuentas de usuario: UIDs 0 extras (además de root), usuarios con shell de login válida, homes en rutas inusuales, miembros de grupos privilegiados (sudo, docker, wheel, adm, etc.) y sesiones activas."
|
||||
tags: [bash, cybersecurity, users, audit, linux, privilege-escalation, hardening]
|
||||
tags: [bash, cybersecurity, users, audit, linux, privilege-escalation, hardening, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "encrypt_file(mode: string, file: string) -> void"
|
||||
description: "Cifra o descifra un archivo usando AES-256-CBC con PBKDF2 (310.000 iteraciones) via openssl. La contraseña se lee de la variable de entorno ENCRYPT_PASSWORD o se solicita interactivamente. El archivo cifrado se guarda con extensión .enc; al descifrar se recupera el nombre original."
|
||||
tags: [bash, cybersecurity, encryption, aes256, openssl, crypto, pbkdf2]
|
||||
tags: [bash, cybersecurity, encryption, aes256, openssl, crypto, pbkdf2, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "enumerate_subdomains(domain: string, output_file: string) -> void"
|
||||
description: "Enumera subdominios de un dominio objetivo usando un diccionario integrado de ~100 subdominios comunes (www, mail, api, dev, admin, vpn, etc.). Detecta tanto registros A (IP directa) como CNAME. Muestra progreso cada 20 subdominios y opcionalmente guarda los resultados en un archivo."
|
||||
tags: [bash, cybersecurity, dns, subdomain, enumeration, reconnaissance, osint]
|
||||
tags: [bash, cybersecurity, dns, subdomain, enumeration, reconnaissance, osint, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "generate_password(mode: string, length: int, count: int) -> void"
|
||||
description: "Genera contraseñas seguras en cuatro modos: full (alfanumérico + símbolos, excluye caracteres ambiguos), alpha (solo alfanumérico), passphrase (palabras aleatorias unidas con guión) y pin (numérico). Calcula y muestra la entropía en bits para cada modo."
|
||||
tags: [bash, cybersecurity, password, generator, entropy, security, urandom]
|
||||
tags: [bash, cybersecurity, password, generator, entropy, security, urandom, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "geolocate_ip(target: string) -> void"
|
||||
description: "Geolocaliza una dirección IP o dominio usando la API pública de ip-api.com. Muestra país, región, ciudad, coordenadas, ISP, ASN y detecta VPN, Proxy o infraestructura de hosting."
|
||||
tags: [bash, cybersecurity, network, geoip, ip, osint, reconnaissance]
|
||||
tags: [bash, cybersecurity, network, geoip, ip, osint, reconnaissance, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "inspect_ssl_cert(host: string) -> void"
|
||||
description: "Inspecciona el certificado SSL/TLS de un host: muestra sujeto, emisor, fechas de validez, días hasta expiración, SANs (Subject Alternative Names), cadena de confianza completa y detecta soporte de versiones inseguras TLS 1.0/1.1."
|
||||
tags: [bash, cybersecurity, ssl, tls, certificate, web, openssl, security]
|
||||
tags: [bash, cybersecurity, ssl, tls, certificate, web, openssl, security, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "list_active_connections(mode: string) -> void"
|
||||
description: "Muestra conexiones de red activas del sistema usando ss: puertos en escucha, conexiones establecidas y detección de conexiones hacia IPs externas (excluye RFC1918, loopback y link-local)."
|
||||
tags: [bash, cybersecurity, network, connections, monitoring, ss, ports]
|
||||
tags: [bash, cybersecurity, network, connections, monitoring, ss, ports, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "verify_file_hash(file: string, algorithm: string, expected_hash: string) -> void"
|
||||
description: "Calcula el hash criptográfico de un archivo con el algoritmo especificado (md5, sha1, sha256, sha512) y opcionalmente lo compara con un hash esperado para verificar integridad. Retorna exit code 1 si los hashes no coinciden."
|
||||
tags: [bash, cybersecurity, hash, integrity, checksum, md5, sha256, sha512]
|
||||
tags: [bash, cybersecurity, hash, integrity, checksum, md5, sha256, sha512, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: adb_wsl
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "source adb_wsl.sh [ADB=<path>] [ANDROID_SDK_WIN=<sdk_root>]"
|
||||
description: "Wrapper sourceable para usar adb.exe Windows desde WSL2. Resuelve binario, convierte paths, espera boot del emulador."
|
||||
tags: ["android", "adb", "wsl", "windows"]
|
||||
params:
|
||||
- name: ADB
|
||||
desc: "Env var opcional. Path absoluto a adb.exe. Si no se fija, se construye desde ANDROID_SDK_WIN o el default /mnt/c/Users/lucas/AppData/Local/Android/Sdk."
|
||||
- name: ANDROID_SDK_WIN
|
||||
desc: "Env var opcional. Raiz del Android SDK montado en WSL. Default: /mnt/c/Users/lucas/AppData/Local/Android/Sdk."
|
||||
output: "Source-able shell helpers: adb_run, adb_devices, adb_wsl_to_win, adb_wait_boot. Define ADB env var apuntando a Windows adb.exe via ANDROID_SDK_WIN."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/adb_wsl.sh"
|
||||
---
|
||||
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
# Sourcear (usa SDK default)
|
||||
source bash/functions/infra/adb_wsl.sh
|
||||
|
||||
# Sourcear con SDK custom
|
||||
ANDROID_SDK_WIN=/mnt/d/Android/Sdk source bash/functions/infra/adb_wsl.sh
|
||||
|
||||
# Sourcear con binario fijo
|
||||
ADB=/mnt/c/my/tools/adb.exe source bash/functions/infra/adb_wsl.sh
|
||||
```
|
||||
|
||||
## Funciones expuestas
|
||||
|
||||
### `adb_run "<args...>"`
|
||||
|
||||
Ejecuta `$ADB` con los argumentos dados. Retorna el exit code de `adb.exe`.
|
||||
|
||||
```bash
|
||||
adb_run shell ls /sdcard/
|
||||
adb_run install app.apk
|
||||
```
|
||||
|
||||
### `adb_devices`
|
||||
|
||||
Alias de `adb_run devices`. Lista dispositivos/emuladores conectados.
|
||||
|
||||
```bash
|
||||
adb_devices
|
||||
# List of devices attached
|
||||
# emulator-5554 device
|
||||
```
|
||||
|
||||
### `adb_wsl_to_win <path_wsl>`
|
||||
|
||||
Convierte un path WSL a formato Windows con `wslpath -w`. Si `wslpath` no está disponible retorna el path sin convertir.
|
||||
|
||||
```bash
|
||||
win_path=$(adb_wsl_to_win /home/lucas/proyecto/app.apk)
|
||||
# C:\Users\lucas\AppData\Local\... (o la ruta Windows equivalente)
|
||||
adb_run install "$win_path"
|
||||
```
|
||||
|
||||
### `adb_wait_boot [timeout_s]`
|
||||
|
||||
Espera a que el emulador/dispositivo complete el boot (`sys.boot_completed = 1`). Útil tras lanzar un AVD en CI.
|
||||
|
||||
```bash
|
||||
adb_wait_boot # timeout 120s
|
||||
adb_wait_boot 60 # timeout 60s
|
||||
```
|
||||
|
||||
Retorna `0` si el boot se completó, `1` si expiró el timeout.
|
||||
|
||||
## Smoke test
|
||||
|
||||
```bash
|
||||
bash bash/functions/infra/adb_wsl.sh --self-test
|
||||
# OK
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- El script es **source-able**: define funciones en el shell actual, no crea subshell.
|
||||
- `ADB` se resuelve una sola vez al sourcing. Si el binario no existe en disco, la carga falla con mensaje en stderr y `return 1` / `exit 1`.
|
||||
- `adb_wait_boot` hace polling cada 3 segundos. Ajustar `interval` si el emulador es especialmente lento.
|
||||
- En WSL2 `wslpath` siempre está disponible; el fallback existe para entornos Linux puros que accidentalmente sourceen el archivo.
|
||||
- Si el emulador requiere `-s <serial>`, pasar el flag directamente a `adb_run`: `adb_run -s emulator-5554 shell ...`.
|
||||
---
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# adb_wsl — Wrapper sourceable para usar adb.exe Windows desde WSL2.
|
||||
# Uso: source bash/functions/infra/adb_wsl.sh
|
||||
# Smoke test: bash bash/functions/infra/adb_wsl.sh --self-test
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolver ADB
|
||||
# ---------------------------------------------------------------------------
|
||||
# El caller puede fijar ADB antes de sourcing para apuntar a otro binario.
|
||||
if [[ -z "${ADB:-}" ]]; then
|
||||
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
|
||||
ADB="${_sdk_root}/platform-tools/adb.exe"
|
||||
unset _sdk_root
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ADB" ]]; then
|
||||
echo "adb_wsl: ADB no encontrado en '$ADB'. Fija ADB= o ANDROID_SDK_WIN= antes de sourcear." >&2
|
||||
# Solo abortamos si el script se ejecuta directamente; si se sourcea,
|
||||
# permitimos continuar para que el caller maneje el error.
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_run "<args...>"
|
||||
# Ejecuta el ADB Windows con los argumentos dados.
|
||||
# Retorna el exit code de adb.exe.
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_run() {
|
||||
"$ADB" "$@"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_devices
|
||||
# Lista dispositivos ADB conectados.
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_devices() {
|
||||
adb_run devices
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_wsl_to_win <path_wsl>
|
||||
# Convierte un path WSL a formato Windows usando wslpath.
|
||||
# Si wslpath no está disponible retorna el path tal cual.
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_wsl_to_win() {
|
||||
local path_wsl="$1"
|
||||
if command -v wslpath &>/dev/null; then
|
||||
wslpath -w "$path_wsl"
|
||||
else
|
||||
echo "$path_wsl"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_wait_boot [timeout_s]
|
||||
# Espera a que el dispositivo/emulador complete el boot (sys.boot_completed=1).
|
||||
# timeout_s: segundos máximos de espera (default 120).
|
||||
# Retorna 0 si boot completado, 1 si timeout.
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_wait_boot() {
|
||||
local timeout_s="${1:-120}"
|
||||
local elapsed=0
|
||||
local interval=3
|
||||
|
||||
while (( elapsed < timeout_s )); do
|
||||
local val
|
||||
val=$(adb_run shell getprop sys.boot_completed 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ "$val" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep "$interval"
|
||||
(( elapsed += interval ))
|
||||
done
|
||||
|
||||
echo "adb_wsl: timeout ${timeout_s}s esperando boot del dispositivo." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_pick_serial [--serial <S>] [...]
|
||||
# Resuelve el serial a usar para multi-device. Lee --serial X de los args.
|
||||
# Setea globals ADB_PICK_SERIAL y ADB_PICK_REST (no usa stdout para evitar
|
||||
# perder los globals via subshell de $()).
|
||||
# Exit 1 si no hay device disponible.
|
||||
#
|
||||
# Uso tipico:
|
||||
# adb_pick_serial "$@" || { echo "no device" >&2; exit 3; }
|
||||
# local serial="$ADB_PICK_SERIAL"
|
||||
# set -- "${ADB_PICK_REST[@]}"
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_pick_serial() {
|
||||
ADB_PICK_SERIAL="${ADB_SERIAL:-}"
|
||||
ADB_PICK_REST=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--serial) ADB_PICK_SERIAL="$2"; shift 2 ;;
|
||||
--serial=*) ADB_PICK_SERIAL="${1#--serial=}"; shift ;;
|
||||
*) ADB_PICK_REST+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
if [[ -z "$ADB_PICK_SERIAL" ]]; then
|
||||
ADB_PICK_SERIAL=$(adb_run devices 2>/dev/null | awk '/(emulator-|device$)/ && !/List of/ {print $1; exit}')
|
||||
fi
|
||||
if [[ -z "$ADB_PICK_SERIAL" ]]; then
|
||||
echo "adb_wsl: ningun device/emulador conectado." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! adb_run devices 2>/dev/null | awk '{print $1}' | grep -qx "$ADB_PICK_SERIAL"; then
|
||||
echo "adb_wsl: serial '$ADB_PICK_SERIAL' no encontrado en adb devices." >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# adb_s <serial> <args...>
|
||||
# Atajo: adb_run -s <serial> <args...>
|
||||
# ---------------------------------------------------------------------------
|
||||
adb_s() {
|
||||
local serial="$1"; shift
|
||||
adb_run -s "$serial" "$@"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Smoke test (solo si invocado directamente con --self-test)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${1:-}" == "--self-test" ]]; then
|
||||
adb_run version || exit 1
|
||||
echo "OK"
|
||||
fi
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "analyze_disk_space([target_dir: string], [mode: string]) -> void"
|
||||
description: "Analiza el uso de espacio en disco. Modos: partitions (df con filtros), top-dirs (du top 10), top-files (find top 20), inodes (df -i), all (todos). Emite advertencias si el uso supera el 90%."
|
||||
tags: [bash, disk, space, analysis, filesystem]
|
||||
tags: [bash, disk, space, analysis, filesystem, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: android_apk_install
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_apk_install([--serial S], apk_path: string, package_name?: string, activity_name?: string) -> void"
|
||||
description: "Instala APK en device/emulador via adb y opcionalmente lanza la app. Multi-emulator via --serial."
|
||||
tags: [android, adb, apk, wsl]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Default: first device detected by adb_pick_serial."
|
||||
- name: apk_path
|
||||
desc: "WSL path to APK file"
|
||||
- name: package_name
|
||||
desc: "Optional app package id (e.g. com.fnregistry.voiceguide). Launches the app if provided."
|
||||
- name: activity_name
|
||||
desc: "Optional activity (.MainActivity or fully qualified). Only used with package_name. If omitted, launches via monkey LAUNCHER intent."
|
||||
output: "Stdout con pasos. Exit 0 = install + launch OK. Exit !=0 si install fallo o APK no encontrado."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_apk_install.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Solo instalar
|
||||
android_apk_install /home/lucas/builds/app-debug.apk
|
||||
|
||||
# Instalar y lanzar con activity explícita
|
||||
android_apk_install /home/lucas/builds/app-debug.apk com.fnregistry.voiceguide .MainActivity
|
||||
|
||||
# Instalar y lanzar sin activity (usa monkey LAUNCHER)
|
||||
android_apk_install /home/lucas/builds/app-debug.apk com.fnregistry.voiceguide
|
||||
|
||||
# Llamada directa desde shell (no sourced)
|
||||
bash bash/functions/infra/android_apk_install.sh /path/to/app.apk com.example.app .MainActivity
|
||||
|
||||
# Override ADB path
|
||||
ADB=/custom/path/adb.exe bash bash/functions/infra/android_apk_install.sh /path/to/app.apk
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Requiere WSL2 con `adb.exe` Windows accesible. El path por defecto es
|
||||
`/mnt/c/Users/lucas/AppData/Local/Android/Sdk/platform-tools/adb.exe`.
|
||||
Se puede sobreescribir con `ADB=...` o `ANDROID_SDK_WIN=<sdk_root>` antes
|
||||
de invocar.
|
||||
- `wslpath` se usa para convertir el path WSL a formato Windows (`C:\...`).
|
||||
Si no está disponible (entorno no-WSL), se usa el path tal cual.
|
||||
- La instalación usa `adb install -r` (reinstala si ya existe).
|
||||
- Si `package_name` se da sin `activity_name`, la app se lanza via
|
||||
`adb shell monkey -p <pkg> -c android.intent.category.LAUNCHER 1`,
|
||||
que es equivalente a pulsar el icono del launcher.
|
||||
- El script se puede sourcear (para usar la función en otros scripts) o
|
||||
ejecutar directamente. Cuando se ejecuta directamente, delega en
|
||||
`android_apk_install "$@"`.
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_apk_install — Instala APK en device/emulador via adb y opcionalmente lanza la app.
|
||||
# Multi-emulator via --serial <S>.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source helpers (adb_run, adb_pick_serial, adb_s, adb_wsl_to_win)
|
||||
# shellcheck source=/dev/null
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_apk_install [--serial <S>] <apk_path> [package_name] [activity_name]
|
||||
# ---------------------------------------------------------------------------
|
||||
android_apk_install() {
|
||||
local serial
|
||||
adb_pick_serial "$@" || { echo "android_apk_install: no device/emulator." >&2; return 3; }
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local apk="${1:-}"
|
||||
local package="${2:-}"
|
||||
local activity="${3:-}"
|
||||
|
||||
if [[ -z "$apk" ]]; then
|
||||
echo "android_apk_install: se requiere apk_path como primer argumento." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$apk" ]]; then
|
||||
echo "android_apk_install: APK no encontrado en '$apk'." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local win_path
|
||||
win_path=$(adb_wsl_to_win "$apk")
|
||||
|
||||
echo "android_apk_install: instalando '$win_path' on $serial ..."
|
||||
adb_s "$serial" install -r "$win_path"
|
||||
echo "android_apk_install: instalacion completada."
|
||||
|
||||
if [[ -n "$package" ]]; then
|
||||
if [[ -n "$activity" ]]; then
|
||||
echo "android_apk_install: lanzando $package/$activity ..."
|
||||
adb_s "$serial" shell am start -n "$package/$activity"
|
||||
else
|
||||
echo "android_apk_install: lanzando $package via monkey LAUNCHER ..."
|
||||
adb_s "$serial" shell monkey -p "$package" -c android.intent.category.LAUNCHER 1
|
||||
fi
|
||||
echo "android_apk_install: app lanzada."
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_apk_install "$@"
|
||||
fi
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: android_app_clear
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_app_clear([--serial <S>], package: string) -> void"
|
||||
description: "Wipe app data + cache via pm clear. App keeps installed but factory-state. Multi-emulator via --serial."
|
||||
tags: [android, adb, app, clear, reset, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Auto-detected if omitted."
|
||||
- name: package
|
||||
desc: "App package whose data to clear (e.g. com.example.app)."
|
||||
output: "Stdout 'cleared data for <pkg> on <serial>'. Exit 0 si pm clear OK."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_app_clear.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Limpiar datos de una app (autodetecta device)
|
||||
android_app_clear com.example.myapp
|
||||
|
||||
# Con serial explícito
|
||||
android_app_clear --serial emulator-5554 com.example.myapp
|
||||
|
||||
# Llamada directa
|
||||
bash bash/functions/infra/android_app_clear.sh com.example.myapp
|
||||
bash bash/functions/infra/android_app_clear.sh --serial emulator-5554 com.example.myapp
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Usa `pm clear` internamente — borra SharedPreferences, bases de datos internas,
|
||||
caché y archivos de la app. La app queda como recién instalada.
|
||||
- El source de `adb_wsl.sh` resuelve el binario `adb.exe` Windows desde WSL2.
|
||||
Se puede sobreescribir con `ADB=...` o `ANDROID_SDK_WIN=<sdk_root>` antes de invocar.
|
||||
- `adb_pick_serial` consume `--serial <S>` de los args y deja el resto en
|
||||
`ADB_PICK_REST`. Si no se da, autodetecta el primer device/emulador activo.
|
||||
- Exit 3 si no hay ningún device conectado (propagado desde `adb_pick_serial`).
|
||||
- Exit 1 si no se pasa package.
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_app_clear — Wipe app data + cache via pm clear. App stays installed.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_app_clear [--serial <S>] <package>
|
||||
#
|
||||
# --serial <S> Optional target device/emulator serial.
|
||||
# package App package whose data+cache to clear (e.g. com.example.app).
|
||||
#
|
||||
# Calls: adb shell pm clear <package>
|
||||
# The app remains installed but is reset to factory state (no data, no cache).
|
||||
# Exit 0 on success, exit 1 on bad args, exit 3 if no device found.
|
||||
# ---------------------------------------------------------------------------
|
||||
android_app_clear() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local pkg="${1:-}"
|
||||
if [[ -z "$pkg" ]]; then
|
||||
echo "android_app_clear: se requiere <package> como argumento." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
adb_s "$serial" shell pm clear "$pkg"
|
||||
echo "cleared data for $pkg on $serial"
|
||||
}
|
||||
|
||||
# Run directly if not sourced
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_app_clear "$@"
|
||||
fi
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: android_app_info
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_app_info([--serial <S>], package, [--json]) -> stdout"
|
||||
description: "Inspect installed app: version, target SDK, activities via dumpsys package."
|
||||
tags: [android, adb, app, info, dumpsys, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional ADB serial to target a specific device/emulator. Auto-detected if omitted."
|
||||
- name: "package"
|
||||
desc: "Android package name to inspect (e.g. com.example.myapp)."
|
||||
- name: "--json"
|
||||
desc: "Emit parsed JSON with versionName, versionCode, targetSdk, launcherActivity instead of raw dumpsys output."
|
||||
output: "Raw dumpsys package output, or JSON object {package, versionName, versionCode, targetSdk, launcherActivity}. Outputs JSON null if package not installed (--json mode). Exit 2 if package not found in raw mode, exit 3 if no device."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_app_info.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Raw dumpsys (full output)
|
||||
source bash/functions/infra/android_app_info.sh
|
||||
android_app_info com.example.myapp
|
||||
|
||||
# Target specific device
|
||||
android_app_info --serial emulator-5554 com.example.myapp
|
||||
|
||||
# Parsed JSON
|
||||
android_app_info com.example.myapp --json
|
||||
# {"package":"com.example.myapp","versionName":"2.1.0","versionCode":210,"targetSdk":34,"launcherActivity":"com.example.myapp/.MainActivity"}
|
||||
|
||||
# Package not installed → JSON null
|
||||
android_app_info com.not.installed --json
|
||||
# null
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Sources `adb_wsl.sh` para resolver el binario ADB Windows desde WSL2 y las helpers `adb_pick_serial` / `adb_s`.
|
||||
- `--serial` se consume via `adb_pick_serial`; el resto de los args quedan en `ADB_PICK_REST` y se re-asignan con `set --`.
|
||||
- JSON parsing usa `grep`/`sed`/`awk` sobre la salida de `dumpsys package`. Campos faltantes se emiten como string vacío o 0; no se usa `jq` para no requerir dependencias externas.
|
||||
- `launcherActivity` se extrae buscando el bloque `android.intent.action.MAIN` / `android.intent.category.LAUNCHER` en el listado de intent filters.
|
||||
- Exit codes: 0 = OK, 1 = arg/adb error, 2 = package not found (raw mode), 3 = no device.
|
||||
---
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_app_info — Inspect installed app via dumpsys package.
|
||||
# Usage: android_app_info [--serial <S>] <package> [--json]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_app_info() {
|
||||
# Resolve serial (consumes --serial from args, leaves rest in ADB_PICK_REST)
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
# Parse remaining args: package + --json flag
|
||||
local pkg=""
|
||||
local want_json=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--json) want_json=1; shift ;;
|
||||
-*) echo "android_app_info: unknown flag '$1'" >&2; return 1 ;;
|
||||
*)
|
||||
if [[ -z "$pkg" ]]; then
|
||||
pkg="$1"
|
||||
else
|
||||
echo "android_app_info: unexpected argument '$1'" >&2
|
||||
return 1
|
||||
fi
|
||||
shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$pkg" ]]; then
|
||||
echo "android_app_info: package argument required" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local dump
|
||||
dump=$(adb_s "$serial" shell dumpsys package "$pkg" 2>&1)
|
||||
local rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "android_app_info: adb dumpsys failed (exit $rc)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If dumpsys returns nothing meaningful for the package, treat as not installed
|
||||
if ! echo "$dump" | grep -q "Package \["; then
|
||||
if [[ $want_json -eq 1 ]]; then
|
||||
echo "null"
|
||||
else
|
||||
echo "android_app_info: package '$pkg' not found on device" >&2
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $want_json -eq 0 ]]; then
|
||||
echo "$dump"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# --- JSON extraction ---
|
||||
local versionName versionCode targetSdk launcherActivity
|
||||
|
||||
versionName=$(echo "$dump" | grep -m1 'versionName=' \
|
||||
| sed 's/.*versionName=\([^ ]*\).*/\1/')
|
||||
versionCode=$(echo "$dump" | grep -m1 'versionCode=' \
|
||||
| sed 's/.*versionCode=\([0-9]*\).*/\1/')
|
||||
targetSdk=$(echo "$dump" | grep -m1 'targetSdk=' \
|
||||
| sed 's/.*targetSdk=\([0-9]*\).*/\1/')
|
||||
|
||||
# Primary/launcher activity: look for MAIN/LAUNCHER category block
|
||||
launcherActivity=$(echo "$dump" | awk '
|
||||
/android.intent.action.MAIN/ { found=1 }
|
||||
found && /[a-zA-Z0-9_.]+\/[a-zA-Z0-9_.]+/ {
|
||||
match($0, /[a-zA-Z0-9_.]+\/[a-zA-Z0-9_.]+/)
|
||||
print substr($0, RSTART, RLENGTH)
|
||||
exit
|
||||
}
|
||||
')
|
||||
|
||||
# Emit JSON, quoting strings safely
|
||||
printf '{"package":"%s","versionName":"%s","versionCode":%s,"targetSdk":%s,"launcherActivity":"%s"}\n' \
|
||||
"$pkg" \
|
||||
"${versionName:-}" \
|
||||
"${versionCode:-0}" \
|
||||
"${targetSdk:-0}" \
|
||||
"${launcherActivity:-}"
|
||||
}
|
||||
|
||||
# Run if invoked directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
android_app_info "$@"
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: android_app_kill
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_app_kill([--serial <S>], package: string) -> void"
|
||||
description: "Force-stop running app via am force-stop. Multi-emulator via --serial."
|
||||
tags: [android, adb, app, kill, force-stop, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Auto-detected if omitted."
|
||||
- name: "package"
|
||||
desc: "App package to force-stop (e.g. com.example.myapp)."
|
||||
output: "Stdout 'killed <pkg> on <serial>'. Exit 0."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_app_kill.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Detener app en el emulador activo
|
||||
android_app_kill com.example.myapp
|
||||
|
||||
# Detener app en un dispositivo concreto
|
||||
android_app_kill --serial emulator-5554 com.example.myapp
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
|
||||
Si `--serial` no se pasa, autodetecta el primer device/emulador disponible.
|
||||
Sale con exit 3 si no hay ningun device conectado.
|
||||
`am force-stop` detiene todos los procesos y servicios de la app de forma inmediata.
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_app_kill — Force-stop a running Android app via am force-stop.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_app_kill() {
|
||||
local serial pkg
|
||||
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
pkg="${1:?android_app_kill: package name required}"
|
||||
|
||||
adb_s "$serial" shell am force-stop "$pkg"
|
||||
echo "killed $pkg on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_app_kill "$@"
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: android_app_launch
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_app_launch([--serial <S>], package: string, [activity: string]) -> void"
|
||||
description: "Launch app activity via am start. Multi-emulator via --serial."
|
||||
tags: [android, adb, app, launch, activity, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target serial. Default: first device"
|
||||
- name: "package"
|
||||
desc: "App package id"
|
||||
- name: "activity"
|
||||
desc: "Optional activity. If omitted, launches via LAUNCHER intent"
|
||||
output: "Stdout 'launched <pkg> on <serial>'. Exit 0 ok, 3 no device."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_app_launch.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Lanzar actividad principal explicitamente
|
||||
android_app_launch com.foo.bar .MainActivity
|
||||
|
||||
# Lanzar por LAUNCHER intent (detecta actividad principal automaticamente)
|
||||
android_app_launch com.foo.bar
|
||||
|
||||
# Multi-emulador: elegir serial concreto
|
||||
android_app_launch --serial emulator-5554 com.foo.bar .MainActivity
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el serial objetivo.
|
||||
Si no hay ningun device/emulador disponible, sale con exit code 3.
|
||||
Si `activity` no se especifica, usa `monkey -p <pkg> -c android.intent.category.LAUNCHER 1`
|
||||
para lanzar la actividad principal sin necesidad de conocerla de antemano.
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_app_launch — Launch an Android app via adb am start or monkey LAUNCHER intent.
|
||||
# Usage: android_app_launch [--serial <S>] <package> [<activity>]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_app_launch() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local pkg="${1:-}"
|
||||
local activity="${2:-}"
|
||||
|
||||
if [[ -z "$pkg" ]]; then
|
||||
echo "android_app_launch: package is required." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$activity" ]]; then
|
||||
adb_s "$serial" shell am start -n "$pkg/$activity"
|
||||
else
|
||||
adb_s "$serial" shell monkey -p "$pkg" -c android.intent.category.LAUNCHER 1
|
||||
fi
|
||||
|
||||
echo "launched $pkg on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_app_launch "$@"
|
||||
fi
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: android_app_uninstall
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_app_uninstall([--serial <S>] package [--keep-data]) -> void"
|
||||
description: "Uninstall app via adb uninstall. Optionally keep data with --keep-data."
|
||||
tags: [android, adb, app, uninstall, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Auto-detects first connected device if omitted."
|
||||
- name: "package"
|
||||
desc: "Android package name to uninstall (e.g. com.example.myapp). Mandatory positional argument."
|
||||
- name: "--keep-data"
|
||||
desc: "Keep app data + cache after uninstall (passes -k to pm uninstall)."
|
||||
output: "Stdout 'uninstalled <pkg> on <serial>'. Exit 0 OK."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_app_uninstall.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Desinstalar en el device por defecto
|
||||
android_app_uninstall com.example.myapp
|
||||
|
||||
# Desinstalar en un device concreto
|
||||
android_app_uninstall --serial emulator-5554 com.example.myapp
|
||||
|
||||
# Desinstalar conservando datos y cache
|
||||
android_app_uninstall --serial emulator-5554 com.example.myapp --keep-data
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Sourcea `adb_wsl.sh` para resolver el binario `adb.exe` en WSL2 y usar
|
||||
`adb_pick_serial` / `adb_s`. Si no hay ningún device conectado y no se
|
||||
pasa `--serial`, la función falla con exit 1 antes de invocar adb.
|
||||
|
||||
El flag `--keep-data` pasa `-k` a `adb uninstall`, equivalente a
|
||||
`pm uninstall -k` — el APK se elimina pero los datos y la caché de la app
|
||||
permanecen en el dispositivo.
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_app_uninstall — Desinstala una app Android via adb uninstall.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_app_uninstall() {
|
||||
# Parse --serial (consumes it, rest stays in ADB_PICK_REST)
|
||||
local serial
|
||||
adb_pick_serial "$@" || return 1
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
# Parse --keep-data flag
|
||||
local keep_data=0
|
||||
local args=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--keep-data) keep_data=1; shift ;;
|
||||
*) args+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
set -- "${args[@]}"
|
||||
|
||||
local pkg="${1:-}"
|
||||
if [[ -z "$pkg" ]]; then
|
||||
echo "android_app_uninstall: package obligatorio." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( keep_data )); then
|
||||
adb_s "$serial" uninstall -k "$pkg" || return 1
|
||||
else
|
||||
adb_s "$serial" uninstall "$pkg" || return 1
|
||||
fi
|
||||
|
||||
echo "uninstalled $pkg on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_app_uninstall "$@"
|
||||
fi
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: android_emu_battery
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emu_battery([--serial <S>], level: int, [--charging <true|false>]) -> void"
|
||||
description: "Simulate battery state on emulator (level + charging). Emulator-only."
|
||||
tags: [android, emulator, battery, power, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional emulator serial (e.g. emulator-5554). Auto-detected if omitted."
|
||||
- name: "level"
|
||||
desc: "Battery level 0-100 to set via 'emu power capacity'."
|
||||
- name: "--charging <true|false>"
|
||||
desc: "AC charging state: true maps to 'on', false maps to 'off'. Omit to leave unchanged."
|
||||
output: "Stdout 'battery: <N>% [charging=...] on <serial>'. Exit 3 if no device found, exit 1 on other errors."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emu_battery.sh"
|
||||
notes: "Util para tests bateria baja, modo ahorro energia. Solo funciona con emuladores (serial emulator-*), no con devices fisicos."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Nivel al 15%, sin cambiar estado de carga
|
||||
android_emu_battery 15
|
||||
|
||||
# Nivel al 5%, forzar descarga (AC off)
|
||||
android_emu_battery 5 --charging false
|
||||
|
||||
# Nivel al 80%, forzar carga (AC on), emulador concreto
|
||||
android_emu_battery --serial emulator-5554 80 --charging true
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Util para tests de bateria baja y modo ahorro de energia. Solo funciona con emuladores Android
|
||||
(serial debe empezar con `emulator-`). No aplica a dispositivos fisicos.
|
||||
|
||||
Requiere que `adb_wsl.sh` este en el mismo directorio. El ADB se resuelve via
|
||||
`ANDROID_SDK_WIN` o la ruta por defecto de la instalacion Windows SDK.
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emu_battery — Simulate battery state on Android emulator (level + charging).
|
||||
# Usage: android_emu_battery [--serial <S>] <level 0-100> [--charging <true|false>]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_emu_battery() {
|
||||
# Resolve serial (consumes --serial from args, leaves rest in ADB_PICK_REST)
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
# Require serial to be an emulator
|
||||
if [[ "$serial" != emulator-* ]]; then
|
||||
echo "android_emu_battery: serial '$serial' is not an emulator (must start with emulator-)." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse remaining args: positional level + --charging
|
||||
local level=""
|
||||
local charging=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--charging)
|
||||
charging="$2"
|
||||
shift 2
|
||||
;;
|
||||
--charging=*)
|
||||
charging="${1#--charging=}"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "android_emu_battery: unknown flag '$1'." >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$level" ]]; then
|
||||
level="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate level
|
||||
if [[ -z "$level" ]]; then
|
||||
echo "android_emu_battery: level is required (0-100)." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "$level" =~ ^[0-9]+$ ]] || (( level < 0 || level > 100 )); then
|
||||
echo "android_emu_battery: invalid level '$level' — must be integer 0-100." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Set battery level
|
||||
adb_s "$serial" emu power capacity "$level" || {
|
||||
echo "android_emu_battery: failed to set capacity on $serial." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Set charging state if requested
|
||||
local ch="<unchanged>"
|
||||
if [[ -n "$charging" ]]; then
|
||||
local ac_val
|
||||
case "$charging" in
|
||||
true) ac_val="on" ;;
|
||||
false) ac_val="off" ;;
|
||||
*)
|
||||
echo "android_emu_battery: --charging must be 'true' or 'false', got '$charging'." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
adb_s "$serial" emu power ac "$ac_val" || {
|
||||
echo "android_emu_battery: failed to set AC charging on $serial." >&2
|
||||
return 1
|
||||
}
|
||||
ch="$charging"
|
||||
fi
|
||||
|
||||
echo "battery: ${level}% [charging=${ch}] on ${serial}"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_emu_battery "$@"
|
||||
fi
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: android_emu_geo_fix
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emu_geo_fix([--serial <S>], longitude: string, latitude: string, [altitude: string]) -> void"
|
||||
description: "Fake GPS location on Android emulator via emu geo fix. Emulator-only (not physical devices)."
|
||||
tags: [android, emulator, geo, gps, location, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional emulator serial. Auto-detected if omitted."
|
||||
- name: "longitude"
|
||||
desc: "Longitude (decimal degrees). Passed first — opposite to human lat/lon convention."
|
||||
- name: "latitude"
|
||||
desc: "Latitude (decimal degrees)."
|
||||
- name: "altitude"
|
||||
desc: "Optional altitude in meters."
|
||||
output: "Stdout 'GPS set: <lon>, <lat> (alt=...) on <serial>'. Exit 0."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emu_geo_fix.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Fijar GPS en Madrid (emulador activo)
|
||||
android_emu_geo_fix -3.7038 40.4168
|
||||
|
||||
# Con altitud
|
||||
android_emu_geo_fix -3.7038 40.4168 650
|
||||
|
||||
# Emulador especifico
|
||||
android_emu_geo_fix --serial emulator-5554 -3.7038 40.4168
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El orden de argumentos es **longitud primero, latitud segundo** — opuesto a la convencion humana habitual (lat/lon). Esto sigue el protocolo del comando `emu geo fix` de Android.
|
||||
|
||||
Solo funciona en emuladores (`emulator-*`). Si el serial apunta a un dispositivo fisico, la funcion sale con error y exit 1.
|
||||
|
||||
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
|
||||
Sale con exit 3 si no hay ningun device conectado.
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emu_geo_fix — Fake GPS location on Android emulator via emu geo fix.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_emu_geo_fix() {
|
||||
local serial lon lat alt
|
||||
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
lon="${1:?android_emu_geo_fix: longitude required}"
|
||||
lat="${2:?android_emu_geo_fix: latitude required}"
|
||||
alt="${3:-}"
|
||||
|
||||
# geo fix only works on emulators, not physical devices
|
||||
if [[ "$serial" != emulator-* ]]; then
|
||||
echo "android_emu_geo_fix: geo fix only works on emulators (got '$serial')" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
adb_s "$serial" emu geo fix "$lon" "$lat" ${alt:+"$alt"}
|
||||
|
||||
if [[ -n "$alt" ]]; then
|
||||
echo "GPS set: $lon, $lat (alt=$alt) on $serial"
|
||||
else
|
||||
echo "GPS set: $lon, $lat on $serial"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_emu_geo_fix "$@"
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: android_emu_rotate
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emu_rotate([--serial <S>] [portrait|landscape|0|90|180|270])"
|
||||
description: "Rotate emulator screen. Empty=toggle, or fixed orientation. Locks autorotate."
|
||||
tags: [android, emulator, rotation, orientation, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional emulator serial. Picked automatically if only one is connected."
|
||||
- name: "orientation"
|
||||
desc: "Empty=toggle via emu rotate, or fixed: portrait/landscape/0/90/180/270."
|
||||
output: "Stdout 'rotated: <orient> on <serial>'."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emu_rotate.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Toggle rotation
|
||||
android_emu_rotate
|
||||
|
||||
# Force portrait
|
||||
android_emu_rotate portrait
|
||||
|
||||
# Force landscape on specific emulator
|
||||
android_emu_rotate --serial emulator-5554 landscape
|
||||
|
||||
# Set 270 degrees
|
||||
android_emu_rotate --serial emulator-5554 270
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Deshabilita autorotate (`accelerometer_rotation 0`) antes de aplicar cualquier orientacion fija, de modo que el sistema no la revierta. El toggle (`emu rotate`) no desactiva autorotate: lo usa directamente el daemon del emulador.
|
||||
|
||||
`adb_pick_serial` (de `adb_wsl_bash_infra`) selecciona el unico emulador conectado o falla con exit 3 si hay ambiguedad o ninguno disponible. Los argumentos restantes tras extraer `--serial` quedan en `ADB_PICK_REST`.
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emu_rotate — rotate emulator screen or toggle rotation
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_emu_rotate() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local arg="${1:-}"
|
||||
|
||||
# Disable autorotate before any operation
|
||||
if [[ -n "$arg" ]]; then
|
||||
adb_s "$serial" shell settings put system accelerometer_rotation 0
|
||||
fi
|
||||
|
||||
case "$arg" in
|
||||
"")
|
||||
# Toggle via emu rotate command
|
||||
adb_s "$serial" emu rotate
|
||||
;;
|
||||
portrait|0)
|
||||
adb_s "$serial" shell settings put system accelerometer_rotation 0
|
||||
adb_s "$serial" shell settings put system user_rotation 0
|
||||
;;
|
||||
landscape|90)
|
||||
adb_s "$serial" shell settings put system accelerometer_rotation 0
|
||||
adb_s "$serial" shell settings put system user_rotation 1
|
||||
;;
|
||||
180)
|
||||
adb_s "$serial" shell settings put system accelerometer_rotation 0
|
||||
adb_s "$serial" shell settings put system user_rotation 2
|
||||
;;
|
||||
270)
|
||||
adb_s "$serial" shell settings put system accelerometer_rotation 0
|
||||
adb_s "$serial" shell settings put system user_rotation 3
|
||||
;;
|
||||
*)
|
||||
echo "android_emu_rotate: unknown orientation '$arg'" >&2
|
||||
echo "Usage: android_emu_rotate [--serial <S>] [portrait|landscape|0|90|180|270]" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "rotated: ${arg:-toggle} on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_emu_rotate "$@"
|
||||
fi
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: android_emulator_list
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emulator_list([--json])"
|
||||
description: "Lista los AVDs disponibles invocando emulator.exe Windows desde WSL2."
|
||||
tags: [android, emulator, wsl]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--json"
|
||||
desc: "Optional flag, outputs JSON array instead of newline-separated names"
|
||||
output: "Lista de AVDs disponibles en el SDK Windows. Una por linea, o JSON array con --json."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emulator_list.sh"
|
||||
notes: "Lee env var EMULATOR o ANDROID_SDK_WIN. Default Windows path: /mnt/c/Users/lucas/AppData/Local/Android/Sdk/emulator/emulator.exe. Exit 0 si lista (incluso vacia). Exit 1 solo si el binario no existe o no es ejecutable."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Listar AVDs (una por linea)
|
||||
android_emulator_list
|
||||
|
||||
# Listar AVDs en formato JSON
|
||||
android_emulator_list --json
|
||||
# ["Pixel_7_API_34","Pixel_4_API_30"]
|
||||
|
||||
# Sobreescribir ruta del emulador
|
||||
EMULATOR="/custom/path/emulator.exe" android_emulator_list
|
||||
|
||||
# Sobreescribir SDK base
|
||||
ANDROID_SDK_WIN="/mnt/d/Android/Sdk" android_emulator_list
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El script es ejecutable directamente (`chmod +x`) o invocable con `bash android_emulator_list.sh`.
|
||||
|
||||
`emulator.exe -list-avds` imprime warnings a stderr que se descartan con `2>/dev/null`. La captura con `mapfile` filtra ademas lineas vacias para producir una lista limpia.
|
||||
|
||||
La variable `EMULATOR` tiene prioridad sobre `ANDROID_SDK_WIN`. Si ninguna esta definida se usa el path Windows por defecto de Lucas.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emulator_list — Lista los AVDs disponibles invocando emulator.exe Windows desde WSL2.
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve emulator binary
|
||||
EMULATOR="${EMULATOR:-${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}/emulator/emulator.exe}"
|
||||
|
||||
if [[ ! -x "$EMULATOR" ]]; then
|
||||
echo "error: emulator binary not found or not executable: $EMULATOR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse flags
|
||||
JSON=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) JSON=true ;;
|
||||
*) echo "error: unknown argument: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Collect AVDs, stripping any warnings emulator.exe prints to stderr
|
||||
mapfile -t AVDS < <("$EMULATOR" -list-avds 2>/dev/null || true)
|
||||
|
||||
if $JSON; then
|
||||
# Build JSON array
|
||||
printf '['
|
||||
first=true
|
||||
for avd in "${AVDS[@]}"; do
|
||||
[[ -z "$avd" ]] && continue
|
||||
if $first; then
|
||||
printf '"%s"' "$avd"
|
||||
first=false
|
||||
else
|
||||
printf ',"%s"' "$avd"
|
||||
fi
|
||||
done
|
||||
printf ']\n'
|
||||
else
|
||||
for avd in "${AVDS[@]}"; do
|
||||
[[ -z "$avd" ]] && continue
|
||||
printf '%s\n' "$avd"
|
||||
done
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: android_emulator_start
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emulator_start(avd_name: string, timeout_s: int) -> string"
|
||||
description: "Arranca un AVD en background y espera a que termine de bootear. Idempotente: si ya hay emulador corriendo no lanza otro."
|
||||
tags: [android, emulator, wsl]
|
||||
params:
|
||||
- name: avd_name
|
||||
desc: "Nombre del AVD a arrancar (visible con android_emulator_list o `emulator.exe -list-avds`)"
|
||||
- name: timeout_s
|
||||
desc: "Timeout total en segundos para esperar el boot completo. Opcional, default 180"
|
||||
output: "Serial del device emulado (ej. emulator-5554) en stdout. Exit 0 = boot completo, exit 1 = timeout o emulador murio."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emulator_start.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/android_emulator_start.sh
|
||||
|
||||
# Arrancar AVD con timeout por defecto (180s)
|
||||
serial=$(android_emulator_start "Pixel_6_API_34")
|
||||
echo "Emulador listo: $serial" # emulator-5554
|
||||
|
||||
# Con timeout personalizado
|
||||
serial=$(android_emulator_start "Pixel_6_API_34" 300)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Sourcea `adb_wsl.sh` del mismo directorio si existe (provee `ADB`, `adb_run`, `adb_wait_boot`). Si no, usa implementacion inline.
|
||||
- Resuelve `EMULATOR` y `ADB` desde `ANDROID_SDK_WIN` (default `/mnt/c/Users/lucas/AppData/Local/Android/Sdk`) o desde las variables de entorno `EMULATOR=` / `ADB=` si ya están fijadas.
|
||||
- Idempotente: si `adb devices` ya muestra un `emulator-*`, imprime "already running" + el serial y sale con exit 0 sin lanzar un segundo proceso.
|
||||
- Log del emulador en `/tmp/emulator_<avd>.log`. PID en `/tmp/emulator_<avd>.pid`.
|
||||
- El timeout total se reparte: primera mitad para `adb wait-for-device`, segunda mitad para esperar `sys.boot_completed=1`.
|
||||
- Diseñado para WSL2 con Android SDK instalado en Windows. En Linux nativo basta cambiar las rutas de los binarios via `EMULATOR=` y `ADB=`.
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emulator_start — Arranca un AVD en background y espera a que bootee.
|
||||
# Uso: android_emulator_start <avd_name> [timeout_s]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source adb_wsl si está disponible (provee ADB, adb_run, adb_wait_boot)
|
||||
# ---------------------------------------------------------------------------
|
||||
_ADB_WSL_SH="$(dirname "${BASH_SOURCE[0]}")/adb_wsl.sh"
|
||||
if [[ -f "$_ADB_WSL_SH" ]]; then
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$_ADB_WSL_SH"
|
||||
else
|
||||
# Fallback inline: resolver ADB
|
||||
if [[ -z "${ADB:-}" ]]; then
|
||||
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
|
||||
ADB="${_sdk_root}/platform-tools/adb.exe"
|
||||
unset _sdk_root
|
||||
fi
|
||||
adb_run() { "$ADB" "$@"; }
|
||||
adb_wait_boot() {
|
||||
local timeout_s="${1:-120}"
|
||||
local elapsed=0 interval=3 val
|
||||
while (( elapsed < timeout_s )); do
|
||||
val=$(adb_run shell getprop sys.boot_completed 2>/dev/null | tr -d '[:space:]')
|
||||
[[ "$val" == "1" ]] && return 0
|
||||
sleep "$interval"
|
||||
(( elapsed += interval ))
|
||||
done
|
||||
echo "android_emulator_start: timeout ${timeout_s}s esperando boot." >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolver EMULATOR
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ -z "${EMULATOR:-}" ]]; then
|
||||
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
|
||||
EMULATOR="${_sdk_root}/emulator/emulator.exe"
|
||||
unset _sdk_root
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_emulator_start <avd_name> [timeout_s]
|
||||
# ---------------------------------------------------------------------------
|
||||
android_emulator_start() {
|
||||
local AVD="${1:?android_emulator_start requiere el nombre del AVD como primer argumento}"
|
||||
local timeout_s="${2:-180}"
|
||||
|
||||
# Validaciones de entorno
|
||||
if [[ ! -f "$EMULATOR" ]]; then
|
||||
echo "android_emulator_start: emulator.exe no encontrado en '$EMULATOR'. Fija EMULATOR= o ANDROID_SDK_WIN=." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$ADB" ]]; then
|
||||
echo "android_emulator_start: adb.exe no encontrado en '$ADB'. Fija ADB= o ANDROID_SDK_WIN=." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Idempotencia: si ya hay un emulador corriendo, salir sin lanzar otro
|
||||
if adb_run devices 2>/dev/null | grep -q "emulator-"; then
|
||||
echo "already running"
|
||||
# Imprimir el serial existente
|
||||
adb_run devices 2>/dev/null | grep "emulator-" | awk '{print $1}' | head -n1
|
||||
return 0
|
||||
fi
|
||||
|
||||
local log_file="/tmp/emulator_${AVD}.log"
|
||||
local pid_file="/tmp/emulator_${AVD}.pid"
|
||||
|
||||
# Lanzar emulador en background
|
||||
"$EMULATOR" -avd "$AVD" -no-boot-anim -no-snapshot-load >"$log_file" 2>&1 &
|
||||
local emu_pid=$!
|
||||
echo "$emu_pid" > "$pid_file"
|
||||
|
||||
# Esperar a que el dispositivo aparezca en adb
|
||||
local wait_timeout=$(( timeout_s / 2 ))
|
||||
if ! timeout "$wait_timeout" adb_run wait-for-device 2>/dev/null; then
|
||||
echo "android_emulator_start: timeout esperando que el dispositivo aparezca en adb (${wait_timeout}s)." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verificar que el proceso del emulador sigue vivo
|
||||
if ! kill -0 "$emu_pid" 2>/dev/null; then
|
||||
echo "android_emulator_start: el proceso del emulador (PID $emu_pid) murió antes de completar el boot." >&2
|
||||
echo " Log: $log_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Esperar boot completo (sys.boot_completed=1)
|
||||
local boot_timeout=$(( timeout_s - wait_timeout ))
|
||||
if ! adb_wait_boot "$boot_timeout"; then
|
||||
echo "android_emulator_start: timeout ${timeout_s}s esperando boot completo del AVD '$AVD'." >&2
|
||||
echo " Log: $log_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Obtener serial del dispositivo emulado
|
||||
local serial
|
||||
serial=$(adb_run devices 2>/dev/null | grep "emulator-" | awk '{print $1}' | head -n1)
|
||||
|
||||
echo "$serial"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Ejecutar si se invoca directamente (no sourceado)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_emulator_start "$@"
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: android_emulator_stop
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_emulator_stop(serial?: string) -> void"
|
||||
description: "Para uno o todos los emuladores Android via adb emu kill. Si serial esta vacio, detecta todos los emulator-* activos y los para. Idempotente: exit 0 aunque no haya nada que matar."
|
||||
tags: ["android", "emulator", "wsl", "adb"]
|
||||
params:
|
||||
- name: "serial"
|
||||
desc: "Optional emulator serial (e.g. emulator-5554). Empty = kill all running emulators"
|
||||
output: "Imprime numero de emuladores parados. Exit 0 idempotente."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_emulator_stop.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Parar todos los emuladores en ejecucion
|
||||
android_emulator_stop
|
||||
|
||||
# Parar un emulador concreto
|
||||
android_emulator_stop emulator-5554
|
||||
|
||||
# Sobreescribir ruta de adb
|
||||
ADB=/usr/local/bin/adb android_emulator_stop
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Resuelve `ADB` desde variable de entorno (default: ruta de Android SDK en Windows bajo WSL2).
|
||||
Usa `adb emu kill` en vez de `adb kill-server` para parar solo el emulador sin afectar al daemon adb.
|
||||
`set -euo pipefail` activo, pero los fallos de `adb emu kill` se suprimen con `|| true` para mantener idempotencia.
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_emulator_stop — Para uno o todos los emuladores Android via adb emu kill.
|
||||
set -euo pipefail
|
||||
|
||||
android_emulator_stop() {
|
||||
local serial="${1:-}"
|
||||
local ADB="${ADB:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk/platform-tools/adb.exe}"
|
||||
local killed=0
|
||||
|
||||
if [[ -z "$serial" ]]; then
|
||||
# Detectar todos los emuladores activos
|
||||
local serials
|
||||
serials=$("$ADB" devices 2>/dev/null | grep -E '^emulator-' | awk '{print $1}' || true)
|
||||
|
||||
if [[ -z "$serials" ]]; then
|
||||
echo "android_emulator_stop: no running emulators found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r s; do
|
||||
[[ -z "$s" ]] && continue
|
||||
echo "android_emulator_stop: stopping $s"
|
||||
"$ADB" -s "$s" emu kill 2>/dev/null || true
|
||||
((killed++)) || true
|
||||
done <<< "$serials"
|
||||
else
|
||||
echo "android_emulator_stop: stopping $serial"
|
||||
"$ADB" -s "$serial" emu kill 2>/dev/null || true
|
||||
((killed++)) || true
|
||||
fi
|
||||
|
||||
echo "android_emulator_stop: stopped $killed emulator(s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_emulator_stop "${1:-}"
|
||||
fi
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: android_input_keyevent
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_input_keyevent([--serial <S>] key: string)"
|
||||
description: "Send key event via adb shell input keyevent. Accepts aliases (BACK, HOME, POWER, ENTER, MENU, RECENT_APPS, VOLUME_UP, VOLUME_DOWN), raw numeric codes, or explicit KEYCODE_* names."
|
||||
tags: [android, adb, input, keyevent, ui-test, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. If omitted, adb_pick_serial resolves the single connected device."
|
||||
- name: "key"
|
||||
desc: "Keycode: short alias (BACK/HOME/POWER/ENTER/MENU/RECENT_APPS/VOLUME_UP/VOLUME_DOWN), raw number (e.g. 4, 26), or explicit KEYCODE_* name."
|
||||
output: "Stdout 'key: <code> on <serial>'."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_input_keyevent.sh"
|
||||
notes: "Lista completa de keycodes: https://developer.android.com/reference/android/view/KeyEvent. Exit 3 si adb_pick_serial falla (ningun device o ambiguo sin --serial)."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Pulsar BACK en el unico device conectado
|
||||
android_input_keyevent BACK
|
||||
|
||||
# Pulsar HOME en un emulador especifico
|
||||
android_input_keyevent --serial emulator-5554 HOME
|
||||
|
||||
# Codigo numerico directo
|
||||
android_input_keyevent 26 # POWER
|
||||
|
||||
# KEYCODE_* explicito
|
||||
android_input_keyevent KEYCODE_DPAD_CENTER
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Aliases resueltos internamente:
|
||||
|
||||
| Alias | KEYCODE |
|
||||
|--------------|-----------------------|
|
||||
| BACK | KEYCODE_BACK |
|
||||
| HOME | KEYCODE_HOME |
|
||||
| POWER | KEYCODE_POWER |
|
||||
| ENTER | KEYCODE_ENTER |
|
||||
| MENU | KEYCODE_MENU |
|
||||
| RECENT_APPS | KEYCODE_APP_SWITCH |
|
||||
| VOLUME_UP | KEYCODE_VOLUME_UP |
|
||||
| VOLUME_DOWN | KEYCODE_VOLUME_DOWN |
|
||||
|
||||
Si el argumento no coincide con ningun alias y no es numerico, se construye `KEYCODE_<UPPER>` para pasarlo directo a `adb shell input keyevent`.
|
||||
|
||||
Exit codes: 1 = keycode vacio, 3 = fallo de `adb_pick_serial` (ningun device o ambiguo).
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_input_keyevent — Send key event via adb shell input keyevent.
|
||||
# Accepts aliases (BACK, HOME, POWER, ENTER, MENU, RECENT_APPS),
|
||||
# raw numeric codes, or explicit KEYCODE_* names.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_input_keyevent() {
|
||||
# Resolve serial (consumes --serial <S> from args, remainder in ADB_PICK_REST)
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local raw="${1:-}"
|
||||
if [[ -z "$raw" ]]; then
|
||||
echo "android_input_keyevent: missing keycode argument" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Resolve alias → KEYCODE_*
|
||||
local keycode
|
||||
case "${raw^^}" in
|
||||
BACK) keycode="KEYCODE_BACK" ;;
|
||||
HOME) keycode="KEYCODE_HOME" ;;
|
||||
POWER) keycode="KEYCODE_POWER" ;;
|
||||
ENTER) keycode="KEYCODE_ENTER" ;;
|
||||
MENU) keycode="KEYCODE_MENU" ;;
|
||||
RECENT_APPS) keycode="KEYCODE_APP_SWITCH" ;;
|
||||
VOLUME_UP) keycode="KEYCODE_VOLUME_UP" ;;
|
||||
VOLUME_DOWN) keycode="KEYCODE_VOLUME_DOWN" ;;
|
||||
*)
|
||||
# Already has KEYCODE_ prefix or is a raw number → pass through
|
||||
if [[ "${raw^^}" == KEYCODE_* ]] || [[ "$raw" =~ ^[0-9]+$ ]]; then
|
||||
keycode="$raw"
|
||||
else
|
||||
# Unknown alias: uppercase and prepend KEYCODE_
|
||||
keycode="KEYCODE_${raw^^}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
adb_s "$serial" shell input keyevent "$keycode"
|
||||
echo "key: $keycode on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_input_keyevent "$@"
|
||||
fi
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: android_input_swipe
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_input_swipe([--serial <S>], x1: int, y1: int, x2: int, y2: int, [duration_ms: int])"
|
||||
description: "Send swipe gesture between two points with duration."
|
||||
tags: [android, adb, input, swipe, gesture, ui-test, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. Overrides ADB_SERIAL envvar."
|
||||
- name: x1
|
||||
desc: "Start X coordinate in pixels."
|
||||
- name: y1
|
||||
desc: "Start Y coordinate in pixels."
|
||||
- name: x2
|
||||
desc: "End X coordinate in pixels."
|
||||
- name: y2
|
||||
desc: "End Y coordinate in pixels."
|
||||
- name: duration_ms
|
||||
desc: "Optional swipe duration in milliseconds. Default 300."
|
||||
output: "Stdout swipe summary line: 'swipe x1,y1 → x2,y2 (Nms) on <serial>'."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_input_swipe.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/android_input_swipe.sh
|
||||
|
||||
# Scroll down (swipe up)
|
||||
android_input_swipe 540 1400 540 400
|
||||
|
||||
# Scroll up slowly on a specific device
|
||||
android_input_swipe --serial emulator-5554 540 400 540 1400 800
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Requiere `adb_wsl.sh` (sourceado automáticamente). Usa `adb_pick_serial` para
|
||||
resolver el dispositivo objetivo a partir de `--serial`, `ADB_SERIAL` o el
|
||||
único device disponible.
|
||||
|
||||
Los cuatro argumentos de coordenadas se validan como enteros antes de invocar
|
||||
adb — acepta coordenadas negativas (edge cases de hardware con ejes invertidos).
|
||||
|
||||
Exit 3 si `adb_pick_serial` no puede resolver el serial (sin devices o ambiguo).
|
||||
Exit 1 si faltan coordenadas o alguna no es numérica.
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_input_swipe — Send swipe gesture between two points via adb shell input swipe.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_input_swipe [--serial <S>] <x1> <y1> <x2> <y2> [duration_ms]
|
||||
#
|
||||
# $1 x1 Start X coordinate in pixels (obligatorio).
|
||||
# $2 y1 Start Y coordinate in pixels (obligatorio).
|
||||
# $3 x2 End X coordinate in pixels (obligatorio).
|
||||
# $4 y2 End Y coordinate in pixels (obligatorio).
|
||||
# $5 duration_ms Swipe duration in milliseconds (opcional, default 300).
|
||||
#
|
||||
# Envvar ADB_SERIAL overrides --serial.
|
||||
# ---------------------------------------------------------------------------
|
||||
android_input_swipe() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local x1="${1:-}"
|
||||
local y1="${2:-}"
|
||||
local x2="${3:-}"
|
||||
local y2="${4:-}"
|
||||
local dur="${5:-300}"
|
||||
|
||||
if [[ -z "$x1" || -z "$y1" || -z "$x2" || -z "$y2" ]]; then
|
||||
echo "android_input_swipe: se requieren cuatro argumentos: x1 y1 x2 y2." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validar que los cuatro coordenadas son numericas (enteros o negativos).
|
||||
local coord
|
||||
for coord in "$x1" "$y1" "$x2" "$y2"; do
|
||||
if ! [[ "$coord" =~ ^-?[0-9]+$ ]]; then
|
||||
echo "android_input_swipe: coordenada no numerica: '$coord'." >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
adb_s "$serial" shell input swipe "$x1" "$y1" "$x2" "$y2" "$dur"
|
||||
echo "swipe $x1,$y1 → $x2,$y2 (${dur}ms) on $serial"
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (no sourceado)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_input_swipe "$@"
|
||||
fi
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: android_input_tap
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_input_tap([--serial <S>], x: int, y: int) -> void"
|
||||
description: "Send tap gesture at screen coordinates via adb shell input tap."
|
||||
tags: [android, adb, input, tap, ui-test, gesture, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. Auto-detected if omitted."
|
||||
- name: "x"
|
||||
desc: "X coordinate in pixels (non-negative integer)."
|
||||
- name: "y"
|
||||
desc: "Y coordinate in pixels (non-negative integer)."
|
||||
output: "Stdout 'tap @ <x>,<y> on <serial>'."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_input_tap.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Auto-detect device
|
||||
android_input_tap 540 960
|
||||
|
||||
# Target specific device
|
||||
android_input_tap --serial emulator-5554 540 960
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Sources `adb_wsl.sh` para resolver el binario ADB y exponer `adb_pick_serial` / `adb_s`.
|
||||
Usa `adb_pick_serial` para consumir `--serial` de los args y autodetectar el device si no se pasa.
|
||||
Valida X e Y con regex `^[0-9]+$` antes de invocar adb.
|
||||
Exit 3 si no hay device/emulador disponible (propagado desde `adb_pick_serial`).
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_input_tap — Send tap gesture at screen coordinates via adb shell input tap.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_input_tap [--serial <S>] <x> <y>
|
||||
#
|
||||
# --serial <S> Optional target device serial (also auto-detected).
|
||||
# x X coordinate in pixels (non-negative integer).
|
||||
# y Y coordinate in pixels (non-negative integer).
|
||||
#
|
||||
# Exits:
|
||||
# 0 tap sent successfully
|
||||
# 1 missing or invalid coordinates
|
||||
# 3 no device/emulator available
|
||||
# ---------------------------------------------------------------------------
|
||||
android_input_tap() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local x="${1:-}"
|
||||
local y="${2:-}"
|
||||
|
||||
if [[ -z "$x" || -z "$y" ]]; then
|
||||
echo "android_input_tap: se requieren X e Y como argumentos posicionales." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! "$x" =~ ^[0-9]+$ ]]; then
|
||||
echo "android_input_tap: X debe ser un entero no negativo, recibido '$x'." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! "$y" =~ ^[0-9]+$ ]]; then
|
||||
echo "android_input_tap: Y debe ser un entero no negativo, recibido '$y'." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
adb_s "$serial" shell input tap "$x" "$y"
|
||||
echo "tap @ $x,$y on $serial"
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (no sourceado)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_input_tap "$@"
|
||||
fi
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: android_input_text
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_input_text([--serial <S>], text: string) -> void"
|
||||
description: "Type text in focused field via adb shell input text. Spaces handled."
|
||||
tags: [android, adb, input, text, ui-test, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_input_text.sh"
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. If omitted, autodetects first connected device/emulator."
|
||||
- name: "text"
|
||||
desc: "Text to type (spaces become %s as required by adb)."
|
||||
output: "Stdout 'typed: <text>'. Exit 0."
|
||||
notes: |
|
||||
adb input text replaces spaces with %s. Funcion lo hace automaticamente.
|
||||
Special chars " $ ` se escapan con backslash para evitar interpretacion por el shell.
|
||||
Exit 3 si no hay ningun device disponible (propagado desde adb_pick_serial).
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/android_input_text.sh
|
||||
|
||||
# Tipar en el device por defecto
|
||||
android_input_text "hello world"
|
||||
# → typed: hello world (envia "hello%sworld" a adb)
|
||||
|
||||
# Tipar en un device especifico
|
||||
android_input_text --serial emulator-5554 "user@example.com"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
`adb shell input text` no acepta espacios directos — los convierte a `%s` internamente. Esta funcion hace la sustitucion antes de llamar a adb para que el comportamiento sea predecible.
|
||||
|
||||
Los caracteres `"`, `$` y `` ` `` se escapan con backslash para que el shell no los interprete al construir el comando.
|
||||
|
||||
Depende de `adb_wsl_bash_infra` para resolver el binario `adb.exe` en WSL2 y para `adb_pick_serial` / `adb_s`.
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_input_text — Type text in focused field via adb shell input text.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_input_text [--serial <S>] <text>
|
||||
#
|
||||
# $1 text Text to type in the currently focused field (obligatorio).
|
||||
# Spaces are replaced with %s as required by adb input text.
|
||||
# Special chars " $ ` are escaped with backslash.
|
||||
#
|
||||
# Envvar ADB_SERIAL overrides --serial.
|
||||
# ---------------------------------------------------------------------------
|
||||
android_input_text() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local text="${1:-}"
|
||||
if [[ -z "$text" ]]; then
|
||||
echo "android_input_text: se requiere el texto como primer argumento." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# adb input text does not support raw spaces; replace with %s.
|
||||
# Also escape " $ ` which the shell would interpret inside the adb command.
|
||||
local escaped
|
||||
escaped="${text// /%s}"
|
||||
escaped="${escaped//\"/\\\"}"
|
||||
escaped="${escaped//\$/\\\$}"
|
||||
escaped="${escaped//\`/\\\`}"
|
||||
|
||||
adb_s "$serial" shell input text "$escaped"
|
||||
echo "typed: $text"
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (no sourceado)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_input_text "$@"
|
||||
fi
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: android_logcat
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_logcat([--serial <S>] [--package <name>] [--level <V|D|I|W|E|F>] [--lines <N>] [--clear])"
|
||||
description: "Lee logcat del device/emulador, opcionalmente filtrado por package y nivel. Multi-emulator via --serial."
|
||||
tags: [android, adb, logcat, wsl]
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Default: first device detected."
|
||||
- name: "--package <name>"
|
||||
desc: "Filter by app package (resolves PID via adb shell pidof)"
|
||||
- name: "--level <L>"
|
||||
desc: "Min log level V/D/I/W/E/F, default I"
|
||||
- name: "--lines <N>"
|
||||
desc: "Dump last N lines and exit. Default: follow indefinidamente"
|
||||
- name: "--clear"
|
||||
desc: "Clear log buffer before reading"
|
||||
output: "Logcat output a stdout. Follow indefinido sin --lines. Exit 130 si Ctrl-C. Exit 2 si --package y el proceso no corre."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_logcat.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Follow completo sin filtros
|
||||
android_logcat
|
||||
|
||||
# Solo logs de una app, nivel Warning y superior
|
||||
android_logcat --package com.example.myapp --level W
|
||||
|
||||
# Dump de las últimas 200 líneas y salir
|
||||
android_logcat --lines 200
|
||||
|
||||
# Limpiar buffer y hacer follow solo de errores de la app
|
||||
android_logcat --clear --package com.example.myapp --level E
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Resuelve `adb` o `adb.exe` en PATH (compatible con WSL2 usando el binario Windows).
|
||||
- `--package` usa `adb shell pidof -s` para obtener el PID actual. Si la app no está corriendo, sale con exit 2.
|
||||
- `--lines N` activa modo dump (`-d -t N`); sin él, el follow es indefinido hasta Ctrl-C (exit 130).
|
||||
- `--clear` ejecuta `adb logcat -c` antes de leer, descartando el buffer acumulado.
|
||||
- El filtro de nivel se aplica como `*:<level>` al final del comando logcat.
|
||||
- En follow mode, `trap INT TERM` garantiza exit limpio (exit 130) al interrumpir.
|
||||
- CR (`\r`) del output de `adb.exe` en WSL se limpia al resolver el PID.
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_logcat — Lee logcat del device/emulador, opcionalmente filtrado por package y nivel.
|
||||
# Multi-emulator via --serial <S>.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_logcat() {
|
||||
local serial
|
||||
adb_pick_serial "$@" || { echo "android_logcat: no device/emulator." >&2; return 3; }
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local package=""
|
||||
local level="I"
|
||||
local lines=""
|
||||
local do_clear=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--package) package="$2"; shift 2 ;;
|
||||
--level) level="$2"; shift 2 ;;
|
||||
--lines) lines="$2"; shift 2 ;;
|
||||
--clear) do_clear=1; shift ;;
|
||||
*) echo "android_logcat: unknown argument: $1" >&2; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $do_clear -eq 1 ]]; then
|
||||
adb_s "$serial" logcat -c
|
||||
fi
|
||||
|
||||
local pid_filter=""
|
||||
if [[ -n "$package" ]]; then
|
||||
local pid
|
||||
pid=$(adb_s "$serial" shell pidof -s "$package" 2>/dev/null || true)
|
||||
pid="${pid//$'\r'/}"
|
||||
if [[ -z "$pid" ]]; then
|
||||
echo "android_logcat: package '$package' is not running on $serial" >&2
|
||||
return 2
|
||||
fi
|
||||
pid_filter="--pid=$pid"
|
||||
fi
|
||||
|
||||
local -a cmd=(logcat -v time)
|
||||
[[ -n "$lines" ]] && cmd+=(-d -t "$lines")
|
||||
[[ -n "$pid_filter" ]] && cmd+=("$pid_filter")
|
||||
cmd+=("*:${level}")
|
||||
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
adb_s "$serial" "${cmd[@]}"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
android_logcat "$@"
|
||||
fi
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: android_pull
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_pull [--serial <S>] remote_path local_path"
|
||||
description: "Pull file/dir from Android device to WSL via adb pull."
|
||||
tags: [android, adb, pull, file, transfer, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. If omitted, adb_pick_serial auto-detects the connected device."
|
||||
- name: "remote_path"
|
||||
desc: "Source path on the Android device (e.g. /sdcard/Pictures/foo.png)."
|
||||
- name: "local_path"
|
||||
desc: "Destination path in the WSL filesystem. Parent directories are created automatically."
|
||||
output: "Stdout 'pulled: <remote> → <local> from <serial>'."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_pull.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Pull a single file (auto-detect device)
|
||||
android_pull /sdcard/Pictures/foo.png ~/Downloads/foo.png
|
||||
|
||||
# Pull a directory to a specific local path with explicit serial
|
||||
android_pull --serial emulator-5554 /sdcard/DCIM ~/Downloads/DCIM
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Sources `adb_wsl.sh` for `adb_pick_serial`, `ADB_PICK_REST`, `adb_wsl_to_win`, and `adb_s`.
|
||||
The local path is converted to a Windows path via `adb_wsl_to_win` before passing to `adb pull`,
|
||||
which is required because `adb.exe` (Windows binary) does not understand WSL paths.
|
||||
Exit code 3 when no device serial can be resolved.
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_pull — Pull file/dir from Android device to WSL via adb pull.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_pull() {
|
||||
local serial remote local_path win_local
|
||||
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
remote="${1:?remote_path required}"
|
||||
local_path="${2:?local_path required}"
|
||||
|
||||
mkdir -p "$(dirname "$local_path")"
|
||||
|
||||
win_local=$(adb_wsl_to_win "$local_path")
|
||||
|
||||
adb_s "$serial" pull "$remote" "$win_local"
|
||||
|
||||
echo "pulled: $remote → $local_path from $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_pull "$@"
|
||||
fi
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: android_push
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_push([--serial <S>], local_path: string, remote_path: string) -> void"
|
||||
description: "Push file/dir from WSL to Android device via adb push."
|
||||
tags: [android, adb, push, file, transfer, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device/emulator serial. Auto-detected if omitted."
|
||||
- name: "local_path"
|
||||
desc: "WSL source path to file or directory to push."
|
||||
- name: "remote_path"
|
||||
desc: "Device destination path, e.g. /sdcard/Download/foo.txt."
|
||||
output: "Stdout 'pushed: <local> → <remote> on <serial>'. Exit 0."
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_push.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Push a file to the active emulator
|
||||
android_push /tmp/data.json /sdcard/Download/data.json
|
||||
|
||||
# Push to a specific device
|
||||
android_push --serial emulator-5554 /tmp/data.json /sdcard/Download/data.json
|
||||
|
||||
# Push a directory
|
||||
android_push --serial R5CR1234567 ~/exports/bundle /sdcard/Download/bundle
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
|
||||
Si `--serial` no se pasa, autodetecta el primer device/emulador disponible.
|
||||
Sale con exit 3 si no hay ningun device conectado.
|
||||
Valida que `local_path` existe en WSL antes de convertir y enviar.
|
||||
Convierte el path WSL a Windows con `adb_wsl_to_win` (requiere `wslpath`; si no está disponible usa el path tal cual).
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_push — Push file/dir from WSL to Android device via adb push.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_push() {
|
||||
local serial local_path remote_path win_local
|
||||
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local_path="${1:?android_push: local_path required}"
|
||||
remote_path="${2:?android_push: remote_path required}"
|
||||
|
||||
if [[ ! -e "$local_path" ]]; then
|
||||
echo "android_push: '$local_path' not found." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
win_local=$(adb_wsl_to_win "$local_path")
|
||||
|
||||
adb_s "$serial" push "$win_local" "$remote_path"
|
||||
echo "pushed: $local_path → $remote_path on $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_push "$@"
|
||||
fi
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: android_screen_record
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_screen_record([--serial <S>] [--duration <s>] [--bit-rate <bps>] [--size <WxH>] output_path: string) -> void"
|
||||
description: "Record screen video via adb screenrecord, pulls to local path."
|
||||
tags: [android, adb, screen, record, video, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. If omitted, autodetects first connected device/emulator."
|
||||
- name: "output_path"
|
||||
desc: "WSL destination path for the recorded .mp4 file."
|
||||
- name: "--duration <s>"
|
||||
desc: "Recording duration in seconds. Default 30, max 180 (adb screenrecord built-in limit)."
|
||||
- name: "--bit-rate <bps>"
|
||||
desc: "Video bit rate in bits per second. Default 4000000 (4 Mbps)."
|
||||
- name: "--size <WxH>"
|
||||
desc: "Video dimensions e.g. 720x1280. Default: device native resolution."
|
||||
output: "Stdout 'recorded: <path> (<s>s from <serial>)'. MP4 file written to output_path."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_screen_record.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/android_screen_record.sh
|
||||
|
||||
# Record 15 seconds to a local file
|
||||
android_screen_record --duration 15 /tmp/demo.mp4
|
||||
|
||||
# Specific device, custom resolution, higher bitrate
|
||||
android_screen_record --serial emulator-5554 --duration 60 --bit-rate 8000000 --size 1080x2400 ~/videos/session.mp4
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
`adb screenrecord` tiene un limite maximo de 180 segundos por grabacion. Para capturas mas largas, encadenar multiples llamadas y concatenar los MP4 resultantes (ej. con `ffmpeg -f concat`).
|
||||
|
||||
El archivo temporal en el dispositivo es siempre `/sdcard/__rec.mp4` y se elimina tras el pull. Si la grabacion falla a mitad, el archivo puede quedar en el dispositivo; en ese caso ejecutar `adb shell rm /sdcard/__rec.mp4` manualmente.
|
||||
|
||||
Exit codes: 0 exito, 2 falta output_path, 3 ningun device encontrado.
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_screen_record — Record screen video via adb screenrecord, pulls to local path.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# android_screen_record [--serial <S>] [--duration <s>] [--bit-rate <bps>] [--size <WxH>] <output_path>
|
||||
#
|
||||
# Args:
|
||||
# --serial <S> Optional: target device serial (default: autodetect)
|
||||
# --duration <s> Recording duration in seconds (default: 30, max: 180)
|
||||
# --bit-rate <bps> Video bit rate (default: 4000000)
|
||||
# --size <WxH> Video dimensions e.g. 720x1280 (default: device native)
|
||||
# output_path WSL destination path for the .mp4 file
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success
|
||||
# 1 general error
|
||||
# 2 missing output_path argument
|
||||
# 3 no device/emulator found
|
||||
# ---------------------------------------------------------------------------
|
||||
android_screen_record() {
|
||||
local dur=30
|
||||
local bit_rate=4000000
|
||||
local size=""
|
||||
|
||||
# Parse flags first pass to extract serial; remaining args go to ADB_PICK_REST.
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
# Parse remaining flags
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--duration) dur="$2"; shift 2 ;;
|
||||
--duration=*) dur="${1#--duration=}"; shift ;;
|
||||
--bit-rate) bit_rate="$2"; shift 2 ;;
|
||||
--bit-rate=*) bit_rate="${1#--bit-rate=}"; shift ;;
|
||||
--size) size="$2"; shift 2 ;;
|
||||
--size=*) size="${1#--size=}"; shift ;;
|
||||
-*) echo "android_screen_record: unknown flag '$1'" >&2; return 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local output="${1:-}"
|
||||
if [[ -z "$output" ]]; then
|
||||
echo "android_screen_record: output_path is required." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Build adb screenrecord args
|
||||
local rec_args=("shell" "screenrecord" "--time-limit" "$dur")
|
||||
rec_args+=("--bit-rate" "$bit_rate")
|
||||
[[ -n "$size" ]] && rec_args+=("--size" "$size")
|
||||
rec_args+=("/sdcard/__rec.mp4")
|
||||
|
||||
echo "android_screen_record: recording ${dur}s from $serial..." >&2
|
||||
adb_s "$serial" "${rec_args[@]}" || {
|
||||
echo "android_screen_record: screenrecord failed." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
adb_s "$serial" pull /sdcard/__rec.mp4 "$output" || {
|
||||
echo "android_screen_record: pull failed." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
adb_s "$serial" shell rm /sdcard/__rec.mp4
|
||||
|
||||
echo "recorded: $output (${dur}s from $serial)"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_screen_record "$@"
|
||||
fi
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: android_screenshot
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_screenshot([--serial <S>], output_path: string) -> void"
|
||||
description: "Capture screen as PNG via adb exec-out screencap -p."
|
||||
tags: [android, adb, screenshot, screen, capture, pendiente-usar]
|
||||
uses_functions: [adb_wsl_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional ADB serial to target a specific device/emulator. If omitted, autodetects the first connected device."
|
||||
- name: "output_path"
|
||||
desc: "WSL path where the PNG screenshot will be written (e.g. /tmp/screen.png). Parent directory is created if absent."
|
||||
output: "Stdout 'screenshot: <path> (<bytes> bytes) from <serial>'. PNG file written to disk."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_screenshot.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/android_screenshot.sh
|
||||
android_screenshot /tmp/screen.png
|
||||
# screenshot: /tmp/screen.png (123456 bytes) from emulator-5554
|
||||
|
||||
# Targeting a specific device:
|
||||
android_screenshot --serial emulator-5554 /tmp/screen.png
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Sources `adb_wsl.sh` from its own directory, so `ADB` and `ANDROID_SDK_WIN` env vars
|
||||
are respected as with all other android_* functions.
|
||||
|
||||
Exit codes:
|
||||
- `0` — screenshot captured successfully.
|
||||
- `1` — missing output path, screencap produced empty file, or adb error.
|
||||
- `3` — no device/emulator connected (propagated from `adb_pick_serial`).
|
||||
|
||||
The emptiness check (`! -s`) handles the case where `adb exec-out` exits 0 but writes
|
||||
zero bytes (e.g. device locked, screencap permission denied). In that case the file is
|
||||
removed and exit 1 is returned.
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_screenshot — Capture screen as PNG via adb exec-out screencap -p.
|
||||
|
||||
android_screenshot() {
|
||||
local SCRIPT_DIR
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=bash/functions/infra/adb_wsl.sh
|
||||
source "$SCRIPT_DIR/adb_wsl.sh" || return 1
|
||||
|
||||
# Resolve serial, consume --serial from args
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
local output="${1:-}"
|
||||
if [[ -z "$output" ]]; then
|
||||
echo "android_screenshot: output_path is required." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Ensure parent directory exists
|
||||
mkdir -p "$(dirname "$output")"
|
||||
|
||||
# Capture screen
|
||||
adb_s "$serial" exec-out screencap -p > "$output"
|
||||
|
||||
# Verify file created and non-empty
|
||||
if [[ ! -f "$output" ]] || [[ ! -s "$output" ]]; then
|
||||
rm -f "$output"
|
||||
echo "android_screenshot: screencap produced empty or missing file." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local size
|
||||
size=$(stat -c%s "$output" 2>/dev/null || stat -f%z "$output" 2>/dev/null)
|
||||
echo "screenshot: $output ($size bytes) from $serial"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_screenshot "$@"
|
||||
fi
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: android_shell
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "android_shell([--serial <S>], cmd ...args)"
|
||||
description: "Execute arbitrary shell command on Android device. Multi-emulator via --serial."
|
||||
tags: [android, adb, shell, exec, pendiente-usar]
|
||||
params:
|
||||
- name: "--serial <S>"
|
||||
desc: "Optional target device serial. Omit to auto-pick (single device) or use ADB_SERIAL env."
|
||||
- name: "cmd ...args"
|
||||
desc: "Shell command + args to run on device. Variadic."
|
||||
output: "Passthrough stdout/stderr de adb shell. Exit code = shell command exit."
|
||||
uses_functions: ["adb_wsl_bash_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/android_shell.sh"
|
||||
notes: "Para comandos complejos con pipes/redirects mejor `adb_s $serial shell 'cmd | other'` directo via adb_run."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
android_shell pm list packages
|
||||
android_shell --serial emulator-5554 getprop ro.product.model
|
||||
android_shell df -h /sdcard
|
||||
android_shell ls -la /data/local/tmp
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Sourcea `adb_wsl.sh` para resolver `adb_pick_serial` (maneja `--serial`, `ADB_SERIAL`, y auto-detect de dispositivo unico) y `adb_s` (wrapper de `adb -s`). El array `ADB_PICK_REST` contiene los args restantes tras consumir `--serial`.
|
||||
|
||||
Para comandos con pipes o redirects que bash interpretaria localmente, mejor pasar como string unico: `adb_s "$serial" shell 'cmd | grep foo'`.
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# android_shell — Execute arbitrary shell command on Android device via adb shell
|
||||
|
||||
# shellcheck source=./adb_wsl.sh
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/adb_wsl.sh"
|
||||
|
||||
android_shell() {
|
||||
adb_pick_serial "$@" || exit 3
|
||||
local serial="$ADB_PICK_SERIAL"
|
||||
set -- "${ADB_PICK_REST[@]}"
|
||||
|
||||
adb_s "$serial" shell "$@"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
android_shell "$@"
|
||||
fi
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "append_diary_entry(titulo: string, cuerpo: string) -> string"
|
||||
description: "Añade una entrada al diario del dia en ${DIARY_DIR:-docs/diary}/YYYY-MM-DD.md. Crea el archivo con cabecera si no existe. Nunca reescribe contenido previo. Si cuerpo es vacio escribe solo el header de la seccion."
|
||||
tags: [diary, markdown, append, idempotent, infra]
|
||||
tags: [diary, markdown, append, idempotent, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "build_cpp_linux(target?: string) -> void"
|
||||
description: "Compila las funciones y apps C++ del registry para Linux nativo usando cmake"
|
||||
tags: [cpp, build, cmake, linux, imgui]
|
||||
tags: [cpp, build, cmake, linux, imgui, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "build_cpp_windows(target?: string) -> void"
|
||||
description: "Cross-compila las funciones y apps C++ del registry para Windows usando mingw-w64"
|
||||
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui]
|
||||
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui, cpp-windows]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -1,34 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# build_cpp_windows — Cross-compila apps C++ del registry para Windows con
|
||||
# mingw-w64. Configura el build dir cpp/build/windows/ con la toolchain la
|
||||
# primera vez y construye el target indicado (o todos).
|
||||
#
|
||||
# Uso (funcion via source):
|
||||
# source bash/functions/infra/build_cpp_windows.sh
|
||||
# build_cpp_windows my_app # construye target especifico
|
||||
# build_cpp_windows # construye todos
|
||||
#
|
||||
# Uso (script directo):
|
||||
# bash bash/functions/infra/build_cpp_windows.sh my_app
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
|
||||
CPP_ROOT="$REGISTRY_ROOT/cpp"
|
||||
BUILD_DIR="$CPP_ROOT/build/windows"
|
||||
TOOLCHAIN="$CPP_ROOT/toolchains/mingw-w64.cmake"
|
||||
TARGET="${1:-}"
|
||||
build_cpp_windows() {
|
||||
local target="${1:-}"
|
||||
local registry_root="${FN_REGISTRY_ROOT:-}"
|
||||
if [ -z "$registry_root" ]; then
|
||||
local d="$PWD"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ] && [ -d "$d/cpp" ]; then
|
||||
registry_root="$d"; break
|
||||
fi
|
||||
d="$(dirname "$d")"
|
||||
done
|
||||
fi
|
||||
if [ -z "$registry_root" ]; then
|
||||
echo "[build_cpp_windows] No se localiza la raiz del registry. Exporta FN_REGISTRY_ROOT." >&2
|
||||
return 2
|
||||
fi
|
||||
local cpp_root="$registry_root/cpp"
|
||||
local build_dir="${BUILD_WIN:-$cpp_root/build/windows}"
|
||||
local toolchain="$cpp_root/toolchains/mingw-w64.cmake"
|
||||
|
||||
# Check mingw is available
|
||||
if ! command -v x86_64-w64-mingw32-g++ &>/dev/null; then
|
||||
echo "[build_cpp_windows] Error: mingw-w64 not found. Install with: sudo apt install mingw-w64"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v x86_64-w64-mingw32-g++ &>/dev/null; then
|
||||
echo "[build_cpp_windows] Error: mingw-w64 not found. Install with: sudo apt install mingw-w64" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Configure if needed
|
||||
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
|
||||
echo "[build_cpp_windows] Configuring cmake with mingw-w64 toolchain..."
|
||||
cmake -B "$BUILD_DIR" -S "$CPP_ROOT" -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN"
|
||||
fi
|
||||
if [ ! -f "$build_dir/CMakeCache.txt" ]; then
|
||||
echo "[build_cpp_windows] Configuring cmake with mingw-w64 toolchain..." >&2
|
||||
cmake -B "$build_dir" -S "$cpp_root" -DCMAKE_TOOLCHAIN_FILE="$toolchain"
|
||||
else
|
||||
# Re-run cmake to pick up new add_subdirectory entries cuando se anade
|
||||
# una app nueva al CMakeLists.txt (no rompe builds incrementales).
|
||||
cmake "$build_dir" >/dev/null
|
||||
fi
|
||||
|
||||
# Build
|
||||
if [ -n "$TARGET" ]; then
|
||||
echo "[build_cpp_windows] Cross-compiling target: $TARGET"
|
||||
cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
|
||||
else
|
||||
echo "[build_cpp_windows] Cross-compiling all targets..."
|
||||
cmake --build "$BUILD_DIR" -- -j"$(nproc)"
|
||||
fi
|
||||
if [ -n "$target" ]; then
|
||||
echo "[build_cpp_windows] Cross-compiling target: $target" >&2
|
||||
cmake --build "$build_dir" --target "$target" -- -j"$(nproc)"
|
||||
else
|
||||
echo "[build_cpp_windows] Cross-compiling all targets..." >&2
|
||||
cmake --build "$build_dir" -- -j"$(nproc)"
|
||||
fi
|
||||
|
||||
echo "[build_cpp_windows] Done. Windows binaries in $BUILD_DIR"
|
||||
if [ -n "$TARGET" ]; then
|
||||
file "$BUILD_DIR"/**/"$TARGET".exe 2>/dev/null || file "$BUILD_DIR/$TARGET".exe 2>/dev/null || true
|
||||
echo "[build_cpp_windows] Done. Windows binaries in $build_dir" >&2
|
||||
}
|
||||
|
||||
# Invocacion directa como script (compatibilidad).
|
||||
if [ "${BASH_SOURCE[0]:-}" = "${0:-}" ] && [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
build_cpp_windows "$@"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: build_wasm_cpp_app
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "build_wasm_cpp_app(app_name: string, [--no-budget-check]) -> void"
|
||||
description: "Compila una app C++ del registry (cpp/apps/<name>) a WASM via emscripten. Sale build/wasm/<name>/<name>.{html,js,wasm,wasm.gz}. Falla si gzip > 2 MB."
|
||||
tags: [wasm, emscripten, cpp, build, gamedev, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: "bash bash/functions/infra/build_wasm_cpp_app.sh engine_smoke"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/build_wasm_cpp_app.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre del directorio bajo cpp/apps/. Debe contener CMakeLists.txt self-sufficient (top-level project) con guard `if(EMSCRIPTEN)` para flags wasm."
|
||||
- name: "--no-budget-check"
|
||||
desc: "Opcional. Salta verificacion de tamaño (gzip < 2 MB hard, < 1.5 MB soft)."
|
||||
output: "Reporte de tamaños en stdout. Crea build/wasm/<app>/<app>.html/.js/.wasm/.wasm.gz. Exit 3 si excede budget hard."
|
||||
---
|
||||
|
||||
# build_wasm_cpp_app
|
||||
|
||||
Compila apps C++ del registry a WebAssembly. Issue 0072d (parte del stack gamedev).
|
||||
|
||||
## Requisitos
|
||||
|
||||
- `emsdk` instalado y activo en el shell, o presente en `<repo>/emsdk/` (autoactiva).
|
||||
- `cpp/apps/<app>/CMakeLists.txt` con bloque `if(EMSCRIPTEN) ... endif()` que define los flags wasm (USE_WEBGL2, FULL_ES3, ALLOW_MEMORY_GROWTH, etc.).
|
||||
- `cpp/CMakeLists.txt` debe seguir tolerando configuracion via `emcmake`. La app target se elige con `cmake --build $BUILD_DIR --target <app>`.
|
||||
|
||||
## Flujo
|
||||
|
||||
1. Localiza `emcc` en PATH o autoactiva `<repo>/emsdk/emsdk_env.sh`.
|
||||
2. `emcmake cmake -S cpp -B build/wasm/<app> -DCMAKE_BUILD_TYPE=MinSizeRel`
|
||||
3. `cmake --build build/wasm/<app> --target <app> -j`
|
||||
4. `gzip -9 -k <app>.wasm` y `brotli -q 11 -k <app>.wasm` (si brotli disponible).
|
||||
5. Reporta tamaños y compara contra budget (1.5 MB gzip soft, 2 MB hard).
|
||||
|
||||
## Budgets
|
||||
|
||||
| Limite | Valor | Comportamiento |
|
||||
|---|---|---|
|
||||
| Soft | 1.5 MB gzip | Warning, sigue |
|
||||
| Hard | 2 MB gzip | Exit 3, falla |
|
||||
|
||||
Skip con `--no-budget-check`.
|
||||
|
||||
## Apps soportadas
|
||||
|
||||
Cualquier app bajo `cpp/apps/<name>/` cuyo `CMakeLists.txt` defina target con flags emscripten. Probada con: `engine_smoke` (issue 0072a).
|
||||
|
||||
## Errores comunes
|
||||
|
||||
- `emcc no encontrado` → instalar emsdk segun instrucciones del propio script.
|
||||
- `<app>.wasm no encontrado` → fallo de build. Re-ejecutar con `2>&1 | tee` para ver compiler errors.
|
||||
- `wasm.gz excede budget` → revisar bloat, usar `twiggy top` o `wasm-objdump -h`. Ver issue 0072d.
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# build_wasm_cpp_app — compila app cpp/apps/<name> a WASM via emscripten.
|
||||
#
|
||||
# Uso:
|
||||
# build_wasm_cpp_app.sh <app_name> [--no-budget-check]
|
||||
#
|
||||
# Salida: build/wasm/<name>/<name>.{html,js,wasm}
|
||||
# + <name>.wasm.gz (gzip -9) y <name>.wasm.br (brotli -11 si esta).
|
||||
#
|
||||
# Requiere: emsdk activo en el shell (source emsdk/emsdk_env.sh) o que
|
||||
# exista emsdk/ en la raiz del repo y se autoactive.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP="${1:?Uso: $0 <app_name> [--no-budget-check]}"
|
||||
SHIFT_FLAG="${2:-}"
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. && pwd)"
|
||||
SRC_DIR="$REPO_ROOT/cpp/apps/$APP"
|
||||
BUILD_DIR="$REPO_ROOT/build/wasm/$APP"
|
||||
|
||||
if [ ! -d "$SRC_DIR" ]; then
|
||||
echo "ERROR: $SRC_DIR no existe" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Activate emsdk if not already in PATH.
|
||||
if ! command -v emcc >/dev/null 2>&1; then
|
||||
if [ -f "$REPO_ROOT/emsdk/emsdk_env.sh" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$REPO_ROOT/emsdk/emsdk_env.sh" >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
if ! command -v emcc >/dev/null 2>&1; then
|
||||
echo "ERROR: emcc no encontrado. Instala emsdk:" >&2
|
||||
echo " git clone https://github.com/emscripten-core/emsdk.git" >&2
|
||||
echo " cd emsdk && ./emsdk install latest && ./emsdk activate latest" >&2
|
||||
echo " source ./emsdk_env.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "── emcc: $(emcc --version | head -n1)"
|
||||
echo "── source: $SRC_DIR"
|
||||
echo "── build: $BUILD_DIR"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
# Build the app directly (NOT the full cpp/ tree). Each app's CMakeLists.txt
|
||||
# is expected to be self-sufficient as top-level (issue 0072a pattern).
|
||||
emcmake cmake -S "$SRC_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=MinSizeRel
|
||||
cmake --build "$BUILD_DIR" --target "$APP" -j
|
||||
|
||||
WASM_DIR=$(find "$BUILD_DIR" -name "$APP.wasm" -printf '%h\n' -quit 2>/dev/null || true)
|
||||
if [ -z "$WASM_DIR" ]; then
|
||||
echo "ERROR: no se encontro $APP.wasm en $BUILD_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cd "$WASM_DIR"
|
||||
gzip -9 -k -f "$APP.wasm"
|
||||
if command -v brotli >/dev/null 2>&1; then
|
||||
brotli -q 11 -k -f "$APP.wasm"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "── Sizes (in $WASM_DIR) ──"
|
||||
for f in "$APP".html "$APP".js "$APP".wasm "$APP".wasm.gz "$APP".wasm.br; do
|
||||
[ -f "$f" ] && printf "%-32s %10d bytes\n" "$f" "$(stat -c%s "$f")"
|
||||
done
|
||||
|
||||
# Budget check (1.5 MB gzip soft, 2 MB hard)
|
||||
if [ "$SHIFT_FLAG" != "--no-budget-check" ]; then
|
||||
SIZE_GZ=$(stat -c%s "$APP.wasm.gz")
|
||||
HARD=$((2 * 1024 * 1024))
|
||||
SOFT=$((1572864)) # 1.5 MB
|
||||
if [ "$SIZE_GZ" -gt "$HARD" ]; then
|
||||
echo "❌ $APP.wasm.gz = $SIZE_GZ bytes > $HARD (2 MB hard limit)" >&2
|
||||
exit 3
|
||||
elif [ "$SIZE_GZ" -gt "$SOFT" ]; then
|
||||
echo "⚠ $APP.wasm.gz = $SIZE_GZ bytes > $SOFT (1.5 MB soft limit)"
|
||||
else
|
||||
echo "✓ $APP.wasm.gz = $SIZE_GZ bytes within soft limit (1.5 MB)"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: cuda_toolkit_check
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "cuda_toolkit_check() -> void"
|
||||
description: "Detecta componentes CUDA instalados en el sistema y emite pares key=value a stdout: nvcc (version o missing), nvidia_smi (present/missing), driver_version, cuda_libs (path o missing) y overall (ok|partial|missing). Exit code 0 siempre — funcion informativa, no fatal."
|
||||
tags: [cuda, nvidia, gpu, hardware, probe, infra, toolkit, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: (ninguno)
|
||||
desc: "No toma parametros. Lee el estado del sistema via nvcc, nvidia-smi y busqueda en rutas canonicas de CUDA."
|
||||
output: "Cinco pares key=value en stdout: nvcc, nvidia_smi, driver_version, cuda_libs, overall. overall=ok si los tres componentes principales estan presentes; partial si algunos; missing si ninguno."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/cuda_toolkit_check.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/cuda_toolkit_check.sh
|
||||
cuda_toolkit_check
|
||||
```
|
||||
|
||||
Salida en maquina con CUDA completo:
|
||||
```
|
||||
nvcc=12.4
|
||||
nvidia_smi=present
|
||||
driver_version=550.54.15
|
||||
cuda_libs=/usr/local/cuda
|
||||
overall=ok
|
||||
```
|
||||
|
||||
Salida en maquina sin CUDA:
|
||||
```
|
||||
nvcc=missing
|
||||
nvidia_smi=missing
|
||||
driver_version=missing
|
||||
cuda_libs=missing
|
||||
overall=missing
|
||||
```
|
||||
|
||||
Invocar directamente:
|
||||
```bash
|
||||
bash bash/functions/infra/cuda_toolkit_check.sh
|
||||
```
|
||||
|
||||
Parsear desde otro script:
|
||||
```bash
|
||||
eval "$(cuda_toolkit_check)"
|
||||
echo "CUDA overall: $overall"
|
||||
if [[ "$overall" == "ok" ]]; then
|
||||
echo "CUDA completo: nvcc=$nvcc driver=$driver_version libs=$cuda_libs"
|
||||
fi
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Idempotente: no instala, no modifica nada, solo consulta.
|
||||
- Exit code 0 siempre — ausencia de CUDA es informacion, no fallo.
|
||||
- Busca `libcuda.so` en `/usr/local/cuda*`, `/opt/cuda*` y via `ldconfig -p`.
|
||||
- `driver_version` refleja el driver NVIDIA del kernel, reportado por nvidia-smi.
|
||||
- `nvcc` reporta la version del compilador CUDA toolkit (puede diferir de la version soportada por el driver).
|
||||
- Para obtener la version CUDA maxima soportada por el driver, usar `get_gpu_info_go_infra` (campo CudaVersion del struct GpuInfo).
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# cuda_toolkit_check — Detecta componentes CUDA instalados en el sistema.
|
||||
#
|
||||
# Emite pares key=value a stdout:
|
||||
# nvcc=<version|missing>
|
||||
# nvidia_smi=<present|missing>
|
||||
# driver_version=<version|missing>
|
||||
# cuda_libs=<path|missing>
|
||||
# overall=<ok|partial|missing>
|
||||
#
|
||||
# Exit code 0 siempre (funcion informativa, no fatal).
|
||||
# Idempotente: se puede invocar multiples veces sin efectos secundarios.
|
||||
|
||||
cuda_toolkit_check() {
|
||||
local nvcc_ver="missing"
|
||||
local nvidia_smi_status="missing"
|
||||
local driver_version="missing"
|
||||
local cuda_libs_path="missing"
|
||||
|
||||
# --- nvcc ---
|
||||
if command -v nvcc &>/dev/null; then
|
||||
# nvcc --version imprime algo como:
|
||||
# Cuda compilation tools, release 12.4, V12.4.131
|
||||
local raw
|
||||
raw="$(nvcc --version 2>&1)"
|
||||
# Extraer "12.4" de "release 12.4,"
|
||||
local ver
|
||||
ver="$(echo "$raw" | grep -oP 'release \K[0-9]+\.[0-9]+')"
|
||||
nvcc_ver="${ver:-present}"
|
||||
fi
|
||||
|
||||
# --- nvidia-smi + driver_version ---
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
nvidia_smi_status="present"
|
||||
# nvidia-smi --query-gpu=driver_version --format=csv,noheader retorna la version
|
||||
local drv
|
||||
drv="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1 | tr -d ' ')"
|
||||
if [[ -n "$drv" ]]; then
|
||||
driver_version="$drv"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- cuda_libs: buscar en rutas canonicas ---
|
||||
local search_dirs=(
|
||||
"/usr/local/cuda"
|
||||
"/usr/local/cuda-"*
|
||||
"/opt/cuda"
|
||||
"/opt/cuda-"*
|
||||
"/usr/lib/x86_64-linux-gnu/libcuda.so"*
|
||||
"/usr/lib/aarch64-linux-gnu/libcuda.so"*
|
||||
)
|
||||
|
||||
for candidate in "${search_dirs[@]}"; do
|
||||
# shellcheck disable=SC2206
|
||||
# Expandir globs: si el candidato no existe el glob no expande
|
||||
for path in $candidate; do
|
||||
if [[ -e "$path" ]]; then
|
||||
# Normalizar: tomar solo el directorio raiz /usr/local/cuda*
|
||||
local base
|
||||
base="${path%%/lib*}"
|
||||
cuda_libs_path="$base"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Si no encontramos directorio CUDA pero si libcuda.so en rutas de lib estandar
|
||||
if [[ "$cuda_libs_path" == "missing" ]]; then
|
||||
local libcuda
|
||||
libcuda="$(ldconfig -p 2>/dev/null | grep 'libcuda\.so' | head -n1 | awk '{print $NF}')"
|
||||
if [[ -n "$libcuda" ]]; then
|
||||
cuda_libs_path="$(dirname "$libcuda")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- overall ---
|
||||
local found_count=0
|
||||
[[ "$nvcc_ver" != "missing" ]] && ((found_count++))
|
||||
[[ "$nvidia_smi_status" != "missing" ]] && ((found_count++))
|
||||
[[ "$cuda_libs_path" != "missing" ]] && ((found_count++))
|
||||
|
||||
local overall
|
||||
if [[ $found_count -eq 0 ]]; then overall="missing"
|
||||
elif [[ $found_count -eq 3 ]]; then overall="ok"
|
||||
else overall="partial"
|
||||
fi
|
||||
|
||||
# --- emitir resultados ---
|
||||
echo "nvcc=${nvcc_ver}"
|
||||
echo "nvidia_smi=${nvidia_smi_status}"
|
||||
echo "driver_version=${driver_version}"
|
||||
echo "cuda_libs=${cuda_libs_path}"
|
||||
echo "overall=${overall}"
|
||||
}
|
||||
|
||||
# Ejecutar si se invoca directamente
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
cuda_toolkit_check "$@"
|
||||
fi
|
||||
@@ -7,7 +7,7 @@ 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)."
|
||||
tags: [cpp, deploy, windows, exe, dll, assets, rsync]
|
||||
tags: [cpp, deploy, windows, exe, dll, assets, rsync, cpp-windows]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -54,6 +54,14 @@ deploy_cpp_exe_to_windows() {
|
||||
"$app_dir/enrichers/" "$assets/enrichers/"
|
||||
fi
|
||||
|
||||
# --- 7b. collectors/ del app_dir -> assets/collectors/ (odr_console) ---
|
||||
if [ -d "$app_dir/collectors" ]; then
|
||||
rsync -a --delete \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '*.pyc' \
|
||||
"$app_dir/collectors/" "$assets/collectors/"
|
||||
fi
|
||||
|
||||
# --- 8. runtime/ Python embebido -> assets/runtime/ ---
|
||||
if grep -q '^python_runtime:[[:space:]]*true' "$app_dir/app.md" 2>/dev/null; then
|
||||
if [ ! -d "$app_dir/runtime/python" ] || \
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "detect_wsl([--check]) -> void"
|
||||
description: "Detecta si el sistema es WSL (Windows Subsystem for Linux). Con --check retorna solo exit code (0=WSL, 1=no WSL) sin output. Sin argumentos imprime versión WSL, usuario Windows, distribución, hostname, unidades montadas y ruta Windows del directorio actual."
|
||||
tags: [bash, wsl, windows, detect, integration]
|
||||
tags: [bash, wsl, windows, detect, integration, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -36,6 +36,7 @@ discover_git_repos() {
|
||||
-not -path "*/sources/*" \
|
||||
-not -path "*/temp/*" \
|
||||
-not -path "*/subrepos/*" \
|
||||
-not -path "*/emsdk/*" \
|
||||
2>/dev/null | sort)
|
||||
|
||||
# Imprimir resultados ordenados (uno por linea)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user