diff --git a/.claude/agents/fn-orquestador/SKILL.md b/.claude/agents/fn-orquestador/SKILL.md index feaf663a..91d88317 100644 --- a/.claude/agents/fn-orquestador/SKILL.md +++ b/.claude/agents/fn-orquestador/SKILL.md @@ -30,6 +30,16 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks 6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id. 7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run. 8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas. +9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq__/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `/home/lucas/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main. +10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `/home/lucas/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas: + a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix. + b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon. + NO modificar archivos en main directamente. +11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar: + ```bash + git -C /home/lucas/fn_registry status --short + ``` + Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano. --- diff --git a/.claude/commands/flow.md b/.claude/commands/flow.md new file mode 100644 index 00000000..24affc23 --- /dev/null +++ b/.claude/commands/flow.md @@ -0,0 +1,131 @@ +--- +description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2." +--- + +# /flow — Gestionar flows del registry + +Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria. + +**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow. + +Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear ` en la tabla correspondiente. Nada de inventar nombres. + +Diferencia con `dev/issues/`: +- Issues = bugs / features de implementacion. +- Flows = trabajos reutilizables que cruzan varias apps. + +## Sintaxis + +``` +/flow create # nuevo flow desde template, ID auto +/flow list # tabla resumen +/flow show # imprime contenido + acceptance % +/flow status # status + acceptance % + ultima run +/flow done [--notes "..."] # cierra flow (status=done, mueve a completed/) +/flow run # fase 2 — runner automatizado (NO IMPLEMENTADO) +``` + +## Implementacion por subcomando + +### `create ` + +Pasos: +1. Valida `` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error. +2. Comprueba que no existe ya: `ls dev/flows/*-.md`. Si existe, error. +3. Calcula siguiente ID libre: + - `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1` + - Suma 1, zero-pad a 4 digitos. +4. Lee `dev/flows/template.md`. +5. Sustituye ``, `NNNN`, `YYYY-MM-DD` (hoy). +6. Escribe `dev/flows/NNNN-.md`. +7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc). +8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance". + +### `list` + +Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app ` filtra por app. + +Tambien anade columna `Accept%` calculada desde body: +- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`. +- `% = checked / total * 100` redondeo entero. + +### `show ` + +`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error. + +### `status ` + +Imprime resumen del frontmatter + acceptance %: + +``` +=== flow 0001 === +name: hn-top-stories +status: pending +risk: low +priority: high +apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots +acceptance: 2/6 (33%) +updated: 2026-05-16 + +Pending checks: +- [ ] Recipe creada y validada +- [ ] DAG corre OK 2 veces consecutivas via scheduler +- [ ] data_factory.runs tiene >=2 entries +- [ ] Schema extraido cubre 6/6 fields +``` + +### `done [--notes "..."]` + +Pasos: +1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente". +2. Edita frontmatter: `status: done`, `updated: `. +3. Si `--notes`, append a seccion `## Notas`. +4. `git mv dev/flows/NNNN-.md dev/flows/completed/`. +5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred). + +### `run ` — FASE 2 (NO IMPLEMENTADO AUN) + +Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.` + +Diseño futuro: +- Parsea `## Flow` en pasos. +- Cada paso tipo `function: ` -> ejecuta `./fn run `. +- Cada paso tipo `cmd: ` -> ejecuta subprocess. +- Texto libre -> "MANUAL: " + pause user input. +- Persistencia ejecuciones en `dev/flows/runs/-.jsonl`. +- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.). + +## Conventions + +- Numeracion 0001+, propia (no comparte con `dev/issues/`). +- Status: `pending | running | done | failed | deferred`. +- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales). +- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra. +- Acceptance es la fuente de verdad del progreso. + +## Output style + +Caveman. Tablas markdown. Sin emojis. Sin verbosidad. + +Errores: 1 linea con el problema + sugerencia. + +## Ejemplos + +``` +/flow create reddit-sentiment-tracker +# crea dev/flows/0008-reddit-sentiment-tracker.md +# anade fila a INDEX + +/flow list --pending +# muestra solo flows no cerrados + +/flow status 0001 +# acceptance 0/6, todos los checks pendientes + +# Tras correr el flow manualmente: +# editas el .md, marcas [x] los checks completados +/flow status 0001 +# acceptance 6/6 +/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta" +# mueve a completed/, marca status=done +``` diff --git a/.claude/commands/fn_claude.md b/.claude/commands/fn_claude.md index 18d2670b..e752f2ee 100644 --- a/.claude/commands/fn_claude.md +++ b/.claude/commands/fn_claude.md @@ -152,6 +152,20 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo: cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose ``` +### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6) + +Por cada funcion creada con exito, llama: + +```bash +bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "" "" +``` + +El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-](reference_fn_.md) — `. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup. + +`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo: +- fn_id: `parse_http_log_go_infra` +- purpose: "parsea log Apache/Nginx a struct; pure" + Reporta: - N funciones nuevas creadas (con IDs) - N proposals nuevas en `registry.db.proposals` diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md new file mode 100644 index 00000000..5aa0a386 --- /dev/null +++ b/.claude/commands/issue.md @@ -0,0 +1,93 @@ +--- +description: "Gestiona issues del registry en dev/issues/. Subcomandos: list, show, status, board, dep, roadmap, tag, done, stale, create. Frontmatter YAML canonico (issue 0100)." +--- + +# /issue — Gestionar issues del registry + +Issues viven en `dev/issues/NNNN-.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags). + +Allowlists en `dev/TAXONOMY.md` (no inventar valores). + +Diferencia con `dev/flows/`: +- **Issues** = bugs, features, refactors, chores, epics de implementacion. +- **Flows** = casos de uso end-to-end multi-app. + +## Sintaxis + +``` +/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN] +/issue show NNNN +/issue status NNNN # acceptance % + estado deps +/issue board # kanban pendiente/in-progress/bloqueado/done +/issue dep NNNN # arbol bloquea/depende +/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...) +/issue tag NNNN +X -Y # mantenimiento tags/domain +/issue done NNNN # mueve a completed/, valida deps +/issue stale [--days 30] +/issue create --type T --domain D [--prio P] [--depends NNNN] +``` + +## Implementacion + +**Fase 1 (manual via Claude):** + +El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla. + +```python +import yaml, pathlib, re +issues = [] +for f in pathlib.Path("dev/issues").glob("*.md"): + if f.name in {"README.md", "template.md"}: continue + txt = f.read_text() + m = re.match(r"^---\n(.*?)\n---", txt, re.S) + if not m: continue + fm = yaml.safe_load(m.group(1)) or {} + fm["_path"] = str(f) + issues.append(fm) +# filter + print +``` + +**Fase 2 (cuando 0101 dev_console exista):** + +Cada subcomando se mapea a `./apps/dev_console/dev_console issue $ARGS`. + +## Subcomandos clave + +### `list` + +Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags. + +### `show NNNN` + +Read directo del .md + render del frontmatter como tabla + body como markdown. + +### `status NNNN` + +Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`. + +### `board` + +Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`. + +### `roadmap NNNN` + +Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic". + +### `done NNNN` + +1. Lee frontmatter. +2. Verifica todos `depends` cerrados (sino, error). +3. Cuenta `## Acceptance` 100% (sino, error). +4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`. +5. Actualiza `status: completado` + `updated: today`. + +### `create --type T --domain D` + +Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado. + +## Reglas + +- Domain debe estar en `dev/TAXONOMY.md` allowlist. +- Scope/type/priority idem. +- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`). +- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite). diff --git a/.claude/commands/work.md b/.claude/commands/work.md new file mode 100644 index 00000000..75e54d29 --- /dev/null +++ b/.claude/commands/work.md @@ -0,0 +1,67 @@ +--- +description: "Vista cross-cutting de issues + flows. Subcomandos: today, weekly, search, dashboard. Mezcla los dos universos en una lista priorizable." +--- + +# /work — Vista cross-cutting issues + flows + +Issues = trabajo de implementacion. Flows = casos de uso multi-app. `/work` los muestra juntos para responder "que hago ahora" sin saltar entre dos sitios. + +## Sintaxis + +``` +/work today # top items prio alta + deps satisfechas (issues + flows) +/work weekly # review semanal: closed vs planeados +/work search "texto" # FTS sobre issues + flows + completed +/work dashboard # JSON consumible por tab Work (issue 0102) +``` + +## Implementacion + +**Fase 1 (manual via Claude):** + +El agente lee `dev/issues/*.md` + `dev/flows/*.md`, parsea frontmatter YAML, ordena por: + +1. `priority: alta` primero. +2. `status: pendiente` con `depends` todos `completado` (no bloqueados). +3. Items con DoD/Acceptance >=80% (a punto de cerrar). +4. Fecha `updated` mas reciente. + +Imprime tabla unificada: + +``` +KIND | ID | TITLE | PRIO | STATUS | NEXT STEP +issue| 0099 | datahub app launcher | alta | pendiente | revisar deps +flow | 0001 | hn-top-stories | high | pending | cerrar DoD user-facing +issue| 0100 | migrate issue frontmatter | alta | pendiente | ejecutar pipeline +... +``` + +**Fase 2 (cuando 0101 dev_console exista):** + +`./apps/dev_console/dev_console work $ARGS`. + +## Subcomandos + +### `today` + +Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola". + +### `weekly` + +Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: ` -> ratio in/out. + +### `search "texto"` + +`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`. + +### `dashboard` + +Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura: + +```json +{ + "issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]}, + "flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}], + "telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N} +} +``` diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index ddbcedd1..6d0e71bc 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -34,3 +34,5 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 | | 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/.md` para desbloquear clusters de funciones en un read. Issue 0086 | | 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 | +| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 | +| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 | diff --git a/.claude/rules/autonomous_loop.md b/.claude/rules/autonomous_loop.md new file mode 100644 index 00000000..4ffcc330 --- /dev/null +++ b/.claude/rules/autonomous_loop.md @@ -0,0 +1,75 @@ +## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069 + +`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/`, NUNCA merge a master. + +### Cuando se invoca + +- Skill `/autonomous-task ` (humano lanza explicitamente). +- Cron / Dagu (planificable; no implementado por defecto). +- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita. + +### Reglas duras + +1. **Sandbox obligatorio**: rama `auto/-`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master. +2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`. +3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen: + - `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable. + - `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano). + - `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`. +4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`. +5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h. +6. **Idempotencia**: re-ejecutar `/autonomous-task ` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`). +7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar. +8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`. +9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT. +10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main. + +### Estructura task_run + +Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`. + +### Fases por iteracion + +``` +loop: + 1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada + 2. fn-executor - corre build + tests + smoke + 3. fn-recopilador - audita operations.db de la app + 4. fn-analizador - corre e2e_checks (registra e2e_runs) + 5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit. + 6. SI no progreso N iteraciones -> abort. status=stalled. + 7. fn-mejorador - crea proposals desde fallos + 8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1. +``` + +### Output al humano + +``` +=== /autonomous-task 0068 === +task_run_id: run_e2e_a1b2c3 +branch: auto/0068-e2e-validation +iterations: 4 +status: done +checks_pass: 8/8 +proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003) +proposals_skipped: 1 (refactor — needs human review) +PR: https://gitea.../pulls/42 +``` + +### Anti-patrones + +| Anti-patron | Por que es malo | +|---|---| +| Mergear `auto/` a master sin PR + humano | Salta gate, riesgo de regresion | +| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision | +| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review | +| Bucle infinito sin watchdog | Coste descontrolado de tokens | +| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria | +| Override de paths protegidos via env var | Bypass de seguridad | + +### Relacion con otras reglas + +- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate. +- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD. +- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR. +- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor. diff --git a/.claude/rules/cpp_apps.md b/.claude/rules/cpp_apps.md index e2c295bf..515ebf49 100644 --- a/.claude/rules/cpp_apps.md +++ b/.claude/rules/cpp_apps.md @@ -20,14 +20,14 @@ Razones: Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`. -### 1. Ubicacion +### 1. Ubicacion (issue 0096 estandarizada) | Caso | Donde vive | |---|---| -| App independiente | `cpp/apps//` | +| App independiente | `apps//` | | App de un proyecto | `projects//apps//` | -NUNCA en `cpp/apps//` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. +NUNCA en `cpp/apps//` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. ### 2. Estructura minima @@ -189,20 +189,105 @@ WMs). Activado por defecto, sin opt-in: con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame). 2. **Per-frame viewport sync** al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente. -3. **Win32 WndProc subclass** (`#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). +3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa + `WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada + drag. El subclass se instala en la ventana principal Y en cada HWND + secundario que el backend de ImGui crea cuando un panel se arrastra fuera + del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta + abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` + + `glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag + native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer + existente). El flag `g_in_sizemove` es global a proposito: una sola + sesion de sizemove externo pausa todo el render para que ninguna ventana + compita con el OS. -Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases: + Estado del subclass: + - `g_subclassed` = `unordered_map`. Chain a la proc + original via `CallWindowProcW`. + - `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa). + - Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada + `pio.Viewports[i]->PlatformHandle` nuevo. + - `uninstall_sizemove_subclass_all()` restaura cada HWND al exit. + +#### Iconified main no pierde paneles flotantes (2026-05-16) + +El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO +el frame loop. Con multi-viewport activo eso significa que +`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de +refrescar los viewports secundarios — los floating panels aparecen congelados +o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta +viewports secundarios primero; si hay alguno, fall-through al frame normal +(la swap del main HWND minimizado es harmless, los contexts GL secundarios +siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`. + +#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16) + +WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`): + +- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` + + `PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo. +- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/ + BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite + `PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo. + +Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a +cada viewport flotante porque el subclass per-frame ya cubre todos los HWND. +El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa +render → cero jitter automatico, mismo contrato que el title-bar drag. + +**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut +UI. Aceptable porque Alt-modifier en clicks UI es muy raro. + +#### Title-bar-only move para ImGui windows (2026-05-16) + +`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico +para viewports secundarios: un viewport flotante = OS window borderless con +UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas +arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport +entero, el OS window sigue al cursor sin modifier. Con el flag, floating +panels obedecen el contrato "solo header arrastra" (igual que main que tiene +title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido +antes por el subclass). + +#### Test observability — `fn::internal::*` (2026-05-16) + +Counters monotonicos para validar el subclass desde tests headless, +zero-cost en prod: + +```cpp +namespace fn::internal { + int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE + int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido + int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido + int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN + void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests +} +``` + +En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan +pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el +modal de Windows. Path real en prod sigue posteandolos. + +Tests: `apps/altsnap_jitter_test/` corre seis fases: - `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. + burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el + HWND principal, asserta que `render()` no se llama durante el bracket. +- `p3.secondary` (Windows): fuerza viewport secundario + (`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket + sobre el. Valida que el subclass per-viewport tambien pausa el render. +- `p4.minimize` (Windows): state machine 4 steps — captura + `IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow + + glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`. +- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` + + `SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta + `alt_rmb_resize_count` incrementa. +- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta + `alt_lmb_move_count` incrementa. -Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`. +Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh && +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 @@ -261,3 +346,115 @@ de antes: `imgui.ini` es la unica fuente. - App headless / capture mode: `cfg.auto_layouts = false`. - Cambiar nombre del archivo: `cfg.auto_layouts_db = ".db"` (relativo a `local_files/`). + +### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16 + +Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en +`/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro +`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si +`WIN32` + existe `/appicon.ico`, genera un +`_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con +`IDI_ICON1 ICON ""` y lo anade a `add_executable`. El compilador RC +(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`) +lo enlaza al `.exe` como recurso `.rsrc`. + +Verificar: `x86_64-w64-mingw32-objdump -h .exe | grep rsrc` debe +mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara +`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`). + +#### Crear `.ico` para una app nueva + +Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de +`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`, +`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor +legibilidad a 16/24px. + +Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor +sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una +linea por app: + +```python +from infra import generate_app_icon +generate_app_icon( + phosphor_icon_name="chart-bar", + accent_hex="#0ea5e9", + out_ico_path="apps/chart_demo/appicon.ico", +) +``` + +Mapping vive en el frontmatter de cada `app.md` C++: + +```yaml +icon: + phosphor: "chart-bar" + accent: "#0ea5e9" +``` + +Regeneracion batch via pipeline del registry — escanea `app.md`s y compone +`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su +`app.md` y lanzar: + +```bash +./fn run regenerate_app_icons # todas +./fn run regenerate_app_icons chart_demo # solo una +``` + +Convenciones: +- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`). +- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700 + funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.). +- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado. +- **Glyph color**: siempre blanco sobre el fondo accent. + +Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/` +con `ls | grep ` antes de inventar — 1512 disponibles. + +#### Re-deploy tras cambiar icono + +```bash +# 1. Editar icon: en apps/chart_demo/app.md y regenerar +./fn run regenerate_app_icons chart_demo +# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md) + +# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc) +./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build +``` + +Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras +desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer. + +#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16) + +Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts — +pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el +**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a +menos que adjuntemos el recurso al HWND en runtime. + +`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`: + +```cpp +HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101), + IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), LR_SHARED); +HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED); +SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar +SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar +SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall); +SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig); +``` + +Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado +(`101 ICON "/appicon.ico"`). Si la app no tiene `appicon.ico`, el +`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono +GLFW por defecto (sin error). + +Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que +instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre +cada HWND secundario nuevo. Floating panels dragged-out heredan el icono +sin codigo extra en la app. + +Cache shell: el pipeline `redeploy_cpp_app_windows` llama +`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca +`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar +a que detecte el cambio por timestamp. Si Explorer sigue mostrando el +icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer. diff --git a/.claude/rules/fn_doctor.md b/.claude/rules/fn_doctor.md index 38251bd6..a68171f4 100644 --- a/.claude/rules/fn_doctor.md +++ b/.claude/rules/fn_doctor.md @@ -20,10 +20,18 @@ fn doctor sync # Solo drift pc_locations BD vs disco local fn doctor uses-functions # Solo audit imports reales vs uses_functions fn doctor unused # Solo funciones huerfanas del registry fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado) + # + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio) fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts ``` +`fn doctor cpp-apps` produce dos secciones: +1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui. +2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline: + - `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion. + - `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK. + - silencio: 0 BeginTable inline (limpio o completamente migrado). + ### Mapeo subcomando → funcion del registry | Subcomando | Funcion | @@ -33,7 +41,8 @@ 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` | +| `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` | +| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) | 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. @@ -64,6 +73,8 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable | `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio | | `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {".log", 1}` antes de `fn::run_app` | | `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano | +| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` | +| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline | | Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` | ### Para agentes diff --git a/.claude/rules/ids_naming.md b/.claude/rules/ids_naming.md index d417cf23..85870871 100644 --- a/.claude/rules/ids_naming.md +++ b/.claude/rules/ids_naming.md @@ -1,3 +1,35 @@ -IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). +## ids_naming — formato predictible -Nombres de funciones en snake_case. Tipos en PascalCase para Go. +IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087. + +### Reglas + +1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation. +2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo). +3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md. +4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`). + +### Verbos canonicos (allowlist) + +Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente. + +`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, start, stop, kill, restart, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate` + +### Excepciones + +- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita. +- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`. +- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `_`. Validator salta el check de verbo si `kind=component`. + +### Validator + +`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si: +- name no es snake_case. +- name no contiene verbo (excepto component/type). +- domain no esta en lista canonica. + +Error tipico: +``` +naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md. +naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first. +``` diff --git a/.claude/scripts/append_fn_to_memory.sh b/.claude/scripts/append_fn_to_memory.sh new file mode 100755 index 00000000..b0652456 --- /dev/null +++ b/.claude/scripts/append_fn_to_memory.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor +# creates a new registry function. Idempotent: skips if id already present. +# Used by /fn_claude step 5b (issue 0087, pieza 6). +# +# Usage: append_fn_to_memory.sh "" + +set -euo pipefail + +FN_ID="${1:-}" +PURPOSE="${2:-}" + +if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then + echo "usage: append_fn_to_memory.sh " >&2 + exit 2 +fi + +MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}" +MEM_FILE="$MEM_DIR/MEMORY.md" + +[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; } +[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; } + +# Per-function reference file slug +SLUG="reference_fn_${FN_ID}.md" +REF_FILE="$MEM_DIR/$SLUG" + +# Idempotency: if already linked in MEMORY.md, exit 0 +if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then + echo "already in MEMORY.md: $FN_ID" + exit 0 +fi + +# 1. Create reference memory file +cat > "$REF_FILE" <> "$MEM_FILE" + +echo "appended: $FN_ID -> $MEM_FILE" diff --git a/.gitignore b/.gitignore index e9feeedc..07e1d17f 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,10 @@ broken_paths.txt imgui.ini prompts/ kotlin/functions/ui/ + +# Module versioning auto-generated headers (written by `fn index`, issue 0097) +**/version_generated.h +**/app_modules_generated.h + +# Issue migration backups (0100) +dev/issues/.backup_pre_* diff --git a/CHANGELOG.md b/CHANGELOG.md index 121dd009..7bf07a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,46 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar ## [Unreleased] +## 2026-05-16 + +### Added + +- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `` final con `` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`. + +### Fixed + +- **`dag_engine` steps `function:` fallando con `error: function "" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix: + - Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`). + - `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`. + - `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio. + +### Added + +- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `` desplegado. + - Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop. + - Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256). + - Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `/appicon.ico` y genera `_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`). + - Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`. + - Convencion documentada en `.claude/rules/cpp_apps.md §11`. + +- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame). +- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND). +- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes. +- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal. +- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows. +- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`. + +### Changed + +- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui). +- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps//` (canonical issue 0096) ademas de `cpp/apps//` (legacy) y `projects/*/apps//`. Fix retroactivo: `./fn run compile_cpp_app ` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`. + +### Notes + +- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos. +- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`). +- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion. + ## 2026-05-14 ### Added diff --git a/altsnap_jitter_test.log b/altsnap_jitter_test.log new file mode 100644 index 00000000..bda6ca52 --- /dev/null +++ b/altsnap_jitter_test.log @@ -0,0 +1,4 @@ +[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test +[2026-05-15 23:51:44.017] [INFO] app exit +[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test +[2026-05-15 23:52:48.135] [INFO] app exit diff --git a/apps/dag_engine/README.md b/apps/dag_engine/README.md new file mode 100644 index 00000000..8fd9ddfd --- /dev/null +++ b/apps/dag_engine/README.md @@ -0,0 +1,360 @@ +# dag_engine — Guia de uso + +Motor de DAGs propio (reemplazo de Dagu). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`). + +Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**. + +--- + +## 1. Donde viven los DAGs + +| Path | Que | +|---|---| +| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). | +| `~/dagu/dags/` | Path por defecto del binario si no se pasa `--dags-dir`. Vacio tras la migracion del 2026-05-15 (ver tag `dagu_pre_removal`). | +| `~/backups/dagu_pre_removal_.tar.gz` | Backup completo de la carpeta dagu antes de borrar. | + +Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`: + +```ini +ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \ + --port 8090 \ + --dags-dir /home/lucas/fn_registry/apps/dag_engine/dags_migrated \ + --db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \ + --scheduler +``` + +Y reload + restart: +```bash +systemctl --user daemon-reload +systemctl --user restart dag_engine.service +``` + +--- + +## 2. Anadir un DAG nuevo (workflow) + +### Paso a paso + +1. **Crear YAML** en `apps/dag_engine/dags_migrated/.yaml` (ver formato en seccion 3). +2. **Validar** sin ejecutar: + ```bash + ./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/.yaml + ``` + Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico). +3. **Probar ejecucion manual** una vez: + ```bash + ./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/.yaml + ``` +4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir): + ```bash + systemctl --user restart dag_engine.service + journalctl --user-unit dag_engine.service -n 30 --no-pager + ``` + Busca la linea `[scheduler] ticker started for ()` en los logs. +5. **Verificar en frontend**: + - C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece. + - Web: `http://localhost:8090`. + +### Disparo manual desde curl o frontend + +```bash +curl -X POST http://127.0.0.1:8090/api/dags//run +``` + +Devuelve `{"dag":"","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`. + +--- + +## 3. Formato YAML + +dag_engine es **compatible con el formato Dagu**. Los YAMLs heredados de `~/dagu/dags/` validan sin modificaciones. + +### Ejemplo completo + +```yaml +name: my_pipeline +description: "Pipeline diario que importa CSV y actualiza Metabase." +group: finanzas # opcional, agrupa DAGs en listados +type: graph # opcional: graph (default) | chain +tags: [daily, csv, metabase] # opcional, filtros en la UI + +# Variables de entorno (heredadas por todos los steps). +env: + - DATA_DIR: /home/lucas/data + - SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host + +# Cron schedule. Puede ser string o lista. +schedule: + - "0 9 * * *" # 09:00 todos los dias + - "0 21 * * 5" # 21:00 viernes (segundo trigger) + +# Working dir + shell por defecto para todos los steps. +working_dir: /home/lucas/fn_registry +shell: /bin/bash +timeout_sec: 1800 # 30 min para todo el DAG + +steps: + - name: ingest + description: "Descarga CSV." + command: ./bash/functions/pipelines/ingest_csv.sh + timeout_sec: 300 # 5 min para este step + env: + - SOURCE_URL: https://example.com/data.csv + + - name: transform + description: "Limpieza y agregacion." + script: | + #!/usr/bin/env python3 + import pandas as pd + df = pd.read_csv("$DATA_DIR/raw.csv") + df.to_parquet("$DATA_DIR/clean.parquet") + depends: [ingest] # debe terminar OK antes + retry_policy: + limit: 2 # reintentos en caso de fallo + interval_sec: 60 + + - name: load_metabase + command: ./bash/functions/metabase/refresh_dashboard.sh + depends: [transform] + continue_on: + failure: true # no aborta el DAG aunque falle + + - name: notify + command: ./bash/functions/io/slack_send.sh "pipeline OK" + depends: [load_metabase] + +# Hooks de ciclo de vida. +handler_on: + success: ./bash/functions/io/notify_success.sh + failure: ./bash/functions/io/notify_failure.sh + exit: ./bash/functions/io/cleanup.sh +``` + +### Campos del DAG (top-level) + +| Campo | Tipo | Default | Que | +|---|---|---|---| +| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. | +| `description` | string | "" | Texto libre, aparece en la UI. | +| `group` | string | "" | Agrupa DAGs en listados. | +| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. | +| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. | +| `shell` | string | `/bin/sh` | Shell para `command:`. | +| `env` | list/map | [] | Variables de entorno DAG-wide. | +| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. | +| `steps` | list | (obligatorio) | Pasos del DAG (>=1). | +| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. | +| `tags` | list[string] | [] | Filtros en la UI. | +| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. | + +### Campos de cada step + +| Campo | Tipo | Default | Que | +|---|---|---|---| +| `name` | string | (obligatorio) | Identificador del step dentro del DAG. | +| `id` | string | "" | Override del id auto-generado. | +| `description` | string | "" | Texto libre. | +| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). | +| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. | +| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run ` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. | +| `args` | list[string] | [] | Args extra para `command` o para la `function`. | +| `shell` | string | hereda | Override del shell. | +| `dir` / `working_dir` | string | hereda | Working dir para este step. | +| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. | +| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). | +| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. | +| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. | +| `retry_policy.limit` | int | 0 | Reintentos. | +| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. | +| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. | +| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). | +| `tags` | list[string] | [] | Tags por step (UI). | + +### Function steps (coherencia con el registry) + +Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end. + +```yaml +steps: + - name: audit_capabilities + function: audit_capability_groups_go_infra + args: ["--json"] + description: "Audita drift entre tags de capability group y paginas madre" +``` + +Ventajas vs `command: ./fn run ...`: + +- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable). +- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente). +- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step. +- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error. +- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`. +- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad. +- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`. + +Ejemplo completo: `~/.dagu/dags/example-fn-call.yaml`. + +### Cron schedule + +5 campos clasicos: `min hour dom mon dow`. Ejemplos: + +| Expresion | Significado | +|---|---| +| `0 9 * * *` | Todos los dias a las 09:00 | +| `*/15 * * * *` | Cada 15 minutos | +| `0 */6 * * *` | Cada 6 horas en punto | +| `0 9 * * 1-5` | 09:00 lunes-viernes | +| `0 21 * * 5` | 21:00 viernes | + +Multiples cron en `schedule:` -> el DAG dispara por cada uno. + +--- + +## 4. Comandos CLI + +```bash +./dag_engine run # ejecuta un DAG ad-hoc +./dag_engine list [dir] # lista DAGs con schedule + ultimo status +./dag_engine status [dag_name] # historial de ejecuciones +./dag_engine validate # parse + validate (no ejecuta) +./dag_engine server # arranca HTTP + WS hub + frontend embebido +``` + +Flags del `server`: + +| Flag | Default | Que | +|---|---|---| +| `--port` | 8090 | Puerto HTTP. | +| `--dags-dir` | `~/dagu/dags` | Dir scaneado para YAMLs. | +| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. | +| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. | + +--- + +## 5. Que hacer si algo falla + +### 5.1. El DAG no aparece en la UI + +**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista. + +| Causa | Diagnostico | Fix | +|---|---|---| +| YAML invalido | `./dag_engine validate ` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). | +| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_migrated/` | Renombrar a `.yaml`. | +| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. | +| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — | + +### 5.2. Validation: FAIL + +`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas: + +| Mensaje | Causa | Fix | +|---|---|---| +| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. | +| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. | +| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. | +| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. | +| `unknown depends: ` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). | +| `invalid cron: ` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). | + +### 5.3. El DAG corre pero un step falla + +**Sintoma:** `status: failed` en la UI. + +1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`. +2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`. +3. Errores tipicos: + +| stderr | Causa | Fix | +|---|---|---| +| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. | +| `permission denied` | Script sin `chmod +x`. | `chmod +x