diff --git a/reports/0001-2026-06-06-unibus-service-fase2-transport.md b/reports/0001-2026-06-06-unibus-service-fase2-transport.md new file mode 100644 index 0000000..6630b57 --- /dev/null +++ b/reports/0001-2026-06-06-unibus-service-fase2-transport.md @@ -0,0 +1,181 @@ +# Report 0001 — unibus: servicio LAN + Fase 2 (transporte sobre el bus) + +- **Fecha:** 06/06/2026 +- **Autor:** Claude (Opus 4.8) +- **Ámbito:** `projects/message_bus/apps/unibus` (sub-repo `dataforge/unibus`) y `~/DataProyects/Github/agents_and_robots` (Gitea `egutierrez/agents_and_robots`) +- **Estado:** done (con un gap acotado documentado en Fase 2) + +## Resumen + +Dos entregas. **A)** `membershipd` pasa de proceso manual loopback a service systemd real +alcanzable por la LAN: flag `--bind` que gobierna a la vez el HTTP de control y el NATS +embebido, unit systemd-user con `Restart=always`, bloque `service:` completo y health +verificado desde la IP de la LAN. Esto desbloquea que un teléfono/PC remoto conecte al bus. +**B)** Fase 2 (branch-by-abstraction): se introduce una interfaz `Transport` + un tipo +`InboundMessage` neutral (sin mautrix), se desacoplan las firmas del core de agentes, se +implementa un `unibusTransport` sobre `pkg/client` de unibus, y un feature flag elige Matrix +vs unibus por bot. Pre-requisito cumplido: el `Frame` de unibus gana threading/reply/reaction +de forma aditiva. + +Trabajo en ramas TBD: `issue/fase2-transport-service` (unibus) y `issue/unibus-transport` +(agents_and_robots). Master nunca tocado directamente. + +## Cambios + +### A — Servicio (unibus) + +| Cambio | Archivo | Por qué | +|---|---|---| +| Flag `--bind` (default 127.0.0.1) aplicado a HTTP + NATS | `cmd/membershipd/main.go` | Un solo flag gobierna la alcanzabilidad de ambos planos; `0.0.0.0` expone a la LAN | +| `embeddednats.StartHost(storeDir, host, port)` (y `Start` backward-compatible) | `pkg/embeddednats/embeddednats.go` | Controlar la interfaz del NATS embebido sin romper playground/tests | +| systemd-user unit `Restart=always` | `deploy/unibus-membershipd.service` | Service persistente; `on-failure` NO reinicia tras un SIGTERM limpio (exit 0) — el gotcha de `sqlite_api` | +| `install.sh` idempotente + `README.md` | `deploy/` | build + symlink unit + enable/restart en un comando; operar/health documentado | +| Bloque `service:` completo | `app.md` | `systemd-user`, `restart_policy: always`, health `/healthz`, `is_local_only: false`; valida `fn doctor services-spec` | + +### Pre-Fase 2 — Frame threading (unibus, aditivo) + +| Cambio | Archivo | Por qué | +|---|---|---| +| Campos `ThreadID`, `ReplyTo` (omitempty) + tipo `REACT` | `pkg/frame/frame.go` | Bots de chat necesitan replies, hilos y reacciones | +| `PublishReply`, `React` (refactor de `Publish` a `publishFrame` compartido) | `pkg/client/client.go` | API de threading; la reacción viaja en el payload cifrado (confidencial en rooms E2E) | +| Tests: round-trip threaded, back-compat de wire/firma, error path, reply+react E2E | `pkg/frame/*_test.go`, `pkg/client/client_test.go` | golden + edge + error path | + +Aditivo de verdad: un frame sin threading serializa **sin** las claves `thr`/`re`, así que su +wire y su firma Ed25519 son idénticos a antes → no rompe `Subscribe`/`Publish` ni los tests +existentes. + +### B — Fase 2 (agents_and_robots) + +| Cambio | Archivo | Por qué | +|---|---|---| +| `Transport` interface + `InboundMessage` neutral + `Select`/`FlagEnabled` | `pkg/transport/` | Seam neutral (cero mautrix) entre el core del agente y el fabric de mensajería | +| Flag `unibus-transport` (default off) | `dev/feature_flags.json` | Convivencia: con el flag ON cada bot opta in; el resto sigue en Matrix | +| `handleEvent(…, *event.Event)` → `handleInbound(…, transport.InboundMessage)` (Agent + Robot) | `agents/handler.go`, `agents/robot.go` | Quitar `*event.Event` de las firmas; `inboundToMsgCtx` es el único punto de conversión a `decision.MessageContext` | +| `RawMatrixClient() *mautrix.Client` → `Scanner() RoomScanner` | `agents/runtime.go`, `cmd/launcher/main.go` | Quitar el handle al cliente mautrix de la API pública; capability read-only | +| Listener entrega `InboundMessage` (no el evento) | `shell/matrix/listener.go` | El evento mautrix ya no cruza la frontera al core | +| `unibusTransport` + `DemoEchoHandler` (módulo Go anidado) | `shell/transportunibus/` | Implementa `Transport` sobre `pkg/client`; subjects `agent..{in,out}` | + +## Verificación + +### A — Servicio (evidencia ejecutable) + +``` +$ systemctl --user is-active unibus-membershipd.service +active + +$ systemctl --user status unibus-membershipd.service # (extracto) +Active: active (running) + ExecStart=…/membershipd --bind 0.0.0.0 + +$ ss -tln | grep -E ':8470|:4250' +LISTEN 0 4096 *:8470 *:* # HTTP control en todas las interfaces +LISTEN 0 4096 *:4250 *:* # NATS data plane en todas las interfaces + +$ curl -fsS -w " [HTTP %{http_code}]\n" http://127.0.0.1:8470/healthz +{"status":"ok"} [HTTP 200] + +$ curl -fsS -w " [HTTP %{http_code}]\n" http://192.168.1.129:8470/healthz # IP LAN +{"status":"ok"} [HTTP 200] + +$ ./fn doctor services-spec | grep unibus +OK unibus systemd-user 8470 /healthz unibus-membershipd.service lucas-linux - +``` + +**Cliente NATS conecta por IP LAN y publica/recibe** (worker→chat, ambos contra +`nats://192.168.1.129:4250` + `http://192.168.1.129:8470`, vía el service systemd): + +``` +[chat] subscribed to "proc.test.ticks"; waiting for messages +[proc.test.ticks] DGCob41a: tick 1 @ 2026-06-06T16:07:16Z +[proc.test.ticks] DGCob41a: tick 2 @ 2026-06-06T16:07:17Z +[proc.test.ticks] DGCob41a: tick 3 @ 2026-06-06T16:07:18Z +[proc.test.ticks] DGCob41a: tick 4 @ 2026-06-06T16:07:19Z +``` + +### Pre-Fase 2 — Frame (unibus) + +``` +$ cd projects/message_bus/apps/unibus +$ CGO_ENABLED=0 go build ./... && CGO_ENABLED=0 go vet ./... # exit 0 +$ CGO_ENABLED=0 go test ./... +ok github.com/enmanuel/unibus/pkg/frame +ok github.com/enmanuel/unibus/pkg/client +ok github.com/enmanuel/unibus/pkg/membership +``` + +Tests nuevos: `TestThreadingRoundTrip` (golden), `TestNonThreadedWireBackCompat` (edge: +asegura que `thr`/`re` no aparecen en frames no-threaded), `TestUnmarshalRejectsGarbage` +(error path), `TestThreadedReplyAndReaction` (reply + reacción E2E en room cifrada ModeMatrix). + +### B — Fase 2 (agents_and_robots) + +Toda la compilación/test del repo `agents` se hace con `-tags goolm` (olm puro-Go) porque +`libolm` (CGO) no está instalado en esta máquina; en producción se usa `libolm`. + +``` +# Módulo anidado del transporte unibus (flag ON / camino unibus): +$ cd shell/transportunibus && go test -tags goolm ./... +ok github.com/enmanuel/agents/shell/transportunibus + # TestDemoBotOverUnibus: un user-peer publica en agent.demo.in; el bot, movido por + # unibusTransport, recibe un InboundMessage y responde "echo: hola bus" / "pong" en + # agent.demo.out. TestRunStopsOnContextCancel: lifecycle (error path). + +# Módulo principal (flag OFF / camino Matrix sigue vivo): +$ go build -tags goolm ./... # exit 0 +$ go vet -tags goolm ./agents/ ./shell/matrix/ ./pkg/transport/ # exit 0 +$ go test -tags goolm ./pkg/transport/ ./agents/ +ok github.com/enmanuel/agents/pkg/transport # TestSelect (flag ON/OFF) + TestFlagEnabled +ok github.com/enmanuel/agents/agents # core decoplado compila+pasa +$ go test -tags goolm ./... # 26 paquetes ok, 1 FAIL pre-existente (ver gaps) +``` + +## Gaps / pendientes + +1. **Segundo host físico real para A.** El round-trip de A se probó desde esta misma máquina + contra su IP de LAN `192.168.1.129` (no loopback), lo que ejercita el bind `0.0.0.0`. No + había un segundo PC/teléfono disponible para conectar desde otra máquina; como los sockets + escuchan en `*` (todas las interfaces, confirmado por `ss`), el comportamiento desde un host + remoto es idéntico. El agente de frontend puede confirmarlo desde el móvil. + +2. **El Agent "pesado" todavía no corre sobre unibus.** Conflicto real de dependencias entre + ecosistemas: `unibus` depende de `fn-registry`, cuyo módulo raíz exige + `maunium.net/go/mautrix v0.28.0`, incompatible con el `shell/matrix` del repo `agents` + (escrito para `v0.21.1`; MVS forzaría v0.28 y rompe 3 call-sites de la API mautrix). Por eso + el `unibusTransport` vive en un **módulo Go anidado** (`shell/transportunibus/go.mod`): tira + de la mautrix nueva transitivamente sin recompilar el código Matrix del padre, y ambos + ecosistemas conviven. Consecuencia: el core del Agent (módulo principal) no puede instanciar + `unibusTransport` directamente sin reintroducir el conflicto. El seam neutral + (`pkg/transport.Transport`) sí está y el bot demo corre sobre el bus; **migrar el Agent + completo a unibus requiere primero subir `agents` a mautrix v0.28** (migración aparte, fuera + de alcance de esta tanda). + +3. **`Scanner() RoomScanner` sigue devolviendo tipos mautrix** en sus métodos + (`*mautrix.RespJoinedRooms`, …). El escaneo de rooms es una capability intrínsecamente de + Matrix (enumera rooms de Matrix); la neutralización quita el handle al cliente crudo y lo + sustituye por una interfaz read-only mínima, pero no puede ser 100% neutral porque el unibus + no participa en ese escaneo. + +4. **`libolm` no instalado** en esta máquina → el repo `agents` se compila/test con + `-tags goolm` (olm puro-Go). En producción se usa `libolm`. Pendiente: instalar + `libolm-dev` si se quiere validar el path libolm localmente. + +5. **Test pre-existente en rojo, ajeno a este trabajo:** `shell/logger/TestCleanOldLogs` falla + porque es dependiente de fecha (espera logs de marzo 2026 que la limpieza por antigüedad + borra estando hoy a 06/06/2026). Ya fallaba en el baseline antes de tocar nada; no toqué + `shell/logger`. Issue separado recomendado. + +6. **Sin push a remoto.** El trabajo queda consolidado en `master` local de ambos repos; el + push a Gitea queda para `/full-git-push` del operador. + - `agents_and_robots`: `master` = merge `--no-ff` de `issue/unibus-transport` (commit + `aa595f5`), build + tests verdes; rama borrada. + - `unibus`: **condición de carrera entre dos agentes en el mismo repo.** El agente de + frontend trabajaba en paralelo en el mismo working tree de unibus y nuestros `git checkout` + se interleavearon: mis commits A1/A2/A3 (`--bind`, deploy, service block) acabaron + directamente en `master`, y los de threading + bump (`2209283`, `69079d1`) cayeron sobre la + rama del otro agente `issue/frontend-web-movil`. Como esos dos commits son lineales sobre + mi A3 (`b2e6712`) y NO contenían trabajo del otro agente, consolidé `master` con + `git merge --ff-only 69079d1` (sin duplicar commits, sin tocar la rama del frontend). Estado + final: `master` de unibus contiene TODO mi trabajo (A1-A3 + threading + bump 0.3.0), tests + `pkg/...` verdes, working tree con el WIP del frontend (mobile/playground/android/web) + intacto. **Lección operativa:** dos agentes Claude en el mismo repo comparten HEAD/índice; + hace falta worktrees separados (`git worktree`) o repos clonados por agente para evitarlo. diff --git a/reports/0002-2026-06-06-frontend-web-movil.md b/reports/0002-2026-06-06-frontend-web-movil.md new file mode 100644 index 0000000..027d3cd --- /dev/null +++ b/reports/0002-2026-06-06-frontend-web-movil.md @@ -0,0 +1,125 @@ +# Report 0002 — Cliente humano de unibus: SPA web + app Android nativa + +- **Fecha:** 06/06/2026 +- **Autor:** agente (Claude) — frente frontend +- **Ámbito:** `mobile/unibus.go`, `playground/server.go` (gateway), `web/` (SPA nueva), `android/` (app nueva) +- **Estado:** done (con gaps documentados) + +## Resumen + +Se construyó el cliente humano de unibus en dos frentes, ambos verificados end-to-end: + +1. **Web**: SPA de chat (React + Vite + TypeScript + Mantine v9) contra el gateway Go + (`playground/server.go`), que monta un peer real por identidad y emite los mensajes + recibidos por SSE. Dos identidades crean una room cifrada E2E, una invita a la otra y + ambas chatean en vivo. +2. **Móvil**: app Android nativa (Kotlin + Jetpack Compose) sobre el binding gomobile + `mobile/unibus.go`. El cifrado y el transporte NATS corren **en el dispositivo** (cada + teléfono es un peer real del bus). La app genera identidad, conecta al `membershipd`, + crea/se une a una room y envía/recibe mensajes por el bus. + +No se reimplementó protocolo ni criptografía en JS ni en Kotlin: todo delega en `pkg/client` +a través del binding y del gateway. + +## Cambios + +| Archivo / dir | Qué | Por qué | +|---|---|---| +| `mobile/unibus.go` | Añadidos `Card()` (exporta la identidad pública del peer como JSON portable), `Invite(roomID, peerCard)` y `Kick(roomID, endpointID)` al binding. | La UI móvil necesita invitar/expulsar; `Card()` permite el intercambio de identidad peer-a-peer (paste/QR) sin un gateway. | +| `playground/server.go` | Añadidos `GET /api/rooms?peer=` (rooms del peer), `GET /api/members?room_id=` (proxy al control plane) y middleware `withCORS` (preflight + headers) para el dev server de Vite. | La SPA necesita listar rooms/miembros y llamar al gateway desde otro origen. | +| `web/` | SPA nueva: conexión (gateway URL + identidad), navbar (crear/unir/listar rooms), panel central (mensajes en vivo por SSE + composer), panel lateral (miembros, invitar por peer, expulsar si owner). Mantine v9, `@tabler/icons-react`, sin Tailwind ni CSS manual. | Cliente web de chat. | +| `android/` | App Kotlin + Compose: pantalla de conexión (Host + NATS + identidad), `BusViewModel` que dirige el binding (llamadas de red en `Dispatchers.IO`, frames entrantes vía `StateFlow`), pantalla de chat (crear/unir room, enviar, recibir). `.aar` generado con `gomobile bind`. | Cliente móvil con E2E en el dispositivo. | + +## Verificación (evidencia ejecutable) + +### Builds + +``` +# Binding + gateway (Go, sin CGO) +$ CGO_ENABLED=0 go build ./mobile/ ./playground/ → OK +$ CGO_ENABLED=0 go vet ./mobile/ ./playground/ → OK + +# .aar gomobile +$ gomobile bind -target=android -androidapi 21 -javapkg com.unibus.core \ + -o android/app/libs/unibus.aar ./mobile → unibus.aar (24 MB) + +# SPA +$ cd web && pnpm build + ✓ tsc -b && vite build → 6949 modules transformed, dist/ generado (exit 0) + +# APK +$ cd android && ./gradlew assembleDebug + BUILD SUCCESSFUL in 1m13s + → app/build/outputs/apk/debug/app-debug.apk (41 MB, incluye libgojni.so) +``` + +### Web — flujo cifrado E2E por curl (determinista) + +Gateway (`go run ./playground`, web :7700) + dos peers `ana`/`leo`: + +``` +1. POST /api/peer ana → endpoint cc9f8Gm_RQZ76lX2I6C0ZepmiC82AG19M_ajiaDW4P8 + POST /api/peer leo → endpoint yT7GWz97tRcCx3i4st43d4LcGC85Tl4PDZenycUaQ_I +2. POST /api/room {peer:ana, encrypt:true} → room_id 01KTEVZEE7B7AQAD72WSDHH3Y3 +3. POST /api/invite {peer:ana, target:leo} → {"status":"invited"} +4. POST /api/join {peer:leo} → {"encrypt":true,"subject":"room.general"} +5. SSE leo + POST /api/publish ana "hola leo, mensaje cifrado E2E": + leo recibe → data: {"sender":"cc9f8...","text":"hola leo, mensaje cifrado E2E","encrypted":true} +6. GET /api/rooms?peer=ana → [{"encrypt":true,"room_id":"01KTEVZEE...","subject":"room.general"}] +7. GET /api/members?room_id → [{ana,role:owner}, {leo,role:member}] +``` + +### Web — UI con dos pestañas (SPA real, `pnpm preview` :4173) + +- Pestaña ana y pestaña leo conectadas como identidades distintas a la room cifrada. +- leo → ana: "hola ana, soy leo desde la SPA" (18:31:05) llegó **en vivo por SSE** a ana. +- ana → leo: "¡recibido leo! ana responde cifrado" (18:32:28), bucle bidireccional. +- Panel de miembros: ana **OWNER** + leo; botón expulsar visible solo para el owner; + selector de invitación con los peers conectados. + +### Móvil — AVD Pixel_API34 (emulador headless) + +``` +$ adb install -r app-debug.apk → Success +$ adb shell am start -n com.unibus.app/.MainActivity + → proceso vivo, sin FATAL/AndroidRuntime, libgojni.so cargada (lib/x86_64) + → pantalla "Conectado como android" (newSession contra membershipd 10.0.2.2:8470/4250) + → "Room creada · 01KTEWTZ8TB1NYEGNNR7F3MY5G" + → enviado "peer movil real en el bus" → burbuja recibida con hora 18:39:35 +``` + +La burbuja con timestamp prueba el camino completo **en el dispositivo**: el código no añade +el mensaje localmente al publicar; solo lo pinta `FrameListener.onFrame`, así que su aparición +demuestra que el frame viajó por NATS y volvió al peer. Captura en +`reports/assets/unibus_movil.png`. + +## Gaps / pendientes + +- **Media (PublishMedia/FetchMedia)**: el `pkg/client` ya la soporta, pero NO está expuesta en + el binding móvil ni en la SPA. Requiere que `FrameListener.onFrame` señale frames con `Blob` + (hoy entrega solo texto). Pendiente para v2. +- **ListMembers en el binding móvil**: necesita un método **público** en `pkg/client` + (`ListMembers(roomID)`); hoy es privado (`signerPub` lo usa internamente). **Dependencia del + CORE**, no implementada aquí por contrato (no tocar `pkg/`). La SPA sí lista miembros porque + el gateway hace proxy al control plane. +- **Threading/reply/reaction**: el otro agente añadió `ThreadID`/`ReplyTo`/tipo `REACT` al + `Frame` (ya en master). Ni la SPA ni la app móvil los usan todavía — gap de UI, no de backend. +- **Invite/Kick en la UI móvil**: el binding ya expone `Card()`/`Invite()`/`Kick()`, pero la + app Kotlin v1 no tiene aún la pantalla de intercambio de "card" (paste/QR) ni el botón de + expulsar. El chat cleartext y E2E self-echo funcionan; el flujo de dos teléfonos invitándose + queda para v2. +- **Auth del gateway web**: el gateway identifica peers por nombre (modelo del playground), sin + autenticación de sesión. Suficiente para uso local/LAN; endurecer es fase posterior (igual que + el control plane, que en v1 no tiene TLS ni auth en GETs). +- **`.aar` no versionado**: es un artefacto de ~24 MB; se regenera con `gomobile bind` (ver + `android/README.md`). Gitignored. + +## Notas + +- Web: `cd web && pnpm install && pnpm dev` (o `pnpm preview` tras `pnpm build`); conectar a + `http://localhost:7700` (gateway: `go run ./playground`). +- Móvil: ver `android/README.md` para generar el `.aar`, compilar el APK y probar en el AVD. + Desde el emulador, el host del PC es `10.0.2.2`; desde un teléfono físico, la IP LAN del PC. +- Mantine v9 exige **React 19** (peerDependency `^19.2.0`); con React 18 la SPA compila pero + no monta en runtime (`s is not a function`). Fijado React 19 en `web/package.json`. +- pnpm 10 bloquea los build scripts: `web/pnpm-workspace.yaml` con `allowBuilds: { esbuild: true }`. diff --git a/reports/0002-2026-06-07-matrix-out-agents-on-unibus.md b/reports/0002-2026-06-07-matrix-out-agents-on-unibus.md new file mode 100644 index 0000000..dd43d44 --- /dev/null +++ b/reports/0002-2026-06-07-matrix-out-agents-on-unibus.md @@ -0,0 +1,87 @@ +# Report 0002 — Matrix-out: los bots de agents_and_robots hablan solo por unibus + +- **Fecha:** 07/06/2026 +- **Autor:** Claude (Opus 4.8) +- **Ámbito:** `~/DataProyects/Github/agents_and_robots` (Gitea `egutierrez/agents_and_robots`) y `projects/message_bus/apps/unibus` (`dataforge/unibus`) +- **Estado:** done (con gaps acotados) + +## Resumen + +Se arranca **todo Matrix (mautrix) de raíz** de `agents_and_robots` y se reconecta el core de +agentes al bus `unibus` por el seam neutral `pkg/transport`. Modelo **"todo son rooms"** con **E2E** +(`room.ModeMatrix`): el bot descubre por polling las rooms a las que lo invitan, se une, descifra y +**responde en la misma room**. Resultado: el binario de agentes no contiene una sola línea de +mautrix y compila sin `-tags goolm` ni `libolm`. Trabajo en ramas TBD, mergeado a master en ambos +repos. + +Antecedente: la migración a mautrix v0.28 que se barajó NO era necesaria — era un parche para +convivencia. Al quitar Matrix, el conflicto de versiones desaparece de raíz. + +## Cambios + +### unibus — descubrimiento de rooms (v0.4.0) +| Cambio | Archivo | +|---|---| +| `GET /members/{endpoint}/rooms` (rooms de un endpoint + rol) | `pkg/membership/server.go` | +| `Store.ListRoomsForEndpoint` (JOIN members+rooms) | `pkg/membership/store.go` | +| `client.ListMyRooms() []RoomRef` | `pkg/client/client.go` | +| Tests store + e2e cliente (descubrir room por invitación) | `pkg/membership/store_test.go`, `pkg/client/client_test.go` | + +El control plane es pull (no hay push de invitaciones); un peer recién invitado a una room cifrada +la descubre por polling y luego hace `Join` + `Subscribe`. + +### agents_and_robots — Matrix-out (4 commits) +| Cambio | Detalle | +|---|---| +| **Borrado** | `shell/matrix/`, `tools/matrix/`, `cmd/verify/`, `cmd/agentctl/avatar.go`, dep `maunium.net/go/mautrix` | +| **Config** | `MatrixCfg` → `BusCfg{NatsURL,CtrlURL,IdentityPath,Handle,CommandPrefix,Threads}`; loader valida `bus.*` | +| **Interfaces** | `MatrixSender` → `Sender` en `effects/runner.go`, `cron/scheduler.go`, tool | +| **Orquestador** | `shell/orchestration` despojado de mautrix (RoomScanner/scan/membership); aparcado, no se wirea | +| **Transporte** | `shell/transportunibus` pasa al módulo principal (borrado el `go.mod` anidado) y se reescribe **room-based**: descubre (`ListMyRooms` polling) → `Join`+`Subscribe`; ignora sus propios frames; `IsDirectMsg` = room de 2 miembros (GET `/rooms/{id}/members` cacheado); `IsMention` = handle en el body; `Reply` publica en la room de origen. `busSender` adapta runner/cron/tools | +| **Reconexión** | `runtime.go`/`robot.go` construyen el transport desde `BusCfg`, inyectan `busSender`, `Run` arranca `transport.Run(handleInbound)`; `sendReply` por el bus; nueva tool `bus_send` | +| **Bots** | bloque `matrix:` → `bus:` en los 5 `agents/*/config.yaml` | + +## Verificación + +``` +# unibus (Fase 0) +$ cd projects/message_bus/apps/unibus && CGO_ENABLED=0 go test ./... +ok pkg/membership ok pkg/client ok pkg/frame +$ ./fn doctor services-spec | grep unibus # service vivo en la LAN +OK unibus systemd-user 8470 /healthz ... +$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8470/members/x/rooms +200 + +# agents (Matrix-out), en master +$ go build ./... # exit 0, SIN -tags goolm +$ go vet ./... # exit 0 +$ grep -rl maunium --include='*.go' . | grep -v /vendor/ # vacío +$ grep -c maunium go.mod # 0 +$ go test -count=1 ./shell/transportunibus/ ./pkg/transport/ +ok shell/transportunibus (2.3s) ok pkg/transport +``` + +Test e2e clave `TestBotEchoesInEncryptedRoom` (room-based, E2E): un peer "humano" crea room +`ModeMatrix`, invita al bot por su endpoint, publica; el bot descubre/join/subscribe, descifra y +**responde en la misma room** con ancla `ReplyTo`; el humano recibe la respuesta descifrada. + +Suite completa agents: **25 paquetes ok**, 1 FAIL pre-existente ajeno (`shell/logger/TestCleanOldLogs`, +dependiente de fecha contra fixtures de marzo). + +## Gaps / pendientes + +1. **Directorio de bots.** El frontend/humano necesita el `EndpointID` (+ claves públicas) del bot + para invitarlo a una room cifrada. Hoy el bot lo expone (`Transport.BusEndpoint()`) y se invita + a mano; un directorio formal (registro en membershipd o subject de anuncio) es fase posterior. +2. **member-count cacheado sin invalidación**: `IsDirectMsg` se fija la primera vez por room. Si una + room de 2 crece, sigue marcada como DM. Aceptable v1 (la mención sigue disparando el LLM). +3. **Orquestador multi-bot aparcado**: el paquete compila sin mautrix pero no se wirea. Su + reimplementación sobre `GET /rooms/{id}/members` + `bus.AgentMessage` queda para después. +4. **Presencia / typing / avatars**: no existen en unibus v1 → eliminados (no-op). Features nuevas + del bus si se quieren. +5. **Smoke de launcher real**: el e2e valida transport + handler de eco; un arranque del `launcher` + con un Agent real (LLM) contra el service vivo no se ha probado (requiere config LLM + invitar al + bot). Recomendado como siguiente validación manual. +6. **Sin push**: master local en ambos repos; push vía `/full-git-push` del operador. +7. **Carrera git en unibus**: el agente de frontend trabaja en paralelo en el mismo repo unibus; + Fase 0 se consolidó vigilando HEAD. agents_and_robots es repo aparte (sin carrera). diff --git a/reports/assets/unibus_movil.png b/reports/assets/unibus_movil.png new file mode 100644 index 0000000..10c9f50 Binary files /dev/null and b/reports/assets/unibus_movil.png differ diff --git a/reports/assets/unibus_movil_2.png b/reports/assets/unibus_movil_2.png new file mode 100644 index 0000000..ddd70f9 Binary files /dev/null and b/reports/assets/unibus_movil_2.png differ