103 Commits

Author SHA1 Message Date
egutierrez fec8ebd4ec merge: origin/master into local 2026-05-27 18:48:28 +02:00
egutierrez f5f05e4624 chore: auto-commit (2 archivos)
- dev/issues/completed/0126-pipeline-launcher-migration-003.md
- dev/proposals_e2e_checks_0121/pipeline_launcher.yaml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:48:14 +02:00
egutierrez 532f3d0ea8 issue(0128): kanban file attachments — PR draft en dataforge/kanban#1 2026-05-27 10:53:13 +02:00
egutierrez fe65c5e527 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00
egutierrez de9bfec498 feat(compile): Wails support in /compile skill
- New helper: deploy_wails_exe_to_windows_bash_infra
  - taskkill + cp build/bin/<app>.exe to Desktop/apps/<app>/
  - cmd.exe /c start RELAUNCHES the app post-deploy (key diff vs cpp)
  - preserves local_files/, copies appicon.ico if present
- New pipeline: compile_wails_app_bash_pipelines
  - resolve_cpp_app_dir (reused) + wails build -platform windows/amd64
  - auto -tags goolm if app declares matrix_crypto_init
  - delegates deploy + relaunch to deploy_wails_exe_to_windows
- /compile skill dispatches by framework:
  - wails.json present -> compile_wails_app (relaunches)
  - CMakeLists.txt present -> compile_cpp_app (no relaunch)

Refs: matrix_client_pc + matrix_admin_panel (issues 0147, 0163)
2026-05-25 03:11:09 +02:00
egutierrez e9c64a4687 chore(issues): close 0166 livekit TURN deploy
Integrated LiveKit TURN deployed on organic-machine.com:
- UDP 3478 + TCP 5349 (not 443 — Traefik HTTP/3 owns it)
- Wildcard cert *.organic-machine.com extracted from Traefik acme.json
- Subdomain turn-matrix-rtc-320bd4.organic-machine.com (wildcard DNS+cert)
- VPS commit f7f5303 in egutierrez/element_matrix_chat

DoD acceptance items requiring real-world CGNAT call testing
deferred to operator (no agent way to test mobile 4G NAT).
2026-05-25 00:46:43 +02:00
egutierrez 70ec825e32 chore(issues): close 0167+0168+0169+0170 livekit hardening bundle
VPS commit: 8eef89b (egutierrez/element_matrix_chat)

- 0167: STUN leak fixed (use_external_ip:false + node_ip hardcoded)
- 0168: UDP range expanded 50000-50200 -> 50000-50500
- 0169: API secret rotated (old key LK44e009c6e92b -> new LK5f6b38bb)
- 0170: livekit.example.yaml refreshed + header comments cleaned

Verification:
- 0 STUN packets to Google during restart (tcpdump 60s window)
- Endpoint /livekit/sfu/ HTTP 200
- LiveKit logs: nodeIP=135.125.201.30, portICERange=[50000,50500]
- Containers livekit + livekit-jwt healthy

New secret stored in pass: matrix/livekit-secret-rotation-2026-05-25
2026-05-25 00:44:15 +02:00
egutierrez 22692c1ed2 feat(matrix): 4 synapse quick wins applied + 6 follow-up issues
Server-side homeserver.yaml on organic-machine VPS:
- encryption_enabled_by_default_for_room_type: invite -> all
- presence.enabled: false (block EDU metadata leak)
- url_preview_enabled: false (block SSRF + IP leak)
- msc4108 rendezvous endpoint uncommented (QR login)

Synapse restarted, /versions shows e2ee_forced.* + msc4108 unstable
features active. Backup at synapse_data/homeserver.yaml.bak.1779659423.

Issues opened for remaining gaps:
- 0165 LUKS for media_store (at-rest encryption)
- 0166 LiveKit TURN deploy (NAT traversal gap)
- 0167 STUN leak to Google (hardcode external_ip)
- 0168 UDP range expand 200 -> 500
- 0169 LIVEKIT_SECRET rotation (audit exposure)
- 0170 livekit.example.yaml rename hygiene

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:53:37 +02:00
egutierrez d128ad89ac feat(matrix-mas): 3 helpers for matrix_client_pc (issue 0147)
- mas_oidc_loopback_go_infra: OAuth2 PKCE + loopback HTTP for desktop login
- keyring_token_store_go_infra: persist OAuth tokens in SO keychain
- matrix_client_init_go_infra: init mautrix.Client from access_token + whoami

Plus go.work workspace including matrix_client_pc sub-repo for shared
import path during dev. All 3 fns tagged matrix-mas capability group.

Tests: TestMasOidcLoopback (15 cases), TestKeyringTokenStore (5 cases,
SKIP on headless), TestMatrixClientInit (6 cases) — all green/skip.

Refs: dev/issues/0147-matrix-client-pc-scaffold.md
Refs: dataforge/matrix_client_pc commit f28c2b1
2026-05-24 23:23:49 +02:00
egutierrez bd9f0d8437 feat(matrix): MAS migration helpers + 2 flows + 15 issues + capability group
Helper functions (matrix-mas capability group):
- mas_client_register_bash_infra: register/sync OAuth clients via mas-cli
- mas_syn2mas_migration_bash_infra: dry-run + apply user migration to MAS
- synapse_msc3861_enable_go_infra: edit homeserver.yaml MSC3861 block (with diff)
- wellknown_oidc_patch_go_infra: patch well-known JSON with msc2965.authentication
- synapse_login_flows_check_go_infra: health-check post-migration login flows

Flows + issues for custom Matrix clients (PC + Android):
- 0010 matrix-client-pc: Wails + React+Mantine (issues 0147-0153)
- 0011 matrix-client-android: Kotlin + Compose (issues 0154-0161)
- 0162 enable MAS as auth provider (Synapse delegate) — EXECUTED on VPS
- 0163 custom admin panel propio (sustituye synapse-admin)

Production state (organic-machine.com):
- Synapse migrated SQLite -> Postgres
- MSC3861 active, password_config disabled
- 21 users + 41 access_tokens migrated via syn2mas
- 4 MAS clients registered (element, matrix_pc, matrix_android, admin_panel)
- synapse-admin container removed + Coolify route deleted
- well-known patched with org.matrix.msc2965.authentication

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:53:33 +02:00
egutierrez 207c08c3b7 merge(0133): columnar snapshot + string pool + reader rewire (1+2+3)
Foundation (ea5c94fc) + reader rewire (01bc2aeb).

- ColumnSnapshot per col (i64/f64/str_ids) + StringPool per-State
- compute_visible_rows filter/sort uses snapshot direct numeric/id compare
- StringPool realloc-crash fix (reserve before emplace_back)
- Pool staleness sentinel (rebuild when string_pool.size() drift)
- High-cardinality cap (>2048 unique → skip interning, fallback raw)

API publica intacta. Bench 100k sort_numeric +131% vs baseline.
text_editor_smoke RED preexisting unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:22:38 +02:00
egutierrez 01bc2aeb14 feat(0133-3): wire filter/sort readers to columnar snapshot
Change 3 of issue 0133 — rewire compute_visible_rows, filter eval,
and sort comparators to read from the SnapshotCache when available.

Hot paths rewired:
- compute_visible_rows (overload with snap): filter eval uses
  compare_snap (fast i64/f64 numeric compare for Int/Float cols;
  id-compare for low-cardinality string Eq/Neq; raw cells fallback
  for Contains/StartsWith/EndsWith).
- Sort comparators: direct i64/f64 array compare for Int/Float cols
  (goto sort_done skips string fallback); string sort uses uint32_t
  id compare with pool lookup only on mismatch.
- Stage>0 filter/sort: same snapshot overload.

Materialization paths (build_so, s0_backing, mat_backing, config popup)
kept on raw cells — they copy into std::string anyway, no benefit from
snapshot and snprintf-per-cell was 2M extra calls per frame.

Bug fixes (required for correctness):
1. StringPool::intern() realloc safety: force reserve before
   emplace_back so string_view keys in the map never go dangling.
2. SnapshotCache::pool_size_built sentinel: detects when a new State
   is created with an empty pool but same cells pointer (begin_scenario
   pattern). Prevents str_ids from indexing into an empty pool (SIGSEGV).
3. Cardinality cap (2048 uniques / 25% sample): high-cardinality string
   cols (timestamps-as-strings, UUIDs, names) skip interning — str_ids
   stays empty and compare_snap falls back to raw cells. Prevents 30MB+
   pool bloat that hurt cache for filter/sort on other cols.

Bench delta vs baseline (100k rows, LIBGL_ALWAYS_SOFTWARE=1):
  linear_scroll: 16.0 -> 15.5 fps p50  (-3%, baseline already FAIL)
  filter_like:   59.7 -> 56.0 fps p50  (-6%, still PASS at 56fps)
  sort_numeric:   3.9 ->  9.0 fps p50 (+131%, snapshot i64 sort)
  color_rule:    15.2 -> 14.8 fps p50  (-3%, baseline already FAIL)

Build: green for all 10 available Linux consumers (text_editor_smoke
linker failure is preexisting, not caused by this change).

API public intact. TableEvent.row indexing TableInput preserved.
Pointer-identity invalidation preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:21:09 +02:00
egutierrez 9ec7751f6f docs(0133): MIGRATION.md + growth log placeholder + drift fix
- modules/data_table/MIGRATION.md: porting guide + release checklist 1.0.0-stable
- data_table.md: growth log entry commented for post-gate bump
- data_table.md: fix error_type Go remnant ("error_go_core" -> "") in C++ module
- cpp/CMakeLists.txt: SQLite3 optional dep for data_table_bench (cross-windows)
- agent_cleanup_worktree.go: !windows build tag (uses unix-only syscalls)
- dev/issues/0133-cpp-data-table-10m-rows.md: issue tracking

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:56:14 +02:00
egutierrez fef86250a0 fix(0132): terminal_panel black bg + prompt input + cross-platform demos + e2e
terminal_panel.cpp:
  - BeginChild con PushStyleColor(ChildBg, negro) + PushStyleColor(Text, gris claro)
  - PushStyleVar(WindowPadding, 8/6px) para padding terminal real
  - Input prompt siempre visible cuando readonly=false
  - Prefijo "$ " antes del InputText (TextUnformatted + SameLine)
  - BeginDisabled() cuando el shell esta cerrado (en vez de ocultar el widget)
  - Calculo de child_h reserva exactamente GetFrameHeightWithSpacing+6 para el prompt

cpp/tests/e2e/test_terminal_panel_e2e.py (nuevo):
  - 4 asserts: PNG existe, no todo-blanco, region oscura >= 30%, pixels no-negros >= 0.3%
  - Lanza primitives_gallery --capture, busca el binario Linux o Windows.exe automaticamente
  - Skip graceful si no hay GL ni binario (WSL/CI headless)
  - 4/4 pasan en Linux con LIBGL_ALWAYS_SOFTWARE=1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:48:42 +02:00
egutierrez 472b6092bb feat(0133): register data_table_bench in cpp/CMakeLists.txt
Adds the add_subdirectory block for apps/data_table_bench so the build
system picks it up. The app itself lives in its own sub-repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:38:17 +02:00
egutierrez ea5c94fc8a feat(0133-1+2): columnar snapshot + string interning in data_table
Change 1 — Columnar Snapshot Internal:
- Add ColumnSnapshot struct (type + str_ids/i64/f64 per column) in data_table_internal.h
- Add SnapshotCache struct with pointer-identity sentinel (last_cells_ptr)
- Add SnapshotCache field to UiState singleton
- In render(): rebuild snapshot after join materialization when cells ptr changes
  Uses same pointer-identity pattern as existing stats_last_cells in State
  Int/Float columns parsed once via parse_number; String/Auto interned

Change 2 — String Interning:
- Add StringPool struct (strings + unordered_map<string_view, uint32_t>) to data_table_types.h
- StringPool is per-State (NOT global) for table isolation
- intern(sv) inserts if absent, returns stable uint32_t index
- Cleared + rebuilt on each snapshot rebuild for index coherence
- Add string_pool field to State struct

Documentation:
- Extended header comment in data_table_internal.h describing design,
  StringPool API, invariants (pointer-identity, row→snapshot_row),
  and how stats_last_cells and snapshot coexist independently

Build: fn_module_data_table + tables_qa pass, no new errors (only
pre-existing -Wformat-truncation warnings unrelated to this change).
Public API (data_table.h, TableInput, render() signature) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:35:12 +02:00
egutierrez a8b09ad154 feat(0132): cpp terminal_panel module + ansi_parser
Nuevo modulo reutilizable terminal_panel (fn_term) para ImGui:

Sub-fn ansi_parser_cpp_core (cpp/functions/core/):
- Parser ANSI/VT100 byte-a-byte sin heap allocs por evento
- SGR colores FG/BG 16-color + bold + reset
- Cursor moves CUU/CUD/CUF/CUB + CUP absoluto
- Erase ED(2)/EL(2), CR/LF/BS
- Statemachine 4 estados, thread-unsafe por diseno
- 21 tests unitarios (57 assertions), todos pasan

terminal_panel_cpp_viz (cpp/functions/viz/terminal_panel/):
- terminal_panel.cpp: render ImGui + process_output con list clipper
- terminal_panel_linux.cpp: forkpty + reader thread no-blocking
- terminal_panel_windows.cpp: ConPTY CreatePseudoConsole (SDK >= 17763)
- Scrollback circular configurable (default 5000 lineas)
- Toolbar: clear, copy, reset, scroll-lock + status indicator
- readonly mode: sin input box, send() es no-op
- uses_functions: ansi_parser_cpp_core, logger_cpp_core

Tests:
- test_ansi_parser.cpp: 21 test cases, 57 assertions (PASS)
- test_terminal_panel_smoke.cpp: 3 test cases (PASS: spawn echo hello,
  process exits cleanly, readonly ignores send)

CMake:
- cpp/tests/CMakeLists.txt: add test_ansi_parser + test_terminal_panel_smoke
- primitives_gallery (sub-repo): ver commit separado en apps/primitives_gallery

Pendiente (anti-scope v1):
- Windows ConPTY: stub funcional que compila; join() del reader thread
  via std::thread no implementado (usa CreateThread detached)
- ANSI 256/24-bit color, italics, Unicode wide
- Curses pesados (vim, htop, top) — cursor visible basic solo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:35:11 +02:00
egutierrez 6aa874f2b6 done(0131): agents v0.2 — unified control + uptime/msg_24h + data_table + clear/cache 2026-05-22 23:10:32 +02:00
egutierrez 93352a7780 feat(issues): 0131 agents v0.2 — unified control + uptime/msg_24h + data_table + clear/cache 2026-05-22 22:47:02 +02:00
Egutierrez 0ffae6daa4 feat(0130): kanban_cpp v2 — backend Go + 5 registry parser fns + epic/sub-issues
Registry (issue 0130a):
- 5 fns infra: parse_issue_md, write_issue_md, scan_issues_dir,
  scan_flows_dir, watch_dir_fsnotify
- 3 tipos: Issue, Flow, FsEvent
- Tests round-trip + scan reales + watcher fsnotify (all PASS)
- Capability group 'kanban' nuevo (docs/capabilities/kanban.md)

Apps:
- apps/kanban_cpp/ (sub-repo) — frontend ImGui: board drag-drop,
  flows, filters, detail con CSV editors
- apps/kanban_cpp/backend/ — Go service port 8487: REST + SSE +
  fsnotify watcher, parser bidireccional MD<->SQLite cache

Issues:
- dev/issues/0130-kanban-cpp-v2.md (epic)
- 0130a parser, 0130b backend, 0130c frontend

CMakeLists.txt: add_subdirectory apps/kanban_cpp (registrado por
init_cpp_app scaffolder).

End-to-end verde: backend devuelve 189 issues + 9 flows; PATCH a
/api/issues/{id} reescribe .md (solo frontmatter, body intacto);
frontend --self-test exit 0; tests Go infra 5/5 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:20:15 +02:00
egutierrez 74b58cf0d0 fix(http_request): drop "2>&1" on Windows — CreateProcessW has no shell
POSIX popen routes via /bin/sh -c, so "2>&1" is a shell redirect. On
Windows we use CreateProcessW directly (no shell): curl receives "2>&1"
as a positional arg, treats it as a second URL, and fails with exit 3
"URL rejected: Bad hostname".

Stderr is already merged into the same pipe via STARTUPINFOW.hStdError
on Windows, so the redirect is also unnecessary there. Guard with
#ifndef _WIN32.

Also adds FN_HTTP_DEBUG env var to dump the cmdline + req.url for
future bug triage (zero-cost when unset).

Detected via agents_dashboard.exe --connect-test against
https://agents.organic-machine.com — same .exe with the fix now returns
"OK 11" in <2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:15:37 +02:00
egutierrez 9752fb106a done: 0128 + 0129 — agents_and_robots HTTP API + agents_dashboard C++ ImGui
Both issues delivered end-to-end:

0128 (backend, merged via dataforge/agents_and_robots/pulls/1):
- HTTP daemon in cmd/launcher with apikey Bearer auth + SSE
- LIVE at https://agents.organic-machine.com via Coolify Traefik + LE cert
- systemd Restart=always
- Unified status autodetect fix applied

0129 (frontend, merged via dataforge/agents_dashboard/pulls/1):
- C++ ImGui app in projects/element_agents/apps/agents_dashboard
- 4 panels: Connection / Agents / Logs / Status
- secret_store_cpp_infra new function (DPAPI Windows / XOR Linux)
- Deployed to Windows Desktop, App Hub tarjeta visible

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:58:05 +02:00
egutierrez 8cb0121573 merge: 0129 agents_dashboard scaffold + secret_store_cpp_infra + CMakeLists register 2026-05-22 21:57:04 +02:00
egutierrez 90115270d2 chore: auto-commit (3 archivos)
- cpp/functions/infra/secret_store.cpp
- cpp/functions/infra/secret_store.h
- cpp/functions/infra/secret_store.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:52:37 +02:00
egutierrez 11e6e27ad1 fix(auto): fix secret_store.md YAML — returns/error_type/imports format
- returns: [] (was empty string)
- error_type: error_go_core (was empty, required for impure)
- imports: [list] (was string)
- Removed stale id: field (auto-computed from filename)
- Added output: field for params_schema

fn index now clean: 1324 functions, 45 apps

Co-Authored-By: fn-orquestador <noreply@fn-registry.local>
2026-05-22 21:48:21 +02:00
egutierrez a59b12d467 feat(auto): construir iter 1 — add secret_store_cpp_infra registry function
DPAPI Windows + XOR Linux fallback para almacenar credentials sensibles
en SQLite local. Usado por agents_dashboard para cifrar apikeys.
Incluye encrypt/decrypt/is_strong + base64 helpers.

Issue: 0129
Co-Authored-By: fn-constructor <noreply@fn-registry.local>
2026-05-22 21:42:44 +02:00
egutierrez fe4320af89 chore(auto): construir iter 1 — scaffold agents_dashboard + register in CMakeLists
- init_cpp_app_bash_pipelines scaffold:
  projects/element_agents/apps/agents_dashboard/{main.cpp,CMakeLists.txt,app.md}
- git init dentro del sub-repo (apps_subrepo.md regla)
- Registrado en cpp/CMakeLists.txt (add_subdirectory via _AGENTS_DASHBOARD_DIR)

Co-Authored-By: fn-constructor <noreply@fn-registry.local>
2026-05-22 21:37:06 +02:00
egutierrez f71e0f4c9a chore: remove kanban_cpp app
Gitea repo dataforge/kanban_cpp archived (read-only).
Local apps/kanban_cpp/ deleted, CMake subdir registration removed.
registry.db entry + pc_locations row purged (regenerable via fn index +
manual delete since indexer upserts but does not purge orphaned apps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:30:47 +02:00
egutierrez 46b4385331 feat(issues): 0128 agents_and_robots HTTP API + 0129 agents_dashboard C++ ImGui
0128 (backend, blocks 0129): HTTP daemon en cmd/launcher con apikey Bearer auth, SSE pubsub in-memory para status+logs, TLS via Traefik en agents.organic-machine.com, systemd Restart=always. Scope v0.1 lean: list/start/stop/restart/logs SSE. Send-message + config-edit en v0.2.

0129 (frontend): C++ ImGui agents_dashboard en projects/element_agents/apps/. Panels Connection/Agents/Logs/Status. Persistencia local cifrada DPAPI. Depende de 0128.

Ambos issues con dod_evidence_schema completo (9 + 9 items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:11:30 +02:00
egutierrez 580238b32e feat(infra): auto-commit con 8 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:38:16 +02:00
egutierrez ed767360c1 chore: auto-commit (1 archivos)
- cpp/apps/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:04 +02:00
egutierrez 5bac05ce13 refactor(commands): merge /autonomous-task → /autopilot v2
Doble entrada confusa (incidente 2026-05-19 piloto 0121b: cwd
mutation por Path B inline causo commit a branch incorrecta).

Cambios:
- .claude/commands/autopilot.md: v2 simplificado. SOLO pre-flight
  DoD check + delegate fn-orquestador. Sin Path A/B/C inline.
  Self-Q&A migrado al orquestador. Cero cwd mutation.
- .claude/commands/autonomous-task.md: DEPRECADO. Sustitucion 1:1.
  Sigue funcionando como debug primitive sin DoD check.
- dev/issues/0123: revision — eliminar /flow run y /fix-flow (absorbidos
  por /autopilot v2). Mantener fn-meta-orquestador + fn-priorizador
  + fn doctor issues/flows. Anadir tarea: dar a fn-orquestador soporte
  task_type=flow.

Preferencia humano: 1 sola entrada autopilot, "modo que entra y sigue".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:52:19 +02:00
egutierrez d0ceea6f3d done(0121b): fn doctor e2e-coverage funcionando — 42.22% coverage actual
task_run task_d285372493cce2e6 converged 1 iter / ~4 min.
PR https://gitea-.../dataforge/fn_registry/pulls/3 mergeado.

Verificado en master:
  total=45 with_checks=19 coverage=42.22%
  21 apps con propuesta lista en dev/proposals_e2e_checks_0121/ esperando aplicacion (0121c).
  5 apps sin propuesta aun (wave 4 pendiente).

Desbloquea: 0121c (apply N PRs add_e2e_check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:48:15 +02:00
dataforge 0f905b78e0 merge(0121b): fn doctor e2e-coverage
task_run task_d285372493cce2e6 converged 1 iter / 4 min. 4/4 acceptance.

functions/infra/audit_e2e_coverage.go + .md + _test.go
types/infra/e2e_coverage_report.md
cmd/fn/doctor.go (subcmd e2e-coverage)

Cierra: dev/issues/0121b-fn-doctor-e2e-coverage.md
2026-05-18 23:47:32 +00:00
egutierrez 5c7ff8d761 feat(0121b): audit_e2e_coverage_go_infra + fn doctor e2e-coverage subcmd
- Crea functions/infra/audit_e2e_coverage.go: AuditE2ECoverage(roots) escanea
  app.md recursivamente, detecta e2e_checks: en frontmatter, retorna
  E2ECoverageReport{total, with_checks, missing, coverage_pct}.
- Crea functions/infra/e2e_coverage_report.go: tipo E2ECoverageReport con
  JSON tags (total, with_checks, missing, coverage_pct).
- Crea types/infra/e2e_coverage_report.md: metadata del tipo para registry.
- Crea functions/infra/audit_e2e_coverage.md: documentacion self-contained
  con Ejemplo, Cuando usarla, Gotchas.
- Crea functions/infra/audit_e2e_coverage_test.go: 3 tests (empty, all-covered,
  partial) — todos pasan.
- Edita cmd/fn/doctor.go: agrega case "e2e-coverage" -> doctorE2ECoverage().
  Output text (tabla tabwriter + lista de apps missing) y --json (E2ECoverageReport).

Acceptance verificado:
  fn doctor e2e-coverage --json -> {total, with_checks, missing, coverage_pct} OK
  fn doctor e2e-coverage        -> tabla text OK
  go test ./functions/infra/... -> 3/3 PASS
  fn show audit_e2e_coverage_go_infra -> indexada OK

task_run: task_d285372493cce2e6 iter 1

Co-authored-by: fn-orquestador <noreply@fn-registry>
2026-05-19 01:45:54 +02:00
egutierrez 138f4b2713 chore: auto-commit (9 archivos)
- .claude/commands/autopilot.md
- dev/proposals_e2e_checks_0121/altsnap_jitter_test.yaml
- dev/proposals_e2e_checks_0121/app_hub_launcher.yaml
- dev/proposals_e2e_checks_0121/element_matrix_chat.yaml
- dev/proposals_e2e_checks_0121/footprint_geo_stack.yaml
- dev/proposals_e2e_checks_0121/metabase_registry.yaml
- dev/proposals_e2e_checks_0121/script_navegador.yaml
- dev/proposals_e2e_checks_0121/services_monitor.yaml
- dev/proposals_e2e_checks_0121/tables_qa.yaml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:41:49 +02:00
egutierrez 25425a5fd6 chore: auto-commit (1 archivos)
- .claude/commands/autopilot.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:37:33 +02:00
egutierrez 89441539fa docs(issues): 4 issues de deuda detectada lateral en 0121a
Origen: fn-recopilador design-e2e descubrio 6 bugs durante el design
de propuestas e2e_checks. Agrupados en 4 issues:

- 0124 EPIC dag_engine cleanup (registry.db huerfana + Mantine drift
       + --migrate-only flag — 3 sub-tareas)
- 0125 deploy_server: anadir --db a cmdServe
- 0126 pipeline_launcher: aplicar migracion 003_logs
- 0127 docker_tui: go.work path absoluto rompe portabilidad

Todos relacionados con 0121a. Pueden ser candidatos a /autonomous-task
o /autopilot dependiendo del scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:36:53 +02:00
egutierrez 1d3d2f43b3 feat(0121a): wave 2 e2e_checks proposals (8 apps) + README updated
8 fn-recopilador design-e2e paralelos:
- services_api      (Go service, schema custom operations.db)
- registry_mcp      (Go stdio MCP, JSON-RPC handshake test)
- sqlite_api        (Go service read-only HTTP, query_endpoint)
- registry_dashboard (C++ ImGui, NO Go+React como yo supuse)
- primitives_gallery (C++ build gate de toda API C++ del registry, 44 .cpp)
- pipeline_launcher (Go TUI bubbletea)
- docker_tui        (Go TUI + go-duckdb)
- fn_match          (subcmd ./fn, hook helper, fuzzy match)

13/26 apps cubiertas. README documenta:
- 6 bugs/drift descubiertos lateral (dag_engine x3, deploy_server,
  pipeline_launcher, docker_tui).
- 3 correcciones de mi prompt (yo asumi stacks incorrectos).
- Hallazgos arquitectonicos (primitives_gallery = build gate C++).

Pendiente wave 3 (13 apps) + 0121b + 0121c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:43:09 +02:00
egutierrez 2effb688b0 feat(0121a): wave 1 e2e_checks proposals (5 apps)
5 fn-recopilador design-e2e paralelos sobre mix de stacks:
- deploy_server (Go service)
- registry_api  (Go service + FTS5)
- shaders_lab   (C++ ImGui)
- auto_metabase (Python CLI)
- dag_engine    (Go scheduler + react/vite frontend)

Total: 24 checks propuestos. Bloques YAML listos para pegar al
frontmatter de cada app.md tras revision humana.

Advertencias laterales en README.md:
- dag_engine: registry.db huerfana + pnpm build roto + falta --migrate-only
- deploy_server: --db flag no expuesto en cmdServe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:32:55 +02:00
egutierrez eb30074792 chore: auto-commit (8 archivos)
- .claude/rules/registry_calls.md
- apps/dag_engine/README.md
- apps/dag_engine/app.md
- docs/capabilities/INDEX.md
- docs/capabilities/systemd.md
- docs/execution_standard.md
- dev/proposals_e2e_checks_0121/
- docs/capabilities/backends.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:30 +02:00
egutierrez f8efb7d177 split(0121): epic + 3 sub-issues — design/doctor/apply
0121 era scope demasiado grande para 1 orquestador (batch + new function +
new subcmd + N edits). Split:

- 0121a chore: design-e2e batch (Claude orquesta N fn-recopilador paralelos)
- 0121b feature: audit_e2e_coverage + fn doctor e2e-coverage subcmd
       (tipo feature_app_simple, apto /autonomous-task)
- 0121c chore: aplicar propuestas via N /autonomous-task add_e2e_check

Cada hijo tiene Acceptance verificable + tipo orquestador declarado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:22:23 +02:00
egutierrez f428f2c82a done(0120): orquestador piloto verde — PR mergeado en chart_demo
task_run task_b3069559f34415c3 converged en 2 iter / 2m28s.
PR https://gitea-.../dataforge/chart_demo/pulls/1 mergeado a master.

Desbloquea: 0121, 0122, 0123.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:20:38 +02:00
egutierrez f36d091704 docs(0120): hallazgos piloto + regla sub-repos/Gitea API en autonomous_loop
Piloto 0120 convergio en 2 iter (2m28s). PR creado en
dataforge/chart_demo/pulls/1 (no en dataforge/fn_registry — sub-repo).

Anadido a autonomous_loop.md:
- Seccion "Sub-repos vs worktree padre": orquestador opera en sub-repo
  cuando issue toca apps/, projects/*/apps/, cpp/apps/ o analysis/.
- Seccion "Gitea API vs gh": gh auth es smoke, real es curl + pass token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:19:30 +02:00
egutierrez 938853d268 fix(0120): R1+R4 del dry-run — check binary_exists + DB path correcto
R1: fn::run_app no parsea argv. Cambio self_test (--self-test inexistente)
por check estructural test -f binary. Detectado por fn-orquestador dry-run.

R4: schema real de task_runs usa task_id (no issue_id) y DB vive en
apps/deploy_server/operations.db (no agent_runner_api).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:00:11 +02:00
egutierrez b31ea70771 docs(plan): issues 0120-0123 — mejora workflow subagentes
Plan en 4 olas para cerrar gaps detectados en revision critica:
- 0120 piloto fn-orquestador (chart_demo e2e_checks)
- 0121 cobertura e2e_checks masiva (fn-recopilador batch)
- 0122 fn-revisor + auto-apply ampliado (desbloquea fase 5)
- 0123 /flow run + fn-meta-orquestador + fn-priorizador

Dep-chain: 0120 -> 0121 -> 0122 -> 0123. Cada uno con
Acceptance verificable programaticamente para que /autonomous-task
pueda converger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:41:02 +02:00
egutierrez 2e5c630d38 chore: baseline pre-piloto 0120 — apps_subrepo rule + http/sse hardening
WIP previo al lanzamiento de fn-orquestador piloto.
Commit como baseline para que /autonomous-task 0120 arranque con master limpio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:40:52 +02:00
egutierrez c52846d475 feat(cpp/core): sse_client_cpp_core — SSE client con reconnect + Last-Event-ID
Cliente Server-Sent Events C++ reusable (fn_sse::Client) con background
thread, exponential backoff, Last-Event-ID y stop() que no bloquea.

Implementacion clave: fork+execvp curl directamente (sin /bin/sh wrapper)
para tener el PID real del proceso curl en curl_pid_, lo que permite que
stop() → kill(SIGTERM) → fgets NULL → join() funcione sin bloqueo.

4 tests (Catch2): connect_and_receive_3_events, parse_event_field,
reconnect_on_disconnect, stop_kills_thread. Fixture Python SSE con
/health probe via http_request_cpp_core.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:59:50 +02:00
egutierrez b5affae68c merge issue/0119: kanban_cpp issues/flows sync layer 2026-05-18 18:57:11 +02:00
agent 5b4452b9fe feat(0119): kanban_cpp issues/flows sync layer
Closes issue 0119. Implementation lives in the kanban_cpp sub-repo
(apps/kanban_cpp/backend/). See sub-repo commit 0b93a98 for details.

DoD:
- [x] issues_source.go parses frontmatter via yaml.v3
- [x] flows_source.go idem (distinct status->column mapping)
- [x] frontmatter_edit.go atomic PatchFrontmatterField
- [x] GET /api/boards/issues/cards (smoke: 87 cards)
- [x] GET /api/boards/flows/cards (smoke: 9 cards)
- [x] PATCH /api/boards/issues/cards/0119 status=en-curso (mtime change verified)
- [x] POST /api/boards/issues/cards/0119/launch -> 502 with suggestion
- [x] Tests *_test.go: 4 + 3 + 3 + 1 cache cases, all green
- [x] Cache 30s thread-safe (mutex)
- [x] Taxonomy 0103 respected — only canonical statuses accepted on PATCH

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:56:36 +02:00
egutierrez e0f8f3a068 merge issue/0116: skill_tree v2 Launch workflow boton + feature flag legacy_claude_fix 2026-05-18 18:47:36 +02:00
egutierrez b21e7587ad merge issue/0112: kanban_cpp C++ ImGui app + backend Go :8403 (sub-repo apps/kanban_cpp) 2026-05-18 18:47:31 +02:00
Egutierrez d4924f5cab feat(0112): register kanban_cpp app in cpp/CMakeLists.txt + close issue
Adds add_subdirectory block for apps/kanban_cpp (lives in apps/ per issue
0096). The app itself is a sub-repo (gitignored via apps/*/), with its own
git history and master branch initialized.

Six panels reuse registry: http_request_cpp_core, kpi_card_cpp_viz,
sparkline_cpp_viz, agent_runs_timeline_cpp_viz, dod_evidence_panel_cpp_viz.
Backend Go on :8403 (independent operations.db from kanban_web).
2026-05-18 18:46:50 +02:00
agent 853b3c0363 feat(skill_tree): Launch workflow via agent_runner_api + feature flag legacy_claude_fix (issue 0116)
- dev/feature_flags.json: anade 'legacy_claude_fix' (enabled:false,
  issue 0116). Default OFF — el flujo canonico ahora es 'Launch workflow'
  (POST :8486/api/runs); el boton 'Claude fix' legacy (terminal externa +
  claude --dangerously-skip-permissions) solo se renderiza si se activa
  el flag.
- Mueve dev/issues/0116-skill-tree-launch-workflow.md a completed/.

El codigo C++ del boton vive en el sub-repo dataforge/skill_tree
(apps/skill_tree, commit 9ee3be8).
2026-05-18 18:46:25 +02:00
egutierrez f164ef230f merge issue/0118: agent_runs_timeline C++ ImGui + helpers
# Conflicts:
#	cpp/tests/CMakeLists.txt
2026-05-18 18:33:02 +02:00
egutierrez ff255c9a3c merge issue/0117: dod_evidence_panel C++ ImGui + helpers 2026-05-18 18:32:35 +02:00
egutierrez 6c7f60fb6c merge issue/0115: agent_launch_worktree + agent_cleanup_worktree Go fns + capability group agents 2026-05-18 18:32:31 +02:00
egutierrez 75ac96a2d1 docs: cerrar issue 0118 — agent_runs_timeline_cpp_viz registrado
Funcion + helpers + tests + .md indexados.

- cpp/functions/viz/agent_runs_timeline.{h,cpp,md}
- cpp/functions/viz/agent_runs_timeline_helpers.{h,cpp}
- cpp/tests/test_agent_runs_timeline.cpp (17 cases, 53 assertions, PASS)
- cpp/tests/CMakeLists.txt: add_fn_test test_agent_runs_timeline

fn index: 1283 functions (+1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:53 +02:00
egutierrez da56085e74 docs(viz): agent_runs_timeline.md — frontmatter + self-doc
Issue 0118. Frontmatter completo (kind, lang, domain, version, purity,
signature, params, tags=agents/timeline/sse/imgui/viz/panel,
uses_functions=http_request_cpp_core, error_type, tested).

Secciones obligatorias por contrato self-doc:
- ## Ejemplo — wiring concreto con TimelineState g_state + lock + render
- ## Cuando usarla — dashboard cross-app de agentes
- ## Gotchas — SSE stub, ts en segundos, mutex obligatorio, no autoscroll,
  ImGuiSelectableFlags_AllowOverlap (rename desde AllowItemOverlap),
  app chip hex hardcoded, since_ts no determinista en tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:45 +02:00
egutierrez ecd864f2d3 test(viz): test_agent_runs_timeline — 17 cases / 53 assertions
Issue 0118. Catch2 coverage de los helpers puros:

- passes_filter: filtro vacio, filter por app, app+status combinado, since_ts
- filter_and_sort: orden started_at DESC, combina filtro + sort
- format_duration: running, segundos, minutos, horas, timestamps invertidos
- status_color_token: mapping conocido + fallback neutral
- status_icon_id: no-empty + distinto entre statuses
- app_chip_hex: apps conocidas + fallback gris

Mock fixture de 5 AgentRun mixtos en make_mock().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:37 +02:00
egutierrez a91ef5aace feat(viz): agent_runs_timeline ImGui panel + SSE stub
Issue 0118.

fn_viz::render_agent_runs_timeline(TimelineState&):
- Filtros: multi-select apps, multi-select statuses, Since (days).
- Connection badge (● green / ◐ amber / ○ red) por state.connection_status.
- Tabla 7 cols: status icon | app chip | issue/card | branch | dod badge |
  duration | started. Selectable SpanAllColumns dispara on_select callback.
- Footer: contadores per-status sobre el set completo.

Thread-safe: snapshot bajo runs_mutex al inicio del frame. SSE client
NO implementado — poll_sse_runs() es stub documentado en .md ## Gotchas.
Consumer puede usar http_request_cpp_core para polling fallback contra
GET /api/runs hasta que un endpoint /api/runs/stream estable aparezca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:29 +02:00
egutierrez c2bdc586a4 feat(viz): agent_runs_timeline helpers — pure filter/sort/format
Issue 0118.

Pure helpers in fn_viz::timeline namespace, free of ImGui:
- passes_filter / filter_and_sort  (multi-select app + status + since_ts)
- format_duration  (running | Ns | MmSSs | HhMMm | —)
- status_color_token / status_icon_id  (status → fn_tokens index / TI_*)
- app_chip_hex  (app id → accent hex, fallback gray)

Designed for unit-test isolation. Render layer (separate commit) consumes
these via agent_runs_timeline.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:17 +02:00
egutierrez 61a46e4b21 docs: cerrar issue 0117 (dod_evidence_panel C++ implementado)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:46 +02:00
egutierrez 3633d128aa docs(viz): frontmatter + self-doc para dod_evidence_panel (issue 0117)
Ejemplo lanzable con DodPanelState mock + Cuando usarla (HITL DoD
validation) + Gotchas (screenshot stub, URL no validada, log read
each-frame, callbacks pueden mutar state, frame ImGui activo
requerido). Tag agents para capability group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:37 +02:00
egutierrez 892ff4f789 test(viz): cubrir helpers de dod_evidence_panel (issue 0117)
7 test cases via Catch2: count_status (3 escenarios incl. unknown
status y missing_required), find_evidence (2 lookup positivo/negativo)
y status_icon_id/status_color_token (mapeo de 4+2 keys). Linkamos solo
helpers — sin ImGui ni vendor extra.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:30 +02:00
egutierrez 4388b54356 feat(viz): render ImGui dod_evidence_panel (issue 0117)
Panel con header (run_id + counts) + tabla 6-col (status icon / id /
kind / expected / evidence preview / actions). Soporta 4 kinds de
evidence: screenshot (stub textual), log (5-line preview + popup),
url (xdg-open/ShellExecuteA) y cmd (diff expected vs actual).
Botones Validate/Reject invocan callbacks on_validate/on_reject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:23 +02:00
egutierrez b21adb40c9 feat(viz): helpers puros para dod_evidence_panel (issue 0117)
DodItem/DodEvidence/DodPanelState + count_status/find_evidence/
status_icon_id/status_color_token. Sin ImGui — testeable en aislamiento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:16 +02:00
egutierrez 6fd2e9d071 docs: cerrar issue 0115 (worktree launcher Go fn) 2026-05-18 18:24:24 +02:00
egutierrez d9ef4e54f4 docs(capabilities): create 'agents' group + tag audit_dod_schema
New capability group page docs/capabilities/agents.md consolidating:
- agent_launch_worktree_go_infra
- agent_cleanup_worktree_go_infra
- audit_dod_schema_go_infra (added 'agents' tag to its frontmatter)

3 functions = minimum for a capability group page. Adds row to
docs/capabilities/INDEX.md. End-to-end example shows the launch ->
work -> cleanup -> dod-audit cycle that agent_runner_api (0113)
will orchestrate.
2026-05-18 18:24:17 +02:00
egutierrez 2ea9206934 test(infra): agent_launch_worktree + agent_cleanup_worktree
Three tests for launch:
- creates worktree dir + branch off master
- ResetIfExists=true on existing branch+worktree succeeds
- returns Error when required args missing

Two tests for cleanup:
- removes worktree dir and branch after launch
- tolerates missing worktree/branch (cleanup called twice)

Uses initDummyRepo helper with isolated GIT_CONFIG_GLOBAL=/dev/null
so tests do not pick up user's signing/template config. Echo stub
fallback (claude not in PATH) keeps tests hermetic.
2026-05-18 18:24:13 +02:00
egutierrez 355bcac6c7 feat(infra): agent_launch_worktree + agent_cleanup_worktree Go fns
Two Go functions in functions/infra/ for orchestrating headless Claude
agents inside isolated git worktrees:

- AgentLaunchWorktree(cfg): creates worktree off master, spawns
  claude -p in background, redirects stdout/stderr to LogPath. Falls
  back to echo stub when claude binary missing (CI/test friendly).
  ResetIfExists support for re-runs.

- AgentCleanupWorktree(repo, branch, path, pid): SIGTERM with 1s
  grace then SIGKILL, git worktree remove --force, git branch -D.
  Best-effort: only errors when all three steps fail (idempotent
  cleanup-twice).

Promotes inline bash from .claude/skills/parallel-fix-issues/ and
fn-orquestador to first-class registry functions. Closes issue 0115.

Capability group: agents.
2026-05-18 18:24:08 +02:00
egutierrez 4eb4c1cf98 merge issue/0113: agent_runner_api service Go :8486 + agent_runs.db 2026-05-18 18:18:14 +02:00
egutierrez 40aacac590 merge issue/0110: http_request_cpp_core + http_get_json_cpp_core
# Conflicts:
#	cpp/tests/CMakeLists.txt
2026-05-18 18:18:03 +02:00
egutierrez e9bcbecd24 merge issue/0114: DoD evidence schema + fn doctor dod 2026-05-18 18:17:20 +02:00
egutierrez 7eb7b3d0c8 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00
egutierrez 61ec4c8a76 docs: cerrar issue 0110 (http_request + http_get_json cpp/core)
Funciones implementadas, registradas en registry.db via fn index, tests
locales (11 cases, 27 assertions) en verde. Cierra el gap detectado:
patron curl-popen inline duplicado en services_monitor / dag_engine_ui /
data_factory ahora promovido al registry.

Sigue habilitando issue 0111 (apps/process_explorer http_client) cuando
arranque — usara http_request_cpp_core directamente.
2026-05-18 18:15:38 +02:00
egutierrez a843f84a18 docs(cpp/core): registry .md for http_request + http_get_json (issue 0110)
Frontmatter + self-doc sections (Ejemplo, Cuando usarla, Gotchas) following
the contract in .claude/rules/function_growth_and_self_docs.md. Tags include
'http', 'client', 'curl', 'network', 'registry-gap', 'helper' so FTS surfaces
them when an agent asks for HTTP / fetch / GET / Bearer.

http_get_json declares uses_functions: [http_request_cpp_core] so the
dependency is auditable via mcp__registry__fn_uses.
2026-05-18 18:14:56 +02:00
egutierrez 6f3c129a14 test(cpp/core): integration tests for http_request + http_get_json (issue 0110)
Catch2-based tests that fork+exec a local python3 http.server fixture per
test binary. Covers:

  http_request:
    - GET 200 with body
    - GET 404 (HTTP error != transport error)
    - POST with body + Content-Type
    - bearer_token shortcut adds Authorization: Bearer
    - basic_user/basic_pass shortcut adds HTTP Basic (curl --user)
    - invalid URL surfaces transport error (status=0)
    - timeout_ms is honored (bails before server's 3s sleep)

  http_get_json:
    - parses 200 JSON body
    - throws std::runtime_error on 404
    - bearer_token reaches server (verified via echoed Authorization header)
    - throws std::runtime_error on invalid JSON body

Tests skip gracefully if python3 isn't available (server.start() returns
false; SUCCEED with skip message). No external network required.

Local runs (Linux): 21 assertions / 7 cases (http_request), 6 / 4 (get_json),
all passing.
2026-05-18 18:14:48 +02:00
egutierrez bc270db723 docs: cerrar issue 0113 — agent_runner_api scaffold + DoD + worktrees 2026-05-18 18:14:39 +02:00
egutierrez a3a263702b feat(cpp/core): add http_request + http_get_json helpers (issue 0110)
Promotes the inline curl-popen pattern duplicated across apps/services_monitor,
dag_engine_ui, data_factory into two reusable functions in cpp/functions/core/:

- http_request_cpp_core: generic HTTP client (GET/POST/PUT/DELETE/PATCH) via
  cURL CLI through popen. Portable Linux/WSL/MinGW (no link-time libcurl).
  Supports custom headers, raw body, Bearer/Basic auth shortcuts, timeout,
  optional TLS verify skip. Returns status/body/headers/error/duration_ms.

- http_get_json_cpp_core: convenience wrapper over http_request — GET <url>,
  expect 2xx, parse body as nlohmann::json. Throws std::runtime_error on
  transport / non-2xx / parse failure.

Vendors nlohmann/json v3.11.3 single header at cpp/vendor/nlohmann/json.hpp
(MIT). No CMake target needed — header-only; consumers add
cpp/vendor/ to include path.
2026-05-18 18:14:37 +02:00
egutierrez 78c4f593a4 docs: cerrar issue 0114
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:13:14 +02:00
egutierrez 0f72cc8ad3 docs: dod_evidence_schema templates + READMEs (issue 0114)
- docs/templates/issue.md and docs/templates/flow.md include the optional
  dod_evidence_schema: block with realistic example items.
- dev/issues/README.md and dev/flows/README.md document the schema, kinds
  by example, validation rules and the fn doctor dod entrypoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:13:02 +02:00
egutierrez 030e44b027 test(infra): audit_dod_schema covers valid/invalid/malformed/recurse (issue 0114)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:12:54 +02:00
egutierrez ca2e5588cc feat(infra): audit_dod_schema + fn doctor dod (issue 0114)
Adds AuditDodSchema(issuesDir, flowsDir) which scans dev/issues/ and
dev/flows/ frontmatter for the new optional dod_evidence_schema: block.
Validates id uniqueness, kind in {screenshot,log,url,cmd}, expected
non-empty and required bool (default true). Tolerant to malformed YAML
and missing block.

Wires it into fn doctor dod with human-readable caveman output and
--json support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:12:49 +02:00
egutierrez 5fb2269c00 merge issue/0095-frontend: dag_engine_ui + data_table v1.3 + dev_console + work_tab + frontmatter migration + DoD user-facing 2026-05-17 02:44:39 +02:00
egutierrez 5e6a974a5d feat(dev): issues 0100-0104 — dev_console binary + work_tab + DoD user-facing + frontmatter migration de 146 issues + taxonomia canonica
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:44:04 +02:00
egutierrez 5d2a14e50a docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:07:03 +02:00
egutierrez 212875ed0d chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:22 +02:00
egutierrez d6175964e4 data_table v1.3.1: Dots renderer via ImDrawList (font-independent) + recompile all apps with fn_table_viz (issue 0081-O.6)
- Replace TextColored+glyph with ImDrawList::AddCircleFilled in CellRenderer::Dots.
  Dots are now font-independent: no dependency on Unicode glyph coverage. Fixes
  "dots show as ?" on Karla/Roboto/Inter fonts that lack Geometric Shapes block.
- dots_glyph_size now controls circle radius (px) instead of font scale.
- BadgeRule.label is ignored for Dots (documented in data_table_types.h + docs).
- data_table.md bumped to v1.3.1 with capability growth log entry.
- docs/capabilities/data_table_renderers.md: Dots section updated + Common pitfalls
  entry added: "Asumir que cualquier glyph Unicode renderea".
- dag_engine_ui/tabs.cpp: removed stale "● glyph" comment from BadgeRule.
- Recompiled: dag_engine_ui, registry_dashboard, graph_explorer, navegator_dashboard,
  odr_console. All 5 apps deployed to Desktop/apps/. Build Linux + tests 4/4 green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:35:22 +02:00
egutierrez 5974484bd4 data_table v1.3.0: Dots renderer for status timelines + fix dag_engine_ui antipattern + pitfalls doc (issue 0081-O.5)
PARTE A - CellRenderer::Dots (v1.3.0):
- Add Dots=8 to CellRenderer enum (data_table_types.h)
- Add dots_separator/dots_max/dots_show_count/dots_glyph_size fields to ColumnSpec
- Implement draw_cell_custom case Dots in data_table.cpp
  - Parses comma-separated cell value into tokens
  - Looks up each token in badges for color + optional glyph override
  - Per-dot tooltip via tooltip_on_hover
- tql_emit: serialize renderer="dots" + dots_max/dots_show_count/dots_glyph_size/dots_separator
- tql_apply: deserialize all Dots fields
- tql_emit_test: +6 assertions (58 total, 0 failed)
- tql_apply_test: +8 assertions (114 total, 0 failed)
- test_column_specs: +2 tests (10/10 pass)

PARTE B - dag_engine_ui fix: 10 cols -> 6 cols (submodule commit 61314b7)

PARTE C - docs/capabilities/data_table_renderers.md:
- Update to v1.3.0
- Add decision tree for renderer selection
- Add CellRenderer::Dots section with canonical example
- Add Common pitfalls section (multiple columns, badge for free-text, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:24:53 +02:00
egutierrez 67fff0d677 docs(dag_engine): README autoritativo (anadir DAGs + formato YAML + troubleshooting)
apps/dag_engine/README.md cubre:
- Donde viven los DAGs y como apuntar el systemd unit.
- Workflow paso a paso para anadir uno nuevo (crear/validar/probar/recargar/verificar).
- Formato YAML completo: top-level fields + step fields + cron schedule + ejemplo de extremo a extremo (env, depends, retry_policy, continue_on, handlers).
- Comandos CLI (run/list/status/validate/server) + flags.
- 7 secciones de "que hacer si algo falla": DAG invisible, validation fail, step fallido, scheduler no dispara, WS disconnected, cleanup runs viejos, restaurar backup.
- Endpoints HTTP completos.
- Referencias a funciones del registry y commit de migracion.

app.md de dag_engine + dag_engine_ui apuntan a README.md.
gitlink dag_engine_ui actualizado a commit con app.md mejorado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:23:33 +02:00
egutierrez 890f641692 feat(dag_engine_ui): gitlink panel Timeline (ImPlot scatter X=tiempo Y=DAG)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:14:16 +02:00
egutierrez ae5d27a5ec feat(dag_engine): /api/dags devuelve last_runs[] (max 5) + gitlink UI badges
- executor.go: DagInfo anade LastRuns []store.DagRun. Pobla con e.store.ListRuns(name, 5, 0).
- cpp/apps/dag_engine_ui: gitlink al SHA con 5 puntitos R1..R5 via data_table BadgeRule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:07:16 +02:00
egutierrez 0ed949d235 fix(dag_engine_ui): gitlink Win32 ws2_32 link (issue 0095)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:01:06 +02:00
egutierrez c438dc6916 data_table: Phase 2 — Button + events + tooltip + RightClick + TQL persist column_specs (issue 0081-O)
- CellRenderer::Button=5: renders SmallButton per cell; emits TableEvent::ButtonClick on click
- TableEventKind enum (ButtonClick/RowDoubleClick/RowRightClick/CellEdit) + TableEvent struct
- render() extended overload: adds events_out parameter (nullptr = back-compat, no events)
- RowDoubleClick and RowRightClick detection in raw table loop (stage 0)
- RowRightClick also detected in aggregated stage table (stage 1+)
- Tooltip per cell: tooltip_on_hover + tooltip fields on ColumnSpec; "auto" = show cell value
- State::aux_column_specs: TQL-persisted column specs sidecar per table
- tql_emit: serializes aux_column_specs[0] as column_specs block (badge/progress/duration/icon/button/tooltip)
- tql_apply: parses column_specs block back into state.aux_column_specs[0]
- render() merges aux_column_specs into TableInput when caller passes empty column_specs
- test_column_specs: 5->8 tests (Button struct, tooltip fields, both render() signatures link)
- tql_emit_test: 3 new tests (column_specs badge/button/tooltip emit) — 52 passed
- tql_apply_test: 3 new tests (column_specs badge/button/tooltip roundtrip) — 106 passed
- Back-compat: existing apps (graph_explorer, registry_dashboard) unchanged
- Version bump: data_table v1.1.0 -> v1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:59:26 +02:00
egutierrez 4027aeaaf5 feat(dag_engine_ui): gitlink tabs DAG List/Detail/Run Detail (issue 0095 step 5)
cpp/apps/dag_engine_ui: SHA con data_table_cpp_viz integrado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:57:59 +02:00
egutierrez 9ff0b3900c feat(dag_engine_ui): gitlink WS client (issue 0095 step 4)
cpp/apps/dag_engine_ui: SHA con ws_client + panel Live integrado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:47:23 +02:00
egutierrez ce9fa3b451 feat(dag_engine): json tags lowercase + gitlink dag_engine_ui HTTP layer (issue 0095 step 3)
- store/store.go: anade tags JSON lowercase a DagRun + DagStepResult para que REST y WS devuelvan misma forma.
- cpp/apps/dag_engine_ui: gitlink al SHA con http_client + data_http.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:45:18 +02:00
egutierrez e0cce972ea feat(cpp): registrar dag_engine_ui en cpp/CMakeLists.txt (issue 0095 step 2)
Sub-repo Gitea: dataforge/dag_engine_ui (a crear cuando se ejecute /full-git-push).
Gitlink al SHA inicial del scaffolding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:38:43 +02:00
egutierrez 380a7a8f35 data_table: declarative cell renderers Phase 1 (Badge/Progress/Duration/Icon)
Adds TableInput.column_specs sidecar field enabling apps to declare Badge,
Progress, Duration and Icon renderers per column without writing ImGui inline.
Back-compat: apps without column_specs compile and behave identically.

- data_table_types.h: CellRenderer enum, BadgeRule, IconMapEntry, ColumnSpec types
- data_table.cpp: hex_to_imcolor helper, icon_name_to_glyph static map (~30 Tabler icons),
  draw_cell_custom dispatcher, integration in Stage-0 and Stage-N cell loops and draw_extra_panel
- Bump version 1.0.0 -> 1.1.0 with capability growth log
- cpp/tests/test_column_specs.cpp: 5 smoke/linker tests (back-compat + 4 renderer types)
- cpp/tests/CMakeLists.txt: register test_column_specs target linked against fn_table_viz
- types/core/{cell_renderer,badge_rule,icon_map_entry,column_spec}.md: registry type mds
- docs/capabilities/data_table_renderers.md: canonical doc with end-to-end examples
- docs/capabilities/INDEX.md: added data-table-renderers group

All tests green: test_column_specs 5/5, test_fn_table_viz_smoke 8/8,
tql_emit 41/41, tql_apply 88/88, Wave-1 tests 8/8.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:38:24 +02:00
egutierrez 7ecbee1175 feat(dag_engine): WS hub /api/ws/dagruns + migracion DAGs desde dagu
- events.go: DagRunHub broadcastea snapshot+deltas live (500ms tick, 5s recent finished window) sobre dag_runs + dag_step_results.
- api.go: handler GET /api/ws/dagruns upgrade WS, opt-in en RegisterAPI.
- store.go: expone Conn() para read-only desde el hub.
- main.go: construye DagRunHub al arrancar server.
- dags_migrated/: 5 YAMLs migrados desde ~/dagu/dags tras desinstalar dagu (issue 0095 step 1).

Smoke: snapshot inicial OK, trigger /api/dags/test_claude_access/run -> delta WS observa 3 step_results + run success en <1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:36:34 +02:00
egutierrez fe39de8b22 scaffolder: auto-wire fn_table_viz in new C++ apps (issue 0081-M)
- CMakeLists.txt template: adds target_link_libraries fn_table_viz with
  if(TARGET fn_table_viz) guard (compiles even without vendor/lua).
- main.cpp template: adds commented include + data_table::render() panel
  block in render(). Also fixes include to use app_base.h + panel_menu.h
  (matching convention of chart_demo, shaders_lab).
- app.md template: uses_functions lists all 12 fn_table_viz stack IDs
  commented out; uncomment when activating data_table::render().
- Bump version 0.1.0 -> 1.1.0. Add capability growth log entry.
- Tag cpp-tables added to capability group.
- Verified: test app test_scaffolder_default compiled clean, residues
  removed (dir + CMakeLists entry).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:06:06 +02:00
egutierrez 951a77ec7f close issue 0081: tables promoted to registry + fn doctor cpp-apps BeginTable check
- docs/TQL.md: añadidas secciones joins, views, main_source, 24 viz tokens completos
  (extraidos de tql_helpers.cpp), color_rules, fn.* builtins completos (20 funciones),
  funciones bloqueadas del sandbox, tabla de estado de implementacion actualizada.
  Nota al pie referencia los 129 checks roundtrip (41 emit + 88 apply).

- functions/infra/audit_cpp_apps.go: añadida AuditCppTableMigration() que escanea
  .cpp de cada app imgui buscando ImGui::BeginTable; status CANDIDATE/MIXED/clean
  segun si usa data_table_cpp_viz en uses_functions.

- cmd/fn/doctor.go: fn doctor cpp-apps ahora incluye seccion BeginTable migration
  con tabwriter CANDIDATE/MIXED; --json produce {conformance, table_migration}.
  doctorAll incluye cpp_table_migration en el mapa JSON.

- .claude/rules/fn_doctor.md: tabla de subcomandos y acciones complementarias
  actualizadas con el nuevo check.

- dev/issues/0081 movido a completed/ con status done y notas de deuda documentadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:49:56 +02:00
852 changed files with 101409 additions and 20381 deletions
+4 -1
View File
@@ -21,12 +21,14 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
---
@@ -258,6 +260,7 @@ fn check params # Lista funciones sin params_schema
fn doctor # Corre todos los checks
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
fn doctor services # apps tag 'service' + systemctl + puerto
fn doctor services-spec # audita bloque `service:` del app.md (issue 0105)
fn doctor sync # drift pc_locations BD vs disco
fn doctor uses-functions # imports reales vs uses_functions del app.md
fn doctor unused # funciones del registry sin consumidores
+10
View File
@@ -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_<issue>_<ts>/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `/home/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.
---
+1
View File
@@ -0,0 +1 @@
../../projects/aurgi/.claude/commands
+23 -108
View File
@@ -1,121 +1,36 @@
# /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.
---
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
---
## Argumento
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
`$ARGUMENTS``<issue_id>` o `<task_spec_path>` + flags opcionales.
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
```
/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
```
## Por que deprecado
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
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
- Detector issue vs flow.
- Reporte estructurado al humano post-delegate.
- Self-Q&A migrado a fn-orquestador.
---
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
## Comportamiento
## Sustitucion 1:1
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`
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
---
## Modo debug
## Reglas duras (no negociables)
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
- 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/`.
## Migration deadline
---
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
## 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
```
Ver `.claude/commands/autopilot.md` para spec completa.
+212
View File
@@ -0,0 +1,212 @@
---
name: autopilot
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
---
# /autopilot — Comando autonomo unificado
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
## Por que solo delegar
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
`fn-orquestador` ahora soporta dos `task_type`:
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
## Sintaxis
```
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
/autopilot issue:<NNNN> # issue explicito
/autopilot i:<NNNN> # alias
/autopilot flow:<NNNN> # flow NNNN
/autopilot f:<NNNN> # alias
/autopilot check <target> # solo audita DoD readiness, no delega
/autopilot <target> --max-iterations N --max-minutes M --dry-run
```
Detector:
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
- `^(issue|i):\d{4}[a-z]?$` → issue.
- `^(flow|f):\d{4}$` → flow.
- Otra cosa → ABORT con error de sintaxis.
## Pre-flight DoD readiness check (OBLIGATORIO)
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
### Issue (`dev/issues/<NNNN>-*.md`)
1. Archivo existe en `dev/issues/` (no en `completed/`).
2. Frontmatter con `status`, `priority`.
3. Al menos UNA de:
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
- `## Acceptance` con checkboxes `[ ]`.
- `## Tests` + `## Tareas` ambas no vacias.
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
### Flow (`dev/flows/<NNNN>-*.md`)
1. Archivo existe en `dev/flows/`.
2. Frontmatter valido.
3. `## Acceptance` con >=1 checkbox.
4. `## Flow` no vacio.
5. Pre-requisitos declarados.
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
Si falla:
```
=== /autopilot check 0125 ===
status: NOT READY
target: issue 0125 (skill-tree-dashboard-panel)
gaps:
- Sin seccion DoD/Acceptance
- "UX intuitiva" linea 47 — no verificable
fix:
- Anadir ## DoD con 3-5 bullets programaticamente verificables
- Reemplazar criterios subjetivos por mediciones concretas
```
Si OK:
```
=== /autopilot check 0107c ===
status: READY
target: issue 0107c (refactor data_table)
dod_items: 5 checkboxes
task_type: refactor_safe
estimated_iter: 3-5
```
## Dispatch a fn-orquestador
Tras pre-flight OK, ejecuta:
```
Agent(
subagent_type="fn-orquestador",
prompt="""
Issue/Flow: <path al .md>
Modo: REAL (o --dry-run)
task_type: <issue|flow>
Pre-condiciones verificadas: 7/7 verde
Master: <sha> sync con origin
Working tree principal: limpio (baseline)
Max iter: N
Max min: M
Auto-apply proposals: safe
Token Gitea: pass gitea/dataforge-git-token
DB task_runs: apps/deploy_server/operations.db (schema task_id)
Reglas duras: autonomous_loop.md (11 reglas)
""",
run_in_background=true
)
```
Cuando termine, reporta al humano con output canonico del orquestador:
```
=== /autopilot 0121b ===
target: issue 0121b (fn doctor e2e-coverage)
delegated_to: fn-orquestador
status: converged
iterations: 1 / 8
duration: 4 min / 30
task_run_id: task_d285372493cce2e6
branch: auto/0121b-orquestador
worktree: /tmp/fn_orq_0121b_1779147778
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
Siguiente: revisar PR, mergear, mover issue a completed/
```
## Reglas duras (autopilot-level)
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
## Flags
| Flag | Default | Que hace |
|---|---|---|
| `--max-iterations N` | 10 | Pasado al orquestador |
| `--max-minutes M` | 60 | Pasado al orquestador |
| `--dry-run` | off | Pasado al orquestador |
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
## Errores canonicos
| Codigo | Significado | Accion |
|---|---|---|
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
## Ejemplos
```bash
# Issue con DoD claro
/autopilot 0107c
# Flow con piezas faltantes — autoriza creacion antes
/autopilot flow:0008 --allow-construct-missing
# Solo audit
/autopilot check 0125
/autopilot check flow:0008
# Dry run
/autopilot 0107c --dry-run
```
## Relacion con otras reglas
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
- [[apps_tbd]] — politica TBD por tipo de cambio.
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
- [[feature_flags]] — codigo incompleto detras de flag OFF.
- [[registry_calls]] — invocaciones canonicas.
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
- [[delegation]] — spawn fn-constructor antes que escribir inline.
## Migracion desde `/autonomous-task`
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
## Historico
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
+86
View File
@@ -0,0 +1,86 @@
---
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
---
# /commands — Catalogo de slash commands del repo
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
## Sintaxis
```
/commands # listado completo agrupado por namespace
/commands <substring> # filtra por substring en nombre o descripcion
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
/commands --json # salida JSON para agentes
```
## Implementacion
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
```bash
#!/usr/bin/env bash
set -euo pipefail
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
CMD_DIR="$ROOT/.claude/commands"
# Recolecta: ns|name|description
collect() {
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
rel="${f#$CMD_DIR/}"
case "$rel" in
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
*) ns="global"; name="${rel%.md}" ;;
esac
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
done | sort
}
collect | awk -F'|' '
{
if ($1 != prev_ns) {
if (prev_ns) print ""
if ($1 == "global") print "## global (/<cmd>)"
else print "## " $1 " (/" $1 ":<cmd>)"
prev_ns = $1
}
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
}'
```
Filtros:
- Substring: `grep -i "<substring>"` sobre stdout.
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
## Salida (formato humano)
```
## global (/<cmd>)
- /app — Crear, configurar y desplegar apps del registry
- /autopilot — Modo full-auto...
- /commands — Catalogo de slash commands del repo
...
## aurgi (/aurgi:<cmd>)
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
```
## Cuando usarlo
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
- Antes de inventar logica inline: comprobar si ya existe un command.
- Auditoria: verificar que los projects exponen sus commands correctamente.
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
## Gotchas
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
+58 -21
View File
@@ -1,37 +1,74 @@
# /compile — Compila app C++ y la copia al escritorio de Windows
---
description: "Compila app del registry (C++ o Wails Go), copia el .exe a Desktop/apps/<app>/ y relanza en Windows. Wrapper sobre compile_cpp_app o compile_wails_app segun framework declarado en app.md."
---
Wrapper sobre el pipeline `compile_cpp_app_bash_pipelines`. Toda la lógica vive en el registry (resolver app desde CWD/arg, cross-compile MinGW, copiar exe + DLLs + assets/ + enrichers/ + runtime/ a `/mnt/c/Users/lucas/Desktop/apps/<app>/`, taskkill previo, preservar `local_files/`).
# /compile — Compila app C++ o Wails y la copia al escritorio de Windows
Wrapper sobre 2 pipelines del registry segun el framework:
- **C++ (imgui / cmake)** → `compile_cpp_app_bash_pipelines`. Cross-compile MinGW + assets/enrichers/runtime + taskkill, NO relanza.
- **Wails Go (matrix_client_pc, matrix_admin_panel, etc.)** → `compile_wails_app_bash_pipelines`. `wails build -platform windows/amd64` con `-tags goolm` si E2EE + taskkill + **RELANZA** la app tras copy.
Toda la logica vive en el registry (resolver app desde CWD/arg, build, deploy con preservacion de `local_files/`).
## Dispatch
```bash
cd /home/lucas/fn_registry
./fn run compile_cpp_app "$ARGUMENTS"
# Detecta framework via wails.json o CMakeLists.txt en el dir del app
APP="$ARGUMENTS"
RESOLVED=$(bash -c '
source bash/functions/infra/resolve_cpp_app_dir.sh
resolve_cpp_app_dir "'"$APP"'"
' 2>/dev/null) || true
APP_DIR="$(echo "$RESOLVED" | cut -f2)"
if [ -n "$APP_DIR" ] && [ -f "$APP_DIR/wails.json" ]; then
./fn run compile_wails_app "$ARGUMENTS"
elif [ -n "$APP_DIR" ] && [ -f "$APP_DIR/CMakeLists.txt" ]; then
./fn run compile_cpp_app "$ARGUMENTS"
else
echo "ERROR: no se detecto framework (falta wails.json o CMakeLists.txt en $APP_DIR)" >&2
exit 1
fi
```
## Argumento
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`, `matrix_client_pc`).
- Sin argumento: deduce desde `pwd` si estás dentro de `cpp/apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, el pipeline lista las apps disponibles en stderr y aborta.
- Sin argumento: deduce desde `pwd` si estas dentro de `cpp/apps/<X>/`, `apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, lista las apps disponibles en stderr y aborta.
## Qué hace el pipeline
## Que hace el pipeline (C++)
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>` desde arg o CWD.
2. Verifica `CMakeLists.txt` en el dir resuelto.
3. `build_cpp_windows_bash_infra <app>` — cross-compila el target específico con `cpp/build/windows/` (configura toolchain `mingw-w64.cmake` la primera vez).
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>`.
2. Verifica `CMakeLists.txt`.
3. `build_cpp_windows_bash_infra <app>` — cross-compila con MinGW.
4. `deploy_cpp_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F` (pre-autorizado).
- Copia `<app>.exe` + DLLs al top-level de `Desktop/apps/<app>/`.
- rsync `cpp/build/windows/apps/<app>/assets/` `Desktop/apps/<app>/assets/`.
- rsync `<app_dir>/enrichers/``assets/enrichers/` si existe.
- Si `app.md` declara `python_runtime: true`, regenera `runtime/` con `tools/freeze_python_runtime.sh` y rsync a `assets/runtime/`.
- Copia `gx-cli`/`gx-cli.exe` si existen.
- **NUNCA** toca `local_files/` (estado del usuario).
5. Imprime `ls -lh` del `.exe` final.
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` + DLLs.
- rsync `assets/`, `enrichers/`, `runtime/` (si aplica).
- Preserva `local_files/`.
- **NO** relanza.
## Que hace el pipeline (Wails)
1. `resolve_cpp_app_dir_bash_infra` (reusado — sirve para Wails apps tambien).
2. Verifica `wails.json` + `go.mod`.
3. Detecta `-tags goolm` automaticamente (grep `matrix_crypto_init` en `app.md` o `build:tags` en `wails.json`).
4. `wails build -platform windows/amd64 [-tags goolm]`.
5. `deploy_wails_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` (+ `appicon.ico` si existe).
- **Relanza** via `cmd.exe /c start "" <app>.exe`.
- Preserva `local_files/`.
## Notas
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
- Solo target Windows hoy. Linux ya lo da `wails build` / `cpp/build/` nativo.
- Variables override-ables: `BUILD_WIN`, `WIN_DESKTOP_APPS`, `FN_REGISTRY_ROOT`.
- Si la app no está registrada en `cpp/CMakeLists.txt`, `cmake --build --target <app>` falla. Registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Para tocar la lógica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,deploy_cpp_exe_to_windows,compile_cpp_app}.sh`, no este wrapper.
- Si la app C++ no esta registrada en `cpp/CMakeLists.txt`, el build falla — registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Si la app Wails falla build con `no required module provides package`, correr `go mod tidy` en el dir del app primero.
- Para tocar la logica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,build_cpp_windows,deploy_{cpp,wails}_exe_to_windows,compile_{cpp,wails}_app}.sh`, no este wrapper.
+274
View File
@@ -0,0 +1,274 @@
# /cpp-app — Crear o modificar app C++ del registry sin olvidar nada
Recopila TODOS los datos necesarios (frontmatter, trio app_hub, panels, AppConfig, service block, e2e_checks, uses_functions) **antes** de tocar el disco. Tras confirmar, ejecuta scaffolder o edits, regenera iconos, refresca app_hub, compila y deploya a Windows.
Sustituye al flujo manual "edito main.cpp + app.md + CMakeLists.txt a mano". Wrapper sobre `init_cpp_app_bash_pipelines` (create) o edits directos sobre `app.md` (modify) + `regenerate_app_icons` + `refresh_app_hub` + `redeploy_cpp_app_windows`.
---
## Uso
```
/cpp-app # interactivo, modo create
/cpp-app <name> # interactivo, modo create con name pre-rellenado
/cpp-app modify <name> # editar app existente
```
---
## Modo CREATE — flujo turno a turno
Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas como default; si no, pregunta name.
### Paso 0 — verificar que no existe
```bash
test -d "/home/lucas/fn_registry/apps/<name>" \
|| ls /home/lucas/fn_registry/projects/*/apps/<name> 2>/dev/null
```
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
### Paso 1 — Identidad (AskUserQuestion)
1. **name** (texto libre — valida snake_case + contiene verbo segun `ids_naming.md`). Verbos canonicos: `show, render, view, plot, edit, manage, monitor, browse, explore, run, launch, scan, audit, debug, profile, ...`. Si no trae verbo, sugerir alternativas (`viewer` -> `<name>_viewer`).
2. **project** (select: ninguno / lista de `projects/*/`). Si ninguno -> `apps/<name>/`.
3. **domain** (select: `tools` (default), `gfx`, `tui`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `pipelines`, `browser`).
4. **description** 1 linea (texto libre, max 80 chars). **OBLIGATORIO** — sin esto el hub muestra tarjeta vacia.
### Paso 2 — Trio app_hub OBLIGATORIO
Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE juntos.
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
```bash
ls /home/lucas/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
```
Sugiere 3-5 candidatos basados en `description`. Default segun domain: `gfx`->`palette`, `tui`->`terminal`, `tools`->`wrench`, `infra`->`gear`, `finance`->`chart-line-up`, `datascience`->`graph`, `cybersecurity`->`shield`.
6. **icon.accent** hex `#rrggbb` (palette select):
- sky `#0ea5e9`, indigo `#4f46e5`, violet `#7c3aed`, pink `#ec4899`, rose `#f43f5e`, red `#dc2626`, orange `#ea580c`, amber `#d97706`, green `#16a34a`, teal `#0d9488`, cyan `#0891b2`, slate `#475569`. Default segun domain.
### Paso 3 — Tags
7. **tags** (multiSelect): `service`, `launcher`, `dashboard`, `viewer`, `editor`, `monitor`, `debug`, `prototype`. Si selecciona `service` -> activar bloque service (Paso 7).
### Paso 4 — Panels iniciales
8. **panels** (texto libre o select):
- Default: 1 panel `Main` (Ctrl+1).
- Opcion lista: hasta 4 paneles. Por cada uno: `{label, shortcut}`. Generara `PanelToggle k_panels[]` en `main.cpp`.
### Paso 5 — AppConfig flags
9. (multiSelect):
- `init_gl_loader` (true si la app llama `gl*` directo, ej. shaders, GPU renderer custom). Default false.
- `viewports` true (default) / false (single-window).
- `auto_dockspace` true (default) / false (solo si gestiona DockSpace propio tipo `shaders_lab`).
- `fps_overlay` activo de inicio? (controla solo el default; el menu Settings lo toggle).
### Paso 6 — Funciones del registry a usar
10. **uses_functions** lista IDs. Antes de preguntar, busca candidatas segun description:
```
mcp__registry__fn_search query="<keyword>" entity="functions"
```
Y muestra capability groups relevantes (`docs/capabilities/INDEX.md`). El usuario puede aceptar lista, anadir IDs, o dejar vacio (se rellena tras codear).
Cada ID que no este en el registry -> ofrecer spawn `fn-constructor` antes de continuar (regla `delegation.md`).
### Paso 7 — Bloque `service:` (solo si tag=service)
11. Si paso 3 marco `service`, recopilar (regla `function_tags.md` + issue 0105):
- `port` int o null
- `health_endpoint` ruta GET o null
- `health_timeout_s` (default 3)
- `runtime` (select: `systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`)
- `systemd_unit` (obligatorio si runtime empieza por `systemd-`)
- `systemd_scope` (`user|system|null`)
- `restart_policy` (select: `always` (Recommended — gotcha: `on-failure` NO reinicia SIGTERM limpio), `on-failure`, `none`)
- `pc_targets` (multiSelect de pc_locations actuales: `aurgi-pc`, `home-wsl`, ...)
- `is_local_only` (true/false default false)
### Paso 8 — Persistencia
12. (multiSelect):
- BD propia SQLite `<name>.db` en `local_files/`? -> recordar usar `fn::local_path("<name>.db")` (cpp_apps.md §7)
- operations.db (para entities/relations)? -> ejecutar `fn ops init` tras crear
- Archivos config en `local_files/`?
### Paso 9 — e2e_checks (issue 0068)
13. Default sugerido (modificable):
```yaml
e2e_checks:
- id: build
cmd: "cmake --build cpp/build --target <name> -j"
timeout_s: 300
- id: self_test
cmd: "./cpp/build/apps/<name>/<name> --self-test"
timeout_s: 30
severity: warning # si todavia no implementa --self-test
```
Pregunta: ¿anadir mas checks (ops_audit, pytest, smoke)?
### Paso 10 — Resumen y confirmacion
Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffolder + post-acciones. Pedir confirmacion antes de ejecutar.
---
## Modo CREATE — ejecucion
Una vez confirmado:
```bash
cd /home/lucas/fn_registry
# 1. Scaffolder
./fn run init_cpp_app <name> \
[--project <p>] \
[--domain <d>] \
--desc "<description>" \
[--tags "<csv>"]
# 2. Editar app.md generado para anadir:
# - icon: {phosphor, accent}
# - service: {...} (si aplica)
# - uses_functions: [...]
# - e2e_checks: [...]
# (el scaffolder no rellena estos; editarlos con Edit tool)
# 3. Editar main.cpp generado para reflejar:
# - panels[] custom (si != default)
# - cfg.init_gl_loader / cfg.auto_dockspace / cfg.viewports
# - includes de funciones registry usadas
# 4. Editar CMakeLists.txt para anadir paths de funciones del registry:
# ${CMAKE_SOURCE_DIR}/functions/<d>/<f>.cpp
# 5. Si es service -> ofrecer crear systemd unit (skipear si runtime=stdio|manual)
# 6. Si pidio operations.db
./fn ops init apps/<name> # o projects/<p>/apps/<name>
# 7. Generar icono
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# 8. Indexar
./fn index
# 9. Compilar Windows
./fn run redeploy_cpp_app_windows <name> <dir> --build
# 10. Refrescar app_hub
./fn run refresh_app_hub
# 11. Auditoria
./fn doctor cpp-apps
[[ "<tag>" == *service* ]] && ./fn doctor services-spec
```
---
## Modo MODIFY — flujo
`/cpp-app modify <name>`
### Paso 0 — Localizar
```bash
# Buscar apps/<name>/ o projects/*/apps/<name>/
sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
```
Si no existe: abortar, sugerir `/cpp-app` (sin args) para crear.
### Paso 1 — Mostrar config actual
```bash
mcp__registry__fn_show id="<id>"
cat <dir>/app.md
```
### Paso 2 — Que cambiar (multiSelect)
- `description` (1 linea)
- `icon.phosphor` o `icon.accent`
- `tags` (anadir/quitar; si toca `service` -> Paso 7 del create)
- `uses_functions` (anadir/quitar — recordar editar CMakeLists.txt)
- `panels` (anadir/quitar/renombrar)
- `service:` block (si tag=service)
- `e2e_checks`
- `domain`
- `rename` (cambia name, dir, IDs derivados, repo Gitea — operacion delicada, requiere doble confirmacion)
### Paso 3 — Aplicar cambios
Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write` completo de `app.md` (preserva campos que no toques).
### Paso 4 — Post-acciones (segun lo que toco)
```bash
# Siempre
cd /home/lucas/fn_registry && ./fn index
# Si toco icon.* -> regenerar appicon
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# Si toco trio o panels o uses_functions o cambia code:
./fn run redeploy_cpp_app_windows <name> <dir> --build
# Si toco description o icon o tags:
./fn run refresh_app_hub
# Si toco service: o tag service
./fn doctor services-spec
# Siempre al final
./fn doctor cpp-apps
```
---
## Reglas duras
- **NUNCA** crear `main.cpp` + `CMakeLists.txt` + `app.md` a mano. Siempre via `init_cpp_app_bash_pipelines` (regla `cpp_apps.md`).
- **NUNCA** poner el codigo en `cpp/apps/<n>/`. Solo `apps/<n>/` o `projects/<p>/apps/<n>/`.
- **NUNCA** dejar `app.md` sin el trio (description + icon.phosphor + icon.accent). Tarjeta del hub queda gris.
- **NUNCA** declarar funciones del registry en `uses_functions` sin listar su `.cpp` en `CMakeLists.txt` (drift detectado por `fn doctor uses-functions`).
- **NUNCA** usar `Restart=on-failure` en systemd unit de un service C++ — gotcha 2026-05-17 (`sqlite_api.service` cayo 20h). Default `Restart=always`.
- Despues de **cualquier** cambio en el trio: `regenerate_app_icons <name>` + `refresh_app_hub`.
---
## Auto-verificacion final
Tras crear o modificar, reportar al usuario:
```
=== app <name> ===
dir: <abs_dir>
domain: <d>
description: "<desc>"
icon: <phosphor> + <accent>
tags: [<csv>]
uses_functions: N funciones (<list_top_5>)
panels: N (<labels>)
e2e_checks: N checks
service: <si/no — port:<p> health:<h>>
Acciones ejecutadas:
[✓] scaffolder / edits
[✓] generate_app_icon
[✓] fn index (registry.db actualizado)
[✓] redeploy_cpp_app_windows (Desktop/apps/<name>/<name>.exe)
[✓] refresh_app_hub (tarjeta visible en hub)
[✓] fn doctor cpp-apps (limpio | N warnings)
Siguiente paso sugerido:
- Abrir app_hub_launcher en Windows y verificar tarjeta
- Anadir tests visuales si la app tiene paneles propios (cpp/PATTERNS.md §11)
```
$ARGUMENTS
+186
View File
@@ -0,0 +1,186 @@
---
name: fix-issue
description: Implementar un issue de dev/issues/ end-to-end. Crea rama, ejecuta tareas, bumpa version si toca modulos/framework/apps (via /version), tests, cierra issue, integra a master.
---
# /fix-issue
Ejecuta el flujo completo de implementacion/cierre de un issue de `dev/issues/`. Adaptado al stack del registry: Go (`-tags fts5 CGO_ENABLED=1`), Python (`python/.venv/bin/python3`), Bash, TypeScript (`pnpm`), C++ (`cmake`+`mingw-w64` toolchain).
## Inputs
```
/fix-issue <NNNN[a|b|c...]>
```
- `NNNN`: numero del issue (ej. `0107`).
- Si es sub-issue, sufijo letra: `0107a`, `0107b`, ...
Si no se proporciona, preguntar.
## Flujo obligatorio
### 1. Resolver el issue
- `dev/issues/<NNNN>-*.md` → si no existe, STOP.
- Si ya en `dev/issues/completed/`, STOP.
- Si es sub-issue, leer tambien el principal para contexto.
### 2. Leer y extraer
- Objetivo, tareas, arquitectura, prerequisitos, riesgos.
- Identificar archivos afectados — anotar si toca:
- `modules/<X>/` o `cpp/framework/` → bumpa version (paso 8).
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` → indexer + `fn index` al cerrar.
- Apps en `apps/<X>/` o `projects/*/apps/<X>/` → requiere rama TBD (regla `apps_tbd.md`) **+ bumpa version per-app (paso 8)**. Si el issue toca multiples apps, una llamada `/version` por app.
- Registry meta (CLAUDE.md, rules, templates) → push directo a master OK.
### 3. Estrategia de rama
**Registry-only changes** (functions/types/docs/rules):
- Push directo a master OK. NO crear rama.
**Apps changes** (apps/, projects/*/apps/):
- Crear rama TBD:
```bash
git checkout master
git pull --rebase
git checkout -b issue/<NNNN>-<slug>
```
La rama es del registry. Si la app es sub-repo, ademas crear rama dentro del sub-repo.
**Modules/framework changes** (`modules/`, `cpp/framework/`):
- Rama TBD obligatoria (afecta a todas las apps que linkean).
### 4. Plan con TaskCreate
- Crear tarea por bloque logico del issue.
- Incluir SIEMPRE:
- Tarea de tests (unit + smoke).
- Tarea de `fn index` si toco metadata.
- Tarea de `/version` si toco `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/` (una llamada por target).
- Tarea de cleanup/docs.
### 5. Implementar
Reglas registry-first (CLAUDE.md):
- ANTES de escribir codigo reutilizable → `mcp__registry__fn_search` para encontrar lo que existe.
- Si falta funcion reutilizable → spawn `fn-constructor` (no escribir inline).
- Si patron se repite >2x → propose nueva funcion.
- NUNCA `sqlite3 registry.db "SELECT ..."` plano — usar MCP.
Convenciones del stack:
| Stack | Build/test |
|---|---|
| Go | `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` y `CGO_ENABLED=1 go test -tags fts5 ./...` |
| Python | `python/.venv/bin/python3 -m pytest <path>` |
| Bash | `bash -n <script>.sh` + tests inline |
| TypeScript | `cd frontend && pnpm build && pnpm test` |
| C++ (Linux) | `cmake --build build --target <app>` |
| C++ (Windows MinGW) | `cmake -B build/windows -DCMAKE_TOOLCHAIN_FILE=cpp/toolchains/mingw-w64.cmake && cmake --build build/windows --target <app>` |
Commits atomicos por bloque logico con prefijos: `feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`. Mensajes en espanol. NO WIP.
### 6. Tests
Stack-dependent (ver arriba). Si tests pasan parcialmente con failures pre-existentes no causadas por la rama, documentar en cuerpo del commit/PR.
### 7. Feature flags (si aplica)
Si el issue forma parte de un feature multi-issue:
- Editar `dev/feature_flags.json` con el flag (desactivado).
- Activar el flag en el ultimo sub-issue del set.
Flag != WIP. Codigo detras de flag debe compilar + testear.
### 8. Version bump (si toco modulos/framework/apps)
**OBLIGATORIO si el issue toco** alguno de:
- `modules/<X>/` → bumpa `modules/<X>/module.md::version`.
- `cpp/framework/` → bumpa `modules/framework/module.md::version`.
- `apps/<X>/` → bumpa `apps/<X>/app.md::version`.
- `projects/<P>/apps/<X>/` → bumpa `projects/<P>/apps/<X>/app.md::version`.
```
/version <path> <major|minor|patch> "<reason>"
```
Reglas (modulos/framework):
- Major: breaking ABI/API publica.
- Minor: additive (nuevo helper, refactor interno sin cambio de API, nuevo miembro).
- Patch: bugfix puro.
Reglas (apps):
- Major: breaking observable (CLI args, schema BBDD propia, formato wire).
- Minor: feature aditiva visible (nuevo panel, endpoint, opcion).
- Patch: bugfix sin cambio observable, refactor interno, mejora perf.
**Una llamada `/version` por target afectado**. Si el issue toca 1 modulo + 2 apps -> 3 llamadas a `/version` (cada una con su `reason` y bump-type apropiado; pueden diferir).
Diff guard: cambios que solo tocan el `app.md` (correccion typo descripcion, anadir tag) NO requieren bump — son metadata, no comportamiento. Detectar con `git diff --name-only | grep -v '\.md$'` para decidir si hay cambio de codigo real.
`/version` solo edita + stage. NO commit. El bump va junto con el codigo correspondiente en el mismo commit (`feat:` o `fix:` o `refactor:`).
Si NO toco modulos/framework/apps, saltar este paso.
### 9. Cerrar el issue
Mover archivo:
```bash
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
```
Actualizar `dev/issues/README.md`:
- Link → `completed/<NNNN>-<slug>.md`
- Estado → `completado`
Si es feature multi-issue y este es el ultimo sub-issue:
- Flip flag en `dev/feature_flags.json` a `enabled: true` con `enabled_at: <YYYY-MM-DD>`.
- Verificar que todos los sub-issues estan en `completed/`.
### 10. Integrar
**Registry-only changes**: push directo a master.
**Apps/modules/framework changes**: `/full-git-push` o `/git-push` (merge --no-ff de la rama a master, push, delete rama).
### 11. Verificar post-cierre
- `fn index` — registry.db al dia.
- `fn doctor` (subcomandos relevantes: `artefacts`, `services`, `cpp-apps`, `uses-functions`).
- Si toco modulos: `fn doctor modules` (post 0107a) — 0 drift.
## Reglas criticas
- **Registry-first**: SIEMPRE buscar antes de escribir; delegar a `fn-constructor` antes que inline.
- **TBD para apps**: NUNCA push directo a master en apps. Rama corta, merge --no-ff.
- **TBD NO para registry**: push directo OK para functions/types/docs/rules.
- **`/version` obligatorio** si tocas modulos, framework o apps (con cambio de codigo real, no solo metadata). Si no, drift entre `version:` y `## Capability growth log` y se pierde trazabilidad.
- **Tests siempre**: no cerrar issue sin tests pasando (salvo failures pre-existentes documentados).
- **Commits atomicos**: 1 commit = 1 bloque logico. No mezclar `feat:` + `test:` en mismo commit.
- **Cerrar siempre**: nunca dejar issue implementado sin mover a `completed/` + actualizar README.
## Referenciado desde
- `.claude/commands/version.md` — bump semver de modulos.
- `.claude/commands/full-git-push.md` — push del registry + sub-repos.
- `.claude/rules/apps_tbd.md` — politica de TBD por tipo de cambio.
## Ejemplo: implementar 0107c (refactor data_table)
```
/fix-issue 0107c
1. Resolver: dev/issues/0107c-split-data-table.md ✓
2. Extraer: refactor 4777 LOC → 6 sub-funciones. Toca modules/ → /version obligatorio.
3. Rama: issue/0107c-split-data-table desde master.
4. Plan: 8 tareas (lectura + 6 sub-funciones + entrypoint thin + version bump).
5. Implementar: spawn fn-constructor en paralelo si hay >1 sub-funcion independiente.
6. Tests: build + smoke + primitives_gallery --capture diff.
7. Flag: parte de modules-v2, NO activar todavia (espera 0107a-f cerrar).
8. /version modules/data_table major "split data_table.cpp into 6 sub-functions"
9. Cerrar: mv → completed/ + README.
10. /git-push.
11. fn index + fn doctor modules → 0 drift en consumidores limpiados.
```
+131
View File
@@ -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-<slug>.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria.
**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow.
Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear <id>` en la tabla correspondiente. Nada de inventar nombres.
Diferencia con `dev/issues/`:
- Issues = bugs / features de implementacion.
- Flows = trabajos reutilizables que cruzan varias apps.
## Sintaxis
```
/flow create <slug> # nuevo flow desde template, ID auto
/flow list # tabla resumen
/flow show <NNNN> # imprime contenido + acceptance %
/flow status <NNNN> # status + acceptance % + ultima run
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
```
## Implementacion por subcomando
### `create <slug>`
Pasos:
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.md`. Si existe, error.
3. Calcula siguiente ID libre:
- `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1`
- Suma 1, zero-pad a 4 digitos.
4. Lee `dev/flows/template.md`.
5. Sustituye `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
6. Escribe `dev/flows/NNNN-<slug>.md`.
7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc).
8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance".
### `list`
Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app <name>` filtra por app.
Tambien anade columna `Accept%` calculada desde body:
- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`.
- `% = checked / total * 100` redondeo entero.
### `show <NNNN>`
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
### `status <NNNN>`
Imprime resumen del frontmatter + acceptance %:
```
=== flow 0001 ===
name: hn-top-stories
status: pending
risk: low
priority: high
apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots
acceptance: 2/6 (33%)
updated: 2026-05-16
Pending checks:
- [ ] Recipe creada y validada
- [ ] DAG corre OK 2 veces consecutivas via scheduler
- [ ] data_factory.runs tiene >=2 entries
- [ ] Schema extraido cubre 6/6 fields
```
### `done <NNNN> [--notes "..."]`
Pasos:
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
3. Si `--notes`, append a seccion `## Notas`.
4. `git mv dev/flows/NNNN-<slug>.md dev/flows/completed/`.
5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred).
### `run <NNNN>` — FASE 2 (NO IMPLEMENTADO AUN)
Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.`
Diseño futuro:
- Parsea `## Flow` en pasos.
- Cada paso tipo `function: <id>` -> ejecuta `./fn run <id>`.
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
- Texto libre -> "MANUAL: <text>" + pause user input.
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.jsonl`.
- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.).
## Conventions
- Numeracion 0001+, propia (no comparte con `dev/issues/`).
- Status: `pending | running | done | failed | deferred`.
- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales).
- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra.
- Acceptance es la fuente de verdad del progreso.
## Output style
Caveman. Tablas markdown. Sin emojis. Sin verbosidad.
Errores: 1 linea con el problema + sugerencia.
## Ejemplos
```
/flow create reddit-sentiment-tracker
# crea dev/flows/0008-reddit-sentiment-tracker.md
# anade fila a INDEX
/flow list --pending
# muestra solo flows no cerrados
/flow status 0001
# acceptance 0/6, todos los checks pendientes
# Tras correr el flow manualmente:
# editas el .md, marcas [x] los checks completados
/flow status 0001
# acceptance 6/6
/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta"
# mueve a completed/, marca status=done
```
+14
View File
@@ -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" "<fn_id>" "<one-line purpose>"
```
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup.
`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo:
- fn_id: `parse_http_log_go_infra`
- purpose: "parsea log Apache/Nginx a struct; pure"
Reporta:
- N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals`
+93
View File
@@ -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-<slug>.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags).
Allowlists en `dev/TAXONOMY.md` (no inventar valores).
Diferencia con `dev/flows/`:
- **Issues** = bugs, features, refactors, chores, epics de implementacion.
- **Flows** = casos de uso end-to-end multi-app.
## Sintaxis
```
/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN]
/issue show NNNN
/issue status NNNN # acceptance % + estado deps
/issue board # kanban pendiente/in-progress/bloqueado/done
/issue dep NNNN # arbol bloquea/depende
/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...)
/issue tag NNNN +X -Y # mantenimiento tags/domain
/issue done NNNN # mueve a completed/, valida deps
/issue stale [--days 30]
/issue create <slug> --type T --domain D [--prio P] [--depends NNNN]
```
## Implementacion
**Fase 1 (manual via Claude):**
El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
```python
import yaml, pathlib, re
issues = []
for f in pathlib.Path("dev/issues").glob("*.md"):
if f.name in {"README.md", "template.md"}: continue
txt = f.read_text()
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
if not m: continue
fm = yaml.safe_load(m.group(1)) or {}
fm["_path"] = str(f)
issues.append(fm)
# filter + print
```
**Fase 2 (cuando 0101 dev_console exista):**
Cada subcomando se mapea a `./apps/dev_console/dev_console issue <subcomando> $ARGS`.
## Subcomandos clave
### `list`
Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags.
### `show NNNN`
Read directo del .md + render del frontmatter como tabla + body como markdown.
### `status NNNN`
Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`.
### `board`
Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`.
### `roadmap NNNN`
Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic".
### `done NNNN`
1. Lee frontmatter.
2. Verifica todos `depends` cerrados (sino, error).
3. Cuenta `## Acceptance` 100% (sino, error).
4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`.
5. Actualiza `status: completado` + `updated: today`.
### `create <slug> --type T --domain D`
Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado.
## Reglas
- Domain debe estar en `dev/TAXONOMY.md` allowlist.
- Scope/type/priority idem.
- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`).
- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite).
+170
View File
@@ -0,0 +1,170 @@
---
name: version
description: Bumpear semver de un modulo, framework, paquete o app del registry. Edita <target>.md::version + ## Capability growth log. NO commitea.
---
# /version
Bumpea la version de un **modulo, framework, paquete o app** del registry siguiendo SemVer estricto y mantiene el `## Capability growth log` sincronizado con `<target>.md::version`.
Disenado para usarse desde `/fix-issue` cuando el cambio afecte:
- `modules/<X>/` (cualquier modulo C++) — edita `module.md`
- `cpp/framework/` — edita `modules/framework/module.md`
- `apps/<X>/` o `projects/<P>/apps/<X>/` — edita `app.md`
- Otros paquetes versionados con `<target>.md` y campo `version:`
## Inputs
```
/version <path> <major|minor|patch> "<reason>"
```
- `<path>`: directorio del target (ej. `modules/data_table`, `cpp/framework`, `apps/chart_demo`, `projects/fn_monitoring/apps/registry_dashboard`).
- `<major|minor|patch>`: tipo de bump SemVer.
- `<reason>`: 1-frase humana — lo que cambia. Se inserta en el log.
## Resolucion del archivo target
| Path empieza por | Archivo a editar |
|---|---|
| `modules/` | `<path>/module.md` |
| `cpp/framework` | `modules/framework/module.md` |
| `apps/` | `<path>/app.md` |
| `projects/*/apps/` | `<path>/app.md` |
| `projects/*/analysis/` | `<path>/analysis.md` |
Si no encuentra archivo target -> ERROR.
## Reglas SemVer
### Modulos / framework
| Bump | Cuando |
|---|---|
| `major` | Cambios breaking en API publica: firma de entry function, layout de State struct expuesto, eliminacion de members, cambio incompatible de comportamiento. |
| `minor` | Adiciones backwards-compatible: nuevo evento opt-in, nuevo renderer, nuevo helper, nuevo miembro. |
| `patch` | Bugfix sin cambio de API. |
Refactor interno SIN cambio de API publica -> `minor` (no major).
### Apps
| Bump | Cuando |
|---|---|
| `major` | Breaking observable por usuarios: CLI args incompatibles, schema BBDD propia rompe lectores viejos, formato wire (HTTP/gRPC) incompatible, eliminacion de panel/feature que la gente usaba. |
| `minor` | Feature aditiva: nuevo panel, nuevo endpoint, nueva opcion CLI, nueva tab, mejora visible no rompedora. |
| `patch` | Bugfix sin cambio observable. Refactor interno. Mejoras de perf. |
Bump de **dependencia** (modulo/funcion del registry) que mejora la app pero la app no cambia su API -> `patch` (la app no es responsable de la mejora; el modulo si).
## Flujo
### 1. Validar input
- `<target_file>` existe -> si no, ERROR.
- Bump type en {major, minor, patch} -> si no, ERROR.
- Reason no vacia -> si no, ERROR.
### 2. Leer version actual
Parsear frontmatter. Buscar `version: X.Y.Z`. Si no existe:
- Para `module.md` -> ERROR "module.md sin campo version".
- Para `app.md` -> asumir `0.1.0` (baseline) e insertar el campo despues de `domain:`.
### 3. Calcular proxima version
```
1.4.0 + major = 2.0.0
1.4.0 + minor = 1.5.0
1.4.0 + patch = 1.4.1
```
Major bump -> minor y patch a 0. Minor bump -> patch a 0.
### 4. Editar `<target_file>`
Cambiar linea `version: <old>` por `version: <new>`.
### 5. Anadir entrada a `## Capability growth log`
Insertar al inicio de la lista (lineas posteriores al header `## Capability growth log`):
```markdown
- v<new> (<fecha YYYY-MM-DD>) — <reason>
```
Si la seccion no existe -> crearla al final del archivo antes de `## Notes` (o al final si no hay Notes).
### 6. Verificar drift de members (solo modulos, opcional)
Si la herramienta `fn doctor modules` existe (post 0107a) y el target es modulo:
- Compara `members:` actual vs ultima version registrada en `registry.db::modules_history`.
- Si hay diff en members y bump es `patch` -> WARNING.
- Si hay diff en API publica y bump no es `major` -> ERROR (require `--force`).
No aplica a apps (no tienen `members:`).
### 7. Stage en git
`git add <target_file>`. NO commit. El commit final lo hace el flujo padre.
### 8. Reportar
```
/version apps/chart_demo minor "anade tab radar chart"
apps/chart_demo/app.md
version: 1.2.0 -> 1.3.0
## Capability growth log: + v1.3.0 (2026-05-18) — anade tab radar chart
Staged. NO committed.
Next: terminar el fix-issue y hacer commit con el resto de cambios.
```
## Reglas criticas
- **NUNCA commit**. `/version` solo edita + stage. El commit lo hace el flujo padre (`/fix-issue`, `/git-push`).
- **NUNCA saltar version**. No 1.4.0 -> 1.4.2 directo.
- **NUNCA bajar version**. Si rollback, crea nueva version superior con comportamiento viejo restaurado.
- **fecha = HOY** (`date +%Y-%m-%d`).
- **reason** comprensible sin contexto del PR actual.
## Referenciado desde
- `/fix-issue` — al detectar cambios en `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/`, sugiere ejecutar `/version` antes del commit final.
- `.claude/rules/cpp_apps.md` — politica de bump.
- `dev/issues/0107-modules-standardization.md` — origen del flujo (modulos).
## Ejemplos
```
# Bug fix en data_table (modulo)
/version modules/data_table patch "fix off-by-one en seleccion multi-row con shift+click"
# -> 1.4.0 -> 1.4.1
# Feature opt-in en framework
/version cpp/framework minor "anade cfg.auto_dockspace para overlay de paneles flotantes"
# -> 1.1.0 -> 1.2.0
# Feature en app C++
/version apps/chart_demo minor "anade tab radar chart con datos sinteticos"
# -> 1.2.0 -> 1.3.0
# Bug fix en app de proyecto
/version projects/fn_monitoring/apps/registry_dashboard patch "fix tooltip que mostraba duration_ms en segundos"
# -> 0.4.1 -> 0.4.2
# Breaking en app: cambia schema de su BBDD propia
/version apps/kanban major "cards.assignee_id pasa a ser TEXT[] (era TEXT); requiere migracion 008"
# -> 1.0.0 -> 2.0.0
```
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Editar `version:` a mano sin `## Capability growth log` | Drift entre version y log; nadie sabe que cambio. |
| Bumpear major en app por refactor interno | Confunde al usuario; refactor es patch. |
| Patch para feature visible | Usuario no se entera que esta disponible. |
| Reason "cambios varios" / "mejoras" | Inutil para auditar. Una frase concreta. |
| Bump de app sin tocar codigo de la app (solo dep) | Bump va al modulo, no a la app. |
+67
View File
@@ -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 <subcomando> $ARGS`.
## Subcomandos
### `today`
Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola".
### `weekly`
Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: <esta semana>` -> ratio in/out.
### `search "texto"`
`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`.
### `dashboard`
Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura:
```json
{
"issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]},
"flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}],
"telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N}
}
```
+5
View File
@@ -21,6 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
@@ -34,3 +35,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
| 34 | [dod_quality.md](dod_quality.md) | DoD Quality Triada: Mecanica + Cobertura (golden + edge + error path con evidencia ejecutable) + Vida util validada (>=7 dias uso real). Cierra anti-criterios contra checkbox vago. Aplica a `dev/flows/` y issues user-facing. |
+74
View File
@@ -0,0 +1,74 @@
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
### El gotcha
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
### El patron correcto al crear apps en worktrees
```bash
# 1. Agente trabaja en worktree del repo padre
cd /home/lucas/fn_registry/worktrees/<slug>
# 2. Scaffold la app via pipeline canonico
./fn run init_cpp_app <name> # apps C++
# o ./fn run init_jupyter_analysis ... # analysis
# o crear apps/<name>/ a mano (Go service, etc.)
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
cd apps/<name>
git init -b master
git add -A
git -c user.email="agent@fn_registry" -c user.name="agent" \
commit -m "feat: initial scaffold of <name>"
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
```
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
### Que ESTA SI versionado en el repo padre
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
- `docs/capabilities/*.md` si la app aporta a un capability group.
- `dev/feature_flags.json` si introduce flags.
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
### Sintomas de la perdida
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
### Recovery si pasa
1. Re-crear worktree desde master.
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
### Aplica tambien a analysis
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
### Lo que aprende `parallel-fix-issues`
El template del prompt de cada agente DEBE incluir la instruccion:
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
### Relacion con otras reglas
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
+102
View File
@@ -0,0 +1,102 @@
## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069
`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/<issue>`, NUNCA merge a master.
### Cuando se invoca
- Skill `/autonomous-task <issue_id>` (humano lanza explicitamente).
- Cron / dag_engine (`schedule:` en YAML; planificable, no implementado por defecto).
- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita.
### Reglas duras
1. **Sandbox obligatorio**: rama `auto/<issue_id>-<slug>`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master.
2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`.
3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen:
- `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable.
- `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano).
- `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`.
4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`.
5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h.
6. **Idempotencia**: re-ejecutar `/autonomous-task <id>` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`).
7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar.
8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`.
9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C <main_repo> status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT.
10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main.
### Sub-repos vs worktree padre
Cuando el issue toca `app.md` o codigo dentro de `apps/<name>/`, `projects/<p>/apps/<name>/`, `cpp/apps/<name>/`, o `analysis/<a>/` — estos directorios son **sub-repos Gitea independientes** y estan `.gitignore`d en el repo padre `fn_registry` (regla `apps_subrepo.md`). El orquestador:
- **Crea worktree padre** `auto/<issue>` en `/tmp/fn_orq_<issue>_<ts>/` por protocolo, **pero no escribe alli** porque los cambios no se versionan en el padre.
- **Opera DIRECTAMENTE en el sub-repo** de la app/analysis target. Branch `auto/<issue>-<slug>` se crea dentro de `apps/<name>/.git`, NO en el padre.
- **PR draft sale al sub-repo** en `dataforge/<name>` (NO a `dataforge/fn_registry`). Humano revisa+mergea en el sub-repo.
- **Worktree padre queda vacio** y se limpia normal con `git worktree remove` al terminar.
Validado en piloto 0120 (`add_e2e_check` sobre `chart_demo`): PR creado en `dataforge/chart_demo/pulls/1`, sanity check del main repo `fn_registry` confirmo cero contaminacion.
Si el issue toca AMBOS lados (codigo del registry padre + app de sub-repo), el orquestador commitea separado: cambios del padre en `auto/<issue>` (worktree padre), cambios de la app en `auto/<issue>-<slug>` (sub-repo). Dos PRs draft. Humano coordina merge.
### Gitea API vs `gh`
Pre-condicion `gh auth status` es smoke check (target github.com). Mecanismo real de PR es `curl` a Gitea API:
```bash
GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"...","head":"auto/<issue>-<slug>","base":"master","draft":true,"body":"..."}' \
"https://gitea-.../api/v1/repos/dataforge/<repo>/pulls"
```
Validado en pilotos 0076 y 0120.
### Estructura task_run
Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`.
### Fases por iteracion
```
loop:
1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada
2. fn-executor - corre build + tests + smoke
3. fn-recopilador - audita operations.db de la app
4. fn-analizador - corre e2e_checks (registra e2e_runs)
5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit.
6. SI no progreso N iteraciones -> abort. status=stalled.
7. fn-mejorador - crea proposals desde fallos
8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1.
```
### Output al humano
```
=== /autonomous-task 0068 ===
task_run_id: run_e2e_a1b2c3
branch: auto/0068-e2e-validation
iterations: 4
status: done
checks_pass: 8/8
proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003)
proposals_skipped: 1 (refactor — needs human review)
PR: https://gitea.../pulls/42
```
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Mergear `auto/<issue>` a master sin PR + humano | Salta gate, riesgo de regresion |
| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision |
| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review |
| Bucle infinito sin watchdog | Coste descontrolado de tokens |
| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria |
| Override de paths protegidos via env var | Bypass de seguridad |
### Relacion con otras reglas
- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate.
- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD.
- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR.
- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor.
+233 -12
View File
@@ -20,14 +20,14 @@ Razones:
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
### 1. Ubicacion
### 1. Ubicacion (issue 0096 estandarizada)
| Caso | Donde vive |
|---|---|
| App independiente | `cpp/apps/<nombre>/` |
| App independiente | `apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
NUNCA en `cpp/apps/<nombre>/` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
@@ -84,6 +84,7 @@ Plantilla minima para apps C++:
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
version: 0.1.0 # semver per-app, bumped via /version
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
@@ -102,6 +103,7 @@ Reglas:
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
- `version`: semver per-app. Baseline `0.1.0` para apps nuevas. Bump obligatorio via `/version apps/<name> {major|minor|patch} "<reason>"` cuando `/fix-issue` toque codigo de la app. Trazabilidad humana en seccion `## Capability growth log` al final del `app.md` (una linea por bump). Ver `.claude/commands/version.md`.
### 5. Registro en `cpp/CMakeLists.txt`
@@ -189,20 +191,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<HWND, WNDPROC>`. Chain a la proc
original via `CallWindowProcW`.
- `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa).
- Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada
`pio.Viewports[i]->PlatformHandle` nuevo.
- `uninstall_sizemove_subclass_all()` restaura cada HWND al exit.
#### Iconified main no pierde paneles flotantes (2026-05-16)
El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO
el frame loop. Con multi-viewport activo eso significa que
`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de
refrescar los viewports secundarios — los floating panels aparecen congelados
o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta
viewports secundarios primero; si hay alguno, fall-through al frame normal
(la swap del main HWND minimizado es harmless, los contexts GL secundarios
siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`.
#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16)
WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`):
- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` +
`PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo.
- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/
BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite
`PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo.
Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a
cada viewport flotante porque el subclass per-frame ya cubre todos los HWND.
El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa
render → cero jitter automatico, mismo contrato que el title-bar drag.
**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut
UI. Aceptable porque Alt-modifier en clicks UI es muy raro.
#### Title-bar-only move para ImGui windows (2026-05-16)
`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico
para viewports secundarios: un viewport flotante = OS window borderless con
UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas
arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport
entero, el OS window sigue al cursor sin modifier. Con el flag, floating
panels obedecen el contrato "solo header arrastra" (igual que main que tiene
title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido
antes por el subclass).
#### Test observability — `fn::internal::*` (2026-05-16)
Counters monotonicos para validar el subclass desde tests headless,
zero-cost en prod:
```cpp
namespace fn::internal {
int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE
int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido
int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido
int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN
void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests
}
```
En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan
pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el
modal de Windows. Path real en prod sigue posteandolos.
Tests: `apps/altsnap_jitter_test/` corre seis fases:
- `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 +348,137 @@ de antes: `imgui.ini` es la unica fuente.
- App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`).
### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16
Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en
`<app_dir>/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro
`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si
`WIN32` + existe `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
`IDI_ICON1 ICON "<path>"` y lo anade a `add_executable`. El compilador RC
(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`)
lo enlaza al `.exe` como recurso `.rsrc`.
Verificar: `x86_64-w64-mingw32-objdump -h <app>.exe | grep rsrc` debe
mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara
`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`).
#### Crear `.ico` para una app nueva
Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de
`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`,
`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor
legibilidad a 16/24px.
Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor
sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una
linea por app:
```python
from infra import generate_app_icon
generate_app_icon(
phosphor_icon_name="chart-bar",
accent_hex="#0ea5e9",
out_ico_path="apps/chart_demo/appicon.ico",
)
```
Mapping vive en el frontmatter de cada `app.md` C++:
```yaml
description: "Frase corta de 1 linea — que hace la app y por que existe."
icon:
phosphor: "chart-bar"
accent: "#0ea5e9"
```
### Trio obligatorio: description + icon.phosphor + icon.accent
**REGLA DURA:** TODA app C++/imgui declara los **3 campos JUNTOS** en su `app.md`:
1. `description:` (string corta, 1 linea) — texto que el `app_hub_launcher` muestra en la tarjeta y que el dashboard usa para tooltips.
2. `icon.phosphor:` (nombre del glyph Phosphor sin sufijo `-fill`) — glyph del icono.
3. `icon.accent:` (hex `#rrggbb`) — color del fondo redondeado del icono **Y** color del boton/border de la tarjeta en `app_hub_launcher`.
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin `icon:` cae al default (`app-window` slate); sin accent el boton del hub aparece blanco. **Documentar uno sin los otros es bug**, no estilo.
### Refrescar el App Hub tras editar el trio
`app_hub_launcher` cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar `description`/`icon.*` en un `app.md` requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
```bash
./fn run refresh_app_hub # icons + manifest + restart hub
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
```
ID: `refresh_app_hub_bash_pipelines`. Compone `export_hub_icons_py_infra` + `export_hub_manifest_py_infra` + `is_cpp_app_running_windows_bash_infra` + `launch_cpp_app_windows_bash_infra`.
Regeneracion batch via pipeline del registry — escanea `app.md`s y compone
`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su
`app.md` y lanzar:
```bash
./fn run regenerate_app_icons # todas
./fn run regenerate_app_icons chart_demo # solo una
```
Convenciones:
- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`).
- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700
funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.).
- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado.
- **Glyph color**: siempre blanco sobre el fondo accent.
Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/`
con `ls | grep <keyword>` antes de inventar — 1512 disponibles.
#### Re-deploy tras cambiar icono
```bash
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
./fn run regenerate_app_icons chart_demo
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc)
./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build
```
Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras
desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.
#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts —
pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el
**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a
menos que adjuntemos el recurso al HWND en runtime.
`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`:
```cpp
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
```
Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado
(`101 ICON "<app_dir>/appicon.ico"`). Si la app no tiene `appicon.ico`, el
`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono
GLFW por defecto (sin error).
Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que
instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
sin codigo extra en la app.
Cache shell: el pipeline `redeploy_cpp_app_windows` llama
`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca
`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer.
+131
View File
@@ -0,0 +1,131 @@
# DoD Quality Triada
**Definition of Done no es un checkbox que se marca a mano. Es un contrato de calidad con 3 capas obligatorias + evidencia ejecutable + uso real >=7 dias.**
Aplica a todos los `dev/flows/` y, por extension, a issues que cierran capabilities user-facing (`dev/issues/`). El registry mismo (funciones puras, tipos) queda exento: su DoD vive en sus tests unitarios.
---
## Por que existe esta regla
El antipatron a eliminar: "tarea hecha porque pase los tests una vez". Despues:
- El flow funciona en `home-wsl` pero falla en `pc-aurgi`.
- El error path declarado nunca se ejercito y cuando ocurre en produccion no esta manejado.
- El dashboard de observabilidad lleva 30 dias sin abrirse.
- El proceso muere cada noche y nadie lo ve hasta que el operador intenta usarlo.
- El approval flow se salta porque "para test es mas comodo".
Resultado: deuda invisible. Cada flow "done" se rompe al primer uso real, el operador pierde confianza en el sistema, y el bucle reactivo no detecta nada porque la telemetria esta verde (los tests sintenticos pasan).
DoD Quality Triada cambia las reglas: cerrar = probar comportamiento + sobrevivir uso real, no = compilar verde.
---
## Las 3 capas
### Capa 1: Mecanica (pre-requisito, NO es DoD por si misma)
Compilar verde, tests verdes, indexado limpio, `fn doctor` verde, `uses_functions` sin drift.
**Regla**: la mecanica NO basta. Es la base para empezar a probar comportamiento. Si te quedas aqui, el flow no esta hecho.
### Capa 2: Cobertura de comportamiento
Cada escenario relevante con prueba ejecutable y assert material. NO smoke "el comando no peto". Minimo:
- **1 golden path** — el caso feliz documentado con assert sobre output concreto.
- **>=2 edge cases** — inputs limite, estados raros, condiciones de borde.
- **>=1 error path** — fallo provocado intencionalmente, manejado y observable (sin crash, sin silent-fail).
Formato canonico (tabla en `## Definition of Done` del flow/issue):
```markdown
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: <desc> | unit / e2e | `<cmd>` | <output concreto> |
| Edge 1: <desc> | unit / e2e | `<cmd>` | <comportamiento concreto> |
| Error 1: <desc> | e2e | `<cmd que rompe>` | <fallo manejado, no crash> |
```
Cuando aplique, cada fila genera un `e2e_check` en el `app.md` correspondiente (issue 0068). `fn-analizador` los corre periodicamente y deja entry en `e2e_runs`.
### Capa 3: Vida util validada
El flow no esta hecho hasta que sobrevive **uso real durante >=7 dias** sin romperse silenciosamente. Cada metrica con umbral medible y dashboard observable.
Formato canonico:
```markdown
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| <metrica 1> | `>=N` | `<dashboard URL / app panel>` | 7 dias |
| crashes | `0` | `journalctl -u <unit>` | 7 dias |
| huecos audit chain | `0` | `cmd: <verify>` | continuo |
```
Reglas:
- Metricas NO se auto-reportan; las lee el operador del dashboard real.
- Si el dashboard no existe o no se ha abierto en 30 dias, el item se invalida.
- Crashes del proceso = 0, huecos en audit = 0, error_rate < umbral declarado.
### Capa transversal: User-facing reforzado
- Surface concreta NO BD ni log (UI app, room Matrix, dashboard, archivo en vault).
- Usage real: humano usa en su PC, su contexto, >=N veces variadas en >=7 dias.
- Variado: >=3 capabilities/casos distintos (no solo "abre dashboard y mira").
- Onboarding: parrafo en `## Notas` que explica como usar la cosa sin leer el flow.
- Latencia medida (no declarada).
---
## Reglas duras para marcar `status: done`
`/flow done` (y por extension cierres de issues user-facing) DEBE rechazar el cierre si:
1. Falta cualquiera de las 3 capas (mecanica + cobertura + vida).
2. Cobertura tiene <1 golden, <2 edge, o <1 error path con evidencia.
3. Vida util tiene tabla vacia o sin dashboard observable real.
4. User-facing usage real <7 dias o <N usos declarados.
5. Cualquier anti-criterio marcado como cierto.
6. `## Notas` sin parrafo onboarding.
7. Algun item de DoD sin comando/URL/log query asociado — solo texto.
Hoy parte de esta validacion es manual (revision humana del operador). La validacion programatica vive en `audit_dod_schema_go_infra` (issue 0114) + `fn doctor dod` y se ampliara hasta cubrir las 3 capas (TBD).
---
## Antipatrones (invalidan la DoD aunque los checkboxes esten verdes)
| Antipatron | Por que es malo | Sustituir por |
|---|---|---|
| Marcar `done` porque pasa una vez | Tarea "hecha" se rompe al primer uso real | Capa 3: >=7 dias de uso real |
| Checkbox sin evidencia ejecutable | DoD se convierte en placebo | Cada item con `cmd:` / URL / log query |
| Test que solo verifica camino feliz | El error path es donde se pierden datos | Capa 2: >=1 error path ejercitado |
| Observabilidad declarada pero dashboard no abierto en 30 dias | Telemetria muerta = ceguera | Capa 3: dashboard real, operador lo abre |
| "Repetible 3 veces consecutivas" con BD efimera | No prueba sobre datos reales acumulados | Capa 3: PC real del operador, datos vivos |
| Approval saltado en algun camino | Security gate roto pero invisible | Anti-criterio explicito: `audit_log` lo prueba |
| Error path manejado solo "en teoria" | Cuando ocurra en produccion el manejo no existe | Capa 2: entry real en `e2e_runs` o audit |
| Solo-en-mi-PC | Falla en otra maquina del operador | Anti-criterio explicito, probar >=2 PCs |
| Self-test que retorna `pass` sin asserts materiales | False positive sistemico | Asserts sobre output concreto, no exit-0 |
| Silent-fail (proceso muere sin alerta) | Operador no se entera hasta intentar usar | Capa 3: crashes=0 + alerta visible |
---
## Relacion con otras reglas
- [[e2e_validation]] — los escenarios de Capa 2 cuando aplican a apps se materializan como `e2e_checks` en `app.md`. `fn-analizador` (fase 4 del bucle reactivo) los corre.
- [[registry_calls]] — la evidencia de uso (`call_monitor.calls`) alimenta los umbrales de Capa 3.
- [[function_growth_and_self_docs]] — cada funcion del registry tiene su propio contrato self-doc (Ejemplo + Cuando usarla + Gotchas). DoD del flow NO sustituye al self-doc de la funcion; lo complementa para el nivel sistema.
- [[autonomous_loop]] — `fn-orquestador` autonomo NO puede marcar `done` sin que se cumplan las 3 capas. Su criterio de convergencia incluye DoD Quality.
- [[apps_tbd]] — TBD garantiza master desplegable; DoD garantiza que lo desplegado funciona en uso real.
---
## TL;DR
1. **Mecanica** = compilar verde (pre-requisito, NO suficiente).
2. **Cobertura** = golden + >=2 edge + >=1 error path con evidencia ejecutable.
3. **Vida util** = >=7 dias de uso real sin romper silenciosamente, dashboard observable abierto.
4. **User-facing reforzado** = humano usa en PC real, >=N veces variadas.
5. **Anti-criterios** invalidan la DoD aunque todo este verde.
6. Sin evidencia ejecutable (cmd/URL/log), NO es DoD: es deseo.
+12 -1
View File
@@ -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 = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes
+23
View File
@@ -28,3 +28,26 @@ Documentar en el `app.md` del service:
- El puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Como comprobar que esta vivo (health check)
### Bloque `service:` obligatorio (issue 0105)
Toda app con `tag: service` declara el bloque `service:` en su frontmatter. El indexer lo persiste en columnas dedicadas de `apps` + tabla `service_targets`. Consumido por `services_api`/`services_monitor` (issue 0106) y por `fn doctor services-spec`.
```yaml
service:
port: 8484 # null si no expone HTTP (stdio, daemon sin API)
health_endpoint: /api/databases # ruta GET, 2xx/3xx = sano; null si no aplica
health_timeout_s: 3
systemd_unit: sqlite_api.service # obligatorio si runtime empieza con `systemd-`
systemd_scope: user # user|system|null (docker-compose)
restart_policy: always # always|on-failure|none
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
pc_targets: # >=1, pc_id de pc_locations
- aurgi-pc
- home-wsl
is_local_only: false # true => no se monitoriza por SSH (siempre local)
```
Validacion: `fn doctor services-spec` (`functions/infra/audit_services_spec.go`). Hoy 11/11 services con bloque completo.
**Gotcha critico:** usar `Restart=always` (no `on-failure`) en el unit systemd. Un `SIGTERM` limpio es exit success → `on-failure` NO reinicia y el service se queda muerto silenciosamente. `sqlite_api.service` cayo 20h asi el 2026-05-17.
+34 -2
View File
@@ -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 `<noun>_<modifier>`. Validator salta el check de verbo si `kind=component`.
### Validator
`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si:
- name no es snake_case.
- name no contiene verbo (excepto component/type).
- domain no esta en lista canonica.
Error tipico:
```
naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md.
naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first.
```
+52
View File
@@ -0,0 +1,52 @@
## Slash commands por project (namespaced)
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
### Patron canonico
```
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
```
Resultado:
| cwd | Invocacion |
|---|---|
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
### Como anadir un project nuevo
1. `mkdir -p projects/<p>/.claude/commands/`.
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
### Reglas
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
### Listado actual
| Project | Symlink | Commands disponibles desde fn_registry |
|---|---|---|
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
Anadir filas aqui al introducir un project nuevo con commands.
### Catalogo dinamico
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
### Gotchas
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
+1 -1
View File
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
+53
View File
@@ -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 <fn_id> "<one-line purpose>"
set -euo pipefail
FN_ID="${1:-}"
PURPOSE="${2:-}"
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&2
exit 2
fi
MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}"
MEM_FILE="$MEM_DIR/MEMORY.md"
[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; }
[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; }
# Per-function reference file slug
SLUG="reference_fn_${FN_ID}.md"
REF_FILE="$MEM_DIR/$SLUG"
# Idempotency: if already linked in MEMORY.md, exit 0
if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then
echo "already in MEMORY.md: $FN_ID"
exit 0
fi
# 1. Create reference memory file
cat > "$REF_FILE" <<EOF
---
name: fn-$FN_ID
description: Registry function $FN_ID — $PURPOSE
metadata:
type: reference
---
Registry function: \`$FN_ID\`
$PURPOSE
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
EOF
# 2. Append index line to MEMORY.md
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
echo "appended: $FN_ID -> $MEM_FILE"
@@ -0,0 +1,121 @@
#!/bin/bash
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
#
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Para cada slug:
# 1. git merge --no-ff issue/<slug> a master
# 2. Verificar que master compila después del merge
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
#
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
# NO hace push — eso lo decide el usuario.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Asegurar que estamos en master
echo "=== Cambiando a master ==="
cd "$REPO_ROOT"
git checkout master
MERGED=0
FAILED_AT=""
for slug in "$@"; do
branch="issue/${slug}"
echo ""
echo "=== Integrando: ${branch} ==="
# Verificar que la branch existe
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
echo "FAIL: branch ${branch} no existe"
FAILED_AT="$slug"
break
fi
# Merge --no-ff
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
echo ""
echo "CONFLICT: merge de ${branch} tiene conflictos"
echo "Resolver manualmente y luego continuar con los slugs restantes"
echo ""
echo "Para resolver:"
echo " 1. git status (ver archivos en conflicto)"
echo " 2. Resolver conflictos en cada archivo"
echo " 3. git add <archivos>"
echo " 4. git commit"
echo ""
echo "Slugs pendientes después de ${slug}:"
FOUND=0
for remaining in "$@"; do
if [ "$FOUND" -eq 1 ]; then
echo " - ${remaining}"
fi
if [ "$remaining" = "$slug" ]; then
FOUND=1
fi
done
exit 1
fi
echo "MERGED: ${branch}"
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
if [ -n "${BUILD_CMD:-}" ]; then
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
echo ""
echo "FAIL: master no compila despues de mergear ${branch}"
echo "Revertir con: git reset --hard HEAD~1"
echo "Investigar el problema antes de continuar."
FAILED_AT="$slug"
break
fi
echo "OK: build post-merge exitoso"
else
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
fi
MERGED=$((MERGED + 1))
done
echo ""
echo "=== Resumen de integración ==="
echo "Mergeados: ${MERGED} de $#"
if [ -n "$FAILED_AT" ]; then
echo "Falló en: ${FAILED_AT}"
echo ""
echo "Worktrees NO limpiados (resolver primero el fallo)"
exit 1
fi
# Limpieza de worktrees y branches
echo ""
echo "=== Limpieza ==="
for slug in "$@"; do
path="${REPO_ROOT}/worktrees/${slug}"
branch="issue/${slug}"
if [ -d "$path" ]; then
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
fi
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
done
echo ""
echo "=== Integración completa ==="
echo "Master tiene ${MERGED} merges nuevos."
echo ""
echo "Para publicar: git push"
@@ -0,0 +1,74 @@
#!/bin/bash
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
#
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Cada slug genera:
# worktrees/<slug>/ (worktree completo)
# branch: issue/<slug>
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_DIR="${REPO_ROOT}/worktrees"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug de issue"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
echo "=== Verificando master ==="
CURRENT_BRANCH="$(git branch --show-current)"
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
fi
# NO auto-pull. Usuario decide sync con remote.
mkdir -p "$WORKTREE_DIR"
CREATED=0
SKIPPED=0
FAILED=0
for slug in "$@"; do
branch="issue/${slug}"
path="${WORKTREE_DIR}/${slug}"
if [ -d "$path" ]; then
echo "SKIP: worktree ya existe: ${path}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Verificar que la branch no existe ya
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
git worktree add "$path" "$branch" 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
else
echo "CREATE: worktree ${path} (branch ${branch})"
git worktree add -b "$branch" "$path" master 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
fi
CREATED=$((CREATED + 1))
done
echo ""
echo "=== Resumen ==="
echo "Creados: ${CREATED}"
echo "Existentes: ${SKIPPED}"
echo "Fallidos: ${FAILED}"
echo ""
echo "=== Worktrees activos ==="
git worktree list
@@ -0,0 +1,165 @@
#!/bin/bash
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
#
# Uso:
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
#
# Ejemplos:
# ./verify-worktree.sh worktrees/0026-foo
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
#
# Resolucion de comandos (en orden de prioridad):
# 1. Argumentos posicionales (build-cmd, test-cmd)
# 2. Variables de entorno BUILD_CMD / TEST_CMD
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
# 4. Auto-deteccion segun ficheros del proyecto:
# - go.mod → "go build ./..." + "go test ./..."
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
# - Cargo.toml → "cargo build" + "cargo test"
# - package.json → "npm run build" + "npm test"
# - pyproject.toml → "" + "pytest"
# 5. Si nada se detecta, salta build/test con WARN.
#
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
#
# Exit codes:
# 0 = todo OK
# 1 = error de argumento
# 2 = build fallo
# 3 = tests fallaron
# 4 = issue no cerrado (solo WARN, no falla)
# 5 = sin commits propios
set -euo pipefail
if [ $# -lt 1 ]; then
echo "ERROR: se necesita el path del worktree"
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
exit 1
fi
WORKTREE="$1"
ARG_BUILD_CMD="${2:-}"
ARG_TEST_CMD="${3:-}"
# Resolver path absoluto
if [[ "$WORKTREE" != /* ]]; then
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE="${REPO_ROOT}/${WORKTREE}"
fi
if [ ! -d "$WORKTREE" ]; then
echo "ERROR: worktree no encontrado: ${WORKTREE}"
exit 1
fi
SLUG="$(basename "$WORKTREE")"
echo "=== Verificando: ${SLUG} ==="
# --- Resolver build/test commands ---
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
# Manifest opcional
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
fi
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
fi
# Auto-deteccion
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
AUTO_BUILD=""
AUTO_TEST=""
if [ -f "${WORKTREE}/go.mod" ]; then
# Detectar build tag
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
| sort -u | head -1 || true)
TAG_FLAG=""
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
AUTO_BUILD="go build $TAG_FLAG ./..."
AUTO_TEST="go test $TAG_FLAG ./..."
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
CMAKE_DIR="."
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
AUTO_BUILD="cargo build"
AUTO_TEST="cargo test"
echo "INFO: stack detectado: Rust"
elif [ -f "${WORKTREE}/package.json" ]; then
AUTO_BUILD="npm run build --if-present"
AUTO_TEST="npm test --if-present"
echo "INFO: stack detectado: Node"
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
AUTO_BUILD="" # python normalmente no tiene build step
AUTO_TEST="pytest"
echo "INFO: stack detectado: Python"
else
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
fi
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
fi
# 1. Verificar commits propios
echo ""
echo "--- Commits propios ---"
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
if [ "$COMMIT_COUNT" -eq 0 ]; then
echo "FAIL: sin commits propios en la branch"
exit 5
fi
echo "OK: ${COMMIT_COUNT} commits desde master"
cd "$WORKTREE" && git log master..HEAD --oneline
# 2. Build
echo ""
if [ -n "$BUILD_CMD" ]; then
echo "--- Build ($BUILD_CMD) ---"
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
echo "OK: build exitoso"
else
echo "FAIL: build fallo"
exit 2
fi
else
echo "--- Build SKIPPED (sin comando) ---"
fi
# 3. Tests
echo ""
if [ -n "$TEST_CMD" ]; then
echo "--- Tests ($TEST_CMD) ---"
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
echo "OK: tests pasaron"
else
echo "FAIL: tests fallaron"
exit 3
fi
else
echo "--- Tests SKIPPED (sin comando) ---"
fi
# 4. Issue cerrado
echo ""
echo "--- Cierre de issue ---"
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
if [ "$COMPLETED_FILES" -gt 0 ]; then
echo "OK: issue movido a completed/"
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
else
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
fi
echo ""
echo "=== RESULTADO: ${SLUG} — OK ==="
+7
View File
@@ -81,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_*
+62
View File
@@ -8,6 +8,68 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased]
## 2026-05-17
### Added
- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.
### Fixed
- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.
### Changed
- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.
## 2026-05-16
### Added
- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.
### Fixed
- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
- Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
- `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.
### Added
- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
- Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
- Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
- Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
- Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
- Convencion documentada en `.claude/rules/cpp_apps.md §11`.
- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.
### Changed
- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.
### Notes
- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.
## 2026-05-14
### Added
+22
View File
@@ -0,0 +1,22 @@
[2026-05-22 23:18:14.872] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:12.811] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:14.628] [INFO] [connect] testing https://agents.organic-machine.com...
[2026-05-22 23:24:14.758] [INFO] [connect] OK
[2026-05-22 23:24:14.765] [INFO] [db] base_url saved
[2026-05-22 23:24:14.765] [INFO] [fetch_agents] starting
[2026-05-22 23:24:14.766] [INFO] [fetch_agents] requesting https://agents.organic-machine.com/agents
[2026-05-22 23:24:14.903] [INFO] [fetch_agents] response status=200 err= body_len=3146
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] parsed 11 rows
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] done
[2026-05-22 23:24:14.910] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:07.469] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:08.242] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:36.670] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:37.446] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:28:07.068] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:03.025] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:38.605] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:48.267] [INFO] app start: Agents Dashboard
[2026-05-22 23:40:58.931] [INFO] app start: Agents Dashboard
[2026-05-22 23:41:16.455] [INFO] app start: Agents Dashboard
[2026-05-22 23:42:35.646] [INFO] app start: Agents Dashboard
+4
View File
@@ -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
+360
View File
@@ -0,0 +1,360 @@
# dag_engine — Guia de uso
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
---
## 1. Donde viven los DAGs
| Path | Que |
|---|---|
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
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/<nombre>.yaml` (ver formato en seccion 3).
2. **Validar** sin ejecutar:
```bash
./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/<nombre>.yaml
```
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
3. **Probar ejecucion manual** una vez:
```bash
./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/<nombre>.yaml
```
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
```bash
systemctl --user restart dag_engine.service
journalctl --user-unit dag_engine.service -n 30 --no-pager
```
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
5. **Verificar en frontend**:
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
- Web: `http://localhost:8090`.
### Disparo manual desde curl o frontend
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<nombre>/run
```
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
---
## 3. Formato YAML
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
### Ejemplo completo
```yaml
name: my_pipeline
description: "Pipeline diario que importa CSV y actualiza Metabase."
group: finanzas # opcional, agrupa DAGs en listados
type: graph # opcional: graph (default) | chain
tags: [daily, csv, metabase] # opcional, filtros en la UI
# Variables de entorno (heredadas por todos los steps).
env:
- DATA_DIR: /home/lucas/data
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
# Cron schedule. Puede ser string o lista.
schedule:
- "0 9 * * *" # 09:00 todos los dias
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
# Working dir + shell por defecto para todos los steps.
working_dir: /home/lucas/fn_registry
shell: /bin/bash
timeout_sec: 1800 # 30 min para todo el DAG
steps:
- name: ingest
description: "Descarga CSV."
command: ./bash/functions/pipelines/ingest_csv.sh
timeout_sec: 300 # 5 min para este step
env:
- SOURCE_URL: https://example.com/data.csv
- name: transform
description: "Limpieza y agregacion."
script: |
#!/usr/bin/env python3
import pandas as pd
df = pd.read_csv("$DATA_DIR/raw.csv")
df.to_parquet("$DATA_DIR/clean.parquet")
depends: [ingest] # debe terminar OK antes
retry_policy:
limit: 2 # reintentos en caso de fallo
interval_sec: 60
- name: load_metabase
command: ./bash/functions/metabase/refresh_dashboard.sh
depends: [transform]
continue_on:
failure: true # no aborta el DAG aunque falle
- name: notify
command: ./bash/functions/io/slack_send.sh "pipeline OK"
depends: [load_metabase]
# Hooks de ciclo de vida.
handler_on:
success: ./bash/functions/io/notify_success.sh
failure: ./bash/functions/io/notify_failure.sh
exit: ./bash/functions/io/cleanup.sh
```
### Campos del DAG (top-level)
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
| `description` | string | "" | Texto libre, aparece en la UI. |
| `group` | string | "" | Agrupa DAGs en listados. |
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
| `shell` | string | `/bin/sh` | Shell para `command:`. |
| `env` | list/map | [] | Variables de entorno DAG-wide. |
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
| `tags` | list[string] | [] | Filtros en la UI. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
### Campos de cada step
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
| `id` | string | "" | Override del id auto-generado. |
| `description` | string | "" | Texto libre. |
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
| `shell` | string | hereda | Override del shell. |
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
| `retry_policy.limit` | int | 0 | Reintentos. |
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
| `tags` | list[string] | [] | Tags por step (UI). |
### Function steps (coherencia con el registry)
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
```yaml
steps:
- name: audit_capabilities
function: audit_capability_groups_go_infra
args: ["--json"]
description: "Audita drift entre tags de capability group y paginas madre"
```
Ventajas vs `command: ./fn run ...`:
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
### Cron schedule
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
| Expresion | Significado |
|---|---|
| `0 9 * * *` | Todos los dias a las 09:00 |
| `*/15 * * * *` | Cada 15 minutos |
| `0 */6 * * *` | Cada 6 horas en punto |
| `0 9 * * 1-5` | 09:00 lunes-viernes |
| `0 21 * * 5` | 21:00 viernes |
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
---
## 4. Comandos CLI
```bash
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
./dag_engine status [dag_name] # historial de ejecuciones
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
./dag_engine server # arranca HTTP + WS hub + frontend embebido
```
Flags del `server`:
| Flag | Default | Que |
|---|---|---|
| `--port` | 8090 | Puerto HTTP. |
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
---
## 5. Que hacer si algo falla
### 5.1. El DAG no aparece en la UI
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
| Causa | Diagnostico | Fix |
|---|---|---|
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_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: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
### 5.3. El DAG corre pero un step falla
**Sintoma:** `status: failed` en la UI.
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
3. Errores tipicos:
| stderr | Causa | Fix |
|---|---|---|
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
### 5.4. El scheduler no dispara
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
1. Verifica que el server arranco con `--scheduler`:
```bash
systemctl --user cat dag_engine.service | grep scheduler
```
2. Logs:
```bash
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
```
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<name>/run
```
4. Hora del sistema descalibrada:
```bash
timedatectl status
```
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
### 5.5. El frontend C++ no conecta WS
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
| Causa | Fix |
|---|---|
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
| Puerto cambiado | El cliente apunta a `127.0.0.1:8090` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
### 5.6. Cleanup de runs viejos
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
```bash
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
DELETE FROM dag_step_results WHERE run_id IN (
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
);
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
VACUUM;
SQL
```
### 5.7. Restaurar desde backup
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
```bash
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
apps/dag_engine/dags_migrated/ 2>/dev/null || \
git checkout HEAD -- apps/dag_engine/dags_migrated/
systemctl --user restart dag_engine.service
```
---
## 6. Endpoints HTTP
| Metodo | Path | Que |
|---|---|---|
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
| GET | `/api/dags/{name}` | Detalle + validation. |
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
| GET | `/api/scheduler/status` | Tickers activos. |
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
| POST | `/api/scheduler/stop` | Para scheduler. |
---
## 7. Referencias
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
- WS hub: `apps/dag_engine/events.go`.
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
+9 -1
View File
@@ -6,7 +6,7 @@ import (
)
// RegisterAPI sets up all HTTP routes on the given mux.
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, hub *DagRunHub, frontendFS fs.FS) {
// API routes.
mux.HandleFunc("GET /api/dags", handleListDags(executor))
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
@@ -15,10 +15,18 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, f
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
// Function lookup proxy a registry.db (read-only).
mux.HandleFunc("GET /api/functions/{id}", handleGetFunction())
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
// Live updates (WS hub).
if hub != nil {
mux.HandleFunc("GET /api/ws/dagruns", handleDagRunsWS(hub))
}
// Frontend SPA fallback.
if frontendFS != nil {
mux.Handle("/", spaHandler(frontendFS))
+65 -7
View File
@@ -2,7 +2,8 @@
name: dag_engine
lang: go
domain: infra
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
version: 0.1.0
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
@@ -27,6 +28,18 @@ uses_types:
framework: "net/http + vite + react"
entry_point: "main.go"
dir_path: "apps/dag_engine"
service:
port: 8090
health_endpoint: /api/dags
health_timeout_s: 3
systemd_unit: dag_engine.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- aurgi-pc
- home-wsl
is_local_only: false
---
## Arquitectura
@@ -71,15 +84,60 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```bash
# CLI
./dag-engine run ~/dagu/dags/example.yaml
./dag-engine list ~/dagu/dags/
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
./dag-engine list apps/dag_engine/dags_migrated/
# Servidor web
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
# Browser: http://localhost:8090
```
## Notas
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags_migrated/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 8090.
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
Fix:
- Borrado `apps/dag_engine/registry.db` stale.
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
### 2026-05-16 — BBDDs canonicas (referencia rapida)
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
## Lo siguiente que pega
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
## Documentacion de usuario
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
**[apps/dag_engine/README.md](README.md)**.
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
@@ -0,0 +1,26 @@
name: fn_backup
description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry
schedule:
- "0 3 * * *"
tags: [backup, registry, daily]
env:
- BACKUP_ROOT: /home/lucas/backups/fn_registry
steps:
- name: ensure_dirs
command: mkdir -p ${BACKUP_ROOT}
- name: run_backup_all
description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12"
function: backup_all_bash_pipelines
args: ["${BACKUP_ROOT}"]
continue_on:
exit_code: [4]
depends: [ensure_dirs]
- name: report_status
command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20'
depends: [run_backup_all]
@@ -0,0 +1,51 @@
name: revision-viernes-finanzas
description: Revisión semanal de finanzas personales - ingesta, informe y push a Gitea
tags: [finanzas, semanal]
type: graph
schedule: "0 9 * * 5"
env:
- PROJECT_DIR: /home/lucas/analysis/finanzas_personales
- PYTHON: /home/lucas/analysis/finanzas_personales/.venv/bin/python
handler_on:
failure:
command: echo "[$(date)] FALLÓ revision-viernes-finanzas" >> /home/lucas/dagu/logs/failures.log
steps:
- id: ingest
description: Procesar archivos nuevos del inbox (BBVA xlsx + Revolut csv)
working_dir: ${PROJECT_DIR}
command: ./bin/ingest -skip-notebooks
continue_on:
failure: true
- id: informe
description: Generar informe semanal de cumplimiento del presupuesto
command: ${PYTHON} /home/lucas/dagu/scripts/informe_finanzas.py
depends: [ingest]
- id: git_push
description: Commit y push del informe a Gitea
working_dir: ${PROJECT_DIR}
script: |
#!/bin/bash
set -euo pipefail
if git diff --quiet data/04_output/informe_semanal.md 2>/dev/null && \
! git ls-files --others --exclude-standard | grep -q informe_semanal.md; then
echo "Sin cambios en el informe, skip push"
exit 0
fi
git add data/04_output/informe_semanal.md
git add data/03_processed/ 2>/dev/null || true
git commit -m "Informe semanal $(date +%Y-%m-%d)
Co-Authored-By: Dagu Automation <noreply@dagu.dev>"
git push origin master:main
echo "Push completado"
depends: [informe]
+438
View File
@@ -0,0 +1,438 @@
package main
// WebSocket hub para live updates de dag_runs + dag_step_results.
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
//
// Diseño:
// - Hub global con N subscribers WS.
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
// + recientes finished (ultimos 5s) -> broadcast upsert.
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
// - El cliente trata `runs` y `steps` como upserts por id.
import (
"context"
"database/sql"
"log"
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"dag-engine/store"
)
const (
dagWSTickInterval = 500 * time.Millisecond
dagWSTickIntervalIdle = 2 * time.Second
dagWSIdleThreshold = 30 * time.Second
dagWSSnapshotRuns = 50
dagWSBroadcastTimeout = 2 * time.Second
dagWSRecentFinishedS = 5
)
type wsRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
type wsStep struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
type wsWatermark struct {
Runs int64 `json:"runs"`
Steps int64 `json:"steps"`
}
type wsDagMessage struct {
Type string `json:"type"` // snapshot|delta|ping
Watermark wsWatermark `json:"watermark"`
Dags []DagInfo `json:"dags,omitempty"`
Runs []wsRun `json:"runs,omitempty"`
Steps []wsStep `json:"steps,omitempty"`
ServerTime int64 `json:"server_time"`
}
type wsDagClientCmd struct {
Watermark wsWatermark `json:"watermark,omitempty"`
}
type dagSubscriber struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
out chan wsDagMessage
watermark wsWatermark
}
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
type DagRunHub struct {
db *store.DB
executor *Executor
mu sync.Mutex
subscribers map[*dagSubscriber]struct{}
tickerStop chan struct{}
tickerOn bool
watermark wsWatermark
lastEventAt time.Time
}
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
return &DagRunHub{
db: db,
executor: executor,
subscribers: make(map[*dagSubscriber]struct{}),
}
}
func (h *DagRunHub) register(s *dagSubscriber) {
h.mu.Lock()
h.subscribers[s] = struct{}{}
shouldStart := !h.tickerOn
if shouldStart {
h.tickerStop = make(chan struct{})
h.tickerOn = true
h.lastEventAt = time.Now()
}
h.mu.Unlock()
if shouldStart {
go h.tickerLoop()
}
}
func (h *DagRunHub) unregister(s *dagSubscriber) {
h.mu.Lock()
if _, ok := h.subscribers[s]; !ok {
h.mu.Unlock()
return
}
delete(h.subscribers, s)
close(s.out)
shouldStop := h.tickerOn && len(h.subscribers) == 0
if shouldStop {
close(h.tickerStop)
h.tickerOn = false
}
h.mu.Unlock()
}
func (h *DagRunHub) tickerLoop() {
interval := dagWSTickInterval
t := time.NewTimer(interval)
defer t.Stop()
for {
select {
case <-h.tickerStop:
return
case <-t.C:
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
if err != nil {
log.Printf("[dagws] fetchDelta: %v", err)
} else if len(runs) > 0 || len(steps) > 0 {
h.setWatermark(wm)
h.recordActivity()
h.broadcast(wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
})
}
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
interval = dagWSTickIntervalIdle
} else {
interval = dagWSTickInterval
}
t.Reset(interval)
}
}
}
func (h *DagRunHub) getWatermark() wsWatermark {
h.mu.Lock()
defer h.mu.Unlock()
return h.watermark
}
func (h *DagRunHub) setWatermark(v wsWatermark) {
h.mu.Lock()
if v.Runs > h.watermark.Runs {
h.watermark.Runs = v.Runs
}
if v.Steps > h.watermark.Steps {
h.watermark.Steps = v.Steps
}
h.mu.Unlock()
}
func (h *DagRunHub) recordActivity() {
h.mu.Lock()
h.lastEventAt = time.Now()
h.mu.Unlock()
}
func (h *DagRunHub) lastActivityAt() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
return h.lastEventAt
}
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
// OR (recently finished). Watermark devuelto = max rowid visto.
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
conn := h.db.Conn()
if conn == nil {
return nil, nil, since, nil
}
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Runs, cutoff)
if err != nil {
return nil, nil, since, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Steps, cutoff)
if err != nil {
return runs, nil, since, err
}
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
if out.Runs < since.Runs {
out.Runs = since.Runs
}
if out.Steps < since.Steps {
out.Steps = since.Steps
}
return runs, steps, out, nil
}
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
dags, err := h.executor.ListDAGs()
if err != nil {
log.Printf("[dagws] list dags: %v", err)
dags = nil
}
conn := h.db.Conn()
if conn == nil {
return dags, nil, nil, wsWatermark{}, nil
}
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
ORDER BY started_at DESC
LIMIT ?`, dagWSSnapshotRuns)
if err != nil {
return dags, nil, nil, wsWatermark{}, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
ORDER BY rowid ASC`, dagWSSnapshotRuns)
if err != nil {
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
}
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
}
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsRun
var max int64
for rows.Next() {
var r wsRun
var rowid int64
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, r)
}
return out, max, rows.Err()
}
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsStep
var max int64
for rows.Next() {
var s wsStep
var rowid int64
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
&s.DurationMs, &s.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, s)
}
return out, max, rows.Err()
}
func (h *DagRunHub) broadcast(msg wsDagMessage) {
h.mu.Lock()
subs := make([]*dagSubscriber, 0, len(h.subscribers))
for s := range h.subscribers {
subs = append(subs, s)
}
h.mu.Unlock()
for _, s := range subs {
select {
case s.out <- msg:
default:
log.Printf("[dagws] dropping frame for slow subscriber")
}
}
}
// handleDagRunsWS upgrade WS y gestiona lifecycle.
// Endpoint: GET /api/ws/dagruns
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
log.Printf("[dagws] accept: %v", err)
return
}
defer conn.Close(websocket.StatusInternalError, "closing")
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
sub := &dagSubscriber{
conn: conn,
ctx: ctx,
cancel: cancel,
out: make(chan wsDagMessage, 64),
}
hub.register(sub)
defer hub.unregister(sub)
dags, runs, steps, wm, err := hub.fetchSnapshot()
if err != nil {
log.Printf("[dagws] snapshot: %v", err)
conn.Close(websocket.StatusInternalError, "snapshot failed")
return
}
hub.setWatermark(wm)
initial := wsDagMessage{
Type: "snapshot",
Watermark: wm,
Dags: dags,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}
if err := wsjson.Write(ctx, conn, initial); err != nil {
return
}
readErr := make(chan error, 1)
go func() {
for {
var cmd wsDagClientCmd
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
readErr <- err
return
}
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
if err == nil && (len(runs) > 0 || len(steps) > 0) {
hub.setWatermark(wm)
select {
case sub.out <- wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}:
default:
}
}
}
}
}()
for {
select {
case <-ctx.Done():
return
case err := <-readErr:
if err != nil {
return
}
case msg, ok := <-sub.out:
if !ok {
return
}
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
err := wsjson.Write(wctx, conn, msg)
wcancel()
if err != nil {
return
}
}
}
}
}
+53 -20
View File
@@ -156,22 +156,49 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
stepID := generateID()
now := time.Now()
// Resolve command source: function (registry) takes precedence over command/script.
var command string
var stepFunctionID string
var fnRegistryRoot string
if step.Function != "" {
stepFunctionID = step.Function
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
if fnRegistryRoot == "" {
fnRegistryRoot = "/home/lucas/fn_registry"
}
fnBin := os.Getenv("FN_BIN")
if fnBin == "" {
fnBin = fnRegistryRoot + "/fn"
}
parts := []string{fnBin, "run", step.Function}
parts = append(parts, step.Args...)
command = strings.Join(parts, " ")
} else if step.Command != "" {
command = step.Command
} else if step.Script != "" {
command = step.Script
}
e.store.InsertStepResult(&store.DagStepResult{
ID: stepID,
RunID: runID,
StepName: stepName(step),
Status: "running",
StartedAt: &now,
ID: stepID,
RunID: runID,
StepName: stepName(step),
FunctionID: stepFunctionID,
Status: "running",
StartedAt: &now,
})
// Build environment.
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// Determine command.
command := step.Command
if command == "" && step.Script != "" {
command = step.Script
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
if stepFunctionID != "" {
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
@@ -182,11 +209,15 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory.
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
// so `fn` resolves registry.db correctly via go.mod walk-up.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
if dir == "" && stepFunctionID != "" {
dir = fnRegistryRoot
}
shell := step.Shell
if shell == "" {
@@ -326,14 +357,15 @@ func generateID() string {
// DagInfo summarizes a DAG file for listing.
type DagInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
}
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
@@ -379,10 +411,11 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
Valid: true,
}
// Attach last run info.
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
// Attach last 5 runs (most recent first).
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
info.LastRuns = runs
}
dags = append(dags, info)
@@ -9,12 +9,63 @@ import {
Paper,
Alert,
Loader,
CopyButton,
Tooltip,
ActionIcon,
Code,
} from "@mantine/core";
import { IconArrowLeft } from "@tabler/icons-react";
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
import { getRun } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import { StepTimeline } from "../components/StepTimeline";
import type { RunDetail as RunDetailType } from "../types";
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
const lines: string[] = [];
const started = run.StartedAt ? new Date(run.StartedAt) : null;
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
const durationMs =
started && finished ? finished.getTime() - started.getTime() : null;
lines.push(`=== DAG run ${run.ID} ===`);
lines.push(`dag: ${run.DagName}`);
lines.push(`path: ${run.DagPath}`);
lines.push(`status: ${run.Status}`);
lines.push(`trigger: ${run.Trigger}`);
lines.push(`started: ${started ? started.toISOString() : "-"}`);
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
lines.push(
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
);
if (run.Error) {
lines.push("");
lines.push("run error:");
lines.push(run.Error);
}
lines.push("");
lines.push(`--- steps (${steps.length}) ---`);
for (const s of steps) {
lines.push("");
lines.push(
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
);
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
if (s.Error) {
lines.push(" error:");
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stdout) {
lines.push(" stdout:");
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stderr) {
lines.push(" stderr:");
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
}
}
return lines.join("\n");
}
export function RunDetail() {
const { id } = useParams<{ id: string }>();
@@ -100,6 +151,41 @@ export function RunDetail() {
</Text>
)}
</Paper>
<Paper p="md" withBorder>
<Group justify="space-between" mb="sm">
<Title order={4}>Logs</Title>
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copiado" : "Copiar log completo"}
withArrow
position="left"
>
<ActionIcon
variant={copied ? "filled" : "light"}
color={copied ? "teal" : "blue"}
onClick={copy}
aria-label="Copiar logs"
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code
block
style={{
maxHeight: 480,
overflow: "auto",
whiteSpace: "pre",
fontSize: 12,
}}
>
{buildLogText(run, steps || [])}
</Code>
</Paper>
</Stack>
);
}
+9 -6
View File
@@ -5,6 +5,7 @@ go 1.25.0
require (
fn-registry v0.0.0-00010101000000-000000000000
github.com/mattn/go-sqlite3 v1.14.37
nhooyr.io/websocket v1.8.17
)
require (
@@ -28,19 +29,21 @@ require (
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+18 -10
View File
@@ -111,44 +111,50 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -166,3 +172,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+4 -4
View File
@@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc {
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
resp := map[string]interface{}{
"info": info,
"dag": dag,
"validation": validation,
"runs": runs,
"info": info,
"dag": dag,
"validation": validation,
"recent_runs": runs,
}
writeJSON(w, http.StatusOK, resp)
}
+2 -1
View File
@@ -286,6 +286,7 @@ func cmdServer(args []string) {
executor := NewExecutor(db, cfg.DagsDir)
scheduler := NewScheduler(executor, cfg.DagsDir)
dagRunHub := NewDagRunHub(db, executor)
// Prepare frontend FS.
var feFS iofs.FS
@@ -303,7 +304,7 @@ func cmdServer(args []string) {
}
mux := http.NewServeMux()
RegisterAPI(mux, executor, scheduler, feFS)
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
handler := corsMiddleware(loggingMiddleware(mux))
+63 -28
View File
@@ -2,15 +2,43 @@ package store
import (
"database/sql"
_ "embed"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/001_init.sql
var migrationSQL string
//go:embed migrations/*.sql
var migrationsFS embed.FS
// applyMigrations executes every embedded migrations/*.sql in order.
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil {
return err
}
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil {
return fmt.Errorf("%s: read: %w", f, err)
}
if _, err := conn.Exec(string(b)); err != nil {
if strings.Contains(err.Error(), "duplicate column") ||
strings.Contains(err.Error(), "already exists") {
continue
}
return fmt.Errorf("%s: %w", f, err)
}
}
return nil
}
// DB wraps a SQLite connection for DAG run persistence.
type DB struct {
@@ -24,7 +52,7 @@ func Open(path string) (*DB, error) {
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", path, err)
}
if _, err := conn.Exec(migrationSQL); err != nil {
if err := applyMigrations(conn); err != nil {
conn.Close()
return nil, fmt.Errorf("store: migrate: %w", err)
}
@@ -36,18 +64,24 @@ func (db *DB) Close() error {
return db.conn.Close()
}
// Conn exposes the underlying *sql.DB for read-only queries from other
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
func (db *DB) Conn() *sql.DB {
return db.conn
}
// --- DagRun CRUD ---
// DagRun mirrors infra.DagRun for the store layer.
type DagRun struct {
ID string
DagName string
DagPath string
Status string
Trigger string
StartedAt time.Time
FinishedAt *time.Time
Error string
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
// CreateRun inserts a new run record.
@@ -123,17 +157,18 @@ func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error)
// DagStepResult mirrors infra.DagStepResult for the store layer.
type DagStepResult struct {
ID string
RunID string
StepName string
Status string
ExitCode int
Stdout string
Stderr string
StartedAt *time.Time
FinishedAt *time.Time
DurationMs int64
Error string
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
FunctionID string `json:"function_id,omitempty"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
// InsertStepResult inserts a new step result.
@@ -148,9 +183,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
finishedAt = &s
}
_, err := db.conn.Exec(
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
startedAt, finishedAt, r.DurationMs, r.Error,
)
return err
@@ -173,7 +208,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s
// ListStepResults returns all step results for a given run.
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
rows, err := db.conn.Query(
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
)
if err != nil {
@@ -185,7 +220,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
for rows.Next() {
var r DagStepResult
var startedAt, finishedAt sql.NullString
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
return nil, err
}
+15 -1
View File
@@ -2,6 +2,7 @@
name: shaders_lab
lang: cpp
domain: gfx
version: 0.1.0
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
@@ -30,8 +31,11 @@ uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab"
dir_path: "apps/shaders_lab"
repo_url: ""
icon:
phosphor: "palette"
accent: "#ea580c"
---
## Arquitectura
@@ -102,3 +106,13 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
@@ -6,16 +6,21 @@
scan_secrets_in_dirty() {
local repo_dir="${1:-.}"
if [[ ! -d "$repo_dir/.git" ]]; then
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
return 1
fi
# Listar archivos modificados o nuevos (excluyendo borrados)
# y filtrar por patron de secret en el nombre del archivo
# y filtrar por patron de secret en el nombre del archivo.
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
# propio scanner ni docs que hablen de "secret"/"token".
git -C "$repo_dir" status --porcelain \
| awk '{print $NF}' \
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
| grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \
|| true
}
@@ -3,7 +3,7 @@ name: deploy_cpp_exe_to_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.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)."
@@ -63,3 +63,8 @@ Desktop/apps/<APP>/
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
## Capability growth log
v1.1.0 (2026-05-17) — Bugfix: el `cp` del .exe no chequeaba exit status y la funcion reportaba OK aunque fallase por "Permission denied" (proceso aun vivo). Ahora: (1) tras `taskkill.exe`, poll de hasta 3s sobre `tasklist.exe` esperando muerte real del proceso; (2) `cp` envuelto en retry 5 veces con backoff 0.5s y re-taskkill entre intentos; (3) si los 5 intentos fallan, `return 1` (antes: silently continued).
v1.0.0 — Initial.
@@ -30,12 +30,38 @@ deploy_cpp_exe_to_windows() {
mkdir -p "$dest" "$assets"
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
# Windows libera el file handle async tras taskkill. Hacemos poll hasta que
# el proceso desaparezca de tasklist o se agote el timeout.
if command -v taskkill.exe >/dev/null 2>&1; then
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
if ! tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null \
| grep -qi "^${app}.exe"; then
break
fi
sleep 0.3
done
fi
# --- 4. Copiar .exe al top level ---
cp -v "$exe_src" "$dest/"
# --- 4. Copiar .exe al top level con retry ---
# Windows puede tener el archivo aun bloqueado momentaneamente; reintentar.
local cp_ok=0
local attempt
for attempt in 1 2 3 4 5; do
if cp -v "$exe_src" "$dest/"; then
cp_ok=1
break
fi
echo "deploy_cpp_exe_to_windows: cp intento $attempt fallo, reintentando..." >&2
# Reintentar taskkill por si el proceso resucito o quedo zombie.
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
sleep 0.5
done
if [ "$cp_ok" -ne 1 ]; then
echo "ERROR: cp del .exe fallo tras 5 intentos. $exe_src -> $dest/" >&2
return 1
fi
# --- 5. DLLs al top level (Windows DLL search convention) ---
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
@@ -0,0 +1,61 @@
---
name: deploy_wails_exe_to_windows
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "deploy_wails_exe_to_windows <app_name> <app_dir>"
description: "Copia el .exe de una app Wails desde <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso anterior (taskkill /F) y relanza la app via cmd.exe. Single-binary: no copia DLLs (Webview2 nativo en SO). Preserva local_files/ si existe."
tags: ["wails", "windows", "deploy", "cross-compile", "mingw", "infra", "launch", "matrix-mas"]
params:
- name: app_name
desc: "Nombre del binario sin extension (ej. matrix_client_pc). Debe coincidir con el nombre del .exe generado por wails build."
- name: app_dir
desc: "Ruta absoluta al directorio raiz de la app, donde vive build/bin/<app>.exe. Puede estar en projects/<project>/apps/<app>/ o apps/<app>/."
output: "Imprime pasos en stderr. En stdout: ls -lh del .exe desplegado. Exit 0 si ok, exit 1 si build/bin/<app>.exe no existe o los args estan vacios."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "args vacios devuelven error con mensaje de uso"
- "app_dir inexistente devuelve exit 1"
- "build/bin exe inexistente devuelve exit 1"
test_file_path: "bash/functions/infra/deploy_wails_exe_to_windows_test.sh"
file_path: "bash/functions/infra/deploy_wails_exe_to_windows.sh"
---
## Ejemplo
```bash
source bash/functions/infra/deploy_wails_exe_to_windows.sh
# Desplegar matrix_client_pc tras wails build -platform windows/amd64
deploy_wails_exe_to_windows matrix_client_pc \
/home/lucas/fn_registry/projects/element_agents/apps/matrix_client_pc
```
Con override de destino:
```bash
WIN_DESKTOP_APPS=/mnt/c/Users/lucas/Desktop/apps \
deploy_wails_exe_to_windows matrix_admin_panel \
/home/lucas/fn_registry/projects/element_agents/apps/matrix_admin_panel
```
## Cuando usarla
Tras un `wails build -platform windows/amd64` exitoso, para desplegar el binario compilado en Windows y relanzarlo en el mismo paso. Ideal en el ciclo de iteracion rapida: compilar → desplegar → ver cambios. Equivalente a `deploy_cpp_exe_to_windows_bash_infra` pero para apps Wails (single-binary sin DLLs extras).
## Gotchas
- **taskkill /F fuerza muerte** sin permitir guardado en disco. Las apps Wails persisten estado en keyring de Windows y AppData — este kill es seguro para ellas. Si la app tuviera autosave en progreso, se perderia (aceptable en ciclos de dev).
- **UNC paths prohibidos en cmd.exe**: `cmd.exe /c start` debe ejecutarse con `cd` previo al directorio Windows (`/mnt/c/...`). Intentar lanzar con path `\\wsl.localhost\...` falla con "UNC paths are not supported as the current directory".
- **cmd.exe start no bloquea**: la funcion espera 3s y verifica via `tasklist.exe`. Si la app cierra sola tras el arranque (error de inicio), el warn final lo indica pero no causa exit 1. Revisar logs en `%APPDATA%\<app>\` o `%LOCALAPPDATA%\<app>\`.
- **Single-binary Wails**: no copiar DLLs. Webview2 es nativo del SO (Windows 10+ ya lo incluye). Si una version vieja de Windows no tuviera Webview2, la app falla al arrancar — solucion: instalar Webview2 Runtime en esa maquina.
- **Build previo es responsabilidad del caller**: esta funcion NO compila. Para matrix_client_pc usa `-tags goolm` por el crypto de Matrix: `wails build -platform windows/amd64 -tags goolm`.
- **WIN_DESKTOP_APPS override**: variable de entorno para cambiar el destino. Util en CI o maquinas con escritorio en otra ruta.
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# deploy_wails_exe_to_windows — Copia el .exe de una app Wails compilado en
# <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso
# anterior y relanza la app. Single-binary: no copia DLLs (Webview2 nativo SO).
# Pre-authorized: taskkill.exe /F — idempotente, sin prompt.
set -euo pipefail
deploy_wails_exe_to_windows() {
local app="${1:-}"
local app_dir="${2:-}"
if [ -z "$app" ] || [ -z "$app_dir" ]; then
echo "ERROR: uso: deploy_wails_exe_to_windows <app_name> <app_dir>" >&2
echo " app_name: nombre del binario sin extension (ej. matrix_client_pc)" >&2
echo " app_dir: ruta absoluta al directorio de la app (donde vive build/bin/)" >&2
return 1
fi
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
# --- 1. Validar que el .exe existe ---
local exe_src="${app_dir}/build/bin/${app}.exe"
if [ ! -f "$exe_src" ]; then
echo "ERROR: no se encontro $exe_src" >&2
echo "Compila primero con: wails build -platform windows/amd64" >&2
return 1
fi
# --- 2. Crear directorio destino (preserva local_files/ si existe) ---
local dest="${win_desktop_apps}/${app}"
mkdir -p "$dest"
echo "[deploy_wails] dest: $dest" >&2
# --- 3. Matar proceso si esta corriendo en Windows ---
# Pre-authorized. Wails apps usan AppData+keyring para estado, kill /F es seguro.
if command -v taskkill.exe >/dev/null 2>&1; then
echo "[deploy_wails] matando ${app}.exe si corre..." >&2
taskkill.exe /IM "${app}.exe" /F 2>/dev/null || true
fi
# --- 4. Esperar a que Windows libere el file handle ---
sleep 1
# --- 5. Copiar .exe (cp -f: overwrite sin borrar el directorio) ---
echo "[deploy_wails] copiando ${app}.exe..." >&2
cp -f "$exe_src" "$dest/${app}.exe"
# --- 6. Copiar appicon.ico si existe (opcional, algunos hubs lo leen) ---
local icon_src="${app_dir}/appicon.ico"
if [ -f "$icon_src" ]; then
echo "[deploy_wails] copiando appicon.ico..." >&2
cp -f "$icon_src" "$dest/appicon.ico"
fi
# --- 7. Relanzar la app desde su dir Windows ---
# Usar cmd.exe /c start desde el dir destino (no UNC paths — falla en cmd.exe).
echo "[deploy_wails] lanzando ${app}.exe..." >&2
(
cd "$dest"
cmd.exe /c start "" "${app}.exe"
)
# --- 8. Dar tiempo a que el proceso arranque ---
sleep 3
# --- 9. Verificar que el proceso esta corriendo ---
if command -v tasklist.exe >/dev/null 2>&1; then
local tasklist_out
tasklist_out=$(tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null || true)
if echo "$tasklist_out" | grep -qi "^${app}.exe"; then
local pid
pid=$(echo "$tasklist_out" | grep -i "^${app}.exe" | awk '{print $2}' | head -n1)
echo "[deploy_wails] ${app}.exe corriendo con PID $pid" >&2
else
echo "WARN: ${app}.exe no aparece en tasklist tras el lanzamiento." >&2
echo " Puede que la app cerro con error. Revisar AppData para logs." >&2
fi
fi
# --- 10. Resumen final en stdout ---
ls -lh "$dest/${app}.exe"
echo "[deploy_wails] OK: ${app} deployado en $dest" >&2
if [ -d "$dest/local_files" ]; then
echo "[deploy_wails] local_files/ preservado: $(du -sh "$dest/local_files" | cut -f1)" >&2
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
deploy_wails_exe_to_windows "$@"
fi
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Tests para deploy_wails_exe_to_windows
# Solo prueba validacion de argumentos y rutas — no ejecuta taskkill/cmd.exe reales.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/deploy_wails_exe_to_windows.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $test_name"
PASS=$((PASS + 1))
else
echo "FAIL: $test_name — expected '$expected', got '$got'"
FAIL=$((FAIL + 1))
fi
}
# --- Test 1: args vacios devuelven error con mensaje de uso ---
actual_exit=0
deploy_wails_exe_to_windows >/dev/null 2>&1 || actual_exit=$?
assert_eq "args vacios devuelven error con mensaje de uso" "1" "$actual_exit"
# --- Test 2: app_dir inexistente devuelve exit 1 ---
actual_exit=0
deploy_wails_exe_to_windows "myapp" "/tmp/nonexistent_dir_$(date +%s)" >/dev/null 2>&1 || actual_exit=$?
assert_eq "app_dir inexistente devuelve exit 1" "1" "$actual_exit"
# --- Test 3: build/bin exe inexistente devuelve exit 1 ---
TMPDIR_APP=$(mktemp -d)
# Crear estructura de dir de app pero SIN el exe
mkdir -p "$TMPDIR_APP/build/bin"
actual_exit=0
deploy_wails_exe_to_windows "myapp" "$TMPDIR_APP" >/dev/null 2>&1 || actual_exit=$?
rm -rf "$TMPDIR_APP"
assert_eq "build/bin exe inexistente devuelve exit 1" "1" "$actual_exit"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -17,7 +17,9 @@ git_hook_audit_app_drift() {
echo "ERROR: repo_dir required" >&2
return 2
fi
if [[ ! -d "$repo_dir/.git" ]]; then
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
echo "ERROR: $repo_dir is not a git repo" >&2
return 2
fi
+6 -12
View File
@@ -3,7 +3,7 @@ name: gradle_run
kind: function
lang: bash
domain: infra
version: "1.1.0"
version: "1.0.0"
purity: impure
signature: "gradle_run(project_dir: string, task...: string) -> int"
description: "Wrapper canonico para invocar gradlew Android en WSL2 con JDK 17 + ANDROID_HOME validados."
@@ -24,7 +24,7 @@ tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gradle_run.sh"
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre SDK Linux en $HOME/android-sdk (install_android_sdk_bash_infra), $HOME/Android/Sdk (Android Studio), y SDK Windows (/mnt/c/...) montado en WSL."
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre tanto SDK Linux (~/Android/Sdk via install_android_sdk) como SDK Windows (/mnt/c/...) montado en WSL."
---
## Ejemplo
@@ -50,12 +50,10 @@ Si no esta fijado en el entorno, busca en orden:
Si ninguno existe → error en stderr y `return 1`.
### ANDROID_HOME
Si no esta fijado, busca en orden (requiere que el directorio tenga `platform-tools/`):
1. `$HOME/android-sdk` — default de `install_android_sdk_bash_infra` (lowercase)
2. `$HOME/Android/Sdk` — default de Android Studio en Linux
3. `$ANDROID_SDK_WIN` (o `/mnt/c/Users/$USER/AppData/Local/Android/Sdk`) — SDK Windows montado en WSL2
Si ninguno existe con `platform-tools/`, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
Si no esta fijado:
1. Intenta `$HOME/Android/Sdk` (SDK Linux via `install_android_sdk_bash_infra`)
2. Si no existe, intenta `$ANDROID_SDK_WIN` (SDK Windows montado en `/mnt/c/...`)
3. Si ninguno, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
## Exit codes
@@ -71,8 +69,4 @@ Si ninguno existe con `platform-tools/`, lo deja vacio — gradle mostrara el er
Source-able y ejecutable directo. Al sourcear, el caller importa la funcion `gradle_run` sin ejecutarla. Al ejecutar directamente, delega `"$@"` a `gradle_run`.
No exporta `JAVA_HOME`/`ANDROID_HOME` al entorno del shell padre — los variables se pasan solo al subshell de gradlew para evitar contaminar el entorno.
## Capability growth log
- v1.1.0 (2026-05-15) — ANDROID_HOME detection order: prioriza `$HOME/android-sdk` (install_android_sdk default) sobre `$HOME/Android/Sdk`; requiere platform-tools/ presente; anade WSL2 Windows path como fallback explicito (issue 0076)
---
+8 -17
View File
@@ -44,25 +44,16 @@ gradle_run() {
fi
# ---- Resolver ANDROID_HOME ---------------------------------------------
# Orden de busqueda (de mas probable a menos para entorno Linux/WSL2):
# 1. $HOME/android-sdk — instalado por install_android_sdk_bash_infra (default)
# 2. $HOME/Android/Sdk — ruta de Android Studio en Linux
# 3. $ANDROID_SDK_WIN — SDK Windows montado en WSL2 via /mnt/c/...
# Solo se acepta un candidato si tiene platform-tools/, no solo el directorio raiz.
local android_home="${ANDROID_HOME:-}"
if [[ -z "$android_home" ]]; then
local _sdk_candidates=(
"$HOME/android-sdk"
"$HOME/Android/Sdk"
"${ANDROID_SDK_WIN:-/mnt/c/Users/$USER/AppData/Local/Android/Sdk}"
)
for _candidate in "${_sdk_candidates[@]}"; do
if [[ -d "$_candidate" && -d "$_candidate/platform-tools" ]]; then
android_home="$_candidate"
break
fi
done
unset _sdk_candidates _candidate
local _default_linux="$HOME/Android/Sdk"
if [[ -d "$_default_linux" ]]; then
android_home="$_default_linux"
elif [[ -n "${ANDROID_SDK_WIN:-}" && -d "${ANDROID_SDK_WIN}" ]]; then
# SDK Windows montado en WSL via /mnt/c/...
android_home="${ANDROID_SDK_WIN}"
fi
unset _default_linux
fi
# ANDROID_HOME puede quedar vacio si no hay SDK instalado; gradle mostrara
+11 -1
View File
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
@@ -68,3 +68,13 @@ launch_cpp_app_windows "registry_dashboard"
```
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
## Gotchas
- Si `FN_REGISTRY_ROOT_WSL` no es tu ruta default de fn_registry (`/home/<user>/fn_registry`), setea la variable antes de invocar esta función: `FN_REGISTRY_ROOT_WSL=/ruta/custom launch_cpp_app_windows <app>`.
- El proceso hijo hereda `FN_REGISTRY_ROOT` como path Windows (backslashes) y `FN_REGISTRY_ROOT_WSL` como path Linux. En el exe C++, `py_resolve_interpreter()` usa `FN_REGISTRY_ROOT_WSL` para construir el invocation `wsl.exe -- /path/python3`.
- PowerShell escapa `$` con `\$` para evitar expansión de variables en el string del comando.
## Capability growth log
- v1.1.0 (2026-05-16) — auto-propaga `FN_REGISTRY_ROOT` (Windows path) + `FN_REGISTRY_ROOT_WSL` (Linux path) al proceso hijo para que pueda invocar WSL python via `wsl.exe`.
+12 -2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
# v1.1.0: propaga FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# al proceso hijo para que pueda invocar WSL python via wsl.exe.
launch_cpp_app_windows() {
local app="${1:-}"
@@ -26,10 +28,18 @@ launch_cpp_app_windows() {
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
win_exe="$win_app_dir\\$app.exe"
# Deducir raiz del registry en Linux (WSL) y traducir a Windows path.
# FN_REGISTRY_ROOT_WSL puede sobreescribirse en el entorno del llamante.
local linux_root win_root
linux_root="${FN_REGISTRY_ROOT_WSL:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
win_root=$(wslpath -w "$linux_root")
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
# Se inyectan FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# para que el exe pueda localizar el venv WSL y hacer: wsl.exe -- python3 ...
powershell.exe -NoProfile -Command \
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
>/dev/null 2>&1
local ts
@@ -0,0 +1,89 @@
---
name: mas_client_register
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "mas_client_register(ssh_host: string, container: string, config_file: string, dry_run: bool) -> json"
description: "Registra y sincroniza clientes OAuth en Matrix Authentication Service (MAS) ejecutando mas-cli config sync dentro del container Docker remoto via SSH. Verifica sintaxis YAML, soporte dry-run para ver diff antes de aplicar, y emite JSON estructurado con resultado. Idempotente: re-ejecucion con misma config no genera cambios."
tags: [matrix, mas, oauth, oidc, migration, mas-migration, infra, docker, ssh, matrix-mas]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: ssh_host
desc: "alias SSH del VPS donde corre MAS (ej. organic-machine.com). Debe estar en ~/.ssh/config con key auth."
- name: container
desc: "nombre del container Docker con MAS (ej. element_matrix_chat-mas-1). El config dentro del container se espera en /data/config.yaml."
- name: config_file
desc: "ruta absoluta en el VPS al archivo mas/config.yaml (ej. /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml). MAS lo monta como /data/config.yaml."
- name: dry_run
desc: "flag opcional --dry-run: ejecuta mas-cli config dump y devuelve el estado sin aplicar cambios. Util para verificar antes de activar MSC3861."
output: "JSON con: status ('ok'|'dry-run'|'error'), applied (bool), clients_total (int), clients_diff (array de lineas del output de mas-cli), stderr (string con logs de error si aplica)."
tested: true
tests:
- "help flag emite JSON parseable"
- "args faltantes retornan JSON de error sin ssh"
- "jq disponible en host local"
test_file_path: "bash/functions/infra/mas_client_register_test.sh"
file_path: "bash/functions/infra/mas_client_register.sh"
---
## Ejemplo
```bash
# Dry-run: verificar que clients se aplicarian correctamente
source bash/functions/infra/mas_client_register.sh
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml \
--dry-run
# Aplicar sync real (con --prune para eliminar clients viejos)
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml
```
Salida esperada (sync OK):
```json
{
"status": "ok",
"applied": true,
"clients_total": 6,
"clients_diff": ["synced client element-web", "synced client synapse-admin", "..."],
"stderr": ""
}
```
Salida dry-run:
```json
{
"status": "dry-run",
"applied": false,
"clients_total": 42,
"clients_diff": ["clients:", " - client_id: element-web", " ..."],
"stderr": ""
}
```
## Cuando usarla
Usar despues de editar `mas/config.yaml` localmente y antes de hacer restart a Synapse con `msc3861` habilitado en `homeserver.yaml`. Ejecutar primero con `--dry-run` para verificar que los 6 clients OAuth (Element Web, Synapse-Admin, matrix_client_pc, matrix_client_android, matrix_admin_panel, Synapse-internal) estan correctamente definidos, luego sin `--dry-run` para aplicar el sync.
## Gotchas
- **`--prune` elimina clients no declarados en config**: el sync real usa `--prune`, lo que borra cualquier client OAuth que exista en MAS pero no este en el `config.yaml`. Verificar con `--dry-run` antes de aplicar en produccion.
- **Requiere `jq` en el host local**: el JSON output se construye con `jq`. Si no esta instalado, la funcion falla con error claro antes de conectar al VPS.
- **`mas-cli` debe estar en el container**: la funcion asume que `mas-cli` esta en el PATH dentro del container MAS. Si el container usa una imagen diferente, verificar con `docker exec <container> mas-cli --version`.
- **Config dentro del container siempre en `/data/config.yaml`**: el `--config-file` apunta a la ruta en el VPS (para que el operador sepa que archivo editar), pero el comando dentro del container usa `/data/config.yaml` (el mount point estandar de MAS). Si el compose monta el archivo en otro path, ajustar la constante `container_config` en el script.
- **SSH key debe estar en agent o `~/.ssh/config`**: la funcion usa `ssh <alias>` directamente. Si la key requiere passphrase, ejecutar `ssh-add` antes.
- **Si `config.yaml` es invalido, sync aborta sin tocar estado**: el paso 1 (`mas-cli config check`) detecta errores de sintaxis YAML antes de intentar sync. El estado de MAS no se modifica si la config tiene errores.
- **Idempotente**: re-ejecutar con la misma config no genera cambios en MAS (mas-cli detecta que el estado ya coincide).
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
# mas_client_register — Registra/sincroniza clientes OAuth en Matrix Authentication Service (MAS)
# via mas-cli config sync ejecutado en container Docker remoto a traves de SSH.
set -euo pipefail
mas_client_register() {
local ssh_host=""
local container=""
local config_file=""
local dry_run=false
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--ssh-host)
ssh_host="$2"
shift 2
;;
--container)
container="$2"
shift 2
;;
--config-file)
config_file="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
--help|-h)
cat >&2 <<'USAGE'
mas_client_register - Sincroniza clientes OAuth en MAS via mas-cli config sync
Usage:
mas_client_register --ssh-host <host> --container <name> --config-file <path> [--dry-run]
Options:
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
--container Nombre del container MAS (ej. element_matrix_chat-mas-1)
--config-file Ruta en el VPS al mas/config.yaml (ej. /home/ubuntu/project/mas/config.yaml)
--dry-run Solo valida config y muestra diff, sin aplicar cambios
Output: JSON en stdout con status, applied, clients_total, clients_diff, stderr
USAGE
# emit minimal valid JSON so callers that parse stdout don't break
echo '{"status":"help","applied":false,"clients_total":0,"clients_diff":[],"stderr":""}'
return 0
;;
*)
echo "mas_client_register: argumento desconocido: $1" >&2
return 1
;;
esac
done
# Validar argumentos obligatorios
local errors=()
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
[[ -z "$container" ]] && errors+=("--container es obligatorio")
[[ -z "$config_file" ]] && errors+=("--config-file es obligatorio")
if [[ ${#errors[@]} -gt 0 ]]; then
for err in "${errors[@]}"; do
echo "ERROR: $err" >&2
done
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"missing required arguments"}'
return 1
fi
# Verificar dependencias locales
if ! command -v jq &>/dev/null; then
echo "ERROR: jq no encontrado en el host local. Instalar: apt install jq / brew install jq" >&2
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"jq not found on local host"}'
return 1
fi
echo "mas_client_register: ssh-host=$ssh_host container=$container dry-run=$dry_run" >&2
# La ruta de config dentro del container siempre es /data/config.yaml (mount convention de MAS)
local container_config="/data/config.yaml"
# ---- PASO 1: Verificar sintaxis YAML con mas-cli config check ----
echo "mas_client_register: verificando sintaxis de config con mas-cli config check..." >&2
local check_stdout check_stderr check_exit
check_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config check --config ${container_config}" 2>/tmp/mas_check_stderr_$$ || true)
check_exit=$?
check_stderr=$(cat /tmp/mas_check_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_check_stderr_$$
if [[ $check_exit -ne 0 ]]; then
echo "mas_client_register: config check falló (exit=$check_exit)" >&2
echo "$check_stderr" >&2
local escaped_stderr
escaped_stderr=$(printf '%s' "${check_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
echo "mas_client_register: config check OK" >&2
# ---- PASO 2: dry-run o sync ----
if [[ "$dry_run" == "true" ]]; then
# Ejecutar mas-cli config dump para mostrar el estado actual y lo que se aplicaria
echo "mas_client_register: modo dry-run — ejecutando mas-cli config dump..." >&2
local dump_stdout dump_stderr dump_exit
dump_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config dump --config ${container_config}" 2>/tmp/mas_dump_stderr_$$ || true)
dump_exit=$?
dump_stderr=$(cat /tmp/mas_dump_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_dump_stderr_$$
if [[ $dump_exit -ne 0 ]]; then
echo "mas_client_register: config dump falló (exit=$dump_exit)" >&2
echo "$dump_stderr" >&2
local escaped_stderr
escaped_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
# Extraer listado de clients del dump (buscar lineas con client_id o type: client)
local clients_diff_raw
clients_diff_raw=$(printf '%s\n' "$dump_stdout" | grep -E "client_id:|client_name:" | \
sed 's/^[[:space:]]*//' | head -50 || true)
local diff_json
diff_json=$(printf '%s\n' "$dump_stdout" | jq -Rs 'split("\n") | map(select(length > 0)) | map(ltrimstr(" "))' 2>/dev/null \
|| echo '["(jq parse error — ver stderr)"]')
local escaped_dump_stderr
escaped_dump_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
echo "mas_client_register: dry-run completado. dump lines=$(echo "$dump_stdout" | wc -l)" >&2
jq -n \
--argjson diff "$diff_json" \
--argjson stderr_str "$escaped_dump_stderr" \
'{
status: "dry-run",
applied: false,
clients_total: ($diff | length),
clients_diff: $diff,
stderr: $stderr_str
}'
return 0
fi
# ---- PASO 3: sync real ----
echo "mas_client_register: ejecutando mas-cli config sync --prune..." >&2
local sync_stdout sync_stderr sync_exit
sync_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config sync --config ${container_config} --prune" \
2>/tmp/mas_sync_stderr_$$ || true)
sync_exit=$?
sync_stderr=$(cat /tmp/mas_sync_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_sync_stderr_$$
echo "mas_client_register: sync exit=$sync_exit" >&2
if [[ -n "$sync_stderr" ]]; then
echo "mas_client_register stderr: $sync_stderr" >&2
fi
if [[ $sync_exit -ne 0 ]]; then
local escaped_stderr
escaped_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
# Parsear output del sync para extraer lineas con cambios aplicados
local diff_lines
diff_lines=$(printf '%s\n' "$sync_stdout" | grep -E "^\s*(created|updated|deleted|unchanged|synced)" || true)
local diff_json
diff_json=$(printf '%s\n' "$sync_stdout" | jq -Rs 'split("\n") | map(select(length > 0))' 2>/dev/null \
|| echo '[]')
local clients_count
clients_count=$(printf '%s\n' "$sync_stdout" | grep -cE "client" 2>/dev/null || echo 0)
local escaped_sync_stderr
escaped_sync_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
echo "mas_client_register: sync completado con exito" >&2
jq -n \
--argjson diff "$diff_json" \
--argjson total "$clients_count" \
--argjson stderr_str "$escaped_sync_stderr" \
'{
status: "ok",
applied: true,
clients_total: $total,
clients_diff: $diff,
stderr: $stderr_str
}'
}
# Ejecutar si se llama directamente (no sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
mas_client_register "$@"
fi
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Tests para mas_client_register
# No requiere SSH real — prueba paths locales (arg validation, --help, JSON output)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — expected to contain '$needle', got: $haystack"
((FAIL++))
fi
}
assert_json_parseable() {
local test_name="$1" json="$2"
if command -v jq &>/dev/null; then
if echo "$json" | jq . >/dev/null 2>&1; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — output no es JSON valido: $json"
((FAIL++))
fi
else
if [[ "$json" == \{* ]]; then
echo "PASS: $test_name (jq no disponible, verificacion basica OK)"
((PASS++))
else
echo "FAIL: $test_name — output no parece JSON: $json"
((FAIL++))
fi
fi
}
# Test: help flag emite JSON parseable
# Cada invocacion en subshell aislada para no contaminar el runner con set -e del script fuente
bash "$SCRIPT_DIR/mas_client_register.sh" --help >/tmp/mas_test_help_$$ 2>/dev/null || true
output_help=$(cat /tmp/mas_test_help_$$ 2>/dev/null || true)
rm -f /tmp/mas_test_help_$$
assert_json_parseable "help flag emite JSON parseable" "$output_help"
# Test: args faltantes retornan JSON de error sin ssh
bash "$SCRIPT_DIR/mas_client_register.sh" >/tmp/mas_test_noargs_$$ 2>/dev/null || true
output_noargs=$(cat /tmp/mas_test_noargs_$$ 2>/dev/null || true)
rm -f /tmp/mas_test_noargs_$$
assert_json_parseable "args faltantes retornan JSON de error sin ssh" "$output_noargs"
assert_contains "args faltantes contienen status error" '"status":"error"' "$output_noargs"
# Test: jq disponible en host local
if command -v jq &>/dev/null; then
echo "PASS: jq disponible en host local"
((PASS++))
else
echo "FAIL: jq disponible en host local — instalar: apt install jq"
((FAIL++))
fi
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -0,0 +1,83 @@
---
name: mas_syn2mas_migration
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "mas_syn2mas_migration --ssh-host <host> --mas-container <name> --synapse-config-path <path-on-host> --log-dir <local-path> [--max-conflicts N] [--apply]"
description: "Migra usuarios Synapse a Matrix Authentication Service (MAS) via mas-cli syn2mas. Fuerza dry-run primero, archiva el log, aborta si los conflicts superan el threshold, y solo ejecuta la migracion real con --apply."
tags: [matrix, mas, syn2mas, migration, mas-migration, infra, users, docker, ssh, matrix-mas]
params:
- name: ssh-host
desc: "Alias SSH del VPS donde corren los containers (ej. organic-machine.com)"
- name: mas-container
desc: "Nombre del container Docker de MAS (ej. element_matrix_chat-mas-1)"
- name: synapse-config-path
desc: "Ruta en el VPS al homeserver.yaml de Synapse (ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml). El container debe tener el archivo accesible en /data/homeserver.yaml via volume mount."
- name: log-dir
desc: "Directorio local donde archivar logs dry-run y apply. Se crea con chmod 0700 y los logs con 0600 (contienen userIDs)."
- name: max-conflicts
desc: "Tope de conflictos detectados en dry-run. Si conflicts > max-conflicts, status=aborted exit 2. Default 0 (abortar ante cualquier conflict)."
- name: apply
desc: "Flag booleano. Sin --apply: solo dry-run (status=ok, sin cambios). Con --apply: ejecuta la migracion real tras pasar el threshold."
output: "JSON en stdout: {\"status\":\"ok|aborted|error\",\"dry_run_log\":\"path\",\"apply_log\":\"path|null\",\"conflicts\":N,\"users_migrated\":N,\"duration_s\":N}. Exit 0=ok, 1=error, 2=aborted por conflicts."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "aborta con error cuando faltan args obligatorios"
- "help no devuelve error"
- "argumento desconocido retorna exit 1"
- "max-conflicts invalido retorna exit 1"
test_file_path: "bash/functions/infra/mas_syn2mas_migration_test.sh"
file_path: "bash/functions/infra/mas_syn2mas_migration.sh"
---
## Ejemplo
```bash
# Paso 1: dry-run OBLIGATORIO (sin --apply — no modifica nada)
mas_syn2mas_migration \
--ssh-host organic-machine.com \
--mas-container element_matrix_chat-mas-1 \
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
--log-dir ~/matrix_migration_logs \
--max-conflicts 0
# Salida esperada (si hay 0 conflicts):
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}
# Revisar el log antes de continuar:
# cat ~/matrix_migration_logs/syn2mas_dryrun_*.log
# Paso 2: tras revisar el log dry-run, aplicar la migracion real
mas_syn2mas_migration \
--ssh-host organic-machine.com \
--mas-container element_matrix_chat-mas-1 \
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
--log-dir ~/matrix_migration_logs \
--max-conflicts 0 \
--apply
# Salida esperada tras migracion exitosa:
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":"/home/lucas/matrix_migration_logs/syn2mas_apply_1234567890.log","conflicts":0,"users_migrated":42,"duration_s":15}
```
## Cuando usarla
Usar en el paso 4 de la migracion del issue 0162 (Synapse a MAS auth), tras activar MSC3861 en `homeserver.yaml` y verificar que MAS esta corriendo con `syn2mas: true` en su config. NUNCA ejecutar antes de activar MSC3861 — sin ese flag activo, `syn2mas` no puede mapear usuarios a las tablas MAS y la migracion resultara en estado inconsistente.
## Gotchas
- **Dry-run NO modifica nada** — siempre ejecutar primero sin `--apply` y revisar el log manualmente antes de aplicar.
- Si el dry-run detecta usuarios con **guest accounts**, **application services** (bots), o **passwords externos** (LDAP/OIDC), revisar manualmente el log antes de aplicar — estos casos pueden requerir steps adicionales documentados en el issue 0162.
- **Backup postgres pre-migracion NO esta cubierto** por esta funcion. El operador es responsable de hacer `pg_dump` de la DB de Synapse antes de ejecutar con `--apply`. Ver issue 0162 paso 1.
- Si la migracion real falla **a mitad**, MAS puede quedar en estado inconsistente con usuarios parcialmente migrados. El rollback consiste en restaurar el backup postgres de Synapse + revertir `homeserver.yaml` a la configuracion pre-MSC3861.
- Los logs archivados en `--log-dir` **incluyen userIDs** (datos personales). Se crean con permisos `0600` (solo propietario puede leer). Mantener el directorio con `chmod 0700`. No subir los logs a repos publicos.
- El comando `mas-cli syn2mas` en el container asume que `homeserver.yaml` esta montado en `/data/homeserver.yaml`. Si el volume mount del container usa otra ruta, el comando fallara con "file not found". Verificar con `docker inspect <container> | jq '.[].Mounts'`.
- La postcondicion compara el count de usuarios MAS con una segunda ejecucion de dry-run para obtener el count esperado. Si el conteo no esta disponible (salida inesperada de mas-cli), la funcion emite `status=ok` con `users_migrated` del count real de MAS — no aborta por este motivo para evitar falsos negativos.
@@ -0,0 +1,325 @@
#!/usr/bin/env bash
# mas_syn2mas_migration — Migra usuarios Synapse a MAS via mas-cli syn2mas.
# Fuerza dry-run primero, archiva el log, aborta si conflicts > threshold,
# y solo ejecuta la migracion real cuando se pasa --apply.
#
# Usage:
# mas_syn2mas_migration --ssh-host <host> --mas-container <name> \
# --synapse-config-path <path-on-host> --log-dir <local-path> \
# [--max-conflicts N] [--apply]
#
# Output: JSON en stdout con status, dry_run_log, apply_log, conflicts, users_migrated, duration_s
set -euo pipefail
mas_syn2mas_migration() {
local ssh_host=""
local mas_container=""
local synapse_config_path=""
local log_dir=""
local max_conflicts=0
local do_apply=false
# ---- Parse args ----
while [[ $# -gt 0 ]]; do
case "$1" in
--ssh-host)
ssh_host="$2"
shift 2
;;
--mas-container)
mas_container="$2"
shift 2
;;
--synapse-config-path)
synapse_config_path="$2"
shift 2
;;
--log-dir)
log_dir="$2"
shift 2
;;
--max-conflicts)
max_conflicts="$2"
shift 2
;;
--apply)
do_apply=true
shift
;;
--help|-h)
cat >&2 <<'USAGE'
mas_syn2mas_migration - Migra usuarios Synapse a Matrix Authentication Service (MAS)
Usage:
mas_syn2mas_migration \
--ssh-host <host> \
--mas-container <name> \
--synapse-config-path <path-on-host> \
--log-dir <local-path> \
[--max-conflicts N] \
[--apply]
Opciones:
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
--mas-container Nombre del container MAS (ej. element_matrix_chat-mas-1)
--synapse-config-path Ruta en el VPS al homeserver.yaml
(ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml)
--log-dir Directorio local donde archivar logs dry-run y apply
--max-conflicts N Tope de conflictos en dry-run antes de abortar (default 0)
--apply Ejecutar migracion real. Sin esta flag: solo dry-run.
Comportamiento:
1. Siempre ejecuta dry-run primero y archiva el log.
2. Si conflicts > max-conflicts -> status=aborted, exit 2.
3. Sin --apply -> status=ok (dry-run completado), exit 0.
4. Con --apply -> ejecuta migracion real, archiva log, verifica postcondicion.
Output JSON: {"status":"ok|aborted|error","dry_run_log":"path","apply_log":"path|null","conflicts":N,"users_migrated":N,"duration_s":N}
USAGE
echo '{"status":"help","dry_run_log":"","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}'
return 0
;;
*)
echo "mas_syn2mas_migration: argumento desconocido: $1" >&2
return 1
;;
esac
done
# ---- Validar argumentos obligatorios ----
local errors=()
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
[[ -z "$mas_container" ]] && errors+=("--mas-container es obligatorio")
[[ -z "$synapse_config_path" ]] && errors+=("--synapse-config-path es obligatorio")
[[ -z "$log_dir" ]] && errors+=("--log-dir es obligatorio")
if [[ ${#errors[@]} -gt 0 ]]; then
for err in "${errors[@]}"; do
echo "ERROR: $err" >&2
done
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# Validar que max_conflicts es un entero no negativo
if ! [[ "$max_conflicts" =~ ^[0-9]+$ ]]; then
echo "ERROR: --max-conflicts debe ser un entero >= 0, recibido: $max_conflicts" >&2
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# ---- Dependencias locales ----
if ! command -v jq &>/dev/null; then
echo "ERROR: jq no encontrado. Instalar: apt install jq / brew install jq" >&2
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# ---- Crear log-dir con permisos restringidos ----
mkdir -p "$log_dir"
chmod 0700 "$log_dir"
local ts
ts=$(date +%s)
local dry_run_log="${log_dir}/syn2mas_dryrun_${ts}.log"
local apply_log_path="null"
local apply_log_file="${log_dir}/syn2mas_apply_${ts}.log"
# La ruta del homeserver.yaml dentro del container MAS se pasa como --synapse-config
# MAS monta el directorio del synapse bajo /data/ por convencion, pero la ruta real
# puede variar — usamos la ruta tal como existe en el host (montada via volume).
# El comando real esperado: docker exec <container> mas-cli syn2mas --synapse-config <path>
# donde <path> es la ruta tal como el container la ve (via volume mount).
# Asumimos que el VPS tiene el config accesible en la misma ruta dentro del container.
local container_config="/data/homeserver.yaml"
echo "mas_syn2mas_migration: ssh-host=${ssh_host} container=${mas_container} max-conflicts=${max_conflicts} apply=${do_apply}" >&2
# =========================================================================
# PASO 1: DRY-RUN obligatorio
# =========================================================================
echo "mas_syn2mas_migration: ejecutando dry-run..." >&2
local dry_exit=0
# Capturar stdout+stderr del dry-run en el log y tambien en variable para parsing
local dry_output
dry_output=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas \
--synapse-config '${container_config}' \
--dry-run" \
2>&1) || dry_exit=$?
# Archivar log con timestamp + header informativo
{
echo "# mas_syn2mas_migration dry-run"
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
echo "# synapse-config-path=${synapse_config_path}"
echo "# exit=${dry_exit}"
echo "# ---"
printf '%s\n' "$dry_output"
} > "$dry_run_log"
chmod 0600 "$dry_run_log"
echo "mas_syn2mas_migration: dry-run exit=${dry_exit}, log=${dry_run_log}" >&2
if [[ $dry_exit -ne 0 ]]; then
# Si el comando SSH falla completamente (no es fallo de syn2mas sino de conectividad)
echo "mas_syn2mas_migration: ERROR — dry-run falló con exit ${dry_exit}" >&2
local escaped_out
escaped_out=$(printf '%s' "${dry_output}" | jq -Rs '.')
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
return 1
fi
# =========================================================================
# PASO 2: Parsear conflicts del dry-run
# =========================================================================
# Regex sobre lineas tipo:
# "Conflict:", "Skipping:", "Error processing user", "conflict"
# También contamos líneas que indiquen usuarios problemáticos.
local conflicts=0
local conflict_lines
conflict_lines=$(printf '%s\n' "$dry_output" | \
grep -ciE '(conflict|skipping|error processing user|cannot migrate|already exists)' 2>/dev/null || true)
# grep -c devuelve string; convertir a int defensivamente
if [[ "$conflict_lines" =~ ^[0-9]+$ ]]; then
conflicts=$conflict_lines
else
# Parser falló de forma inesperada — abortar defensivamente
echo "mas_syn2mas_migration: ERROR — no se pudo parsear el conteo de conflicts del dry-run (parser defensivo)" >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
return 1
fi
echo "mas_syn2mas_migration: conflicts detectados en dry-run: ${conflicts} (max permitido: ${max_conflicts})" >&2
# =========================================================================
# PASO 3: Verificar threshold de conflicts
# =========================================================================
if [[ $conflicts -gt $max_conflicts ]]; then
echo "mas_syn2mas_migration: ABORTADO — conflicts (${conflicts}) > max-conflicts (${max_conflicts})" >&2
echo "Revisar: ${dry_run_log}" >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"aborted\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
return 2
fi
# =========================================================================
# PASO 4: Si no --apply, terminar aqui con status=ok (dry-run completado)
# =========================================================================
if [[ "$do_apply" == "false" ]]; then
echo "mas_syn2mas_migration: dry-run completado (${conflicts} conflicts). Revisar log y re-ejecutar con --apply." >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"ok\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
return 0
fi
# =========================================================================
# PASO 5: Migracion REAL (--apply)
# =========================================================================
echo "mas_syn2mas_migration: ejecutando migracion REAL..." >&2
local apply_start
apply_start=$(date +%s)
local apply_exit=0
local apply_output
apply_output=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas \
--synapse-config '${container_config}'" \
2>&1) || apply_exit=$?
local apply_end
apply_end=$(date +%s)
local duration_s=$(( apply_end - apply_start ))
# Archivar log de apply
{
echo "# mas_syn2mas_migration apply"
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
echo "# synapse-config-path=${synapse_config_path}"
echo "# exit=${apply_exit} duration_s=${duration_s}"
echo "# ---"
printf '%s\n' "$apply_output"
} > "$apply_log_file"
chmod 0600 "$apply_log_file"
apply_log_path="$apply_log_file"
echo "mas_syn2mas_migration: apply exit=${apply_exit}, duration=${duration_s}s, log=${apply_log_file}" >&2
if [[ $apply_exit -ne 0 ]]; then
echo "mas_syn2mas_migration: ERROR — migracion real falló con exit ${apply_exit}" >&2
local dry_run_log_json apply_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":${duration_s}}"
return 1
fi
# =========================================================================
# PASO 6: Postcondicion — comparar usuarios en MAS vs Synapse
# =========================================================================
echo "mas_syn2mas_migration: verificando postcondicion (usuarios MAS vs Synapse)..." >&2
local mas_user_count=0
local synapse_user_count=0
local users_migrated=0
local post_status="ok"
# Contar usuarios en MAS via mas-cli admin user list
local mas_count_raw
mas_count_raw=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli manage list-users --json 2>/dev/null | jq length" \
2>/dev/null || echo "0")
if [[ "$mas_count_raw" =~ ^[0-9]+$ ]]; then
mas_user_count=$mas_count_raw
else
echo "mas_syn2mas_migration: ADVERTENCIA — no se pudo obtener conteo de usuarios MAS (output: ${mas_count_raw})" >&2
post_status="ok" # No abortar, solo advertir
fi
# Contar usuarios locales en Synapse via psql (excluyendo bots/AS)
# Intentamos obtener el count; si falla, continuamos sin abortar
local synapse_count_raw
synapse_count_raw=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas --synapse-config '${container_config}' --dry-run 2>&1 | grep -oE 'Found [0-9]+ users' | grep -oE '[0-9]+' | head -1" \
2>/dev/null || echo "0")
if [[ "$synapse_count_raw" =~ ^[0-9]+$ ]]; then
synapse_user_count=$synapse_count_raw
fi
users_migrated=$mas_user_count
# Si tenemos ambos counts y difieren significativamente, marcar como warning en log
if [[ $synapse_user_count -gt 0 && $mas_user_count -eq 0 ]]; then
echo "mas_syn2mas_migration: ADVERTENCIA — MAS reporta 0 usuarios pero Synapse tenia ${synapse_user_count}" >&2
post_status="error"
fi
echo "mas_syn2mas_migration: postcondicion: mas_users=${mas_user_count} synapse_users=${synapse_user_count} status=${post_status}" >&2
# =========================================================================
# PASO 7: Emitir JSON final
# =========================================================================
local dry_run_log_json apply_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
echo "{\"status\":\"${post_status}\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":${users_migrated},\"duration_s\":${duration_s}}"
return 0
}
# Ejecutar si se llama directamente (no sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
mas_syn2mas_migration "$@"
fi
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Tests para mas_syn2mas_migration
# Verifica arg parsing sin conectar al VPS real.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/mas_syn2mas_migration.sh"
PASS=0
FAIL=0
assert_exit() {
local test_name="$1" expected_exit="$2"
shift 2
local actual_exit=0
set +e
"$@" >/dev/null 2>&1
actual_exit=$?
set -e
if [[ "$actual_exit" == "$expected_exit" ]]; then
echo "PASS: $test_name"
((PASS++)) || true
else
echo "FAIL: $test_name — expected exit $expected_exit, got $actual_exit"
((FAIL++)) || true
fi
}
assert_stdout_contains() {
local test_name="$1" needle="$2"
shift 2
local output actual_exit=0
set +e
output=$("$@" 2>/dev/null)
actual_exit=$?
set -e
if echo "$output" | grep -q "$needle"; then
echo "PASS: $test_name"
((PASS++)) || true
else
echo "FAIL: $test_name — expected stdout to contain '$needle', got: $output"
((FAIL++)) || true
fi
}
# Test: aborta con error cuando faltan args obligatorios
assert_exit "aborta con error cuando faltan args obligatorios" 1 \
mas_syn2mas_migration
# Test: help no devuelve error
assert_exit "help no devuelve error" 0 \
mas_syn2mas_migration --help
# Test: argumento desconocido retorna exit 1
assert_exit "argumento desconocido retorna exit 1" 1 \
mas_syn2mas_migration --unknown-flag
# Test: max-conflicts invalido retorna exit 1
assert_exit "max-conflicts invalido retorna exit 1" 1 \
mas_syn2mas_migration \
--ssh-host fake-host \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml \
--log-dir "/tmp/test_mas_migration_$$" \
--max-conflicts "not-a-number"
# Test: help emite JSON valido con status=help
assert_stdout_contains "help emite JSON con status help" '"status":"help"' \
mas_syn2mas_migration --help
# Test: falta --ssh-host emite JSON con status=error
assert_stdout_contains "falta ssh-host emite JSON error" '"status":"error"' \
mas_syn2mas_migration \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml \
--log-dir "/tmp/test_mas_migration_$$"
# Test: falta --log-dir emite JSON con status=error
assert_stdout_contains "falta log-dir emite JSON error" '"status":"error"' \
mas_syn2mas_migration \
--ssh-host fake-host \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml
# Limpieza
rm -rf "/tmp/test_mas_migration_$$" 2>/dev/null || true
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -0,0 +1,58 @@
---
name: refresh_windows_icon_cache
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "refresh_windows_icon_cache() -> void"
description: "Fuerza a Windows Explorer a recargar la cache de iconos desde WSL2 via ie4uinit.exe. Best-effort: nunca aborta, retorna 0 si alguna estrategia tuvo exito."
tags: [windows, wsl, deploy, shell, icons, cpp-windows]
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/refresh_windows_icon_cache.sh"
params: []
output: "0 si al menos una estrategia tuvo exito, non-zero si todas fallaron. Imprime una linea de estado en stdout."
---
## Ejemplo
```bash
source bash/functions/infra/refresh_windows_icon_cache.sh
refresh_windows_icon_cache
# icon cache refresh: ok via ie4uinit -show
```
O directamente via `fn run`:
```bash
./fn run refresh_windows_icon_cache_bash_infra
```
Uso tipico en un pipeline de redeploy tras reconstruir el `.exe`:
```bash
source bash/functions/infra/deploy_cpp_exe_to_windows.sh
source bash/functions/infra/refresh_windows_icon_cache.sh
deploy_cpp_exe_to_windows "registry_dashboard" "apps/registry_dashboard"
refresh_windows_icon_cache
```
## Cuando usarla
Despues de redeployar un `.exe` Windows cuyo `appicon.ico` cambio (via windres embebido en el build), antes de que Windows muestre el icono nuevo en taskbar, Alt+Tab y File Explorer. Sin esta llamada Windows puede tardar minutos en reflejar el icono actualizado, o no actualizarlo hasta reiniciar Explorer.
## Gotchas
- `ie4uinit.exe` debe estar en el PATH de WSL2 (normalmente via `/mnt/c/Windows/System32/`). Si Windows esta muy roto puede no encontrarse — la funcion retornara 1 con mensaje de error.
- El cambio puede tardar 1-2 segundos en propagarse visualmente despues de que la funcion retorne.
- Algunos casos extremos (icono cacheado en el dockable taskbar previamente fijado) requieren desanclar y volver a anclar el ejecutable, o reiniciar `explorer.exe`. Esta funcion no mata Explorer — seria demasiado disruptivo.
- Solo funciona desde WSL2 con acceso a herramientas Windows (`/mnt/c/Windows/System32/` en PATH). No tiene efecto en Linux nativo.
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# refresh_windows_icon_cache — Fuerza a Windows Explorer a recargar la cache
# de iconos desde WSL2. Best-effort: nunca aborta, retorna 0 si alguna
# estrategia tuvo exito.
refresh_windows_icon_cache() {
# Estrategia 1: ie4uinit.exe -show (Windows 10/11 — emite SHCNE_ASSOCCHANGED)
if command -v ie4uinit.exe >/dev/null 2>&1; then
if ie4uinit.exe -show >/dev/null 2>&1; then
echo "icon cache refresh: ok via ie4uinit -show"
return 0
fi
# Estrategia 2: ie4uinit.exe -ClearIconCache (fallback para builds viejos)
if ie4uinit.exe -ClearIconCache >/dev/null 2>&1; then
echo "icon cache refresh: ok via ie4uinit -ClearIconCache"
return 0
fi
fi
echo "icon cache refresh: failed (ie4uinit.exe not found or all strategies failed)"
return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
refresh_windows_icon_cache "$@"
fi
+14 -5
View File
@@ -3,11 +3,11 @@ name: resolve_cpp_app_dir
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir"
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
tags: [cpp, resolve, app, directory, infra]
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
tags: [cpp, resolve, app, directory, infra, cpp-windows]
uses_functions: []
uses_types: []
returns: []
@@ -20,7 +20,7 @@ test_file_path: ""
file_path: "bash/functions/infra/resolve_cpp_app_dir.sh"
params:
- name: app_name
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/."
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1."
---
@@ -44,4 +44,13 @@ APP_DIR="$(echo "$resolved" | cut -f2)"
## Notas
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente.
Busca en orden:
1. `$ROOT/apps/<X>` con `CMakeLists.txt` — layout canonical post-issue 0096.
2. `$ROOT/cpp/apps/<X>` — legacy pre-issue 0096.
3. `$ROOT/projects/*/apps/<X>` — apps de un proyecto (primer match gana).
Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. Helper interno `_list_cpp_apps` evita duplicar codigo en los paths de error.
### Growth log
- v1.1.0 (2026-05-16) — busca tambien en `apps/<X>/` (canonical issue 0096). Antes solo cubria `cpp/apps/<X>/` y `projects/*/apps/<X>/`, lo que hacia que `./fn run compile_cpp_app <name>` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`).
+25 -20
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry.
# Sin arg: deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/.
# Con arg: usa el nombre directamente y busca en ambas ubicaciones.
# Sin arg: deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
# Con arg: usa el nombre directamente y busca en las tres ubicaciones.
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
# Error: lista apps disponibles en stderr + exit 1.
@@ -9,18 +9,28 @@ resolve_cpp_app_dir() {
local app_arg="${1:-}"
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
_list_cpp_apps() {
ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//'
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
}
# --- Deducir desde CWD si no hay argumento ---
if [ -z "$app_arg" ]; then
local cwd
cwd="$(pwd)"
case "$cwd" in
"$root"/apps/*/|"$root"/apps/*)
local rel="${cwd#"$root/apps/"}"
app_arg="${rel%%/*}"
;;
"$root"/cpp/apps/*/|"$root"/cpp/apps/*)
# Extraer primer segmento tras cpp/apps/
local rel="${cwd#"$root/cpp/apps/"}"
app_arg="${rel%%/*}"
;;
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
# Extraer primer segmento tras la ultima /apps/
local rel="${cwd#"$root/projects/"}"
rel="${rel#*/apps/}"
app_arg="${rel%%/*}"
@@ -33,12 +43,7 @@ resolve_cpp_app_dir() {
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
echo "" >&2
echo "Apps disponibles:" >&2
{
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
} >&2
_list_cpp_apps >&2
echo "" >&2
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
return 1
@@ -47,12 +52,17 @@ resolve_cpp_app_dir() {
# --- Buscar directorio real ---
local app_dir=""
# Primero: cpp/apps/<X>
if [ -d "$root/cpp/apps/$app_arg" ]; then
# Primero (issue 0096 canonical): apps/<X>
if [ -d "$root/apps/$app_arg" ] && [ -f "$root/apps/$app_arg/CMakeLists.txt" ]; then
app_dir="$root/apps/$app_arg"
fi
# Segundo (legacy): cpp/apps/<X>
if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then
app_dir="$root/cpp/apps/$app_arg"
fi
# Segundo: projects/*/apps/<X> (primer match)
# Tercero: projects/*/apps/<X> (primer match)
if [ -z "$app_dir" ]; then
for cand in "$root"/projects/*/apps/"$app_arg"; do
if [ -d "$cand" ]; then
@@ -63,15 +73,10 @@ resolve_cpp_app_dir() {
fi
if [ -z "$app_dir" ]; then
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2
echo "" >&2
echo "Apps disponibles:" >&2
{
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
} >&2
_list_cpp_apps >&2
return 1
fi
+68
View File
@@ -0,0 +1,68 @@
---
name: wg_client_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_client_install(config_path_or_stdin, [interface_name]) -> json"
description: "Device-side: instala wg0.conf en /etc/wireguard/, habilita systemd wg-quick@wg0, verifica handshake con hub. Idempotente. Acepta config por path o stdin (para pipes desde wg_client_config)."
tags: [wireguard, client, install, mesh, systemd]
uses_functions: [wg_install_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: config_path_or_stdin
desc: "path al archivo .conf existente, o '-' para leer de stdin (compatible con pipe desde wg_client_config)"
- name: interface_name
desc: "nombre de la interfaz WireGuard (default: wg0). Determina /etc/wireguard/<iface>.conf y la unit systemd wg-quick@<iface>"
output: "JSON {status, interface, hub_endpoint, handshake_seen}. status: installed | already-configured | installed-no-handshake | installed-no-systemd"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_client_install.sh"
---
## Ejemplo
```bash
source bash/functions/infra/wg_client_install.sh
# Desde pipe (caso más común en flow 0009):
wg_client_config_go_infra | jq -r '.INI' | wg_client_install -
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Desde archivo .conf generado previamente:
wg_client_install /tmp/peer_laptop.conf
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Con interfaz personalizada:
wg_client_install /tmp/peer_laptop.conf wg1
# {"status":"installed","interface":"wg1","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Segunda ejecución con misma config (idempotente):
wg_client_install /tmp/peer_laptop.conf
# {"status":"already-configured","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":false}
```
## Cuando usarla
Cuando necesites conectar un nuevo peer al mesh WireGuard en el flow 0009. Úsala justo después de `wg_client_config` (que genera el .conf) para instalarlo en el device peer. Es el paso final del onboarding de un nodo: config generada → instalada → verificada con handshake.
## Gotchas
- **Requiere root/sudo** para escribir en `/etc/wireguard/`, hacer `chmod 600`, y ejecutar `systemctl`. El operador debe tener `sudo` sin password para estos comandos, o ejecutar la función como root.
- **Idempotente por contenido**: si `/etc/wireguard/<iface>.conf` ya existe con el mismo contenido, retorna `status=already-configured` sin tocar nada. Si el contenido difiere, hace backup automático con timestamp antes de sobreescribir.
- **NetworkManager**: si NM gestiona la interfaz wg0, `wg-quick` puede fallar con conflicto. Solución: crear `/etc/NetworkManager/conf.d/99-wg.conf` con `[keyfile]\nunmanaged-devices=interface-name:wg0` y reiniciar NM antes de ejecutar esta función.
- **WSL2 sin systemd** (variantes antiguas o sin `/etc/wsl.conf` con `[boot] systemd=true`): `systemctl` no está disponible. La función detecta esto, emite `status=installed-no-systemd` con instrucciones en stderr para levantar la interfaz manualmente con `sudo wg-quick up wg0`. Para autostart en WSL2 sin systemd: añadir `sudo wg-quick up wg0` al final de `~/.bashrc`.
- **WSL2 con systemd**: kernel WSL2 >= 5.6 (default en distros recientes) incluye WireGuard built-in. Habilitar systemd en WSL2 con `[boot]\nsystemd=true` en `/etc/wsl.conf` y reiniciar WSL. Luego esta función funciona igual que en Linux nativo.
- **Android / Termux**: NO usar esta función. Termux no tiene systemd ni `/etc/wireguard/`. En Android usar la app WireGuard oficial (F-Droid / Play Store) e importar el .conf generado por `wg_client_config` directamente desde la app.
- **handshake_seen=false con status=installed-no-handshake**: la interfaz está activa pero el hub no ha respondido en 10s. No es un error fatal — puede tardar más si el hub está ocupado o hay NAT traversal pendiente. Verificar: endpoint accesible por UDP, hub corriendo con `wg show`, claves public/preshared coincidentes.
- Los logs van siempre a stderr con prefijo `[wg_client_install]`; stdout es exclusivamente el JSON de resultado.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# wg_client_install — Device-side: instala wg0.conf en /etc/wireguard/, habilita
# systemd wg-quick@<iface>, verifica handshake con hub. Idempotente.
# Acepta config por path o stdin ("-").
# Exit 0 = éxito (installed o already-configured), 1 = error fatal.
wg_client_install() {
local config_src="${1:--}"
local iface="${2:-wg0}"
local conf_dest="/etc/wireguard/${iface}.conf"
local config_content="" hub_endpoint="" handshake_seen="false"
_wg_ci_log() { echo "[wg_client_install] $*" >&2; }
# ── Prereq: wg debe estar instalado ──────────────────────────────────────
if ! command -v wg &>/dev/null; then
_wg_ci_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
return 1
fi
# ── Leer contenido del .conf ──────────────────────────────────────────────
if [[ "${config_src}" == "-" ]]; then
_wg_ci_log "Leyendo config desde stdin"
config_content=$(cat) || { _wg_ci_log "ERROR: fallo al leer stdin"; return 1; }
elif [[ -f "${config_src}" ]]; then
_wg_ci_log "Leyendo config desde ${config_src}"
config_content=$(cat "${config_src}") || { _wg_ci_log "ERROR: fallo al leer ${config_src}"; return 1; }
else
_wg_ci_log "ERROR: '${config_src}' no es un path existente ni '-' (stdin)"
return 1
fi
if [[ -z "${config_content}" ]]; then
_wg_ci_log "ERROR: contenido de config vacío"
return 1
fi
# ── Extraer endpoint del hub para incluirlo en el JSON de salida ──────────
hub_endpoint=$(printf '%s\n' "${config_content}" | grep -m1 '^Endpoint\s*=' | sed 's/.*=\s*//' | tr -d '[:space:]' || true)
# ── Idempotencia: comparar con conf existente ─────────────────────────────
if [[ -f "${conf_dest}" ]]; then
local existing_content
existing_content=$(sudo cat "${conf_dest}" 2>/dev/null || cat "${conf_dest}" 2>/dev/null || true)
if [[ "${existing_content}" == "${config_content}" ]]; then
_wg_ci_log "Configuración idéntica ya presente en ${conf_dest}; nada que hacer"
printf '{"status":"already-configured","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
# Contenido difiere → backup + rewrite
local backup="${conf_dest}.bak.$(date +%Y%m%d%H%M%S)"
_wg_ci_log "Configuración existente difiere; backup → ${backup}"
sudo cp "${conf_dest}" "${backup}" \
|| { _wg_ci_log "ERROR: no se pudo hacer backup de ${conf_dest}"; return 1; }
fi
# ── Crear directorio y escribir conf ─────────────────────────────────────
sudo mkdir -p "/etc/wireguard" \
|| { _wg_ci_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
printf '%s\n' "${config_content}" | sudo tee "${conf_dest}" >/dev/null \
|| { _wg_ci_log "ERROR: no se pudo escribir ${conf_dest}"; return 1; }
sudo chmod 600 "${conf_dest}" \
|| { _wg_ci_log "WARN: no se pudo chmod 600 ${conf_dest}"; }
_wg_ci_log "Config escrita en ${conf_dest} (chmod 600)"
# ── Habilitar + arrancar systemd unit ─────────────────────────────────────
if ! command -v systemctl &>/dev/null; then
_wg_ci_log "WARN: systemctl no disponible."
_wg_ci_log " En WSL2 sin systemd: ejecuta 'sudo wg-quick up ${iface}' manualmente."
_wg_ci_log " Para autostart en WSL2: añade 'sudo wg-quick up ${iface}' a ~/.bashrc o usa WSL2 con systemd habilitado."
printf '{"status":"installed-no-systemd","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
_wg_ci_log "Habilitando y arrancando wg-quick@${iface}"
if ! sudo systemctl enable --now "wg-quick@${iface}" 2>&1 | tee /dev/stderr >&2; then
_wg_ci_log "ERROR: systemctl enable --now wg-quick@${iface} falló."
_wg_ci_log " En WSL2: asegúrate de tener kernel >= 5.6 y systemd habilitado (/etc/wsl.conf: [boot] systemd=true)."
_wg_ci_log " Si NetworkManager gestiona ${iface}: añade 'unmanaged-devices=interface-name:${iface}' a /etc/NetworkManager/conf.d/99-wg.conf"
return 1
fi
_wg_ci_log "wg-quick@${iface} habilitado y activo"
# ── Esperar handshake (hasta 10 s) ────────────────────────────────────────
local deadline=$(( $(date +%s) + 10 ))
_wg_ci_log "Esperando handshake en ${iface} (timeout 10s)..."
while [[ $(date +%s) -lt ${deadline} ]]; do
local hs_output
hs_output=$(sudo wg show "${iface}" latest-handshakes 2>/dev/null || true)
# latest-handshakes devuelve "<pubkey> <unix_ts>"; ts > 0 = handshake visto
if printf '%s\n' "${hs_output}" | awk '{print $2}' | grep -qE '^[1-9][0-9]+$'; then
handshake_seen="true"
_wg_ci_log "Handshake confirmado en ${iface}"
break
fi
sleep 1
done
if [[ "${handshake_seen}" == "false" ]]; then
_wg_ci_log "WARN: timeout esperando handshake en ${iface}. La interfaz está activa pero el hub no ha respondido aún."
_wg_ci_log " Verifica: endpoint accesible, hub corriendo, claves correctas."
printf '{"status":"installed-no-handshake","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
printf '{"status":"installed","interface":"%s","hub_endpoint":"%s","handshake_seen":true}\n' \
"${iface}" "${hub_endpoint}"
return 0
}
+66
View File
@@ -0,0 +1,66 @@
---
name: wg_hub_setup
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_hub_setup(private_key, subnet_cidr, listen_port) -> json"
description: "Configura el host como hub WireGuard (servidor). Crea /etc/wireguard/wg0.conf con clave privada + IP pool + ListenPort. Abre UDP en firewall (ufw o iptables), habilita ip_forward persistente en /etc/sysctl.d/99-wireguard.conf, persiste y arranca systemd unit wg-quick@wg0. Idempotente: misma PrivateKey = no-op; PrivateKey distinta = backup + rewrite."
tags: [wireguard, hub, infra, mesh, systemd]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: private_key
desc: "base64 WG private key del hub (44 chars, generada por wg_keygen o `wg genkey`)"
- name: subnet_cidr
desc: "subnet hub con bits del host, ej. 10.42.0.1/24. El hub recibe la .1"
- name: listen_port
desc: "UDP port donde escucha WireGuard (default 51820, rango 1024-65535)"
output: "JSON {status, config_path, interface, hub_ip}. status: configured | reconfigured | already-configured"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_hub_setup.sh"
---
## Ejemplo
```bash
# Generar clave (o usar wg_keygen del registry)
PRIVKEY=$(wg genkey)
source bash/functions/infra/wg_hub_setup.sh
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
# {"status":"configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
# Segunda ejecución con la misma clave → no-op
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
# {"status":"already-configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
# Cambiar clave → backup de conf anterior + rewrite
wg_hub_setup "$NUEVA_PRIVKEY" "10.42.0.1/24" 51820
# {"status":"reconfigured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
```
## Cuando usarla
Cuando necesites convertir un VPS/host en el nodo central (hub) de una red mesh WireGuard. Úsala inmediatamente después de `wg_install` para dejar el hub listo para recibir peers. El hub escucha en un puerto UDP público; los peers se conectan a él con su propia clave y la AllowedIPs del hub.
## Gotchas
- Requiere `sudo` con NOPASSWD para: `tee /etc/wireguard/`, `chmod`, `sysctl`, `iptables`/`ufw`, `systemctl`. Configurar antes en sudoers.
- NUNCA reusar la misma `private_key` entre hubs distintos. Cada hub tiene su propio par de claves independiente.
- El bloque `PostUp`/`PostDown` usa `eth0` como interfaz de salida para NAT. En VPS con interfaz distinta (ens3, enp3s0) editar `/etc/wireguard/wg0.conf` manualmente antes de reiniciar.
- Conflicto de subnet con docker0 si usas 172.17.0.0/16. Evitar solapamiento — usar 10.42.x.x o 192.168.200.x para WireGuard.
- `systemd-resolved` en VPS Ubuntu puede interferir con resolución DNS cuando WireGuard está activo si el conf añade `DNS =`. Esta función NO setea DNS para evitar el problema — configurarlo a nivel peer si se necesita.
- Si `systemctl start wg-quick@wg0` falla, revisar logs con `journalctl -u wg-quick@wg0 -n 50`.
- En entornos cloud (AWS/GCP/Azure) el security group / firewall de red del proveedor también debe abrir el puerto UDP, independientemente de ufw/iptables local.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# wg_hub_setup — Configura el host como hub WireGuard (servidor central).
# Crea /etc/wireguard/wg0.conf con [Interface] block, abre UDP en firewall,
# habilita ip_forward persistente, arranca y verifica wg-quick@wg0.
# Idempotente: si el conf existe con la misma PrivateKey -> no-op.
# Emite JSON a stdout. Logs a stderr con prefijo [wg_hub_setup].
# Exit 0 = éxito, 1 = fallo.
wg_hub_setup() {
local private_key="${1:-}"
local subnet_cidr="${2:-10.42.0.1/24}"
local listen_port="${3:-51820}"
_wg_hub_log() { echo "[wg_hub_setup] $*" >&2; }
# ── Validación de entradas ──────────────────────────────────────────────
# private_key: base64 estándar de 44 caracteres (32 bytes)
if [[ -z "${private_key}" ]]; then
_wg_hub_log "ERROR: private_key requerida (base64 44 chars, generada por wg genkey)"
return 1
fi
if ! [[ "${private_key}" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
_wg_hub_log "ERROR: private_key no parece base64 válida (se esperan 44 chars terminando en '=')"
return 1
fi
# subnet_cidr: 10.x.x.x/nn
if ! [[ "${subnet_cidr}" =~ ^10\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
_wg_hub_log "ERROR: subnet_cidr debe ser 10.x.x.x/nn, recibido: '${subnet_cidr}'"
return 1
fi
# listen_port: 1024-65535
if ! [[ "${listen_port}" =~ ^[0-9]+$ ]] || (( listen_port < 1024 || listen_port > 65535 )); then
_wg_hub_log "ERROR: listen_port debe ser un entero entre 1024 y 65535, recibido: '${listen_port}'"
return 1
fi
# ── Verificar que wireguard-tools esté instalado ────────────────────────
if ! command -v wg &>/dev/null; then
_wg_hub_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
return 1
fi
if ! command -v wg-quick &>/dev/null; then
_wg_hub_log "ERROR: 'wg-quick' no encontrado. Instala wireguard-tools."
return 1
fi
# ── Extraer hub_ip (parte sin CIDR prefix) y determinar config_path ────
local hub_ip="${subnet_cidr%%/*}"
local config_path="/etc/wireguard/wg0.conf"
local interface="wg0"
local action_status=""
# ── Idempotencia: comparar PrivateKey existente ─────────────────────────
if [[ -f "${config_path}" ]]; then
local existing_key
existing_key=$(sudo grep -E '^\s*PrivateKey\s*=' "${config_path}" 2>/dev/null \
| head -n1 | sed 's/.*=\s*//')
if [[ "${existing_key}" == "${private_key}" ]]; then
_wg_hub_log "Config existente con misma PrivateKey — no-op (status=already-configured)"
printf '{"status":"already-configured","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
"${config_path}" "${interface}" "${hub_ip}"
return 0
else
_wg_hub_log "Config existente con PrivateKey DIFERENTE — haciendo backup y reescribiendo"
local backup_path="${config_path}.bak.$(date +%Y%m%d%H%M%S)"
sudo cp "${config_path}" "${backup_path}" \
|| { _wg_hub_log "ERROR: no se pudo hacer backup en ${backup_path}"; return 1; }
_wg_hub_log "Backup guardado en ${backup_path}"
action_status="reconfigured"
fi
else
action_status="configured"
fi
# ── Asegurar que /etc/wireguard existe con permisos correctos ───────────
if [[ ! -d /etc/wireguard ]]; then
sudo mkdir -p /etc/wireguard \
|| { _wg_hub_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
sudo chmod 700 /etc/wireguard
_wg_hub_log "Directorio /etc/wireguard creado"
fi
# ── Escribir /etc/wireguard/wg0.conf ────────────────────────────────────
_wg_hub_log "Escribiendo ${config_path} (Address=${subnet_cidr}, ListenPort=${listen_port})"
sudo tee "${config_path}" > /dev/null <<EOF
[Interface]
Address = ${subnet_cidr}
ListenPort = ${listen_port}
PrivateKey = ${private_key}
SaveConfig = false
# NAT: permite que los peers accedan a internet via este hub (opcional, comentar si no se desea)
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF
if [[ $? -ne 0 ]]; then
_wg_hub_log "ERROR: no se pudo escribir ${config_path}"
return 1
fi
sudo chmod 600 "${config_path}" \
|| { _wg_hub_log "ERROR: chmod 600 ${config_path} falló"; return 1; }
_wg_hub_log "Permisos 600 aplicados a ${config_path}"
# ── Habilitar ip_forward persistente ────────────────────────────────────
local sysctl_file="/etc/sysctl.d/99-wireguard.conf"
if [[ ! -f "${sysctl_file}" ]] || ! grep -q "net.ipv4.ip_forward" "${sysctl_file}" 2>/dev/null; then
_wg_hub_log "Habilitando ip_forward en ${sysctl_file}"
echo "net.ipv4.ip_forward = 1" | sudo tee "${sysctl_file}" > /dev/null \
|| { _wg_hub_log "ERROR: no se pudo escribir ${sysctl_file}"; return 1; }
fi
sudo sysctl -p "${sysctl_file}" >&2 \
|| _wg_hub_log "WARN: sysctl -p falló (puede ignorarse si el kernel ya tiene ip_forward=1)"
# ── Abrir puerto en firewall ─────────────────────────────────────────────
if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
_wg_hub_log "ufw activo — abriendo UDP/${listen_port}"
sudo ufw allow "${listen_port}/udp" >&2 \
|| _wg_hub_log "WARN: ufw allow ${listen_port}/udp falló (verificar manualmente)"
elif command -v iptables &>/dev/null; then
_wg_hub_log "ufw inactivo — usando iptables para abrir UDP/${listen_port}"
sudo iptables -C INPUT -p udp --dport "${listen_port}" -j ACCEPT 2>/dev/null \
|| sudo iptables -A INPUT -p udp --dport "${listen_port}" -j ACCEPT >&2 \
|| _wg_hub_log "WARN: iptables INPUT rule falló (verificar manualmente)"
else
_wg_hub_log "WARN: ni ufw ni iptables disponibles — abre el puerto ${listen_port}/udp manualmente"
fi
# ── Detener interfaz si estaba corriendo (para aplicar nueva config) ────
if sudo wg show "${interface}" &>/dev/null 2>&1; then
_wg_hub_log "Interfaz ${interface} activa — deteniendo antes de reconfigurar"
sudo systemctl stop "wg-quick@${interface}" 2>/dev/null \
|| sudo wg-quick down "${interface}" 2>/dev/null \
|| _wg_hub_log "WARN: no se pudo detener ${interface} (puede que no estuviera activa)"
fi
# ── Habilitar y arrancar wg-quick@wg0 ────────────────────────────────────
_wg_hub_log "Habilitando systemd unit wg-quick@${interface}"
sudo systemctl enable "wg-quick@${interface}" >&2 \
|| { _wg_hub_log "ERROR: systemctl enable wg-quick@${interface} falló"; return 1; }
_wg_hub_log "Arrancando wg-quick@${interface}"
sudo systemctl start "wg-quick@${interface}" >&2 \
|| { _wg_hub_log "ERROR: systemctl start wg-quick@${interface} falló"; return 1; }
# ── Verificar que la interfaz está UP ────────────────────────────────────
local retries=5
local up=0
for (( i=0; i<retries; i++ )); do
if sudo wg show "${interface}" &>/dev/null 2>&1; then
up=1
break
fi
sleep 1
done
if [[ "${up}" -eq 0 ]]; then
_wg_hub_log "ERROR: 'wg show ${interface}' falló tras ${retries}s — la interfaz no arrancó"
return 1
fi
_wg_hub_log "Interfaz ${interface} UP (status=${action_status})"
printf '{"status":"%s","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
"${action_status}" "${config_path}" "${interface}" "${hub_ip}"
return 0
}
+51
View File
@@ -0,0 +1,51 @@
---
name: wg_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_install() -> json"
description: "Instala wireguard + wireguard-tools en Linux (debian/ubuntu/fedora/arch). Idempotente. Carga modulo kernel. Emite JSON con distro detectada y version instalada."
tags: [wireguard, install, infra, mesh, deploy]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON {status, distro, version}. status=installed o already-present."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_install.sh"
---
## Ejemplo
```bash
source bash/functions/infra/wg_install.sh
wg_install
# {"status":"installed","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
# Si ya está instalado:
wg_install
# {"status":"already-present","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
```
## Cuando usarla
Cuando necesites asegurarte de que wireguard-tools está disponible en un host antes de configurar un peer o hub WireGuard. Úsala como paso previo en pipelines de bootstrapping de nodos mesh (flow wireguard).
## Gotchas
- Requiere `sudo` con NOPASSWD para apt-get/dnf/pacman y para modprobe. El operador debe haberlo configurado antes.
- `modprobe wireguard` puede fallar en kernels < 5.6 sin DKMS instalado (wireguard-dkms). La función lo trata como advertencia, no como error fatal — la instalación de las herramientas igual se completa.
- En RHEL/CentOS instala `epel-release` automáticamente antes de wireguard-tools.
- Distros no reconocidas en `/etc/os-release ID` producen exit 1 con mensaje de error explícito en stderr.
- Los logs van siempre a stderr con prefijo `[wg_install]`; stdout es exclusivamente el JSON de resultado.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# wg_install — Instala wireguard + wireguard-tools en Linux (debian/ubuntu/fedora/arch).
# Idempotente: si wg ya está instalado emite JSON con status=already-present y sale.
# Carga módulo kernel wireguard. Emite JSON a stdout. Logs a stderr con prefijo [wg_install].
# Exit 0 = éxito, 1 = fallo.
wg_install() {
local distro="" version="" status=""
_wg_log() { echo "[wg_install] $*" >&2; }
# Detectar distro via /etc/os-release
if [[ -f /etc/os-release ]]; then
distro=$(. /etc/os-release && echo "${ID:-unknown}")
else
_wg_log "ERROR: /etc/os-release no encontrado; no se puede detectar distro"
return 1
fi
_wg_log "Distro detectada: ${distro}"
# Comprobar si wg ya está instalado (idempotencia)
if command -v wg &>/dev/null; then
version=$(wg --version 2>/dev/null | head -n1 || echo "unknown")
_wg_log "wireguard-tools ya presente (${version}); cargando módulo kernel"
# Intentar cargar módulo igualmente (no fatal)
sudo modprobe wireguard 2>/dev/null || true
printf '{"status":"already-present","distro":"%s","version":"%s"}\n' "${distro}" "${version}"
return 0
fi
# Instalar según distro
case "${distro}" in
debian|ubuntu|linuxmint|pop|kali|raspbian)
_wg_log "Usando apt-get (${distro})"
sudo apt-get update -y >&2 || { _wg_log "ERROR: apt-get update falló"; return 1; }
sudo apt-get install -y wireguard wireguard-tools >&2 \
|| { _wg_log "ERROR: apt-get install wireguard falló"; return 1; }
;;
fedora)
_wg_log "Usando dnf (fedora)"
sudo dnf install -y wireguard-tools >&2 \
|| { _wg_log "ERROR: dnf install wireguard-tools falló"; return 1; }
;;
rhel|centos|rocky|almalinux)
_wg_log "Usando dnf (rhel/centos/rocky/alma)"
sudo dnf install -y epel-release >&2 || true
sudo dnf install -y wireguard-tools >&2 \
|| { _wg_log "ERROR: dnf install wireguard-tools falló"; return 1; }
;;
arch|manjaro|endeavouros)
_wg_log "Usando pacman (arch)"
sudo pacman -S --noconfirm wireguard-tools >&2 \
|| { _wg_log "ERROR: pacman install wireguard-tools falló"; return 1; }
;;
*)
_wg_log "ERROR: distro '${distro}' no soportada (soportadas: debian/ubuntu/fedora/rhel/arch)"
return 1
;;
esac
# Verificar instalación
if ! command -v wg &>/dev/null; then
_wg_log "ERROR: 'wg' no encontrado tras la instalación"
return 1
fi
version=$(wg --version 2>/dev/null | head -n1 || echo "unknown")
_wg_log "wireguard-tools instalado: ${version}"
# Cargar módulo kernel (no fatal: kernels >=5.6 lo incluyen built-in)
if sudo modprobe wireguard 2>/dev/null; then
_wg_log "Módulo kernel wireguard cargado"
else
_wg_log "WARN: modprobe wireguard falló (puede estar built-in en el kernel o requerir DKMS)"
fi
status="installed"
printf '{"status":"%s","distro":"%s","version":"%s"}\n' "${status}" "${distro}" "${version}"
return 0
}
+79
View File
@@ -0,0 +1,79 @@
---
name: wg_status
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_status([interface_name]) -> json"
description: "Parsea `wg show <iface> dump` a JSON estructurado con peers, handshake age, status (online/stale/never), bytes rx/tx. Resuelve device_id desde comentarios en wg0.conf. Para dashboards (agents_dashboard Mesh panel)."
tags: [wireguard, status, observability, json, infra]
params:
- name: interface_name
desc: "Nombre de la interface WireGuard (default wg0)"
output: "JSON con interface info + array de peers. Cada peer incluye public_key, device_id (de comentario # DeviceID:<id> en wg0.conf), endpoint, allowed_ips, latest_handshake_unix, latest_handshake_ago_s, rx_bytes, tx_bytes, persistent_keepalive, status (online/stale/never)."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "interface con 2 peers online y stale"
- "interface sin peers devuelve array vacio"
- "interface inexistente devuelve error JSON"
- "WG_FAKE_DUMP carga dump de archivo"
test_file_path: "bash/functions/infra/wg_status_test.sh"
file_path: "bash/functions/infra/wg_status.sh"
---
## Ejemplo
```bash
# Estado real de wg0
source bash/functions/infra/wg_status.sh
wg_status | jq .
# Interface distinta
wg_status wg1 | jq .peers[].status
# Sin sudo real (testing / CI)
WG_FAKE_DUMP=bash/functions/infra/wg_status_test_dump.tsv wg_status wg0 | jq .
```
Salida representativa:
```json
{
"interface": "wg0",
"public_key": "abcXYZ123...",
"listen_port": "51820",
"peers": [
{
"public_key": "peerKey1...",
"device_id": "pc-aurgi",
"endpoint": "1.2.3.4:54321",
"allowed_ips": ["10.42.0.10/32"],
"latest_handshake_unix": 1716000000,
"latest_handshake_ago_s": 42,
"rx_bytes": 12345,
"tx_bytes": 67890,
"persistent_keepalive": 25,
"status": "online"
}
]
}
```
## Cuando usarla
Cuando necesites saber el estado del mesh WireGuard desde un script, dashboard o agente. Usa antes de mostrar el panel Mesh en `agents_dashboard`. Llama cada N segundos para polling ligero desde shell sin depender de la API de WireGuard.
## Gotchas
- Requiere `CAP_NET_ADMIN` / root: `wg show` falla sin permisos. En produccion ejecutar via `sudo -n wg show wg0 dump` o dar permiso al binario. Para tests sin sudo: `WG_FAKE_DUMP=<path>` carga el dump desde archivo.
- `listen_port` se devuelve como string (tal como lo emite `wg show dump`). El campo es `"0"` si wg no esta activo pero la interface existe.
- `device_id` queda `""` si no hay comentario `# DeviceID:<id>` antes del `[Peer]` correspondiente en `/etc/wireguard/<iface>.conf`.
- Status `stale` cubre desde 180s hasta cualquier valor mayor. No hay distincion entre "hace 5 min" y "hace 3 dias" — ambos son `stale`. Para un threshold mas fino, usar `latest_handshake_ago_s` directamente.
- Si `/etc/wireguard/<iface>.conf` no existe o no es legible, `device_id` sera `""` para todos los peers (la funcion no falla, solo omite el lookup).
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# wg_status — Parsea `wg show <iface> dump` a JSON estructurado con peers,
# handshake age, status (online/stale/never), bytes rx/tx.
# Resuelve device_id desde comentarios # DeviceID:<id> en wg0.conf.
#
# Usage:
# wg_status [interface_name] # default: wg0
#
# Env:
# WG_FAKE_DUMP=<path> # lee dump de archivo en vez de llamar wg show (para tests)
wg_status() {
local iface="${1:-wg0}"
local conf="${WG_FAKE_CONF:-/etc/wireguard/${iface}.conf}"
local now
now=$(date +%s)
# --- obtener dump (real o fake) ---
local dump
if [[ -n "${WG_FAKE_DUMP:-}" ]]; then
if [[ ! -f "$WG_FAKE_DUMP" ]]; then
printf '{"error":"WG_FAKE_DUMP file not found: %s"}\n' "$WG_FAKE_DUMP"
return 1
fi
dump=$(cat "$WG_FAKE_DUMP")
else
if ! command -v wg &>/dev/null; then
printf '{"error":"wg command not found"}\n'
return 1
fi
if ! dump=$(wg show "$iface" dump 2>&1); then
if echo "$dump" | grep -qi "no such device\|does not exist\|unable to access interface"; then
printf '{"error":"interface not found"}\n'
return 1
fi
printf '{"error":"%s"}\n' "$(echo "$dump" | head -n1 | sed 's/"/\\"/g')"
return 1
fi
fi
# --- primera linea: info de la propia interface ---
# formato: <private_key>\t<public_key>\t<listen_port>\t<fwmark>
local iface_line
iface_line=$(echo "$dump" | head -n1)
local iface_pubkey iface_port
iface_pubkey=$(echo "$iface_line" | awk -F'\t' '{print $2}')
iface_port=$(echo "$iface_line" | awk -F'\t' '{print $3}')
# --- leer DeviceID map desde wg0.conf ---
# Busca patron:
# # DeviceID:<id>
# [Peer]
# PublicKey = <pk>
# Producimos pares "pk\tdevice_id" en un archivo temporal para lookup via awk
local device_map
device_map=$(awk '
/^#[[:space:]]*DeviceID:/ {
split($0, a, "DeviceID:")
did = a[2]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", did)
pending_did = did
}
/^\[Peer\]/ {
in_peer = 1
}
in_peer && /^PublicKey[[:space:]]*=/ {
pk = $0
sub(/^PublicKey[[:space:]]*=[[:space:]]*/, "", pk)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk)
if (pending_did != "") {
print pk "\t" pending_did
pending_did = ""
}
in_peer = 0
}
' "$conf" 2>/dev/null)
# --- parsear peers (lineas 2..N del dump) ---
# formato peer: <public_key>\t<preshared_key>\t<endpoint>\t<allowed_ips>\t<latest_handshake>\t<rx_bytes>\t<tx_bytes>\t<persistent_keepalive>
local peers_json
peers_json=$(echo "$dump" | tail -n +2 | awk -v now="$now" -v dmap="$device_map" '
BEGIN {
# construir lookup device_id
n = split(dmap, lines, "\n")
for (i = 1; i <= n; i++) {
if (lines[i] != "") {
split(lines[i], parts, "\t")
pk_to_did[parts[1]] = parts[2]
}
}
first = 1
printf "["
}
NF >= 7 {
pk = $1
endpoint = $3
allowed = $4
hs = $5 + 0
rx = $6 + 0
tx = $7 + 0
ka = $8
# device_id lookup
did = (pk in pk_to_did) ? pk_to_did[pk] : ""
# handshake age y status
if (hs == 0) {
ago = 0
status = "never"
} else {
ago = now - hs
if (ago < 180) status = "online"
else if (ago < 86400) status = "stale"
else status = "stale"
}
# persistent_keepalive
ka_val = (ka == "off" || ka == "") ? 0 : ka + 0
# endpoint null si "(none)"
ep_val = (endpoint == "(none)") ? "null" : "\"" endpoint "\""
# allowed_ips array
n_ips = split(allowed, ips_arr, ",")
ips_json = "["
for (j = 1; j <= n_ips; j++) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", ips_arr[j])
ips_json = ips_json "\"" ips_arr[j] "\""
if (j < n_ips) ips_json = ips_json ","
}
ips_json = ips_json "]"
if (!first) printf ","
first = 0
printf "{"
printf "\"public_key\":\"%s\"", pk
printf ",\"device_id\":\"%s\"", did
printf ",\"endpoint\":%s", ep_val
printf ",\"allowed_ips\":%s", ips_json
printf ",\"latest_handshake_unix\":%d", hs
printf ",\"latest_handshake_ago_s\":%d",ago
printf ",\"rx_bytes\":%d", rx
printf ",\"tx_bytes\":%d", tx
printf ",\"persistent_keepalive\":%d", ka_val
printf ",\"status\":\"%s\"", status
printf "}"
}
END { printf "]" }
' FS='\t')
# --- output final ---
printf '{"interface":"%s","public_key":"%s","listen_port":%s,"peers":%s}\n' \
"$iface" "$iface_pubkey" "$iface_port" "$peers_json"
}
# Permitir invocacion directa: bash wg_status.sh [iface]
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
wg_status "$@"
fi
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Tests para wg_status
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/wg_status.sh"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
assert_not_contains() {
local test_name="$1" needle="$2" haystack="$3"
if ! echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected NOT to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
# --- fixtures ---
FAKE_DUMP=$(mktemp)
FAKE_DUMP_EMPTY=$(mktemp)
FAKE_CONF=$(mktemp)
trap 'rm -f "$FAKE_DUMP" "$FAKE_DUMP_EMPTY" "$FAKE_CONF"' EXIT
NOW=$(date +%s)
HS_ONLINE=$(( NOW - 60 )) # 60s ago → online
HS_STALE=$(( NOW - 500 )) # 500s ago → stale
# dump con 2 peers (tabs como separador)
printf '%s\n' \
"privKeyBase64== ifacePubKey== 51820 off" \
"peerKey1== (none) 1.2.3.4:54321 10.42.0.10/32 ${HS_ONLINE} 12345 67890 25" \
"peerKey2== (none) 5.6.7.8:12345 10.42.0.20/32 ${HS_STALE} 111 222 0" \
> "$FAKE_DUMP"
# dump vacío (solo línea de interface, sin peers)
printf '%s\n' "privKeyBase64== ifacePubKey== 51820 off" > "$FAKE_DUMP_EMPTY"
# conf con DeviceID comments
cat > "$FAKE_CONF" <<'CONF'
[Interface]
PrivateKey = privKeyBase64==
Address = 10.42.0.1/24
ListenPort = 51820
# DeviceID:pc-aurgi
[Peer]
PublicKey = peerKey1==
AllowedIPs = 10.42.0.10/32
# DeviceID:home-wsl
[Peer]
PublicKey = peerKey2==
AllowedIPs = 10.42.0.20/32
CONF
# --- Test: interface con 2 peers online y stale ---
result=$(WG_FAKE_DUMP="$FAKE_DUMP" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "interface con 2 peers online y stale" '"interface":"wg0"' "$result"
assert_contains "interface con 2 peers online y stale" '"listen_port":51820' "$result"
assert_contains "interface con 2 peers online y stale" '"public_key":"ifacePubKey=="' "$result"
assert_contains "interface con 2 peers online y stale" '"status":"online"' "$result"
assert_contains "interface con 2 peers online y stale" '"status":"stale"' "$result"
assert_contains "interface con 2 peers online y stale" '"device_id":"pc-aurgi"' "$result"
assert_contains "interface con 2 peers online y stale" '"device_id":"home-wsl"' "$result"
assert_contains "interface con 2 peers online y stale" '"rx_bytes":12345' "$result"
assert_contains "interface con 2 peers online y stale" '"persistent_keepalive":25' "$result"
# --- Test: interface sin peers devuelve array vacio ---
result_empty=$(WG_FAKE_DUMP="$FAKE_DUMP_EMPTY" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "interface sin peers devuelve array vacio" '"peers":[]' "$result_empty"
assert_not_contains "interface sin peers devuelve array vacio" '"error"' "$result_empty"
# --- Test: interface inexistente devuelve error JSON ---
result_err=$(wg_status nonexistent_iface_xyz 2>/dev/null || true)
assert_contains "interface inexistente devuelve error JSON" '"error"' "$result_err"
# --- Test: WG_FAKE_DUMP carga dump de archivo ---
result_fake=$(WG_FAKE_DUMP="$FAKE_DUMP" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "WG_FAKE_DUMP carga dump de archivo" '"public_key":"ifacePubKey=="' "$result_fake"
assert_contains "WG_FAKE_DUMP carga dump de archivo" '"peers":[{' "$result_fake"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -0,0 +1,65 @@
---
name: compile_wails_app
kind: pipeline
lang: bash
domain: pipelines
version: "0.1.0"
purity: impure
signature: "compile_wails_app(app_name_or_empty: string) -> void"
description: "Pipeline que resuelve la app Wails desde el nombre o CWD, la compila para Windows con wails build -platform windows/amd64 (detectando -tags goolm automaticamente si la app usa E2EE Matrix), y despliega el .exe al escritorio de Windows + relanza el proceso. Equivalente a compile_cpp_app pero para apps Wails (Go + WebView2)."
tags: [wails, windows, compile, pipelines, launch, matrix-mas]
uses_functions:
- resolve_cpp_app_dir_bash_infra
- deploy_wails_exe_to_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/compile_wails_app.sh"
params:
- name: app_name_or_empty
desc: "Nombre de la app Wails a compilar (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de projects/*/apps/<X>/ o apps/<X>/. Lista apps disponibles si no puede deducirlo."
output: "Compila el .exe con wails build, lo despliega al escritorio de Windows y relanza el proceso. Imprime progreso por steps a stderr y resumen final con ls -lh del .exe resultante."
---
## Ejemplo
```bash
# Desde el directorio de la app (deduce nombre automaticamente)
cd projects/element_agents/apps/matrix_client_pc
./fn run compile_wails_app
# Desde la raiz del registry, con nombre explicito
cd /home/lucas/fn_registry
./fn run compile_wails_app matrix_admin_panel
# Directo sin fn run
bash bash/functions/pipelines/compile_wails_app.sh matrix_client_pc
```
## Cuando usarla
Usar cuando quieras rebuild + redeploy + relanzar una app Wails con un solo comando durante iteracion activa de desarrollo. Equivale al slash command `/compile` aplicado a targets Wails. El pipeline detecta automaticamente si la app necesita `-tags goolm` (apps Matrix con E2EE).
## Gotchas
- Requiere `wails` CLI instalado en PATH y mingw-w64 configurado para cross-compile (`GOARCH=amd64 GOOS=windows` via toolchain Wails).
- Si la app usa `-tags goolm` (E2EE Matrix), esta pipeline lo detecta automaticamente: busca `matrix_crypto_init` en `app.md` o `"build:tags": "goolm"` en `wails.json`. Si la deteccion falla, pasar la variable `TAGS` o editar el `wails.json`.
- El relanzar despues del deploy es la diferencia clave con `compile_cpp_app`: las apps Wails son single-binary (no DLLs adicionales) y arrancan en <1s, lo que hace iteracion muy rapida.
- Si el build falla con `no required module provides package`, ejecutar `go mod tidy` en el directorio de la app antes de volver a compilar.
- `matrix_client_pc` tiene helpers en `internal/infra/` que son copias vendored de `functions/infra/` del registry padre. Si actualizas un helper en el registry padre, debes copiarlo manualmente a la app antes de compilar — el build de Wails no ve el modulo padre.
- El deploy mata el proceso anterior con `taskkill.exe /F` (pre-autorizado) antes de copiar el .exe, para evitar "Permission denied" de Windows al sobreescribir un binario en uso.
- Variable de entorno `WIN_DESKTOP_APPS` controla el destino; default `/mnt/c/Users/lucas/Desktop/apps`.
## Flujo
1. `resolve_cpp_app_dir` — deduce nombre y directorio absoluto de la app (desde CWD o arg)
2. Verifica `wails.json` y `go.mod` en el directorio de la app
3. Detecta si necesita `-tags goolm` (app.md referencia `matrix_crypto_init` o wails.json lo declara)
4. `wails build -platform windows/amd64 [tags]` desde el directorio de la app
5. `deploy_wails_exe_to_windows` — mata proceso, copia .exe, relanza y verifica PID
6. Imprime `ls -lh` del exe final en `Desktop/apps/<APP>/`
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Pipeline: compile_wails_app — Resuelve la app Wails desde el nombre o CWD,
# la compila para Windows con wails build y despliega al escritorio + relanza.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$SCRIPT_DIR/../infra"
source "$INFRA_DIR/resolve_cpp_app_dir.sh"
source "$INFRA_DIR/deploy_wails_exe_to_windows.sh"
compile_wails_app() {
local app_arg="${1:-}"
# --- Paso 1: Resolver nombre y directorio de la app ---
echo "[1/3] Resolviendo app..." >&2
local resolved
resolved=$(resolve_cpp_app_dir "$app_arg")
local APP APP_DIR
APP="$(echo "$resolved" | cut -f1)"
APP_DIR="$(echo "$resolved" | cut -f2)"
echo " App: $APP" >&2
echo " Dir: $APP_DIR" >&2
# --- Verificar que es una app Wails (no C++) ---
if [ ! -f "$APP_DIR/wails.json" ]; then
echo "ERROR: $APP_DIR/wails.json no encontrado." >&2
echo "La app '$APP' no es una app Wails." >&2
echo "Si es C++, usa compile_cpp_app en su lugar." >&2
return 1
fi
if [ ! -f "$APP_DIR/go.mod" ]; then
echo "ERROR: $APP_DIR/go.mod no encontrado." >&2
echo "Una app Wails requiere go.mod. Ejecuta 'go mod init' en $APP_DIR." >&2
return 1
fi
# --- Paso 2: Compilar para Windows con wails ---
echo "" >&2
echo "[2/3] Compilando '$APP' para Windows (wails + mingw)..." >&2
# Detectar si necesita -tags goolm:
# 1. app.md declara matrix_crypto_init en uses_functions (E2EE habilitado)
# 2. wails.json tiene "build:tags": "goolm" (o "buildTags": "goolm")
local TAGS=""
local app_md="${APP_DIR}/app.md"
local wails_json="${APP_DIR}/wails.json"
local needs_goolm=0
if [ -f "$app_md" ] && grep -q "matrix_crypto_init" "$app_md" 2>/dev/null; then
needs_goolm=1
echo " Detectado matrix_crypto_init en app.md -> usando -tags goolm" >&2
fi
if [ "$needs_goolm" -eq 0 ] && [ -f "$wails_json" ]; then
if grep -qE '"(build:tags|buildTags)"\s*:\s*"goolm"' "$wails_json" 2>/dev/null; then
needs_goolm=1
echo " Detectado goolm en wails.json -> usando -tags goolm" >&2
fi
fi
if [ "$needs_goolm" -eq 1 ]; then
TAGS="-tags goolm"
fi
(
cd "$APP_DIR"
# shellcheck disable=SC2086
wails build -platform windows/amd64 $TAGS
)
# --- Paso 3: Desplegar al escritorio + relanzar ---
echo "" >&2
echo "[3/3] Desplegando '$APP' al escritorio + relanzar..." >&2
deploy_wails_exe_to_windows "$APP" "$APP_DIR"
# --- Resumen final ---
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
local final_exe="$win_desktop_apps/$APP/$APP.exe"
echo "" >&2
if [ -f "$final_exe" ]; then
echo "===== compile_wails_app: OK =====" >&2
ls -lh "$final_exe" >&2
else
echo "WARN: no se encuentra $final_exe" >&2
fi
}
compile_wails_app "${1:-}"
@@ -0,0 +1,56 @@
---
name: fn_sync_with_pass
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "fn_sync_with_pass [status|locations|<args>...]"
description: "Wrapper de fn sync que lee credenciales del password-store pass y exporta FN_REGISTRY_API y REGISTRY_API_TOKEN antes de invocar el CLI. Evita persistir secretos en ~/.zshrc."
tags: [sync, registry, pass, gpg, launcher]
uses_functions:
- pass_get_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "registry/basicauth-user"
desc: "Entry de pass con el usuario para basicAuth del registry API (linea 1)"
- name: "registry/basicauth-pass"
desc: "Entry de pass con la contraseña para basicAuth del registry API (linea 1)"
- name: "registry/api-token"
desc: "Entry de pass con el REGISTRY_API_TOKEN (linea 1)"
- name: "args"
desc: "Argumentos opcionales forwarded a fn sync: status, locations, o nada para push+pull completo"
output: "Mismo output que ./fn sync (stdin/stdout/stderr heredados). Exit code del subproceso fn sync."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/fn_sync_with_pass.sh"
---
## Ejemplo
```bash
# Sync simple (push+pull completo)
./fn run fn_sync_with_pass_bash_pipelines
# Ver estado local: PC, API, conteos
./fn run fn_sync_with_pass_bash_pipelines status
# Mapa de ubicaciones cross-PC
./fn run fn_sync_with_pass_bash_pipelines locations
```
## Cuando usarla
Cuando necesites ejecutar `fn sync` sin tener las credenciales exportadas en el entorno. Sustituye al bloque de `export FN_REGISTRY_API=...` que de otro modo habria que poner en `~/.zshrc`.
## Gotchas
- Si GPG no tiene la clave desbloqueada, `pass show` abre el prompt del agente gpg. Dejarlo pasar — no capturar stderr para no interferir con el pinentry.
- Requiere que el password-store este inicializado (`pass init`). Si no existe, `pass show` falla con error claro.
- `FN_REGISTRY_ROOT` debe apuntar a la raiz del registry donde vive el binario `./fn`. Si no esta seteado, se resuelve via `git rev-parse --show-toplevel`.
- Los tres entries de pass deben tener el valor en la **linea 1** (convencion estandar de pass). Metadata adicional en lineas siguientes es ignorada.
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# fn_sync_with_pass — Wrapper de fn sync que lee credenciales desde pass.
set -euo pipefail
FN_ROOT="${FN_REGISTRY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
fn_sync_with_pass() {
command -v pass >/dev/null 2>&1 || {
echo "fn_sync_with_pass: 'pass' CLI no instalado. Instala con: apt install pass" >&2
return 127
}
local u p t
u=$(pass show registry/basicauth-user 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/basicauth-user en pass. Crea con: pass insert registry/basicauth-user" >&2
return 1
}
p=$(pass show registry/basicauth-pass 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/basicauth-pass en pass. Crea con: pass insert registry/basicauth-pass" >&2
return 1
}
t=$(pass show registry/api-token 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/api-token en pass. Crea con: pass insert registry/api-token" >&2
return 1
}
export FN_REGISTRY_API="https://${u}:${p}@registry.organic-machine.com"
export REGISTRY_API_TOKEN="$t"
cd "$FN_ROOT"
./fn sync "$@"
}
# Ejecucion directa (no library mode)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
fn_sync_with_pass "$@"
fi
+29 -7
View File
@@ -3,11 +3,11 @@ name: init_cpp_app
kind: pipeline
lang: bash
domain: pipelines
version: "0.1.0"
version: "1.1.0"
purity: impure
signature: "init_cpp_app(name: string, [--project <p>] [--domain <d>] [--desc <s>] [--tags <csv>]) -> void"
description: "Scaffolder estandar de apps C++ del registry. Genera main.cpp + CMakeLists.txt + app.md siguiendo el patron canonico (cfg.about/log/panels, sin app_menubar manual, dockspace via framework), registra la app en cpp/CMakeLists.txt, inicializa repo Gitea dataforge/<name> y ejecuta fn index."
tags: [cpp, imgui, scaffold, pipeline, bash, launcher]
tags: [cpp, imgui, scaffold, pipeline, bash, launcher, cpp-tables]
uses_functions:
- ensure_repo_synced_bash_infra
uses_types: []
@@ -47,9 +47,9 @@ fn run init_cpp_app finance_panel --project budget --desc "Panel de finanzas" --
```
<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)
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render) + data_table comentado
CMakeLists.txt # add_imgui_app(<name> main.cpp) + target_link_libraries fn_table_viz (con guard)
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url) + uses_functions comentados
```
Y ademas:
@@ -66,9 +66,31 @@ La plantilla cumple `cpp/PATTERNS.md`:
- NO llama `DockSpaceOverViewport` (auto_dockspace=true por defecto).
- Declara `panels[]` con un panel "Main" toggleable.
- Setea `cfg.about` (window About) y `cfg.log` (logger + ventana Logs).
- Include `viz/data_table.h` comentado + panel "Data" comentado en `render()` — descomentar para activar `data_table::render()`.
## Activar data_table::render()
1. En `main.cpp`: descomentar `#include "viz/data_table.h"` y el bloque del panel Data en `render()`.
2. En `app.md`: descomentar los 12 IDs del stack `fn_table_viz` en `uses_functions`.
3. El `CMakeLists.txt` ya linka `fn_table_viz` via guard `if(TARGET fn_table_viz)` — sin cambio manual.
4. Poblar `data_tables` con tus `data_table::TableInput` y el panel aparece en el DockSpace.
## Cuando usarla
Cuando necesites crear una app C++ nueva que siga el patron canonico del registry. Es el unico camino autorizado para crear apps en `cpp/apps/` o `projects/*/apps/` — nunca escribir `main.cpp` + `CMakeLists.txt` a mano.
## Gotchas
- Si `GITEA_URL`/`GITEA_TOKEN` no estan seteados, solo hace `git init` local (no crea repo remoto).
- `fn_table_viz` requiere que `vendor/lua` este presente en `cpp/`; el guard `if(TARGET fn_table_viz)` evita errores de link si no esta disponible.
- El bloque de `uses_functions` en `app.md` queda comentado intencionalmente — descomenta solo las funciones que la app realmente use para mantener el grafo de dependencias limpio.
## Despues de crear
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
2. Anadir las funciones del registry al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
1. Si usas `data_table::render()`: descomentar include + panel en `main.cpp`, descomentar IDs en `app.md`, ejecutar `fn index`.
2. Para otras funciones del registry: anadir paths absolutos en `CMakeLists.txt` y los IDs en `uses_functions` de `app.md`.
3. Build: `cd cpp && cmake --build build --target <name> -j`.
## Capability growth log
v1.1.0 (2026-05-15) — Auto-wires fn_table_viz; new apps get target_link_libraries + commented data_table template.
+36 -7
View File
@@ -8,7 +8,7 @@
# Uso:
# init_cpp_app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
#
# Por defecto domain=tools, sin proyecto (cpp/apps/<name>/).
# Por defecto domain=tools, sin proyecto (apps/<name>/, issue 0096).
set -euo pipefail
@@ -55,7 +55,7 @@ init_cpp_app() {
fi
rel_dir="projects/$project/apps/$name"
else
rel_dir="cpp/apps/$name"
rel_dir="apps/$name"
fi
abs_dir="$FN_ROOT/$rel_dir"
@@ -69,9 +69,11 @@ init_cpp_app() {
# ---------- main.cpp ----------
cat > "$abs_dir/main.cpp" <<EOF
#include <imgui.h>
#include "framework/app_base.h"
#include "app_base.h"
#include "core/panel_menu.h"
#include "core/icons_tabler.h"
#include "core/logger.h"
// #include "viz/data_table.h" // uncomment to enable data_table::render() panel
// Toggles de paneles (visibles desde el menu View del menubar canonico)
static bool g_show_main = true;
@@ -90,6 +92,11 @@ static void render() {
// DockSpaceOverViewport central (auto_dockspace=true por defecto).
// Aqui solo se dibujan los paneles propios de la app.
if (g_show_main) draw_main();
// === Data panel (uncomment to enable) ===
// static data_table::State data_state;
// static std::vector<data_table::TableInput> data_tables; // populate from your source
// data_table::render("main_data", data_tables, data_state);
}
int main(int /*argc*/, char** /*argv*/) {
@@ -115,6 +122,12 @@ add_imgui_app($name
)
target_include_directories($name PRIVATE \${CMAKE_CURRENT_SOURCE_DIR})
# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
# Guard keeps the app compilable in builds where vendor/lua is absent.
if(TARGET fn_table_viz)
target_link_libraries($name PRIVATE fn_table_viz)
endif()
if(WIN32)
set_target_properties($name PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
@@ -135,7 +148,20 @@ lang: cpp
domain: $domain
description: "$desc"
tags: $tags_yaml
uses_functions: []
uses_functions:
# Uncomment when using data_table::render() — provided via fn_table_viz:
# - data_table_cpp_viz
# - viz_render_cpp_viz
# - compute_stage_cpp_core
# - compute_pipeline_cpp_core
# - compute_column_stats_cpp_core
# - auto_detect_type_cpp_core
# - tql_emit_cpp_core
# - tql_apply_cpp_core
# - lua_engine_cpp_core
# - join_tables_cpp_core
# - tql_to_sql_cpp_core
# - llm_anthropic_cpp_core
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
@@ -175,11 +201,14 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
endif()
EOF
else
local upper
upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')"
cat >> "$cpp_cmake" <<EOF
# --- $name ---
if(EXISTS \${CMAKE_CURRENT_SOURCE_DIR}/apps/$name/CMakeLists.txt)
add_subdirectory(apps/$name)
# --- $name (lives in apps/, issue 0096) ---
set(_${upper}_DIR \${CMAKE_SOURCE_DIR}/../apps/$name)
if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
add_subdirectory(\${_${upper}_DIR} \${CMAKE_BINARY_DIR}/apps/$name)
endif()
EOF
fi
@@ -0,0 +1,92 @@
---
name: redeploy_all_cpp_apps
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "redeploy_all_cpp_apps(filter?: string) -> void"
description: "Cross-compila TODOS los apps C++ del registry en un solo cmake pass y despliega cada .exe al Desktop de Windows. Mas rapido que N builds individuales. Acepta filtro de nombre para despliegue parcial."
tags: [cpp, windows, deploy, redeploy, bulk, cpp-windows]
uses_functions:
- build_cpp_windows_bash_infra
- deploy_cpp_exe_to_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/redeploy_all_cpp_apps.sh"
params:
- name: filter
desc: "Opcional. Substring para limitar el deploy a apps cuyo nombre lo contenga (ej: 'graph' solo despliega apps con 'graph' en el nombre). Sin valor = todas las apps."
output: "Imprime tabla resumen con OK/SKIPPED/FAILED y nombres de cada app. Exit 1 si al menos una app fallo el deploy."
---
## Ejemplo
```bash
# Recompilar y redesplegar TODAS las apps C++ tras un cambio en cpp/framework/
./fn run redeploy_all_cpp_apps
# Solo apps cuyo nombre contenga "graph"
./fn run redeploy_all_cpp_apps graph
```
## Cuando usarla
Tras un cambio en `cpp/framework/app_base.cpp`, `cpp/functions/core/*` o cualquier
funcion linkada a multiples apps. Ahorra correr `redeploy_cpp_app_windows <name> <dir>`
N veces — un solo cmake pass compila todo el arbol en paralelo.
## Comportamiento
1. **Build**: invoca `build_cpp_windows` sin argumento (compila todo el arbol con
`-j$(nproc)`). Un solo cmake pass — mucho mas rapido que N builds individuales.
2. **Descubrimiento**: itera `apps/*/CMakeLists.txt` y `projects/*/apps/*/CMakeLists.txt`.
**No** usa `cpp/apps/` (deprecado tras issue 0096).
3. **Filtro** (opcional): si se paso un argumento, solo procesa apps cuyo `basename`
contiene el substring.
4. **Por cada app**:
- Localiza `.exe` en `cpp/build/windows/apps/<name>/<name>.exe`; si no existe,
busca bajo `cpp/build/windows/` como fallback.
- Si no hay `.exe`: log SKIP, continua (no aborta — apps headless o sub-repos no
clonados no tienen build target).
- `taskkill.exe /IM <name>.exe /F` silencioso (no aborta si falla).
- `deploy_cpp_exe_to_windows <name> <app_dir>` (copia exe + DLLs + assets +
enrichers + runtime, preserva `local_files/`).
- Error por app: log FAILED, continua con la siguiente.
5. **Resumen final**: tabla `OK / SKIPPED / FAILED` con nombres. Exit 1 si hay
al menos un FAILED.
## Variables de entorno
| Variable | Default | Descripcion |
|---|---|---|
| `FN_REGISTRY_ROOT` | auto-detect | Raiz del registry (busca hacia arriba desde el script) |
| `BUILD_WIN` | `$root/cpp/build/windows` | Directorio de build Windows |
| `WIN_DESKTOP_APPS` | `/mnt/c/Users/lucas/Desktop/apps` | Destino de deploy en Windows |
## Gotchas
- Solo Windows (cross-compile mingw-w64 + Desktop deploy via WSL2). En Linux puro no aplica.
- `taskkill.exe` requiere WSL2 con interop habilitado. No funciona en WSL1 ni Linux nativo.
- Algunas apps pueden no estar en el grafo cmake actual (sub-repo no clonado, `add_subdirectory`
protegido por `if(EXISTS ...)`). El pipeline las SKIPea sin abortar — comportamiento esperado.
- Build paralelo puede consumir varios GB de RAM. Si hay OOM, reducir paralelismo exportando
`BUILD_JOBS=4` antes de invocar (actualmente la funcion `build_cpp_windows` usa `$(nproc)`;
si necesitas override edita `BUILD_JOBS` como variable de entorno custom o fork la funcion).
- El loop de deploy atrapa errores por app (`|| { failed+=...; continue; }`) para no abortar
en el primer fallo — todas las apps se intentan aunque alguna falle.
## Capability growth log
- v1.0.0 (2026-05-16) — creacion. Tras issue 0096 (apps movidas a `apps/<X>/`) el patron "recompilar+desplegar todas tras un cambio en `cpp/framework/`" se repitio varias veces sin un wrapper. Pipeline tolerante a fallos: build best-effort (test_* roto en mingw no aborta), deploy por app captura fallos individuales, summary OK/SKIPPED/FAILED al final. Primera corrida real (16 May 2026): 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED.
## Notas operativas (2026-05-16)
- `build_cpp_windows` sin arg compila el arbol entero. Si hay targets rotos (ej. `test_llm_anthropic`, `test_graph_icons` usan `setenv()` no disponible en mingw-w64), el pipeline logea `[1/2] Build returned exit=N — continuing with deploy of available exes` y sigue con la fase de deploy. Cada app sin `.exe` queda SKIPPED.
- Tras una corrida exitosa, los `.exe` quedan en `/mnt/c/Users/lucas/Desktop/apps/<name>/<name>.exe`. Lanzar individualmente con `./fn run is_cpp_app_running_windows <name>` para chequear y `launch_cpp_app_windows <name>` para arrancar.
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# redeploy_all_cpp_apps — Cross-compila TODOS los apps C++ del registry en un solo
# cmake pass y despliega cada .exe al Desktop de Windows.
# Uso: redeploy_all_cpp_apps [filter]
# filter substring opcional para limitar el deploy a apps cuyo nombre lo contenga
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
redeploy_all_cpp_apps() {
local filter="${1:-}"
# --- Localizar raiz del registry ---
local root="${FN_REGISTRY_ROOT:-}"
if [ -z "$root" ]; then
local d="$SCRIPT_DIR"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -d "$d/cpp" ]; then
root="$d"; break
fi
d="$(dirname "$d")"
done
fi
if [ -z "$root" ]; then
echo "[redeploy_all_cpp_apps] ERROR: no se localiza la raiz del registry. Exporta FN_REGISTRY_ROOT." >&2
return 2
fi
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
# --- Paso 1: compilar TODO el arbol (un solo cmake pass) ---
# Tolerante a fallos: si algun target (ej. test_* roto en mingw, app con
# bug puntual) falla, los demas exes que SI se construyeron siguen siendo
# desplegables. El loop de deploy hace SKIP por cada app sin .exe, asi que
# el modo "build best-effort + deploy lo que haya" es seguro.
echo "[1/2] Cross-compiling all C++ targets (best-effort)..."
local build_rc=0
build_cpp_windows || build_rc=$?
if [ "$build_rc" -ne 0 ]; then
echo "[1/2] Build returned exit=$build_rc — continuing with deploy of available exes" >&2
else
echo "[1/2] Build OK"
fi
# --- Descubrir apps con CMakeLists.txt ---
# Busca en apps/*/ y projects/*/apps/*/ (no en cpp/apps/ — deprecado)
local -a app_dirs=()
while IFS= read -r cmakelists; do
app_dirs+=("$(dirname "$cmakelists")")
done < <(
find "$root/apps" -maxdepth 2 -name "CMakeLists.txt" 2>/dev/null | sort
find "$root/projects" -maxdepth 4 -path "*/apps/*/CMakeLists.txt" 2>/dev/null | sort
)
if [ ${#app_dirs[@]} -eq 0 ]; then
echo "[redeploy_all_cpp_apps] WARN: no se encontraron apps con CMakeLists.txt" >&2
return 0
fi
# --- Paso 2: deploy por app ---
echo "[2/2] Deploying apps to Windows Desktop..."
local -a ok=() skipped=() failed=()
for app_dir in "${app_dirs[@]}"; do
local name
name="$(basename "$app_dir")"
# Aplicar filtro si se indico
if [ -n "$filter" ] && [[ "$name" != *"$filter"* ]]; then
continue
fi
# Localizar el .exe en la ubicacion canonica
local exe_path="$build_win/apps/$name/$name.exe"
if [ ! -f "$exe_path" ]; then
# Fallback: buscar bajo build_win/
exe_path="$(find "$build_win" -name "$name.exe" -type f 2>/dev/null | head -n1 || true)"
fi
if [ -z "$exe_path" ] || [ ! -f "$exe_path" ]; then
echo " SKIP: $name — .exe no encontrado en $build_win" >&2
skipped+=("$name")
continue
fi
# taskkill silencioso (pre-autorizado; deploy_cpp_exe_to_windows lo hace internamente,
# pero si deploy falla antes de llegar ahi nos aseguramos de liberar el lock)
if command -v taskkill.exe >/dev/null 2>&1; then
taskkill.exe /IM "${name}.exe" /F >/dev/null 2>&1 || true
fi
if deploy_cpp_exe_to_windows "$name" "$app_dir"; then
ok+=("$name")
else
echo " FAILED: $name" >&2
failed+=("$name")
fi
done
# --- Resumen ---
echo ""
echo "===== redeploy_all_cpp_apps — summary ====="
printf " OK : %d\n" "${#ok[@]}"
printf " SKIPPED : %d\n" "${#skipped[@]}"
printf " FAILED : %d\n" "${#failed[@]}"
if [ ${#ok[@]} -gt 0 ]; then
echo " Deployed:"
for n in "${ok[@]}"; do printf " + %s\n" "$n"; done
fi
if [ ${#skipped[@]} -gt 0 ]; then
echo " Skipped (no .exe):"
for n in "${skipped[@]}"; do printf " - %s\n" "$n"; done
fi
if [ ${#failed[@]} -gt 0 ]; then
echo " Failed:"
for n in "${failed[@]}"; do printf " x %s\n" "$n"; done
return 1
fi
}
# Ejecutar si se llama directamente (fn run lo invoca como script)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
redeploy_all_cpp_apps "$@"
fi
@@ -3,16 +3,17 @@ name: redeploy_cpp_app_windows
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "redeploy_cpp_app_windows(app_name: string, app_dir: string, [--build]) -> void"
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify."
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify e incluye refresh del icon cache del shell."
tags: [cpp, windows, redeploy, pipeline, wsl, launcher, cpp-windows]
uses_functions:
- build_cpp_windows_bash_infra
- deploy_cpp_exe_to_windows_bash_infra
- launch_cpp_app_windows_bash_infra
- is_cpp_app_running_windows_bash_infra
- refresh_windows_icon_cache_bash_infra
uses_types: []
returns: []
returns_optional: false
@@ -47,6 +48,7 @@ redeploy_cpp_app_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_de
1. **Parsear flag `--build`** (default off, opt-in).
2. **Si `--build`**: invocar `build_cpp_windows <app_name>` para compilar `cpp/build/windows/apps/<app_name>/<app_name>.exe`. Si falla, exit 1 sin tocar el Desktop.
3. **Deploy**: invocar `deploy_cpp_exe_to_windows "<app_name>" "<app_dir>"`. Esta función mata el proceso si está vivo (taskkill.exe pre-autorizado), copia exe + DLLs + assets + runtime + enrichers, y preserva `local_files/`.
3b. **Refresh icon cache** (v1.1.0+): invocar `refresh_windows_icon_cache` (best-effort). Llama `ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar al timestamp. Si falla, no aborta el pipeline.
4. **Launch**: invocar `launch_cpp_app_windows "<app_name>"` para arrancar la app en Windows.
5. **Wait**: `sleep 1` — espera arranque corto.
6. **Verify**: invocar `is_cpp_app_running_windows "<app_name>"`. Si NO está vivo → exit 1 con mensaje claro.
@@ -7,6 +7,7 @@ source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
source "$SCRIPT_DIR/../infra/refresh_windows_icon_cache.sh"
redeploy_cpp_app_windows() {
local app_name=""
@@ -63,6 +64,12 @@ redeploy_cpp_app_windows() {
fi
echo "[2/4] Deploy OK"
# Refrescar cache de iconos del shell. Sin esto el .exe nuevo puede salir
# con el icono generico (Windows cachea por timestamp/path en iconcache.db
# y a veces no detecta el cambio inmediatamente). Best-effort: si falla
# no abortamos el redeploy.
refresh_windows_icon_cache || true
# Paso 3: lanzar la app
echo "[3/4] Launching $app_name..."
if ! launch_cpp_app_windows "$app_name"; then
@@ -0,0 +1,83 @@
---
name: refresh_app_hub
kind: pipeline
lang: bash
domain: pipelines
version: "1.1.0"
purity: impure
signature: "refresh_app_hub([--hub-dir <path>] [--size <px>] [--no-restart] [--style <s>]) -> void"
description: "Pipeline orquestador que regenera los iconos PNG y el manifest TSV del App Hub desde registry.db y reinicia el proceso app_hub_launcher en Windows. Cubre el ciclo completo: export icons → export manifest → taskkill → relaunch. Default style=white_duotone (duotone Phosphor blanco sobre bg accent). Override con --style fill_white | adaptive_duotone."
tags: [hub, launcher, icons, manifest, cpp, windows, wsl, cpp-windows, deploy]
uses_functions:
- export_hub_icons_py_infra
- export_hub_manifest_py_infra
- is_cpp_app_running_windows_bash_infra
- launch_cpp_app_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/refresh_app_hub.sh"
params:
- name: "--hub-dir"
desc: "Directorio local_files del hub en Windows (accesible desde WSL). Default: /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files. Los PNGs se escriben en <hub-dir>/icons/ y el manifest en <hub-dir>/hub_manifest.tsv."
- name: "--size"
desc: "Lado en pixels de cada PNG de icono. Default 64. Pasar 128 para pantallas HiDPI."
- name: "--no-restart"
desc: "Si está presente, solo regenera icons + manifest sin tocar el proceso del hub (pasos 3 y 4 se marcan como skipped). Útil para actualizar los archivos mientras el hub está cerrado manualmente."
output: "Imprime resumen de 4 pasos (icons exported, manifest rows, hub killed/skipped, hub launched) y finaliza con 'OK: app_hub refreshed'. Exit 1 si falla cualquier paso, con mensaje indicando cuál."
---
## Ejemplo
```bash
# Caso habitual: regenerar todo y reiniciar el hub
./fn run refresh_app_hub
# Solo regenerar iconos y manifest, sin tocar el proceso
./fn run refresh_app_hub --no-restart
# Iconos a 128px para pantalla HiDPI o hub-dir personalizado
./fn run refresh_app_hub --size 128
./fn run refresh_app_hub --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
# Combinado
./fn run refresh_app_hub --size 128 --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
```
Salida esperada:
```
[1/4] Exporting PNG icons (size=64px) → /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files/icons ...
[1/4] PNG icons exported: 12
[2/4] Exporting manifest → .../hub_manifest.tsv ...
[2/4] Manifest exported: 12 rows
[3/4] Hub running → killing app_hub_launcher.exe ...
[3/4] Hub running → killed
[4/4] Launching app_hub_launcher ...
[4/4] Hub launched
OK: app_hub refreshed
```
## Cuando usarla
Ejecutar este pipeline después de:
- Cambiar el bloque `icon:` (`phosphor`, `accent`) en cualquier `app.md` C++.
- Modificar la `description` de una app C++ (se refleja en el manifest TSV).
- Añadir una app nueva con `lang: cpp` y `framework: imgui` al registry.
- Cambiar el `name` de una app imgui (el PNG y la fila TSV usan el `name`).
En todos esos casos el hub cachea los archivos al arrancar, así que el reinicio es obligatorio para ver los cambios.
## Gotchas
- **Cache al arrancar**: `app_hub_launcher` carga `local_files/icons/*.png` y `local_files/hub_manifest.tsv` una sola vez al inicio. Sin el reinicio los cambios no se ven aunque los archivos estén actualizados. Usar `--no-restart` solo si el hub está cerrado o si quieres preparar los archivos antes de lanzarlo a mano.
- **Paths Windows requieren WSL `/mnt/c/`**: el `--hub-dir` debe ser una ruta accesible desde WSL2. Las rutas `C:\...` nativas no funcionan en Bash — convertirlas con `wslpath -u 'C:\...'` antes de pasar el flag.
- **`taskkill` solo funciona en WSL con acceso a Windows tools**: si `tasklist.exe` y `taskkill.exe` no están en `$PATH` (instalación WSL sin interop habilitado), el paso 3 fallará. Verificar con `command -v tasklist.exe`.
- **`powershell.exe` necesario para el lanzamiento**: `launch_cpp_app_windows` usa `Start-Process` de PowerShell. Si PowerShell no está en `$PATH`, el paso 4 fallará.
- **`export_hub_icons` requiere Phosphor SVGs**: los SVGs deben existir en `sources/phosphor-core/assets/fill/`. Si no están clonados, las apps sin SVG se omiten (skip) sin abortar; el count final puede ser menor al esperado. Clonar con: `git clone https://github.com/phosphor-icons/core.git sources/phosphor-core`.
- **Idempotente**: lanzable N veces. Si el hub no está corriendo, el paso 3 se salta y el paso 4 lo lanza igualmente. Si ya está corriendo, lo mata y lo relanza.
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# refresh_app_hub — Pipeline: regenera icons + manifest del App Hub y reinicia el proceso
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
PYTHON="${REGISTRY_ROOT}/python/.venv/bin/python3"
HUB_APP="app_hub_launcher"
refresh_app_hub() {
local hub_dir="/mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files"
local size=64
local no_restart=0
local style="white_duotone"
# Parsear flags
while [[ $# -gt 0 ]]; do
case "$1" in
--hub-dir)
hub_dir="$2"
shift 2
;;
--size)
size="$2"
shift 2
;;
--no-restart)
no_restart=1
shift
;;
--style)
style="$2"
shift 2
;;
-*)
echo "refresh_app_hub: flag desconocido: $1" >&2
return 1
;;
*)
echo "refresh_app_hub: argumento inesperado: $1" >&2
return 1
;;
esac
done
local icons_dir="${hub_dir}/icons"
local manifest_path="${hub_dir}/hub_manifest.tsv"
# Paso 1: exportar PNGs de iconos
echo "[1/4] Exporting PNG icons (size=${size}px) → ${icons_dir} ..."
local icons_json
icons_json=$(
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
"$PYTHON" \
"${REGISTRY_ROOT}/python/functions/infra/export_hub_icons.py" \
"$icons_dir" \
--size "$size" \
--registry-root "$REGISTRY_ROOT" \
--style "$style"
) || {
echo "ERROR [1/4]: export_hub_icons falló" >&2
return 1
}
local icon_count
icon_count=$(echo "$icons_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
echo "[1/4] PNG icons exported: ${icon_count}"
# Paso 2: exportar TSV manifest
echo "[2/4] Exporting manifest → ${manifest_path} ..."
local manifest_json
manifest_json=$(
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
"$PYTHON" \
"${REGISTRY_ROOT}/python/functions/infra/export_hub_manifest.py" \
"$manifest_path" \
--registry-root "$REGISTRY_ROOT"
) || {
echo "ERROR [2/4]: export_hub_manifest falló" >&2
return 1
}
local manifest_count
manifest_count=$(echo "$manifest_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
echo "[2/4] Manifest exported: ${manifest_count} rows"
# Si --no-restart, terminar aqui
if [[ $no_restart -eq 1 ]]; then
echo "[3/4] Kill skipped (--no-restart)"
echo "[4/4] Launch skipped (--no-restart)"
echo "OK: app_hub refreshed (no-restart)"
return 0
fi
# Paso 3: matar el hub si está corriendo
local running=0
if is_cpp_app_running_windows "$HUB_APP" >/dev/null 2>&1; then
running=1
fi
if [[ $running -eq 1 ]]; then
echo "[3/4] Hub running → killing ${HUB_APP}.exe ..."
taskkill.exe /IM "${HUB_APP}.exe" /F >/dev/null 2>&1 || {
echo "ERROR [3/4]: taskkill falló para ${HUB_APP}.exe" >&2
return 1
}
# Pequeña pausa para que Windows libere el handle antes del relanzamiento
sleep 1
echo "[3/4] Hub running → killed"
else
echo "[3/4] Hub not running → skip kill"
fi
# Paso 4: relanzar el hub
echo "[4/4] Launching ${HUB_APP} ..."
if ! launch_cpp_app_windows "$HUB_APP"; then
echo "ERROR [4/4]: launch_cpp_app_windows falló para '${HUB_APP}'" >&2
return 1
fi
echo "[4/4] Hub launched"
echo "OK: app_hub refreshed"
}
# Ejecutar si se llama directamente (fn run lo invoca como script)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
refresh_app_hub "$@"
fi
+255 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
@@ -39,6 +40,8 @@ func cmdDoctor(args []string) {
doctorArtefacts(r, jsonOut)
case "services":
doctorServices(r, jsonOut)
case "services-spec":
doctorServicesSpec(r, jsonOut)
case "sync":
doctorSync(r, jsonOut)
case "uses-functions":
@@ -59,6 +62,14 @@ func cmdDoctor(args []string) {
} else {
doctorCapabilities(r, jsonOut)
}
case "app-location":
doctorAppLocation(r, jsonOut)
case "modules":
doctorModules(r, jsonOut)
case "dod":
doctorDod(r, jsonOut)
case "e2e-coverage":
doctorE2ECoverage(r, jsonOut)
default:
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
doctorUsage()
@@ -76,6 +87,7 @@ Subcommands:
(none)|all Corre todos los checks
artefacts Salud de apps y analyses (git, venv, app.md, upstream)
services Estado de apps con tag 'service' (systemd + puerto)
services-spec Audit del bloque service: en app.md de apps tag 'service' (issue 0105)
sync Drift entre pc_locations BD y disco
uses-functions Audit imports reales vs uses_functions del app.md
unused Funciones del registry sin consumidores
@@ -84,6 +96,10 @@ Subcommands:
vaults Salud de vaults: directorio, layout, índice, staleness, drift
copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k)
capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas <grupo>.md (issue 0086)
app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096
modules Drift entre uses_modules (app.md) y fn_module_<x> link calls (CMakeLists.txt) - issue 0097
dod Audita bloque dod_evidence_schema en dev/issues/ y dev/flows/ (issue 0114)
e2e-coverage Porcentaje de apps con e2e_checks declarado en su app.md (issue 0121b)
Flags:
--json Salida JSON (para scripting/agentes)
@@ -123,6 +139,11 @@ func doctorAll(root string, jsonOut bool) {
} else {
all["cpp_apps_error"] = err.Error()
}
if v, err := infra.AuditCppTableMigration(root); err == nil {
all["cpp_table_migration"] = v
} else {
all["cpp_table_migration_error"] = err.Error()
}
if v, err := infra.AuditMlEnv(root); err == nil {
all["ml"] = v
} else {
@@ -168,10 +189,21 @@ func doctorCppApps(root string, jsonOut bool) {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
tableAudits, err2 := infra.AuditCppTableMigration(root)
if err2 != nil {
fmt.Fprintf(os.Stderr, "warning: table migration audit failed: %v\n", err2)
tableAudits = nil
}
if jsonOut {
emit(audits)
emit(map[string]any{
"conformance": audits,
"table_migration": tableAudits,
})
return
}
// Conformance section.
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
@@ -187,6 +219,31 @@ func doctorCppApps(root string, jsonOut bool) {
}
w.Flush()
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
// BeginTable migration section.
if len(tableAudits) == 0 {
return
}
hasMigrationNotes := false
for _, t := range tableAudits {
if t.Status != "clean" {
hasMigrationNotes = true
break
}
}
if !hasMigrationNotes {
return
}
fmt.Println("\n--- BeginTable migration (issue 0081) ---")
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "STATUS\tAPP\tTABLES\tMESSAGE")
for _, t := range tableAudits {
if t.Status == "clean" {
continue
}
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", strings.ToUpper(t.Status), t.AppID, t.BeginTableCount, t.Message)
}
tw.Flush()
}
func doctorArtefacts(root string, jsonOut bool) {
@@ -244,6 +301,58 @@ func doctorServices(root string, jsonOut bool) {
w.Flush()
}
func doctorServicesSpec(root string, jsonOut bool) {
audits, err := infra.AuditServicesSpec(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(audits)
return
}
if len(audits) == 0 {
fmt.Println("No services declared (no apps with tag 'service').")
return
}
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tRUNTIME\tPORT\tHEALTH\tUNIT\tTARGETS\tISSUES")
for _, a := range audits {
status := "OK"
issues := "-"
if !a.OK {
status = "FAIL"
issues = strings.Join(a.Issues, "; ")
bad++
}
port := "-"
if a.Port > 0 {
port = fmt.Sprintf("%d", a.Port)
}
health := a.HealthPath
if health == "" {
health = "-"
}
unit := a.SystemdUnit
if unit == "" {
unit = "-"
}
targets := strings.Join(a.PCTargets, ",")
if targets == "" {
targets = "-"
}
runtime := a.Runtime
if runtime == "" {
runtime = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
status, a.Name, runtime, port, health, unit, targets, issues)
}
w.Flush()
fmt.Printf("\n%d/%d services with complete service: block.\n", len(audits)-bad, len(audits))
}
func doctorSync(root string, jsonOut bool) {
drifts, err := infra.PcLocationsDrift(root, "")
if err != nil {
@@ -472,3 +581,148 @@ func doctorCopiedCode(root string, jsonOut bool) {
w.Flush()
fmt.Printf("\n%d suspected copy match(es).\n", len(entries))
}
func doctorAppLocation(root string, jsonOut bool) {
violations, err := infra.AuditAppLocation(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(violations)
return
}
if len(violations) == 0 {
fmt.Println("OK: no artefacts under language-named folders.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "KIND\tLANG\tPATH")
for _, v := range violations {
fmt.Fprintf(w, "%s\t%s\t%s\n", v.Kind, v.Lang, v.Path)
}
w.Flush()
fmt.Printf("\n%d violation(s): move artefact to apps/<name>/ or projects/<p>/apps/<name>/ (issue 0096).\n", len(violations))
}
func doctorModules(root string, jsonOut bool) {
checks, err := infra.AuditModulesDrift(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(checks)
return
}
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tDECLARED\tLINKED\tMISSING\tEXTRA")
for _, c := range checks {
status := "OK"
if !c.OK {
status = "DRIFT"
bad++
}
decl := strings.Join(c.Declared, ",")
if decl == "" {
decl = "-"
}
link := strings.Join(c.Linked, ",")
if link == "" {
link = "-"
}
missing := strings.Join(c.MissingLinks, ",")
if missing == "" {
missing = "-"
}
extra := strings.Join(c.ExtraLinks, ",")
if extra == "" {
extra = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", status, c.AppID, decl, link, missing, extra)
}
w.Flush()
fmt.Printf("\n%d/%d apps with module drift.\n", bad, len(checks))
if bad > 0 {
fmt.Println("Fix: align uses_modules in app.md with target_link_libraries(fn_module_*) in CMakeLists.txt.")
}
}
func doctorDod(root string, jsonOut bool) {
issuesDir := filepath.Join(root, "dev", "issues")
flowsDir := filepath.Join(root, "dev", "flows")
report, err := infra.AuditDodSchema(issuesDir, flowsDir)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
fmt.Println("=== DoD Schema Audit ===")
fmt.Printf("files scanned: %d\n", report.TotalFiles)
fmt.Printf("with schema: %d\n", report.FilesWithItems)
fmt.Printf("total items: %d\n", report.TotalItems)
fmt.Printf("invalid items: %d\n", report.InvalidItems)
if report.InvalidItems == 0 {
fmt.Println("\nAll DoD schemas valid.")
return
}
fmt.Println()
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, f := range report.Files {
if len(f.Errors) == 0 {
continue
}
for _, e := range f.Errors {
fmt.Printf("%s : %s\n", rel(f.Path), e)
}
}
}
func doctorE2ECoverage(root string, jsonOut bool) {
roots := []string{
filepath.Join(root, "apps"),
filepath.Join(root, "cpp", "apps"),
filepath.Join(root, "projects"),
}
report, err := infra.AuditE2ECoverage(roots)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "METRIC\tVALUE")
fmt.Fprintf(w, "total\t%d\n", report.Total)
fmt.Fprintf(w, "with_checks\t%d\n", report.WithChecks)
fmt.Fprintf(w, "missing\t%d\n", len(report.Missing))
fmt.Fprintf(w, "coverage_pct\t%.2f%%\n", report.CoveragePct)
w.Flush()
if len(report.Missing) > 0 {
fmt.Println("\nApps without e2e_checks:")
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, m := range report.Missing {
fmt.Printf(" %s\n", rel(m))
}
}
}
+37 -2
View File
@@ -143,8 +143,8 @@ func cmdIndex() {
}
}
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d unit_tests\n",
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.UnitTests)
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d modules, %d unit_tests\n",
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.Modules, result.UnitTests)
for _, e := range result.ValidationErrors {
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
}
@@ -420,10 +420,42 @@ func cmdShow(args []string) {
return
}
m, errM := db.GetModule(id)
if errM == nil {
printModule(m)
return
}
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
os.Exit(1)
}
func printModule(m *registry.Module) {
fmt.Printf("ID: %s\n", m.ID)
fmt.Printf("Name: %s\n", m.Name)
fmt.Printf("Version: %s\n", m.Version)
fmt.Printf("Lang: %s\n", m.Lang)
fmt.Printf("Description: %s\n", m.Description)
if len(m.Members) > 0 {
fmt.Printf("Members: %s\n", strings.Join(m.Members, ", "))
}
if len(m.Tags) > 0 {
fmt.Printf("Tags: %s\n", strings.Join(m.Tags, ", "))
}
if m.DirPath != "" {
fmt.Printf("DirPath: %s\n", m.DirPath)
}
if m.RepoURL != "" {
fmt.Printf("RepoURL: %s\n", m.RepoURL)
}
if m.Documentation != "" {
fmt.Printf("\nDocumentation:\n%s\n", m.Documentation)
}
if m.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", m.Notes)
}
}
func printFunction(f *registry.Function) {
fmt.Printf("ID: %s\n", f.ID)
fmt.Printf("Name: %s\n", f.Name)
@@ -540,6 +572,9 @@ func printApp(a *registry.App) {
if len(a.UsesTypes) > 0 {
fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", "))
}
if len(a.UsesModules) > 0 {
fmt.Printf("Uses mods: %s\n", strings.Join(a.UsesModules, ", "))
}
if a.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", a.Notes)
}
+14
View File
@@ -27,6 +27,7 @@ type syncRequest struct {
Analysis []registry.Analysis `json:"analysis"`
Projects []registry.Project `json:"projects"`
Vaults []registry.Vault `json:"vaults"`
Modules []registry.Module `json:"modules"`
Proposals []registry.Proposal `json:"proposals"`
Locations []registry.PcLocation `json:"locations"`
}
@@ -37,6 +38,7 @@ type syncResponse struct {
Analysis []registry.Analysis `json:"analysis"`
Projects []registry.Project `json:"projects"`
Vaults []registry.Vault `json:"vaults"`
Modules []registry.Module `json:"modules"`
Proposals []registry.Proposal `json:"proposals"`
Locations []registry.PcLocation `json:"locations"`
Stats struct {
@@ -100,6 +102,7 @@ func syncPushPull() {
analysis, _ := db.AllAnalysis()
projects, _ := db.ListAllProjects()
vaults, _ := db.AllVaults()
modules, _ := db.AllModules()
proposals, _ := db.AllProposals()
// 2. Scan local directories and build pc_locations
@@ -112,6 +115,7 @@ func syncPushPull() {
Analysis: analysis,
Projects: projects,
Vaults: vaults,
Modules: modules,
Proposals: proposals,
Locations: locations,
}
@@ -203,6 +207,14 @@ func applySync(db *registry.DB, resp syncResponse) int {
}
}
for _, m := range resp.Modules {
existing, err := db.GetModule(m.ID)
if err != nil || m.UpdatedAt.After(existing.UpdatedAt) {
db.InsertModule(&m)
imported++
}
}
for _, p := range resp.Proposals {
existing, err := db.GetProposal(p.ID)
if err != nil || p.UpdatedAt.After(existing.UpdatedAt) {
@@ -329,6 +341,7 @@ func syncStatus() {
analysis, _ := db.AllAnalysis()
projects, _ := db.ListAllProjects()
vaults, _ := db.AllVaults()
modules, _ := db.AllModules()
proposals, _ := db.AllProposals()
locs, _ := db.ListAllPcLocations()
@@ -337,6 +350,7 @@ func syncStatus() {
fmt.Printf(" analysis: %d\n", len(analysis))
fmt.Printf(" projects: %d\n", len(projects))
fmt.Printf(" vaults: %d\n", len(vaults))
fmt.Printf(" modules: %d\n", len(modules))
fmt.Printf(" proposals: %d\n", len(proposals))
fmt.Printf(" locations: %d\n", len(locs))
+29
View File
@@ -0,0 +1,29 @@
package main
import (
"flag"
"fmt"
"os"
"fn-registry/functions/browser"
)
func main() {
port := flag.Int("port", 9222, "CDP debug port")
headless := flag.Bool("headless", false, "headless mode")
chromePath := flag.String("chrome-path", "", "explicit chrome.exe path (optional)")
userDataDir := flag.String("user-data-dir", "", "user-data-dir (optional; WSL2 auto-translates)")
flag.Parse()
pid, err := browser.ChromeLaunch(browser.ChromeLaunchOpts{
Port: *port,
Headless: *headless,
ChromePath: *chromePath,
UserDataDir: *userDataDir,
})
if err != nil {
fmt.Fprintf(os.Stderr, "chrome_launch failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("OK pid=%d port=%d\n", pid, *port)
}
+205 -31
View File
@@ -1,5 +1,9 @@
cmake_minimum_required(VERSION 3.16)
project(fn_registry_cpp LANGUAGES C CXX)
if(WIN32)
project(fn_registry_cpp LANGUAGES C CXX RC)
else()
project(fn_registry_cpp LANGUAGES C CXX)
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -236,10 +240,75 @@ endif()
set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
function(add_imgui_app target)
add_executable(${target} ${ARGN})
# Windows icon: si la app tiene <app_dir>/appicon.ico, generamos un .rc
# apuntando a ese .ico y lo anadimos como fuente. mingw-w64 windres
# (CMAKE_RC_COMPILER en la toolchain) lo enlaza en el .exe.
set(_extra_sources "")
if(WIN32 AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico)
set(_rc_file ${CMAKE_CURRENT_BINARY_DIR}/${target}_appicon.rc)
# Forward slashes para que windres no se confunda con escapes.
file(TO_CMAKE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico _ico_path)
# Numeric ID 101 = FN_APP_ICON_ID (ver cpp/framework/app_base.cpp).
# Usamos ID numerico (no string "IDI_ICON1") para que LoadImageW
# pueda recuperarlo en runtime y attacharlo al HWND (WM_SETICON).
file(WRITE ${_rc_file} "101 ICON \"${_ico_path}\"\n")
list(APPEND _extra_sources ${_rc_file})
endif()
# Modules manifest (issue 0097): siempre generamos <target>_modules_generated.cpp.
# Si la app tiene app.md con uses_modules, el .cpp resultante define
# fn::app_modules_array[] con sus modulos. Si no, genera un stub vacio
# (apps sin app.md no rompen el linkage de framework's app_about).
set(_modules_gen ${CMAKE_CURRENT_BINARY_DIR}/${target}_modules_generated.cpp)
set(_codegen_script ${FN_CPP_ROOT_DIR}/../python/functions/infra/codegen_app_modules.py)
set(_modules_root ${FN_CPP_ROOT_DIR}/../modules)
set(_app_md ${CMAKE_CURRENT_SOURCE_DIR}/app.md)
if(NOT EXISTS ${_app_md})
# No app.md: emit empty stub directamente (sin invocar Python).
file(WRITE ${_modules_gen}
"// Auto-generated stub (no app.md).
#include \"app_modules.h\"
namespace fn {
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
const unsigned long app_modules_count = 0;
}
")
else()
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_FOUND AND EXISTS ${_codegen_script})
execute_process(
COMMAND ${Python3_EXECUTABLE} ${_codegen_script}
--app-md ${_app_md}
--modules-root ${_modules_root}
--app-name ${target}
--out ${_modules_gen}
RESULT_VARIABLE _codegen_rc
OUTPUT_VARIABLE _codegen_out
ERROR_VARIABLE _codegen_err
)
if(NOT _codegen_rc EQUAL 0 AND NOT _codegen_rc EQUAL 2)
message(WARNING "codegen_app_modules failed for ${target}: ${_codegen_err}")
endif()
endif()
# Si python falla o el script no esta, emit stub vacio.
if(NOT EXISTS ${_modules_gen})
file(WRITE ${_modules_gen}
"// Auto-generated stub (codegen unavailable).
#include \"app_modules.h\"
namespace fn {
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
const unsigned long app_modules_count = 0;
}
")
endif()
endif()
list(APPEND _extra_sources ${_modules_gen})
add_executable(${target} ${ARGN} ${_extra_sources})
target_link_libraries(${target} PRIVATE fn_framework)
target_include_directories(${target} PRIVATE
${FN_CPP_ROOT_DIR}/functions
${FN_CPP_ROOT_DIR}/framework
)
# Convencion de layout (cpp_apps.md §7):
# <exe_dir>/<app>.exe + <app>.dll (binario + DLLs Windows convention)
@@ -274,14 +343,29 @@ endfunction()
# Functions are compiled as part of apps that use them via add_imgui_app.
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
# --- Demo app ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
add_subdirectory(apps/chart_demo)
# --- fn_module_data_table (issue 0097 modules) ---
# Static lib defined in modules/data_table/CMakeLists.txt. Replaces former
# fn_module_data_table target. Apps opt-in via:
# target_link_libraries(<app> PRIVATE fn_module_data_table)
# Lua is a hard dep only build the module when the vendored lua tree exists.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
add_subdirectory(${CMAKE_SOURCE_DIR}/../modules/data_table ${CMAKE_BINARY_DIR}/modules/data_table)
endif()
# --- Shaders Lab ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
add_subdirectory(apps/shaders_lab)
# --- Demo app (lives in apps/, issue 0096 standardization) ---
if(NOT DEFINED _CHART_DEMO_DIR)
set(_CHART_DEMO_DIR ${CMAKE_SOURCE_DIR}/../apps/chart_demo)
endif()
if(EXISTS ${_CHART_DEMO_DIR}/CMakeLists.txt)
add_subdirectory(${_CHART_DEMO_DIR} ${CMAKE_BINARY_DIR}/apps/chart_demo)
endif()
# --- Shaders Lab (lives in apps/) ---
if(NOT DEFINED _SHADERS_LAB_DIR)
set(_SHADERS_LAB_DIR ${CMAKE_SOURCE_DIR}/../apps/shaders_lab)
endif()
if(EXISTS ${_SHADERS_LAB_DIR}/CMakeLists.txt)
add_subdirectory(${_SHADERS_LAB_DIR} ${CMAKE_BINARY_DIR}/apps/shaders_lab)
endif()
# --- Lua 5.4 vendored (para playground tables / DSL formulas) ---
@@ -289,30 +373,39 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
add_subdirectory(vendor/lua)
endif()
# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery)
# --- Primitives Gallery (lives in apps/) ---
if(NOT DEFINED _PG_DIR)
set(_PG_DIR ${CMAKE_SOURCE_DIR}/../apps/primitives_gallery)
endif()
if(EXISTS ${_PG_DIR}/CMakeLists.txt)
add_subdirectory(${_PG_DIR} ${CMAKE_BINARY_DIR}/apps/primitives_gallery)
endif()
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz
# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery/playground/tables)
# --- Tables playground DEPRECATED (issue 0108) ---
# Sustituido por apps/tables_qa. El playground legacy queda solo como historia
# del split data_table 0107c. NO se builda mas su self_test (430 checks
# contra logica legacy) ya esta cubierto por:
# - cpp/tests/ (Catch2 unit tests de la logica pura del registry)
# - apps/tables_qa/ (testbed del modulo data_table v2.0.0+)
# Para revivirlo (temporal, debugging): descomentar el bloque if(EXISTS ...).
# if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
# add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
# endif()
# --- text_editor + file_watcher smoke test (lives in apps/) ---
if(NOT DEFINED _TES_DIR)
set(_TES_DIR ${CMAKE_SOURCE_DIR}/../apps/text_editor_smoke)
endif()
if(EXISTS ${_TES_DIR}/CMakeLists.txt)
add_subdirectory(${_TES_DIR} ${CMAKE_BINARY_DIR}/apps/text_editor_smoke)
endif()
# --- text_editor + file_watcher smoke test (issue 0025) ---
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt)
add_subdirectory(apps/text_editor_smoke)
# --- AltSnap viewport-jitter regression test (lives in apps/) ---
if(NOT DEFINED _AJT_DIR)
set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test)
endif()
# --- AltSnap viewport-jitter regression test ---
# Headless harness que conduce glfwSetWindowPos cada frame y verifica que
# ImGui viewport->Pos sigue al OS dentro de 1px. Sin la patch del framework
# (callback GLFW + per-frame sync) este test falla exit=1.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt)
add_subdirectory(apps/altsnap_jitter_test)
if(EXISTS ${_AJT_DIR}/CMakeLists.txt)
add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test)
endif()
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) ---
@@ -328,11 +421,17 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sdl3/CMakeLists.txt
set(SDL_INSTALL OFF CACHE BOOL "" FORCE)
set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt)
add_subdirectory(apps/engine_smoke)
if(NOT DEFINED _ES_DIR)
set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt)
add_subdirectory(apps/runtime_test)
if(EXISTS ${_ES_DIR}/CMakeLists.txt)
add_subdirectory(${_ES_DIR} ${CMAKE_BINARY_DIR}/apps/engine_smoke)
endif()
if(NOT DEFINED _RT_DIR)
set(_RT_DIR ${CMAKE_SOURCE_DIR}/../apps/runtime_test)
endif()
if(EXISTS ${_RT_DIR}/CMakeLists.txt)
add_subdirectory(${_RT_DIR} ${CMAKE_BINARY_DIR}/apps/runtime_test)
endif()
endif()
@@ -379,3 +478,78 @@ if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
# --- dag_engine_ui (lives in apps/, issue 0096) ---
if(NOT DEFINED _DAG_UI_DIR)
set(_DAG_UI_DIR ${CMAKE_SOURCE_DIR}/../apps/dag_engine_ui)
endif()
if(EXISTS ${_DAG_UI_DIR}/CMakeLists.txt)
add_subdirectory(${_DAG_UI_DIR} ${CMAKE_BINARY_DIR}/apps/dag_engine_ui)
endif()
# --- data_factory (lives in apps/, issue 0096) ---
set(_DATA_FACTORY_DIR ${CMAKE_SOURCE_DIR}/../apps/data_factory)
if(EXISTS ${_DATA_FACTORY_DIR}/CMakeLists.txt)
add_subdirectory(${_DATA_FACTORY_DIR} ${CMAKE_BINARY_DIR}/apps/data_factory)
endif()
# --- app_hub_launcher (lives in apps/, issue 0096) ---
set(_APP_HUB_LAUNCHER_DIR ${CMAKE_SOURCE_DIR}/../apps/app_hub_launcher)
if(EXISTS ${_APP_HUB_LAUNCHER_DIR}/CMakeLists.txt)
add_subdirectory(${_APP_HUB_LAUNCHER_DIR} ${CMAKE_BINARY_DIR}/apps/app_hub_launcher)
endif()
# --- services_monitor (lives in apps/, issue 0096) ---
set(_SERVICES_MONITOR_DIR ${CMAKE_SOURCE_DIR}/../apps/services_monitor)
if(EXISTS ${_SERVICES_MONITOR_DIR}/CMakeLists.txt)
add_subdirectory(${_SERVICES_MONITOR_DIR} ${CMAKE_BINARY_DIR}/apps/services_monitor)
endif()
# --- app_gestion (lives in apps/, issue 0096) ---
set(_APP_GESTION_DIR ${CMAKE_SOURCE_DIR}/../apps/app_gestion)
if(EXISTS ${_APP_GESTION_DIR}/CMakeLists.txt)
add_subdirectory(${_APP_GESTION_DIR} ${CMAKE_BINARY_DIR}/apps/app_gestion)
endif()
# --- skill_tree (lives in apps/, issue 0096) ---
set(_SKILL_TREE_DIR ${CMAKE_SOURCE_DIR}/../apps/skill_tree)
if(EXISTS ${_SKILL_TREE_DIR}/CMakeLists.txt)
add_subdirectory(${_SKILL_TREE_DIR} ${CMAKE_BINARY_DIR}/apps/skill_tree)
endif()
# --- tables_qa (lives in apps/, issue 0096) ---
set(_TABLES_QA_DIR ${CMAKE_SOURCE_DIR}/../apps/tables_qa)
if(EXISTS ${_TABLES_QA_DIR}/CMakeLists.txt)
add_subdirectory(${_TABLES_QA_DIR} ${CMAKE_BINARY_DIR}/apps/tables_qa)
endif()
# --- process_explorer (lives in apps/, issue 0096) ---
set(_PROCESS_EXPLORER_DIR ${CMAKE_SOURCE_DIR}/../apps/process_explorer)
if(EXISTS ${_PROCESS_EXPLORER_DIR}/CMakeLists.txt)
add_subdirectory(${_PROCESS_EXPLORER_DIR} ${CMAKE_BINARY_DIR}/apps/process_explorer)
endif()
# --- agents_dashboard (lives in projects/element_agents/apps/) ---
set(_AGENTS_DASHBOARD_DIR ${CMAKE_SOURCE_DIR}/../projects/element_agents/apps/agents_dashboard)
if(EXISTS ${_AGENTS_DASHBOARD_DIR}/CMakeLists.txt)
add_subdirectory(${_AGENTS_DASHBOARD_DIR} ${CMAKE_BINARY_DIR}/apps/agents_dashboard)
endif()
# --- kanban_cpp (lives in apps/, issue 0096) ---
set(_KANBAN_CPP_DIR ${CMAKE_SOURCE_DIR}/../apps/kanban_cpp)
if(EXISTS ${_KANBAN_CPP_DIR}/CMakeLists.txt)
add_subdirectory(${_KANBAN_CPP_DIR} ${CMAKE_BINARY_DIR}/apps/kanban_cpp)
endif()
# --- data_table_bench (lives in apps/, issue 0133) ---
# Requires SQLite3 dev libs. Skip silently when not available (e.g. cross-windows build).
set(_DATA_TABLE_BENCH_DIR ${CMAKE_SOURCE_DIR}/../apps/data_table_bench)
if(EXISTS ${_DATA_TABLE_BENCH_DIR}/CMakeLists.txt)
find_package(SQLite3 QUIET)
if(SQLite3_FOUND)
add_subdirectory(${_DATA_TABLE_BENCH_DIR} ${CMAKE_BINARY_DIR}/apps/data_table_bench)
else()
message(STATUS "Skipping data_table_bench (SQLite3 dev libs not found)")
endif()
endif()
Submodule cpp/apps/altsnap_jitter_test deleted from 6e52b658a3
Submodule cpp/apps/chart_demo added at 026f514bb7
-22
View File
@@ -1,22 +0,0 @@
add_imgui_app(chart_demo
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
# fps_overlay vive en fn_framework
)
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(chart_demo_tests
main.cpp
tests/chart_demo_tests.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
)
# Excludes int main() from main.cpp so the test target provides its own.
target_compile_definitions(chart_demo_tests PRIVATE FN_TEST_BUILD)
endif()
-60
View File
@@ -1,60 +0,0 @@
---
name: chart_demo
lang: cpp
domain: viz
description: "Demo ImGui de primitivos viz del registry: line_plot, scatter_plot, bar_chart, heatmap. Cada chart en su propia tab del TabBar. Usado como showcase y como build gate de las funciones viz/."
tags: [imgui, demo, charts, viz, showcase]
uses_functions:
- line_plot_cpp_viz
- scatter_plot_cpp_viz
- bar_chart_cpp_viz
- heatmap_cpp_viz
# logger, app_menubar viven en fn_framework — no se listan aqui
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/chart_demo"
repo_url: ""
---
## Que hace
App de una sola ventana con cuatro tabs (Line / Scatter / Bar / Heatmap) que
renderiza datos sinteticos para mostrar el aspecto y la API de los primitivos
viz del registry. Sirve como:
- **Showcase visual** de las funciones viz existentes — al añadir una nueva
primitiva, anadir su tab aqui es la forma natural de probar el binding.
- **Build gate**: si una de las funciones rompe API, esta app deja de
compilar y lo cazamos sin tener que tocar `registry_dashboard` o
`graph_explorer`.
## Estructura
`main.cpp` (~93 lineas):
- `init_data()` — genera arrays sinteticos una vez (estado modulo).
- `render()` — DockSpaceOverViewport + TabBar con 4 tabs, cada una invoca
un primitivo del registry.
- `main()``fn::run_app(...)` con AppConfig estandar (titulo, tamaño,
about, log).
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target chart_demo
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target chart_demo
```
## Decisiones
- `viewports = true` (default de `fn::run_app`): las ventanas se pueden
arrastrar fuera del main window.
- `init_gl_loader = false`: solo usa ImGui/ImPlot, sin gl* directo.
- Sin persistencia propia (no abre BD).
- `log: file_path = "chart_demo.log"` con nivel Debug — el `init_data`
emite info+debug para verificar que el logger funciona.
-89
View File
@@ -1,89 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "implot.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/bar_chart.h"
#include "viz/heatmap.h"
#include "core/app_menubar.h"
#include "core/logger.h"
#include <cmath>
#include <vector>
// Generate sample data
static constexpr int N = 500;
static float xs[N], ys_sin[N], ys_cos[N];
static float scatter_x[200], scatter_y[200];
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
static float heat_data[10 * 10];
static bool data_initialized = false;
static void init_data() {
if (data_initialized) return;
fn_log::log_info("init_data: generando %d puntos sin/cos, 200 scatter, 10x10 heatmap", N);
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.02f;
ys_sin[i] = sinf(xs[i]);
ys_cos[i] = cosf(xs[i]);
}
for (int i = 0; i < 200; i++) {
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
}
for (int i = 0; i < 100; i++) {
int r = i / 10, c = i % 10;
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
}
data_initialized = true;
fn_log::log_debug("init_data: ok");
}
void render() {
init_data();
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
ImGui::Text("sin(x) — %d points", N);
line_plot("Sine Wave", xs, ys_sin, N);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scatter Plot")) {
ImGui::Text("y = 0.5x + noise — 200 points");
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Bar Chart")) {
ImGui::Text("Functions per language in fn_registry");
bar_chart("Registry Languages", bar_labels, bar_values, 5);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Heatmap")) {
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "fn_registry — Chart Demo",
.width = 1400,
.height = 900,
.about = {.name = "chart demo",
.version = "0.2.0",
.description = "Demo de primitivos viz: line, scatter, bar, heatmap. AppConfig estandar + multi-viewport."},
.log = {.file_path = "chart_demo.log",
.level = static_cast<int>(fn_log::Level::Debug)}
}, render);
}
#endif
@@ -1,41 +0,0 @@
// E2E tests for chart_demo — Dear ImGui Test Engine.
// Built only when -DFN_BUILD_TESTS=ON. The same main.cpp from chart_demo is
// compiled here with FN_TEST_BUILD defined so its int main() is excluded and
// only render() is reused.
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // defined in chart_demo/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
// Smoke test: the main window appears and is non-empty.
t = IM_REGISTER_TEST(e, "chart_demo", "smoke_window_visible");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo"); // em-dash
IM_CHECK(ctx->WindowInfo("").ID != 0);
};
// Cycle through all four tabs. Test engine fails the test if any tab item
// is not found or cannot be activated — that is our implicit assertion.
t = IM_REGISTER_TEST(e, "chart_demo", "tabs_cycle_all");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo");
ctx->ItemClick("##charts/Line Plot");
ctx->ItemClick("##charts/Scatter Plot");
ctx->ItemClick("##charts/Bar Chart");
ctx->ItemClick("##charts/Heatmap");
};
}
int main() {
fn::AppConfig cfg{};
cfg.title = "chart_demo_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
Submodule cpp/apps/engine_smoke deleted from bed33856e7

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