Crea los tres archivos YAML de configuración de seguridad centralizada en
security/ (user-groups.yaml, agent-groups.yaml, permissions.yaml) y el
loader impuro shell/security/loader.go que los lee y construye un
security.SecurityPolicy puro.
- security/user-groups.yaml: grupos de usuarios (admins, everyone)
- security/agent-groups.yaml: grupos de agentes (assistants, all)
- security/permissions.yaml: políticas de permisos por grupo de agentes
- shell/security/loader.go: Load(dir) → SecurityPolicy; usa structs YAML
intermedios para mantener pkg/security/ libre de gopkg.in/yaml.v3
- shell/security/loader_test.go: 6 tests cubren los casos del issue
(dir inexistente, vacío, 3 YAMLs, solo uno, malformado, wildcards)
El código se mergea con feature flag centralized-security-groups = false
(loader creado, todavía no wired al launcher — eso es 0024c).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Crea el paquete puro pkg/security/ con los tipos base del sistema
centralizado de permisos y la función ResolveACL.
Cambios:
- pkg/acl/config.go: añade FromRoles([]Role) ACL como constructor directo
- pkg/security/groups.go: UserGroup, AgentGroup
- pkg/security/policy.go: Permission, AgentPolicy, SecurityPolicy
- pkg/security/resolver.go: ResolveACL(agentID, SecurityPolicy) → acl.ACL
* soporte wildcard de agente ("*") y de usuario ("*")
* políticas acumulativas: unión de permisos entre grupos
* referencia directa por agentID sin definir grupo
- pkg/security/security_test.go: 7 tests cubriendo todos los casos del issue
El paquete es pure core: cero I/O, cero side effects.
Mergeado con feature flag centralized-security-groups = false (no wired).
Registra el flag 'centralized-security-groups' desactivado. Protege el
código del issue 0024 (sistema centralizado de grupos de usuarios y
agentes) hasta que todas las sub-issues estén completas.
Simplifica el flujo de fix-issue para crear la rama directamente con
git checkout -b en lugar de invocar /git-branch, que añadía una capa
de indirección innecesaria. Añade lógica para detectar si ya estamos
en la rama correcta y continuar sin recrearla.
Actualiza la tabla de estructura del proyecto para reflejar los nuevos
directorios creados en el issue 0025. Añade también README.md en
dev-scripts/cron/ con descripción de cada script y ejemplos de uso.
Implementa issue 0025: catálogo central de automatizaciones cron y scaffolder.
- crons/: directorio de automatizaciones nombradas con README explicando la
convención. Incluye dos ejemplos listos para usar:
· good-morning (send_message, 0 9 * * *) — saludo diario
· daily-summary (llm_prompt, 0 18 * * *) — resumen generado por LLM
- dev-scripts/cron/new.sh: scaffolder interactivo — pregunta nombre,
descripción, tipo de acción y cron expression; crea schedule.yaml +
archivo de prompt vacío; imprime el bloque YAML para copiar en config.yaml.
- dev-scripts/cron/list.sh: lista todas las automatizaciones del catálogo
con nombre, tipo, cron y descripción en formato tabular.
- dev-scripts/cron/apply.sh: añade la automatización al config.yaml del
agente indicado usando yq si está disponible; si no, imprime el bloque
YAML para copiar a mano (sin dependencias obligatorias).
- shell/cron/scheduler.go: exporta Fire(ctx, sc) para disparo inmediato
de un schedule sin esperar al timer cron — útil en tests y CLI.
- shell/cron/scheduler_test.go: cuatro tests nuevos para Fire()
(send_message inline, llm_prompt, sin output_room, sin LLM).
TestScheduler_SkipsInvalidSchedule y TestFire_LLMPrompt_NoLLM_Skips
reemplazados por versiones instantáneas usando Fire en lugar de
@every 100ms + sleep, eliminando ~700ms de tiempo de test.
Se instancia shellcron.Scheduler en agents.New() cuando cfg.Schedules tiene
entradas (scheduler queda nil en agentes sin schedules, sin overhead).
En agents.Run() se arranca el scheduler en una goroutine independiente que
termina cuando el ctx del agente es cancelado — el shutdown es limpio gracias
a cron.Stop() que devuelve un contexto que se espera.
La integración no rompe agentes existentes: el campo scheduler es nil por
defecto y todo el código nuevo está tras if a.scheduler != nil.
Nuevo paquete shell/cron con dos archivos:
shell/cron/scheduler.go — Scheduler struct con método Start(ctx) que:
- Registra todas las entradas de config.ScheduleCfg como jobs de robfig/cron
- Omite schedules sin output_room o sin action.kind (warn en log)
- Bloquea hasta que ctx sea cancelado, luego detiene el cron limpiamente
- Recibe MatrixSender, CompleteFunc y *slog.Logger como dependencias (sin importar agents/)
shell/cron/actions.go — ejecutores para fase 1:
- send_message: resuelve contenido desde Message (inline) o Template (archivo .md),
luego llama a matrix.SendMarkdown
- llm_prompt: resuelve prompt desde Prompt o Template, llama al LLM y envía
la respuesta al room configurado; no-op silencioso si no hay LLM
resolveContent() prioriza texto inline sobre ruta de archivo, lo que permite
tanto mensajes cortos en YAML como prompts largos en archivos .md separados.
Fase 2 (run_tool) y fase 3 (inter-bot) quedan pendientes según el issue.
Se añaden tres campos nuevos a ScheduledAction en internal/config/schema.go:
- Message: texto inline para send_message
- Template: ruta a archivo .md para send_message o llm_prompt
- Prompt: texto inline del prompt para llm_prompt
Se agrega github.com/robfig/cron/v3 v3.0.1 como dependencia.
No hay cambios de ruptura: los campos son opcionales y el schema existente
sigue siendo compatible.
El binario register habia sido commiteado por error antes de estar en .gitignore.
Se elimina del repositorio. En adelante todos los binarios se generan en bin/
mediante build.sh o Makefile, nunca en la raiz del proyecto.
Elimina entradas individuales /launcher, /agentctl, /dashboard, /verify
que eran restos de cuando los binarios se generaban en la raiz del repo.
Los binarios ahora solo se generan en bin/ (cubierto por la entrada bin/).
Agrega run/*.txt para ignorar archivos de texto de run-time.
Añade opciones de Reload (hot-reload) separadas de Restart (reinicio
completo) en el dashboard, usando el mecanismo SIGHUP implementado en
el issue 0013.
Cambios en pkg/tui/ (capa pura):
- IntentReloadAgent: hot-reload de un agente individual via SIGHUP
- IntentReloadAll: hot-reload de todos los agentes via SIGHUP
- AgentActionOptions: añade "Reload" antes de "Restart" con descripciones
clarificadas ("sin interrumpir los demás" vs "launcher completo")
- ServerMenuOptions (running): añade "Reload All" como primera opción
- executeAction: maneja "Reload" → IntentReloadAgent
- executeServerAction: maneja "Reload All" → IntentReloadAll
- Mensajes de estado diferenciados: "Reload OK — X recargado sin
interrupciones" vs "Restart OK — launcher reiniciado"
Cambios en shell/tui/ (capa impura):
- reloadAgent(id): escribe run/reload.txt + SIGHUP; error si launcher
no está corriendo (no hay fallback a full restart)
- reloadAll(): elimina reload.txt + SIGHUP; error si no está corriendo
- restartAgent(id): restaurado a su comportamiento original de
stop+start completo del launcher
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implementa recarga en caliente de un agente individual sin detener el launcher:
- Agent.Stop() + Agent.Done() para ciclo de vida individual del agente
- agentRegistry en el launcher para rastrear agentes vivos y recargarlos
- Handler de SIGHUP: lee run/reload.txt para determinar agente objetivo
- TUI: restartAgent() usa SIGHUP en lugar de kill+restart del launcher
- agentctl reload [agent-id]: nuevo subcomando de hot-reload
- Tests: bus.Unsubscribe, readReloadTarget, Agent.Stop/Done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- shell/bus/bus_test.go: tests de Subscribe/Send/Unsubscribe incluyendo
idempotencia, canal cerrado tras unsubscribe y resubscribe posterior.
- cmd/launcher/registry_test.go: tests para readReloadTarget (archivo
ausente, vacío, '*', agentID, whitespace).
- agents/lifecycle_test.go: tests para Agent.Stop()/Done() verificando
que Stop() desbloquea Run y que es seguro llamarlo múltiples veces o
con cancel nil.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nuevo subcomando: agentctl reload [agent-id]
- Sin argumento: elimina run/reload.txt y envía SIGHUP → todos los
agentes se recargan.
- Con agent-id: escribe run/reload.txt con el ID y envía SIGHUP → solo
ese agente se recarga.
Si el launcher no está corriendo, muestra error claro.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
restartAgent() ahora escribe run/reload.txt con el agentID y envía
SIGHUP al launcher en lugar de matar y reiniciar el proceso completo.
Si el launcher no está corriendo, conserva el comportamiento anterior
(stop + start completo).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implementa el mecanismo de hot-reload descrito en el issue 0013:
- agents/runtime.go: añade Agent.Stop() y Agent.Done() para ciclo de vida
individual. Run() crea un contexto hijo cancelable y cierra el canal
done al retornar.
- cmd/launcher/registry.go (nuevo): agentRegistry rastrea agentes vivos
por ID. Métodos: register, stopAndWait, reload, reloadAll, waitAll,
cleanupLogs. reload() sigue el flujo completo: stop→wait→unsubscribe
→reload config→recreate→rewire bus/orch→start nueva goroutine.
- cmd/launcher/main.go: usa agentRegistry en lugar de sync.WaitGroup.
Añade handler de SIGHUP en goroutine separada que lee run/reload.txt
para determinar el agente objetivo (* o vacío = todos). Tras leer,
borra run/reload.txt para no afectar el siguiente SIGHUP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Importa closeThreadPanel y lo llama en beforeEach tras goToRoom.
Si el thread panel queda abierto de sesiones previas, sus sender elements
contaminarían los locators de waitForBotReply en tests posteriores.
Tres cambios principales en matrix-room.ts:
1. closeThreadPanel: nueva funcion exportada que cierra el panel de thread
si esta abierto. Necesario para evitar que sender elements del panel
contaminen locators de waitForBotReply.
2. waitForBotReply reescrito: usa Matrix SDK (mxMatrixClientPeg) en lugar
de locators DOM para detectar respuestas. Captura startEventId antes de
esperar para solo detectar mensajes NUEVOS. Ignora thread replies
(rel_type=m.thread) y filtra por sender via room.getMember().
Elimina la deteccion de sender por DOM que fallaba cuando thread summaries
inyectaban elementos adicionales en el main timeline.
3. startThreadOnLastMessage reescrito: intenta primero via UI (right-click
en el ultimo EventTile → 'Reply in Thread' en context menu → escribir
en el composer del thread panel). Si el context menu no aparece (modo
headless), cae al fallback SDK que envia el mensaje con m.relates_to
correcto. El test de thread ya pasa con el fallback.
Corrige el bug por el que el agente enviaba respuestas de thread sin m.relates_to en el evento m.room.encrypted exterior. mautrix-go usaba getRelatesTo() que requiere pointer receiver, pero se pasaba el content por valor. Element Web no podia detectar la relacion de thread y mostraba la respuesta en la timeline principal.
El metodo OptionalGetRelatesTo() esta definido con pointer receiver en event.MessageEventContent. Al pasarlo como valor (no puntero) a SendMessageEvent, mautrix-go no puede hacer el cast a event.Relatable, getRelatesTo() retorna nil, y el evento m.room.encrypted exterior queda sin m.relates_to.
Esto causaba que Element Web no viera la relacion de thread en el evento cifrado exterior y mostrara la respuesta del agente en la timeline principal en lugar del thread, incluso cuando el payload descifrado tenia m.relates_to correcto.
Fix: cambiar 'content := event.MessageEventContent{...}' a 'content := &event.MessageEventContent{...}' en los tres metodos de envio. Consistente con el propio uso de mautrix en client.go linea 1161.
Corrige el bug por el que los agentes respondia en la timeline principal en lugar del thread.
Causa raiz: mautrix-go solo copia m.relates_to al payload descifrado si el evento cifrado exterior incluye EncryptedEventContent.RelatesTo, lo cual versiones antiguas de Element no hacen. Los dos mecanismos de deteccion existentes fallaban en este caso.
Solucion: cache de eventos cifrados en Listener usando un sync.Map. Un listener global (OnEvent) captura m.relates_to del evento m.room.encrypted antes de que CryptoHelper lo descifre. El handler EventMessage lo consulta como tercer fallback con LoadAndDelete.
Ademas: correccion del fixture Playwright startThreadOnLastMessage (usaba getRooms()[0] en lugar del room de la URL actual) y waitForThreadReplyViaSdk (acotado al room de la URL para evitar falsos positivos).
Marca el issue 0012-threads como completado. El problema de que el agente respondia en el hilo principal en lugar del thread esta resuelto con el cache de eventos cifrados implementado en esta rama.
Corrige dos bugs en los fixtures de Playwright:
1. startThreadOnLastMessage usaba client.getRooms()[0] (el primer room unido) en lugar del room activo, causando que el mensaje threaded se enviara al room equivocado. Ahora lee el room ID de window.location.hash (#/room/!xxx:server), con resolucion de alias si la URL contiene un alias en lugar de un ID.
2. waitForThreadReplyViaSdk iteraba todos los rooms unidos, lo que podia devolver falsos positivos de otros rooms. Ahora esta acotado al room de la URL actual, con logica de fallback para aliases canonicos y alternativos.
Añade un tercer mecanismo de deteccion de thread en listener.go para cubrir el caso en que mautrix-go no propaga m.relates_to al payload descifrado.
El problema ocurria cuando Element Web (matrix-js-sdk versiones antiguas) no incluia m.relates_to en el contenido exterior del evento m.room.encrypted. mautrix-go solo copia m.relates_to al payload descifrado si EncryptedEventContent.RelatesTo != nil, por lo que los dos mecanismos existentes (raw map + typed content) fallaban.
La solucion registra un listener global (OnEvent) que captura m.relates_to del evento cifrado ANTES de que CryptoHelper lo descifre y re-despache (los listeners globales se ejecutan antes que los de tipo especifico segun DefaultSyncer.Dispatch). El valor se guarda en un sync.Map keyed por event ID y se consume con LoadAndDelete en el handler EventMessage.
asistente-2.spec.ts: usa waitForThreadReplyViaSdk en lugar de sendThreadMessage + waitForThreadReply (que dependian del panel UI). Elimina la importacion de sendThreadMessage. Agrega test.setTimeout(120_000) al test de threads para dar tiempo suficiente al ciclo completo.
login.spec.ts: ampliar locators de room tiles con .mx_RoomTile para mayor compatibilidad con Element Web moderno que no siempre usa role=treeitem.
element-auth.ts: ampliar locator de roomsTree con .mx_RoomList, .mx_LeftPanel_roomListContainer, .mx_RoomTile para detectar sesion existente de forma mas robusta, tanto en loginToElement como en waitForLoginResult.
goToRoom: implementa estrategia doble — primero intenta click directo en el sidebar (mas robusto), luego usa Ctrl+K como fallback. Evita el click en el boton Search que a veces es bloqueado por toasts.
startThreadOnLastMessage: en headless Chromium la hover action bar de Element no se renderiza (es React onMouseEnter, no CSS :hover). Ahora usa el Matrix SDK expuesto en window.mxMatrixClientPeg para enviar el mensaje threaded directamente via API, evitando la dependencia del panel UI.
Nueva funcion waitForThreadReplyViaSdk: consulta el timeline del SDK en lugar de depender del panel de thread UI. Busca eventos con m.relates_to.rel_type === 'm.thread' y filtra por sender si se especifica.
Nueva funcion interna waitForRoomLoaded: espera header del room o el composer como fallback, desacoplando la verificacion de la navegacion.
Mueve la logica de cierre de toasts a un modulo compartido (element-utils.ts) para evitar duplicacion. persistent-context.ts importa dismissAllToasts desde ahi y la invoca despues de verificar el sidebar. Se mejora tambien la deteccion del sidebar usando multiples locators alternativos (mx_RoomList, mx_LeftPanel_roomListContainer, mx_RoomTile) para mayor compatibilidad con distintas versiones de Element Web.
Migra los tests E2E de storageState a persistent browser context para
preservar IndexedDB (crypto keys E2EE). Añade reintentos de login,
screenshots de debug, logging detallado, y helpers de threads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
global-setup.ts:
- Usa launchPersistentContext en vez de browser.newContext()
- Reemplaza storageState por marker file para cache de sesion
- Captura logs de consola del browser para debug
- Screenshots y HTML dump en caso de error
playwright.config.ts:
- Elimina storageState (ahora via persistent context fixture)
- Screenshots siempre activas, video y trace en failures
Tests (login, assistant-bot, asistente-2):
- Importan test/expect desde persistent-context fixture
- Usan handleElementDialogs() en vez de espera manual de rooms
- Nuevo test de threads en asistente-2: verifica que el bot
responde dentro del thread cuando se le habla por thread
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
element-auth.ts:
- Reintentos con backoff para M_LIMIT_EXCEEDED (max 3 intentos)
- Screenshots de debug en cada paso del login
- Deteccion de sesion activa (skip login si ya logueado)
- Manejo robusto de cross-signing con fallback si no hay Done button
- waitForLoginResult() detecta errores, rate limits y exito
matrix-room.ts:
- Logging detallado en goToRoom, sendMessage, waitForBotReply
- Screenshots automaticas en errores y timeouts
- Dump del timeline en timeout para diagnóstico
- Nuevos helpers: startThreadOnLastMessage, sendThreadMessage,
waitForThreadReply, assertBotDidNotReplyInMainTimeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nuevo fixture que crea un worker-scoped persistent browser context
compartido entre tests. A diferencia de storageState, preserva IndexedDB
donde Element Web guarda las crypto keys de E2EE.
Incluye handleElementDialogs() que maneja:
- "Element is open in another window" → click Continue
- "Missing session data" → error informativo
- Espera rooms sidebar como señal de sesion cargada
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nueva pantalla Tests en el menu principal del dashboard para ejecutar
Go tests, E2E tests (headless/headed) y todos secuencialmente.
Reemplaza el "Run Tests" del menu Server por navegacion a la nueva pantalla.
Tests para TestMenuOptions, updateTestsScreen (navegacion, seleccion,
generacion de intents), viewTests (render, cursor, last run),
testKindLabel, testKindIntent, y navegacion desde main/server menus.
Issue: 0023
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nueva seccion "Tests" en el menu principal del dashboard que permite
ejecutar Go tests, E2E tests (headless y headed), y todos secuencialmente.
- ScreenTests con menu de seleccion de tipo de test
- TestKind enum para identificar el tipo de test ejecutado
- Nuevos intents: IntentRunGoTests, IntentRunE2ETests, IntentRunE2EHeadTests, IntentRunAllTests
- LastTestKind en Model para re-ejecucion con "r"
- runGoTests, runE2ETests, runAllTests en adapter
- "Run Tests" en Server menu reemplazado por navegacion a ScreenTests
- Test output muestra tipo de test en titulo y vuelve a ScreenTests con "0"
Issue: 0023
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>