chore: agregar issues 0026-0032 y worktrees a gitignore

Registra los nuevos issues pendientes en el indice y excluye
la carpeta worktrees/ del control de versiones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 23:05:47 +00:00
parent ff7600d3be
commit 8f6958f856
9 changed files with 887 additions and 1 deletions
+3
View File
@@ -19,3 +19,6 @@ e2e/.auth/
e2e/.env e2e/.env
e2e/element-web/ e2e/element-web/
e2e/playwright-report/ e2e/playwright-report/
# Parallel worktrees (parallel-fix-issues skill)
worktrees/
+94
View File
@@ -0,0 +1,94 @@
# 0026 — Refactorizar runtime.go: separar el god object
## Objetivo
Dividir `agents/runtime.go` (1,182 lineas, 25+ metodos) en archivos con responsabilidades claras. Reducir el archivo principal a lifecycle (New, Run, Stop) y delegar el resto a archivos especializados.
## Contexto
- `agents/runtime.go` concentra: lifecycle Matrix, command routing, evaluacion de reglas, invocacion LLM, loop de tool-use, gestion de memoria, carga de prompts, sanitizacion, scheduling, comunicacion inter-agente
- Funciones como `runLLM()` (131 lineas) y `handleEvent()` (100 lineas) tienen complejidad ciclomatica estimada de 10-15
- `New()` tiene 262 lineas de inicializacion secuencial para 10+ subsistemas
- El struct `Agent` tiene 25+ campos — señal de responsabilidad excesiva
- No hay tests para runtime.go, y el tamaño dificulta añadirlos
## Arquitectura
```
agents/runtime.go → solo Agent struct, New(), Run(), Stop() (~200 lineas)
agents/handler.go NEW → handleEvent(), command routing, rule evaluation
agents/llm.go NEW → runLLM(), tool-use loop, system prompt loading
agents/memory.go NEW → window management, persistence, ensureWindowLoaded()
agents/registry_build.go NEW → buildToolRegistry() y toda la logica de registro de tools
agents/commands.go → ya existe, mantener como esta
```
### Patron pure core / impure shell
- `pkg/` — sin cambios (el motor de decisiones ya esta separado)
- `shell/` — sin cambios
- `agents/` — refactoring interno, zero cambios en API publica
## Tareas
### Fase 1: Extraer handler
- [ ] **1.1** Crear `agents/handler.go` con `handleEvent()` y metodos de routing de comandos
- [ ] **1.2** Mover logica de evaluacion de reglas y fallback LLM
- [ ] **1.3** Verificar que `runtime.go` solo llama a `a.handleEvent()` como entry point
### Fase 2: Extraer LLM
- [ ] **2.1** Crear `agents/llm.go` con `runLLM()`, `expandLLMActions()`, logica de system prompt
- [ ] **2.2** Mover el loop de tool-use (iteracion + ejecucion + RBAC check)
- [ ] **2.3** Mover la carga de system prompt desde archivo
### Fase 3: Extraer memoria
- [ ] **3.1** Crear `agents/memory.go` con `ensureWindowLoaded()`, `appendToWindow()`, `persistMessage()`
- [ ] **3.2** Mover la inicializacion de memory store desde `New()`
### Fase 4: Extraer registry builder
- [ ] **4.1** Crear `agents/registry_build.go` con `buildToolRegistry()`
- [ ] **4.2** Mover todo el registro condicional de tools
### Fase 5: Tests
- [ ] **5.1** Tests unitarios para `handleEvent()` con MessageContext mock (command routing)
- [ ] **5.2** Tests unitarios para `runLLM()` con CompleteFunc mock (tool-use loop)
- [ ] **5.3** Tests para `buildToolRegistry()` con configs parciales
### Fase 6: Cleanup
- [ ] **6.1** Verificar que `runtime.go` queda < 300 lineas
- [ ] **6.2** Actualizar imports si es necesario
- [ ] **6.3** `go build -tags goolm ./...` y `go test -tags goolm ./...` pasan
---
## Ejemplo de uso
No hay cambio funcional. Antes y despues:
```go
a, err := agents.New(cfg, rules, logger) // mismo API
a.Run(ctx) // mismo comportamiento
```
Solo cambia la organizacion interna.
## Decisiones de diseno
- **Archivos por responsabilidad, no por tamaño**: cada archivo tiene una razon de existir, no es solo "partir en pedazos"
- **Zero cambios en API publica**: `New()`, `Run()`, `Stop()`, `RegisterCommand()` mantienen firma identica
- **Metodos en Agent struct**: los metodos nuevos siguen siendo metodos del mismo struct, solo viven en otro archivo
## Prerequisitos
- Ninguno
## Riesgos
- **Merge conflicts**: si hay PRs en vuelo que tocan runtime.go, el refactor generara conflictos. Mitigacion: hacerlo en una ventana sin otros cambios pendientes
- **Regresiones**: sin tests previos, los tests E2E son la unica red de seguridad. Mitigacion: correr E2E antes y despues
+112
View File
@@ -0,0 +1,112 @@
# 0027 — Limpiar config schema: eliminar codigo muerto
## Objetivo
Eliminar las secciones del config schema (`internal/config/schema.go`) que no estan implementadas ni referenciadas en el codebase. Reducir de 560 lineas / 61 structs a solo lo que realmente se usa.
## Contexto
- `internal/config/schema.go` tiene 560 lineas y 61 tipos struct
- Secciones **nunca referenciadas** en ningun archivo `.go`:
- `ObservabilityCfg` (metrics, tracing, health) — 0 usos
- `ResilienceCfg` (circuit breaker, retry, queue) — 0 usos
- `AgentsCfg` (peers, delegation, protocol) — 0 usos
- `PersonalityCfg.Communication` (18 campos: humor, quirks, catchphrases) — 0 usos
- El template `_template/config.yaml` tiene 414 lineas cuando un agente real necesita ~40
- Esto complica el onboarding y crea confusion sobre que es funcional vs especulativo
## Arquitectura
```
internal/config/schema.go → eliminar structs muertos (~180 lineas)
agents/_template/config.yaml → reducir a lo esencial (~60 lineas)
```
### Patron pure core / impure shell
- `pkg/` — sin cambios
- `shell/` — sin cambios
- `agents/` — template simplificado
- `internal/config/` — poda de tipos
## Tareas
### Fase 1: Auditar uso real
- [ ] **1.1** Grep cada struct/campo del schema contra todo el codebase para confirmar cuales tienen 0 referencias
- [ ] **1.2** Documentar en este issue la lista final de tipos a eliminar
### Fase 2: Podar schema.go
- [ ] **2.1** Eliminar `ObservabilityCfg` y todos sus sub-structs (LoggingCfg, MetricsCfg, HealthCfg, TracingCfg)
- [ ] **2.2** Eliminar `ResilienceCfg` y sub-structs (CircuitBreakerCfg, RetryCfg, ShutdownCfg, QueueCfg)
- [ ] **2.3** Eliminar `AgentsCfg` y sub-structs (PeerCfg, DelegationCfg)
- [ ] **2.4** Eliminar campos no usados de `PersonalityCfg` (Communication, Humor, Quirks, etc.)
- [ ] **2.5** Verificar que los campos eliminados no rompen el parsing YAML (yaml.v3 ignora campos extra por defecto)
### Fase 3: Simplificar template
- [ ] **3.1** Reescribir `agents/_template/config.yaml` con solo los campos funcionales (~60 lineas)
- [ ] **3.2** Añadir comentarios explicativos en el template para cada seccion
### Fase 4: Tests
- [ ] **4.1** Verificar que los configs existentes (`assistant-bot`, `asistente-2`, `meteorologo`) siguen parseando correctamente
- [ ] **4.2** `go build -tags goolm ./...` compila
- [ ] **4.3** `go test -tags goolm ./...` pasa
### Fase 5: Cleanup
- [ ] **5.1** Actualizar `CLAUDE.md` si se mencionan secciones eliminadas
- [ ] **5.2** Si algun config YAML existente usa campos eliminados, limpiar esas lineas
---
## Ejemplo de uso
Antes (template 414 lineas):
```yaml
personality:
tone: friendly
communication:
formality: informal # nunca se usa
humor: light # nunca se usa
quirks: ["dice 'vale'"] # nunca se usa
observability: # nunca se usa
logging: ...
metrics: ...
resilience: # nunca se usa
circuit_breaker: ...
```
Despues (template ~60 lineas):
```yaml
agent:
id: mi-agente
description: "Descripcion"
personality:
tone: friendly
language: es
llm:
primary:
provider: openai
model: gpt-4o
matrix:
threads:
enabled: true
```
## Decisiones de diseno
- **Eliminar, no comentar**: codigo muerto se borra, no se comenta con "// TODO: implement"
- **Si se necesita en el futuro, se re-añade**: Git tiene historial. No mantener especulacion.
- **yaml.v3 es tolerante**: campos extra en YAML no causan error, asi que eliminar structs no rompe configs existentes que tengan esos campos
## Prerequisitos
- Ninguno
## Riesgos
- **Falso negativo en grep**: algun campo podria usarse via reflection o string matching. Mitigacion: buscar tambien por nombre de campo en strings
- **Configs de usuarios existentes**: si alguien tiene un config con `observability:`, no rompera (yaml.v3 ignora), pero el campo sera silenciosamente ignorado. Esto ya era el caso.
+109
View File
@@ -0,0 +1,109 @@
# 0028 — Desacoplar launcher del registro estatico de agentes
## Objetivo
Eliminar la necesidad de editar `cmd/launcher/main.go` cada vez que se añade un agente. Reemplazar el `rulesRegistry` hard-coded con auto-discovery basado en la convencion de directorios.
## Contexto
- Actualmente `cmd/launcher/main.go` importa cada paquete de agente explicitamente:
```go
import (
assistantagent "github.com/enmanuel/agents/agents/assistant-bot"
asistente2agent "github.com/enmanuel/agents/agents/asistente-2"
)
var rulesRegistry = map[string]func() []decision.Rule{...}
```
- Cada agente nuevo requiere: añadir import + añadir entrada al map + recompilar
- El script `dev-scripts/agent/new-agent.sh` ya modifica el launcher automaticamente, pero es fragil (sed sobre codigo Go)
- Contradiccion: el launcher hace glob de `agents/*/config.yaml` para descubrir configs, pero luego necesita imports estaticos para las reglas
## Arquitectura
```
agents/registry.go NEW → registro global de reglas (init-based)
agents/<id>/agent.go → cada agente se auto-registra via init()
cmd/launcher/main.go → eliminar rulesRegistry, usar agents.GetRules(id)
```
### Patron pure core / impure shell
- `pkg/` — sin cambios
- `shell/` — sin cambios
- `agents/` — nuevo registry global + init() en cada agente
- `cmd/launcher/` — simplificacion
## Tareas
### Fase 1: Crear registry de reglas
- [ ] **1.1** Crear `agents/registry.go` con `Register(id, rulesFn)` y `GetRules(id)`
- [ ] **1.2** Usar sync.Mutex o sync.Map para seguridad en init()
### Fase 2: Migrar agentes a auto-registro
- [ ] **2.1** En `agents/assistant-bot/agent.go` añadir `func init() { agents.Register("assistant-bot", Rules) }`
- [ ] **2.2** Repetir para `asistente-2` y `meteorologo`
- [ ] **2.3** Actualizar `agents/_template/agent.go` con el patron init()
### Fase 3: Simplificar launcher
- [ ] **3.1** Eliminar imports explicitos de agentes en `cmd/launcher/main.go`
- [ ] **3.2** Añadir blank import: `_ "github.com/enmanuel/agents/agents/assistant-bot"` (etc.)
- [ ] **3.3** Reemplazar `rulesRegistry[id]` con `agents.GetRules(id)`
- [ ] **3.4** Si no hay reglas registradas para un agent id, log warning y usar reglas vacias (command-only bot)
### Fase 4: Actualizar scripts
- [ ] **4.1** Simplificar `dev-scripts/agent/new-agent.sh` — ya no necesita editar el map, solo añadir blank import
- [ ] **4.2** Actualizar `.claude/rules/create_agent.md` con el nuevo patron
### Fase 5: Tests
- [ ] **5.1** Test para `agents/registry.go` (register, get, get-missing)
- [ ] **5.2** `go build -tags goolm ./...` compila
- [ ] **5.3** `go test -tags goolm ./...` pasa
### Fase 6: Cleanup
- [ ] **6.1** Actualizar `CLAUDE.md` seccion sobre registro en launcher
- [ ] **6.2** Eliminar codigo muerto del launcher
---
## Ejemplo de uso
Antes (crear agente):
```go
// cmd/launcher/main.go — editar manualmente
import newagent "github.com/enmanuel/agents/agents/new-bot"
var rulesRegistry = map[string]func() []decision.Rule{
"new-bot": newagent.Rules, // añadir esta linea
}
```
Despues:
```go
// agents/new-bot/agent.go — auto-registro
func init() {
agents.Register("new-bot", Rules)
}
// cmd/launcher/main.go — solo blank import
import _ "github.com/enmanuel/agents/agents/new-bot"
```
## Decisiones de diseno
- **init() + blank import**: patron estandar en Go (database/sql drivers, image codecs). Simple y familiar
- **Blank imports en launcher**: siguen siendo estaticos en el codigo, pero son una linea trivial sin logica. El script de scaffolding puede añadirla sin riesgo de romper sintaxis Go
- **No plugin system dinamico**: Go no tiene plugins portables. init() es el mecanismo idomatic
## Prerequisitos
- Ninguno (puede hacerse independiente de otros issues)
## Riesgos
- **Orden de init()**: Go garantiza init() dentro de un paquete, pero no entre paquetes. Mitigacion: el registro es un map simple, el orden no importa
- **Olvidar blank import**: si no se añade el blank import, el agente no se registra y el launcher lo trata como command-only. Mitigacion: el script de scaffolding lo añade automaticamente
+99
View File
@@ -0,0 +1,99 @@
# 0029 — Tests para runtime.go y config loader
## Objetivo
Añadir tests unitarios para las dos piezas criticas del sistema que actualmente tienen 0% de cobertura: `agents/runtime.go` y `internal/config/`. Cubrir al menos los flujos principales (command routing, tool-use loop, config parsing).
## Contexto
- `agents/runtime.go` (1,182 lineas) — 0 test files, 0 coverage
- `internal/config/` (schema.go + loader.go) — 0 test files, 0 coverage
- Los tests existentes cubren bien `pkg/` (puro) y parcialmente `tools/`
- Los unicos tests de integracion son E2E con Playwright (lentos, requieren infra)
- La falta de tests hace que refactors futuros (como 0026) sean arriesgados
## Arquitectura
```
agents/runtime_test.go NEW → tests de handleEvent, runLLM, tool-use loop
agents/lifecycle_test.go → ya existe con tests basicos
internal/config/loader_test.go NEW → tests de parsing y validacion
internal/config/schema_test.go NEW → tests de defaults y campos requeridos
```
### Patron pure core / impure shell
- Los tests de `agents/` usaran mocks/stubs para dependencias impuras (Matrix client, LLM)
- Los tests de `internal/config/` son puros (parsing de YAML)
## Tareas
### Fase 1: Test infrastructure
- [ ] **1.1** Crear helpers de test: mock `CompleteFunc` que devuelve respuestas configurables
- [ ] **1.2** Crear mock Matrix client (o interfaz minima para send)
- [ ] **1.3** Crear fixture de `MessageContext` para tests
### Fase 2: Tests de config
- [ ] **2.1** Test: parsear config YAML minimo (solo campos requeridos)
- [ ] **2.2** Test: parsear config completo con todas las secciones
- [ ] **2.3** Test: expansion de env vars funciona (`$VAR` y `${VAR}`)
- [ ] **2.4** Test: config con campos desconocidos no falla (forward compat)
- [ ] **2.5** Test: valores default se aplican correctamente
### Fase 3: Tests de command routing
- [ ] **3.1** Test: mensaje con `!help` resuelve a built-in command
- [ ] **3.2** Test: mensaje con `!unknown` devuelve error
- [ ] **3.3** Test: comando registrado con `RegisterCommand` se ejecuta
- [ ] **3.4** Test: comando custom no sobrescribe built-in
### Fase 4: Tests de rule evaluation + LLM dispatch
- [ ] **4.1** Test: DM sin reglas → fallback a LLM
- [ ] **4.2** Test: DM sin LLM configurado → ignora mensaje
- [ ] **4.3** Test: regla matchea → ejecuta accion correspondiente
- [ ] **4.4** Test: ActionKindReply genera respuesta estatica
- [ ] **4.5** Test: ActionKindLLM invoca CompleteFunc con mensajes correctos
### Fase 5: Tests de tool-use loop
- [ ] **5.1** Test: LLM responde sin tool calls → devuelve texto
- [ ] **5.2** Test: LLM pide tool call → ejecuta tool → devuelve resultado al LLM → respuesta final
- [ ] **5.3** Test: tool call falla → error se pasa al LLM como tool result
- [ ] **5.4** Test: max iterations se respeta (no loop infinito)
- [ ] **5.5** Test: RBAC deniega tool call → error al LLM
### Fase 6: Cleanup
- [ ] **6.1** Verificar cobertura con `go test -cover -tags goolm ./agents/... ./internal/config/...`
- [ ] **6.2** Objetivo minimo: 50% coverage en ambos paquetes
---
## Ejemplo de uso
```bash
# Correr solo los tests nuevos
go test -tags goolm -v ./agents/ -run TestHandleEvent
go test -tags goolm -v ./internal/config/ -run TestLoadConfig
# Cobertura
go test -tags goolm -cover ./agents/... ./internal/config/...
```
## Decisiones de diseno
- **Mocks simples, no frameworks**: usar funciones Go nativas, no testify/mockery. Mantener dependencias minimas
- **Tests de tabla (table-driven)**: para command routing y rule evaluation, usar sub-tests con nombre descriptivo
- **No testear Matrix I/O**: los tests de runtime usan stubs de send, no conectan a un homeserver
## Prerequisitos
- Idealmente despues de 0026 (split runtime.go), pero puede hacerse antes si se estructura bien
## Riesgos
- **Acoplamiento a internals**: tests de runtime.go dependeran de la estructura actual del Agent struct. Mitigacion: testear comportamiento (input → output), no estado interno
- **Mocks divergen**: si el API de shell/ cambia, los mocks quedan desactualizados. Mitigacion: interfaces minimas
+157
View File
@@ -0,0 +1,157 @@
# 0030 — Separacion Robot vs Agente
## Objetivo
Crear un tipo `Robot` como runtime ligero para bots que solo responden comandos, sin LLM, reglas, memoria ni tools. Distinguir en config entre `type: robot` y `type: agent` para que el launcher sepa que runtime instanciar.
## Contexto
- Actualmente todos los bots usan el mismo `Agent` struct (1,182 lineas) con 25+ subsistemas
- Un bot de comandos simples (ej: `!deploy prod`, `!status`) no necesita LLM, memoria, knowledge, skills, sanitizacion, ni tool-use
- Si `llm.primary.provider` esta vacio, `runtime.go` loguea "running as command-only bot" pero sigue inicializando todo el subsistema
- No hay forma idiomatica de crear un bot simple sin arrastrar toda la complejidad
- Ejemplos de robots: bot de deploys, bot de health checks, bot de notificaciones, bot de CI/CD
## Arquitectura
```
agents/robot.go NEW → Robot struct (~150 lineas): Matrix + Commands
agents/robot_test.go NEW → tests del runtime minimo
agents/types.go NEW → interfaz comun Runner { Run(ctx), Stop(), RegisterCommand() }
cmd/launcher/main.go → detectar type: robot y crear Robot en vez de Agent
internal/config/schema.go → añadir campo Agent.Type ("robot" | "agent")
```
### Patron pure core / impure shell
- `pkg/` — sin cambios (el Robot no usa decision engine ni reglas)
- `shell/matrix/` — sin cambios (el Robot reutiliza el mismo cliente Matrix)
- `agents/robot.go` — impuro (tiene Matrix client), pero minimo
- `agents/runtime.go` — sin cambios (Agent sigue igual)
## Tareas
### Fase 1: Definir interfaz comun
- [ ] **1.1** Crear `agents/types.go` con interfaz `Runner`:
```go
type Runner interface {
Run(ctx context.Context) error
Stop()
RegisterCommand(spec command.Spec, handler CommandHandler)
}
```
- [ ] **1.2** Verificar que `Agent` ya satisface `Runner` (o adaptar)
### Fase 2: Implementar Robot
- [ ] **2.1** Crear `agents/robot.go` con struct `Robot`:
- `matrix *matrix.Client`
- `commands *command.Registry` (built-ins + custom)
- `logger *slog.Logger`
- `config config.AgentConfig`
- [ ] **2.2** Implementar `NewRobot(cfg, logger)` — solo inicializa Matrix + commands
- [ ] **2.3** Implementar `Run()` — sync loop que solo despacha comandos
- [ ] **2.4** Implementar `Stop()` — cierra Matrix client
- [ ] **2.5** Implementar `RegisterCommand()` — registra comando custom
- [ ] **2.6** En `handleEvent()`: si no es comando, ignorar silenciosamente (no hay LLM)
### Fase 3: Config y launcher
- [ ] **3.1** Añadir campo `Type string` a `AgentCfg` en schema.go (default: "agent")
- [ ] **3.2** En launcher: si `cfg.Agent.Type == "robot"`, crear `NewRobot()` en vez de `New()`
- [ ] **3.3** El launcher usa la interfaz `Runner` para manejar ambos tipos uniformemente
### Fase 4: Template y scaffolding
- [ ] **4.1** Crear `agents/_template_robot/` con config minimo para robots
- [ ] **4.2** Config de robot ejemplo (~20 lineas):
```yaml
agent:
id: deploy-bot
type: robot
description: "Bot de deploys"
personality:
prefix: "🤖"
matrix:
threads:
enabled: true
```
- [ ] **4.3** Actualizar `dev-scripts/agent/create-full.sh` para aceptar flag `--robot`
### Fase 5: Tests
- [ ] **5.1** Test: Robot responde a `!help` con lista de comandos
- [ ] **5.2** Test: Robot responde a `!ping` con pong
- [ ] **5.3** Test: Robot ignora mensajes normales (no es error, simplemente no responde)
- [ ] **5.4** Test: Robot con comando custom registrado lo ejecuta
- [ ] **5.5** Test: `Runner` interfaz es satisfecha por ambos `Agent` y `Robot`
### Fase 6: Documentacion
- [ ] **6.1** Actualizar `CLAUDE.md` con seccion Robot vs Agent
- [ ] **6.2** Actualizar `.claude/rules/create_agent.md` mencionando la opcion robot
- [ ] **6.3** Añadir tabla comparativa en docs
---
## Ejemplo de uso
```yaml
# agents/deploy-bot/config.yaml
agent:
id: deploy-bot
type: robot
description: "Bot de deploys con comandos directos"
personality:
prefix: "🚀"
matrix:
homeserver: ${MATRIX_HOMESERVER}
user_id: "@deploy-bot:matrix-af2f3d.organic-machine.com"
access_token_env: MATRIX_TOKEN_DEPLOY_BOT
```
```go
// agents/deploy-bot/commands.go
package deploy
func Commands() []agents.CommandEntry {
return []agents.CommandEntry{
{
Spec: command.Spec{Name: "deploy", Usage: "!deploy <env>"},
Handler: func(ctx context.Context, msg decision.MessageContext) string {
return fmt.Sprintf("Deploying to %s...", msg.Args[0])
},
},
}
}
```
Interaccion en Element:
```
Usuario: !deploy staging
Bot: 🚀 Deploying to staging...
Usuario: hola bot
Bot: (silencio — no tiene LLM)
```
## Decisiones de diseno
- **Interfaz `Runner`**: permite al launcher tratar robots y agentes uniformemente sin type switches
- **Robot NO tiene reglas**: las reglas son para routing inteligente. Un robot solo hace dispatch de comandos
- **Robot NO tiene memory/knowledge/skills**: mantener el runtime lo mas ligero posible
- **Config minimo**: un robot funcional se define en ~20 lineas de YAML
- **Silencio ante mensajes normales**: un robot no responde "no entiendo", simplemente ignora. Los comandos tienen `!help` para descubrirse
## Prerequisitos
- Ninguno (puede hacerse independiente)
- Se beneficia de 0026 (split runtime) pero no lo requiere
## Riesgos
- **Duplicacion**: Robot y Agent comparten logica de Matrix, commands, lifecycle. Mitigacion: reutilizar `shell/matrix/` y `pkg/command/` sin duplicar
- **Scope creep**: tentacion de añadir "un poquito de LLM" al Robot. Mitigacion: la linea es clara — si necesita LLM, es un Agent
+179
View File
@@ -0,0 +1,179 @@
# 0031 — Expandir tools/file/ con write, list, append, delete
## Objetivo
Ampliar el paquete `tools/file/` con operaciones de escritura, listado, append y borrado. Mantener el patron deny-by-default, validacion de symlinks, y respetar el flag `read_only` del config.
## Contexto
- `tools/file/file.go` actualmente solo tiene `read_file` (107 lineas)
- Seguridad existente: deny-by-default, symlink resolution via `EvalSymlinks`, output truncation a 64KB
- Helpers existentes: `validatePath()` y `resolveReal()` ya estan listos para reutilizarse
- Config `FileOpsCfg` tiene campos `AllowedPaths []string` y `ReadOnly bool` — ReadOnly ya existe pero no se usa porque no hay operaciones de escritura
- Los agentes necesitan interactuar con carpetas de trabajo (workspaces, proyectos, outputs)
## Arquitectura
```
tools/file/file.go → mantener read_file + validatePath (existente)
tools/file/write.go NEW → write_file tool
tools/file/list.go NEW → list_directory tool
tools/file/append.go NEW → append_file tool
tools/file/delete.go NEW → delete_file tool
tools/file/file_test.go → ampliar tests existentes
tools/file/write_test.go NEW → tests de escritura
tools/file/list_test.go NEW → tests de listado
tools/file/delete_test.go NEW → tests de borrado
agents/runtime.go → registrar nuevas tools en buildToolRegistry()
```
### Patron pure core / impure shell
- `pkg/` — sin cambios
- `tools/file/` — cada tool sigue el patron Def (puro) + Exec (impuro)
- `agents/` — solo cambio en registro de tools
## Tareas
### Fase 1: Refactor de validacion compartida
- [ ] **1.1** Extraer `validatePath()` y `resolveReal()` a `tools/file/validate.go` (ya son funciones internas, solo moverlas para reutilizarlas)
- [ ] **1.2** Crear helper `validateWritePath(absPath, cfg)` que ademas verifica `ReadOnly == false`
### Fase 2: write_file
- [ ] **2.1** Crear `tools/file/write.go` con `NewWriteFile(cfg) tools.Tool`
- [ ] **2.2** Parametros: `path` (string, required), `content` (string, required)
- [ ] **2.3** Validaciones:
- Rechazar si `cfg.ReadOnly == true`
- `validatePath()` contra AllowedPaths
- Crear directorios padre si no existen (`os.MkdirAll`)
- Limite de contenido: max 1MB de input
- [ ] **2.4** Devolver confirmacion con bytes escritos y path
### Fase 3: list_directory
- [ ] **3.1** Crear `tools/file/list.go` con `NewListDirectory(cfg) tools.Tool`
- [ ] **3.2** Parametros: `path` (string, required), `recursive` (boolean, optional, default false)
- [ ] **3.3** Validaciones:
- `validatePath()` contra AllowedPaths
- Limite de entries: max 500 archivos en el output
- No seguir symlinks fuera de AllowedPaths
- [ ] **3.4** Output: lista con nombre, tamaño, tipo (file/dir), fecha modificacion
### Fase 4: append_file
- [ ] **4.1** Crear `tools/file/append.go` con `NewAppendFile(cfg) tools.Tool`
- [ ] **4.2** Parametros: `path` (string, required), `content` (string, required)
- [ ] **4.3** Validaciones:
- Rechazar si `cfg.ReadOnly == true`
- `validatePath()` contra AllowedPaths
- Si el archivo no existe, crearlo (igual que write)
- Limite de tamaño: verificar que archivo existente + contenido nuevo < 10MB
- [ ] **4.4** Devolver confirmacion con bytes añadidos y tamaño total
### Fase 5: delete_file
- [ ] **5.1** Crear `tools/file/delete.go` con `NewDeleteFile(cfg) tools.Tool`
- [ ] **5.2** Parametros: `path` (string, required)
- [ ] **5.3** Validaciones:
- Rechazar si `cfg.ReadOnly == true`
- `validatePath()` contra AllowedPaths
- **Solo archivos**: no permitir borrar directorios (prevencion de `rm -rf` accidental)
- Resolver symlinks antes de borrar (no borrar el symlink si apunta fuera de AllowedPaths)
- [ ] **5.4** Devolver confirmacion con path eliminado
### Fase 6: Registro en runtime
- [ ] **6.1** En `agents/runtime.go``buildToolRegistry()`, registrar las 4 tools nuevas condicionalmente:
```go
if cfg.Tools.FileOps.Enabled {
reg.Register(file.NewReadFile(cfg.Tools.FileOps))
if !cfg.Tools.FileOps.ReadOnly {
reg.Register(file.NewWriteFile(cfg.Tools.FileOps))
reg.Register(file.NewAppendFile(cfg.Tools.FileOps))
reg.Register(file.NewDeleteFile(cfg.Tools.FileOps))
}
reg.Register(file.NewListDirectory(cfg.Tools.FileOps))
}
```
- [ ] **6.2** `list_directory` se registra siempre (no requiere escritura)
### Fase 7: Tests
- [ ] **7.1** Test: `write_file` crea archivo nuevo en AllowedPaths
- [ ] **7.2** Test: `write_file` rechaza si ReadOnly es true
- [ ] **7.3** Test: `write_file` rechaza paths fuera de AllowedPaths
- [ ] **7.4** Test: `write_file` rechaza contenido > 1MB
- [ ] **7.5** Test: `list_directory` lista correctamente archivos y subdirectorios
- [ ] **7.6** Test: `list_directory` respeta limite de 500 entries
- [ ] **7.7** Test: `list_directory` no sigue symlinks fuera de AllowedPaths
- [ ] **7.8** Test: `append_file` añade contenido al final
- [ ] **7.9** Test: `append_file` crea archivo si no existe
- [ ] **7.10** Test: `delete_file` borra archivo existente
- [ ] **7.11** Test: `delete_file` rechaza borrar directorios
- [ ] **7.12** Test: `delete_file` rechaza si ReadOnly es true
- [ ] **7.13** Test: symlink que apunta fuera de AllowedPaths es rechazado en todas las tools
- [ ] **7.14** Test: path traversal (`../`) es rechazado en todas las tools
### Fase 8: Cleanup
- [ ] **8.1** Actualizar `CLAUDE.md` seccion de tools con las nuevas herramientas
- [ ] **8.2** Actualizar `.claude/rules/create_tool.md` si hay nuevos patrones
- [ ] **8.3** `go build -tags goolm ./...` y `go test -tags goolm ./...`
---
## Ejemplo de uso
Config del agente:
```yaml
tools:
file:
enabled: true
allowed_paths:
- "/home/ubuntu/workspace/proyecto-x"
read_only: false
```
Interaccion en Element:
```
Usuario: Lista los archivos en /home/ubuntu/workspace/proyecto-x/src
Bot: [usa list_directory] Encontre 12 archivos:
- main.go (2.3 KB, 2026-04-01)
- handler.go (1.1 KB, 2026-04-02)
- ...
Usuario: Escribe un archivo test.txt con "hola mundo"
Bot: [usa write_file] Archivo creado: /home/ubuntu/workspace/proyecto-x/test.txt (10 bytes)
Usuario: Añade una linea mas al test.txt
Bot: [usa append_file] Contenido añadido: 15 bytes (total: 25 bytes)
Usuario: Borra el test.txt
Bot: [usa delete_file] Archivo eliminado: /home/ubuntu/workspace/proyecto-x/test.txt
```
Intento fuera de AllowedPaths:
```
Usuario: Lee /etc/passwd
Bot: Error: path "/etc/passwd" not under any allowed path
```
## Decisiones de diseno
- **ReadOnly como gate**: `write_file`, `append_file`, `delete_file` solo se registran si `ReadOnly == false`. `read_file` y `list_directory` siempre se registran si file tools esta habilitado
- **Solo archivos en delete**: borrar directorios es demasiado peligroso para un agente autonomo. Si necesita borrar un directorio, puede borrar archivos uno por uno
- **Limites de tamaño**: 1MB para write (evita saturar disco), 64KB para read output (evita saturar contexto LLM), 500 entries para list (evita listados enormes)
- **Crear padres automaticamente**: `write_file` hace `MkdirAll` para crear la estructura de directorios. Simplifica el uso sin riesgo de seguridad (los paths padre tambien estan bajo AllowedPaths)
- **Reutilizar validatePath()**: misma logica de seguridad para todas las operaciones. Un solo punto de validacion
## Prerequisitos
- Ninguno
## Riesgos
- **Escritura accidental**: un agente con LLM podria decidir escribir archivos incorrectos. Mitigacion: AllowedPaths restringe donde puede escribir, y el system prompt debe instruir al agente sobre cuando escribir
- **Race conditions**: dos agentes escribiendo el mismo archivo. Mitigacion: file locking no es necesario en la primera version; los agentes tipicamente tienen workspaces separados
- **Disk exhaustion**: un agente escribiendo en loop. Mitigacion: rate limiting del tool registry + limite de 1MB por write
+126
View File
@@ -0,0 +1,126 @@
# 0032 — E2E: verificar skill /create-agent con agente de prueba
## Objetivo
Crear un agente de prueba con personalidad muy marcada usando la skill `/create-agent` y escribir tests E2E con Playwright que verifiquen que el agente se creo correctamente y responde en Matrix con la personalidad esperada. Esto valida el pipeline completo: scaffold → build → register → verify → respuesta funcional.
## Contexto
- La skill `/create-agent` existe en `.claude/skills/create-agent/` y ejecuta `create-full.sh` internamente
- Ya hay E2E tests para `assistant-bot` y `asistente-2` en `e2e/tests/`
- Los tests E2E usan Playwright contra Element Web + homeserver real
- No hay tests que validen el pipeline de creacion de agentes — solo se testean agentes ya existentes
- El agente de prueba tendra una personalidad exagerada y facilmente verificable (ej: habla como pirata, responde siempre con rimas, etc.) para que los assertions sean robustos
## Arquitectura
```
agents/test-personality/ NEW — agente creado por /create-agent
agents/test-personality/agent.go NEW — reglas puras (llm-all)
agents/test-personality/config.yaml NEW — config con personalidad marcada
agents/test-personality/prompts/ NEW — system prompt con personalidad exagerada
cmd/launcher/main.go MOD — registro del agente en rulesRegistry
e2e/tests/test-personality.spec.ts NEW — tests E2E del agente
e2e/tests/create-agent-pipeline.spec.ts NEW — tests E2E del pipeline de creacion
```
### Patron pure core / impure shell
- `pkg/` — sin cambios
- `shell/` — sin cambios
- `agents/test-personality/` — composicion: agent.go puro (reglas) + config YAML + system prompt
- `tools/` — sin cambios
- `e2e/` — tests Playwright (fuera del modulo Go)
## Tareas
### Fase 1: Crear agente de prueba con /create-agent
- [ ] **1.1** Ejecutar `/create-agent test-personality "Test Personality"` con los siguientes inputs:
- `agent-id`: `test-personality`
- `display-name`: `"Test Personality"`
- `description`: `"Agente de prueba con personalidad de pirata espacial para validar el pipeline de creacion"`
- `llm.provider`: `openai` (default)
- `llm.model`: `gpt-4o` (default)
- `tool_use`: `false`
- System prompt: personalidad de **pirata espacial** — siempre habla con jerga pirata mezclada con terminos de ciencia ficcion, usa emojis de calavera y cohetes, empieza cada respuesta con "¡Arrr, cosmonauta!" o variante, y termina con "¡Que la marea estelar te acompane!"
- [ ] **1.2** Verificar que `create-full.sh` completa las 4 etapas sin errores (scaffold, build, register, verify)
- [ ] **1.3** Personalizar `agents/test-personality/prompts/system.md` con la personalidad de pirata espacial (bien exagerada para que sea facilmente detectable en tests)
- [ ] **1.4** Verificar compilacion: `go build -tags goolm ./...`
- [ ] **1.5** Arrancar el servidor y verificar que el agente responde en Matrix: `./dev-scripts/server/start.sh`
### Fase 2: E2E tests del agente
- [ ] **2.1** Crear `e2e/tests/test-personality.spec.ts` con los siguientes tests:
- **Responde a saludo**: enviar "Hola" → verificar que la respuesta contiene jerga pirata/espacial (keywords: "arrr", "cosmonauta", "estelar", "marea", o similares)
- **Personalidad consistente**: enviar pregunta seria ("Que es la gravedad?") → verificar que responde con contenido correcto pero manteniendo la personalidad (jerga pirata/espacial presente)
- **!help funciona**: enviar `!help` → verificar que lista comandos (built-in commands)
- **!ping funciona**: enviar `!ping` → verificar respuesta
- **Sin errores de descifrado**: verificar `assertNoDecryptionErrors` en cada test
- [ ] **2.2** Seguir el patron de los tests existentes (`assistant-bot.spec.ts`) para fixtures, imports y estructura
- [ ] **2.3** Ejecutar los tests y verificar que pasan: `./dev-scripts/e2e/run.sh test-personality`
### Fase 3: E2E test del pipeline de creacion (validacion estructural)
- [ ] **3.1** Crear `e2e/tests/create-agent-pipeline.spec.ts` (o un test dentro de `test-personality.spec.ts`) que valide la estructura generada por el pipeline:
- Verificar que `agents/test-personality/agent.go` existe y exporta `Rules()`
- Verificar que `agents/test-personality/config.yaml` tiene `agent.id: test-personality`
- Verificar que `agents/test-personality/prompts/system.md` contiene la seccion de seguridad obligatoria
- Verificar que `cmd/launcher/main.go` contiene el import y la entrada en `rulesRegistry`
- [ ] **3.2** Estos tests pueden ser scripts bash o tests de Node.js que lean los archivos — no requieren Playwright
### Fase 4: Tests
- [ ] **4.1** Ejecutar suite E2E completa: `./dev-scripts/e2e/run.sh` (todos los tests, incluyendo los nuevos)
- [ ] **4.2** Verificar que los tests existentes de `assistant-bot` y `asistente-2` siguen pasando (no regresion)
- [ ] **4.3** Verificar build completo: `go build -tags goolm ./...` y `go test -tags goolm ./...`
### Fase 5: Cleanup y docs
- [ ] **5.1** Actualizar `CLAUDE.md` tabla de agentes con `test-personality`
- [ ] **5.2** Documentar en `e2e/README.md` el nuevo spec y la estrategia de personalidad para tests
---
## Ejemplo de uso
```
# 1. Crear el agente con la skill
> /create-agent test-personality "Test Personality"
(skill ejecuta create-full.sh, personaliza archivos)
# 2. Arrancar y probar manualmente
> ./dev-scripts/server/start.sh
> (en Matrix) Hola!
< ¡Arrr, cosmonauta! 🏴‍☠️🚀 Bienvenido a bordo de la nave...
¡Que la marea estelar te acompane!
# 3. Correr E2E
> ./dev-scripts/e2e/run.sh test-personality
✓ responde con personalidad de pirata espacial (15s)
✓ personalidad consistente en respuestas serias (18s)
✓ !help muestra comandos (3s)
✓ !ping responde (2s)
4 passed
```
## Decisiones de diseno
- **Pirata espacial como personalidad**: es suficientemente exagerada para generar keywords detectables (arrr, cosmonauta, estelar, marea) pero no tan absurda como para que el LLM la ignore. Las assertions buscan presencia de al menos una keyword de un set, no matching exacto.
- **Assertions flexibles para LLM**: las respuestas LLM son no-deterministicas, asi que verificamos presencia de keywords del tema pirata/espacial, no texto exacto. Para `!help` y `!ping` si usamos assertions estrictas (son comandos deterministicos).
- **Test de pipeline como script separado**: la validacion estructural (archivos existen, config correcto) no necesita Playwright, asi que puede ser un test de Node.js simple o bash script. Esto lo hace mas rapido y mas facil de debuggear.
- **Agente permanente**: el agente de prueba se queda en el repo como agente real. Sirve como referencia de creacion y como target permanente para E2E tests del pipeline.
## Prerequisitos
- E2E infrastructure funcionando (issue 0022 completado)
- Skill `/create-agent` funcionando (ya existe en `.claude/skills/create-agent/`)
- Variables de entorno del homeserver configuradas (`MATRIX_ADMIN_TOKEN`, etc.)
- Element Web disponible para tests Playwright
## Riesgos
- **LLM no respeta personalidad**: mitigacion — system prompt muy explicito y exagerado, keywords amplias (buscar cualquiera de un set, no todas)
- **Rate limits del LLM**: mitigacion — pocos tests con respuesta LLM (2-3), el resto son comandos directos
- **create-full.sh falla por estado previo**: mitigacion — verificar que no exista `agents/test-personality/` antes de ejecutar, o limpiar si existe
- **Flakiness en E2E por timing**: mitigacion — timeouts generosos (60s para LLM), reintentos en el pipeline de Playwright
+7
View File
@@ -36,3 +36,10 @@ afectados y notas de implementacion.
| 24b | Security loader: shell/security/ | [0024b-security-loader.md](completed/0024b-security-loader.md) | completado | | 24b | Security loader: shell/security/ | [0024b-security-loader.md](completed/0024b-security-loader.md) | completado |
| 24c | Security integration + cleanup | [0024c-security-integration.md](completed/0024c-security-integration.md) | completado | | 24c | Security integration + cleanup | [0024c-security-integration.md](completed/0024c-security-integration.md) | completado |
| 25 | Catálogo cron + scaffolder | [0025-cron-scaffolder.md](completed/0025-cron-scaffolder.md) | completado | | 25 | Catálogo cron + scaffolder | [0025-cron-scaffolder.md](completed/0025-cron-scaffolder.md) | completado |
| 26 | Refactorizar runtime.go | [0026-split-runtime.md](0026-split-runtime.md) | pendiente |
| 27 | Limpiar config schema | [0027-prune-config-schema.md](0027-prune-config-schema.md) | pendiente |
| 28 | Desacoplar launcher | [0028-decouple-launcher.md](0028-decouple-launcher.md) | pendiente |
| 29 | Tests para runtime y config | [0029-core-tests.md](0029-core-tests.md) | pendiente |
| 30 | Separacion Robot vs Agente | [0030-robot-vs-agent.md](0030-robot-vs-agent.md) | pendiente |
| 31 | Expandir tools/file/ | [0031-expand-file-tools.md](0031-expand-file-tools.md) | pendiente |
| 32 | E2E: verificar skill /create-agent | [0032-e2e-create-agent-skill.md](0032-e2e-create-agent-skill.md) | pendiente |