chore: auto-commit (1 archivos)

- reports/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:42:32 +02:00
parent 13238bc139
commit 57a1602e8f
5 changed files with 393 additions and 0 deletions
@@ -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.<name>.{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.
@@ -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 }`.
@@ -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).
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB