From 0cd7e36a1411a6c273ae3700685b023ce5d8e451 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 8 Apr 2026 23:00:08 +0000 Subject: [PATCH 1/4] feat: desacoplar launcher del registro estatico de agentes Introduce un registro global de reglas en agents/registry.go con Register() y GetRules(). Cada paquete de agente se auto-registra via init(), eliminando la necesidad de editar manualmente el map rulesRegistry en cmd/launcher/main.go. Cambios: - agents/registry.go: nuevo registro global con sync.RWMutex - agents/*/agent.go: cada agente llama agents.Register() en init() - agents/_template/agent.go: placeholder AGENT_ID_PLACEHOLDER para scaffold - cmd/launcher/main.go: elimina rulesRegistry, usa blank imports + agents.GetRules() para obtener reglas por agent ID Patron: init() + blank import (estandar Go: database/sql, image codecs) Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/_template/agent.go | 10 +++++- agents/asistente-2/agent.go | 5 +++ agents/assistant-bot/agent.go | 5 +++ agents/meteorologo/agent.go | 5 +++ agents/registry.go | 61 +++++++++++++++++++++++++++++++++++ cmd/launcher/main.go | 25 +++++++------- 6 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 agents/registry.go diff --git a/agents/_template/agent.go b/agents/_template/agent.go index 5b5c706..1f6ee40 100644 --- a/agents/_template/agent.go +++ b/agents/_template/agent.go @@ -1,8 +1,16 @@ // Package _template es un agente plantilla (no lanzable). // Sirve como referencia canonica para crear nuevos agentes. +// Al crear un nuevo agente, new-agent.sh reemplaza _template y AGENT_ID_PLACEHOLDER. package _template -import "github.com/enmanuel/agents/pkg/decision" +import ( + "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/pkg/decision" +) + +func init() { + agents.Register("AGENT_ID_PLACEHOLDER", Rules) +} // Rules devuelve las reglas de este agente (vacio para el template). func Rules() []decision.Rule { diff --git a/agents/asistente-2/agent.go b/agents/asistente-2/agent.go index de69ea3..75547c8 100644 --- a/agents/asistente-2/agent.go +++ b/agents/asistente-2/agent.go @@ -3,9 +3,14 @@ package asistente2 import ( + "github.com/enmanuel/agents/agents" "github.com/enmanuel/agents/pkg/decision" ) +func init() { + agents.Register("asistente-2", Rules) +} + // Rules returns the decision rules for the asistente-2 bot. // Note: !help is now handled by the built-in command system. func Rules() []decision.Rule { diff --git a/agents/assistant-bot/agent.go b/agents/assistant-bot/agent.go index 203dfd6..aff28d7 100644 --- a/agents/assistant-bot/agent.go +++ b/agents/assistant-bot/agent.go @@ -3,9 +3,14 @@ package assistant import ( + "github.com/enmanuel/agents/agents" "github.com/enmanuel/agents/pkg/decision" ) +func init() { + agents.Register("assistant-bot", Rules) +} + // Rules returns the decision rules for the assistant bot. // Note: !help is now handled by the built-in command system. func Rules() []decision.Rule { diff --git a/agents/meteorologo/agent.go b/agents/meteorologo/agent.go index 7b7d649..d221ed6 100644 --- a/agents/meteorologo/agent.go +++ b/agents/meteorologo/agent.go @@ -3,9 +3,14 @@ package meteorologo import ( + "github.com/enmanuel/agents/agents" "github.com/enmanuel/agents/pkg/decision" ) +func init() { + agents.Register("meteorologo", Rules) +} + // Rules returns the decision rules for the meteorologo bot. func Rules() []decision.Rule { return []decision.Rule{ diff --git a/agents/registry.go b/agents/registry.go new file mode 100644 index 0000000..c941a58 --- /dev/null +++ b/agents/registry.go @@ -0,0 +1,61 @@ +// Package agents provides a global registry for agent rule factories. +// +// Each agent package self-registers via init() using Register. +// The launcher retrieves rules via GetRules without importing agent +// packages explicitly (only blank imports are needed). +package agents + +import ( + "sync" + + "github.com/enmanuel/agents/pkg/decision" +) + +// RulesFunc is a factory that returns the decision rules for an agent. +type RulesFunc func() []decision.Rule + +var ( + registryMu sync.RWMutex + registry = make(map[string]RulesFunc) +) + +// Register adds a rule factory for the given agent ID. +// Intended to be called from init() in each agent package. +// Panics if the same ID is registered twice (catches copy-paste errors early). +func Register(id string, fn RulesFunc) { + registryMu.Lock() + defer registryMu.Unlock() + + if _, exists := registry[id]; exists { + panic("agents.Register: duplicate agent id: " + id) + } + registry[id] = fn +} + +// GetRules returns the rule factory for the given agent ID. +// Returns nil if no rules are registered (the agent is command-only). +func GetRules(id string) RulesFunc { + registryMu.RLock() + defer registryMu.RUnlock() + return registry[id] +} + +// RegisteredIDs returns a sorted list of all registered agent IDs. +// Useful for debugging and diagnostics. +func RegisteredIDs() []string { + registryMu.RLock() + defer registryMu.RUnlock() + + ids := make([]string, 0, len(registry)) + for id := range registry { + ids = append(ids, id) + } + return ids +} + +// resetRegistry clears all registrations (for testing only). +func resetRegistry() { + registryMu.Lock() + defer registryMu.Unlock() + registry = make(map[string]RulesFunc) +} diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index f750b58..f080f17 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -20,9 +20,6 @@ import ( "github.com/spf13/cobra" "github.com/enmanuel/agents/agents" - assistantagent "github.com/enmanuel/agents/agents/assistant-bot" - asistente2agent "github.com/enmanuel/agents/agents/asistente-2" - meteorologoagent "github.com/enmanuel/agents/agents/meteorologo" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" @@ -31,15 +28,12 @@ import ( agentlog "github.com/enmanuel/agents/shell/logger" orchshell "github.com/enmanuel/agents/shell/orchestration" shellsecurity "github.com/enmanuel/agents/shell/security" -) -// rulesRegistry maps agent IDs to their rule factories. -// Add a new entry here when you create a new agent package. -var rulesRegistry = map[string]func() []decision.Rule{ - "assistant-bot": assistantagent.Rules, - "asistente-2": asistente2agent.Rules, - "meteorologo": meteorologoagent.Rules, -} + // Blank imports: each agent self-registers its rules via init(). + _ "github.com/enmanuel/agents/agents/assistant-bot" + _ "github.com/enmanuel/agents/agents/asistente-2" + _ "github.com/enmanuel/agents/agents/meteorologo" +) func main() { var ( @@ -289,10 +283,13 @@ func startOrchestrator(agentBus *bus.Bus, logger *slog.Logger) (*orchHandle, err return &orchHandle{orchestrator: orch, cfg: cfg}, nil } +// rulesFor retrieves the rule factory for the given agent ID from the +// global registry (populated by init() in each agent package). +// Returns nil if no rules are registered (command-only bot). func rulesFor(agentID string, logger *slog.Logger) []decision.Rule { - factory, ok := rulesRegistry[agentID] - if !ok { - logger.Warn("no rules registered for agent, using empty ruleset", "id", agentID) + factory := agents.GetRules(agentID) + if factory == nil { + logger.Warn("no rules registered for agent, using empty ruleset (command-only)", "id", agentID) return nil } return factory() From 03742409de0c4a300c56b61a103343717e3f55d5 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 8 Apr 2026 23:01:51 +0000 Subject: [PATCH 2/4] chore: actualizar script y docs para auto-registro de agentes - new-agent.sh: reemplaza edicion del rulesRegistry map con insercion de un blank import simple. Ahora tambien sustituye AGENT_ID_PLACEHOLDER en agent.go con el ID real del agente. - create_agent.md: actualiza template de agent.go con patron init() + agents.Register(), secciones de registro en launcher y checklist. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/create_agent.md | 30 +++++++++++++++++------------- dev-scripts/agent/new-agent.sh | 32 ++++++++++---------------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/.claude/rules/create_agent.md b/.claude/rules/create_agent.md index 9fc2d5f..a2864bb 100644 --- a/.claude/rules/create_agent.md +++ b/.claude/rules/create_agent.md @@ -36,7 +36,14 @@ Template base (generado por el scaffold): ```go package // sin guiones: "monitor-bot" → package monitor (strip hyphens, strip _bot) -import "github.com/enmanuel/agents/pkg/decision" +import ( + "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/pkg/decision" +) + +func init() { + agents.Register("", Rules) +} func Rules() []decision.Rule { return []decision.Rule{ @@ -56,7 +63,8 @@ func Rules() []decision.Rule { ``` **Reglas estrictas:** -- **PURO**: solo imports de `pkg/decision`, cero I/O, cero side effects +- **PURO**: solo imports de `pkg/decision` y `agents` (para Register), cero I/O, cero side effects +- **Auto-registro**: cada agente se registra via `init()` con `agents.Register("", Rules)` - Package name = ID sin guiones ni `_bot` (e.g. `monitor-bot` → `package monitor`) - **No usar reglas para comandos** (`!help`, `!ping`, etc.) — los comandos se gestionan via `RegisterCommand` (ver policy `create_command.md`) - Las reglas solo aplican a mensajes normales (sin prefijo `!`) @@ -139,18 +147,14 @@ Ejemplo de referencia: `agents/asistente-2/prompts/system.md` El script `new-agent.sh` (ejecutado por `create-full.sh`) hace esto automáticamente. Si falla, hacer manualmente: -**Import** (después de los imports de agentes existentes): +**Blank import** (en la sección de blank imports de agentes): ```go -agent "github.com/enmanuel/agents/agents/" +_ "github.com/enmanuel/agents/agents/" ``` -**rulesRegistry** (dentro del map): -```go -"": agent.Rules, -``` - -El `` es el package name del agent.go (sin guiones, sin `_bot`). -**El ID en rulesRegistry DEBE coincidir exactamente con `agent.id` en config.yaml.** +Las reglas se registran automáticamente via `init()` en el paquete del agente. +No se necesita editar ningún map ni registry manualmente. +**El ID en `agents.Register()` DEBE coincidir exactamente con `agent.id` en config.yaml.** ## Convención de env vars — REGLA CRÍTICA @@ -170,7 +174,7 @@ Checklist a verificar antes de considerar el agente listo: - [ ] `go build -tags goolm ./...` compila sin errores - [ ] `agents//agent.go` exporta `Rules()` y es puro (sin I/O) - [ ] `agents//config.yaml` tiene `agent.id` = nombre del directorio -- [ ] `cmd/launcher/main.go` tiene import + entry en rulesRegistry con el mismo ID +- [ ] `cmd/launcher/main.go` tiene blank import del paquete del agente - [ ] `.env` contiene: `MATRIX_TOKEN_`, `MATRIX_PASSWORD_`, `PICKLE_KEY_`, `SSSS_RECOVERY_KEY_` - [ ] `prompts/system.md` tiene contenido real (no el stub) - [ ] `prompts/system.md` incluye la seccion de seguridad anti-injection (de `.claude/templates/security-prompt.md`) @@ -204,7 +208,7 @@ tail -f run/launcher.log - **Nunca** side effects en `agent.go` - **Siempre** compilar con `-tags goolm` -- **Siempre** que `agent.id` coincida entre config.yaml, rulesRegistry y directorio +- **Siempre** que `agent.id` coincida entre config.yaml, `agents.Register()` y directorio - **No** crear `data/` manualmente — se auto-genera - **No** commitear tokens ni passwords - **No** compartir crypto stores entre agentes diff --git a/dev-scripts/agent/new-agent.sh b/dev-scripts/agent/new-agent.sh index 5f52024..26e76e0 100755 --- a/dev-scripts/agent/new-agent.sh +++ b/dev-scripts/agent/new-agent.sh @@ -310,6 +310,7 @@ YAML cp "$TEMPLATE/agent.go" "$DIR/agent.go" sed -i "s/_template/$PACKAGE/g" "$DIR/agent.go" sed -i "s/Package _template/Package $PACKAGE/g" "$DIR/agent.go" +sed -i "s/AGENT_ID_PLACEHOLDER/$ID/g" "$DIR/agent.go" ok "agent.go creado desde template" # ── Copiar prompts/system.md desde template y personalizar ─────────────── @@ -320,21 +321,21 @@ ok "prompts/system.md creado desde template" ok "Scaffold creado en $DIR/" echo "" -# ── Actualizar cmd/launcher/main.go ─────────────────────────────────────── +# ── Actualizar cmd/launcher/main.go — añadir blank import ──────────────── LAUNCHER="cmd/launcher/main.go" +BLANK_IMPORT="_ \"github.com/enmanuel/agents/agents/$ID\"" -if grep -q "\"$ID\":" "$LAUNCHER" 2>/dev/null; then - warn "$ID ya está en rulesRegistry de $LAUNCHER — saltando" +if grep -q "agents/$ID\"" "$LAUNCHER" 2>/dev/null; then + warn "$ID ya tiene blank import en $LAUNCHER — saltando" else TAB=$'\t' - IMPORT_LINE="${TAB}${PACKAGE}agent \"github.com/enmanuel/agents/agents/$ID\"" - REGISTRY_LINE="${TAB}\"$ID\": ${PACKAGE}agent.Rules," + IMPORT_LINE="${TAB}${BLANK_IMPORT}" - # Insertar import después del último import agents/agents/* + # Insertar blank import después del último blank import de agents/ if awk -v new_import="$IMPORT_LINE" ' { lines[NR] = $0 - if ($0 ~ /[a-z_]+agent "github\.com\/enmanuel\/agents\/agents\/[^"]+"/) + if ($0 ~ /_ "github\.com\/enmanuel\/agents\/agents\//) last_import = NR } END { @@ -346,24 +347,11 @@ else } ' "$LAUNCHER" > /tmp/_launcher_tmp; then mv /tmp/_launcher_tmp "$LAUNCHER" - ok "Import añadido en $LAUNCHER" + ok "Blank import añadido en $LAUNCHER" else - warn "No se pudo insertar el import automáticamente — añádelo manualmente:" + warn "No se pudo insertar el blank import — añádelo manualmente:" echo -e " ${GRN}${IMPORT_LINE}${RST}" fi - - # Insertar entry en rulesRegistry antes del cierre } - if awk -v new_entry="$REGISTRY_LINE" ' - /^var rulesRegistry/ { in_reg = 1 } - in_reg && /^\}/ { print new_entry; in_reg = 0 } - { print } - ' "$LAUNCHER" > /tmp/_launcher_tmp; then - mv /tmp/_launcher_tmp "$LAUNCHER" - ok "Registry entry añadida en $LAUNCHER" - else - warn "No se pudo insertar el registry entry — añádelo manualmente:" - echo -e " ${GRN}${REGISTRY_LINE}${RST}" - fi fi echo "" From 57affc2e4416a23e976b525c34cf79f7942e626e Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 8 Apr 2026 23:02:48 +0000 Subject: [PATCH 3/4] test: tests para agents/registry.go Cobertura completa del registro global de reglas: - Register + GetRules: registro exitoso y recuperacion - GetRules con ID inexistente: retorna nil - Register duplicado: panic con mensaje descriptivo - RegisteredIDs: retorna todos los IDs registrados - resetRegistry: limpia el registro (helper para tests) Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/registry_test.go | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 agents/registry_test.go diff --git a/agents/registry_test.go b/agents/registry_test.go new file mode 100644 index 0000000..5e71f80 --- /dev/null +++ b/agents/registry_test.go @@ -0,0 +1,104 @@ +package agents + +import ( + "sort" + "testing" + + "github.com/enmanuel/agents/pkg/decision" +) + +func TestRegisterAndGetRules(t *testing.T) { + resetRegistry() + + called := false + fn := func() []decision.Rule { + called = true + return []decision.Rule{{Name: "test-rule"}} + } + + Register("test-agent", fn) + + got := GetRules("test-agent") + if got == nil { + t.Fatal("GetRules returned nil for registered agent") + } + + rules := got() + if !called { + t.Error("rule factory was not called") + } + if len(rules) != 1 || rules[0].Name != "test-rule" { + t.Errorf("unexpected rules: %+v", rules) + } +} + +func TestGetRulesMissing(t *testing.T) { + resetRegistry() + + got := GetRules("nonexistent") + if got != nil { + t.Errorf("expected nil for unregistered agent, got %v", got) + } +} + +func TestRegisterDuplicatePanics(t *testing.T) { + resetRegistry() + + fn := func() []decision.Rule { return nil } + Register("dup-agent", fn) + + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic on duplicate registration, got none") + } + msg, ok := r.(string) + if !ok { + t.Fatalf("expected string panic, got %T: %v", r, r) + } + if msg != "agents.Register: duplicate agent id: dup-agent" { + t.Errorf("unexpected panic message: %s", msg) + } + }() + + Register("dup-agent", fn) +} + +func TestRegisteredIDs(t *testing.T) { + resetRegistry() + + Register("charlie", func() []decision.Rule { return nil }) + Register("alpha", func() []decision.Rule { return nil }) + Register("bravo", func() []decision.Rule { return nil }) + + ids := RegisteredIDs() + sort.Strings(ids) + + expected := []string{"alpha", "bravo", "charlie"} + if len(ids) != len(expected) { + t.Fatalf("expected %d ids, got %d: %v", len(expected), len(ids), ids) + } + for i, id := range ids { + if id != expected[i] { + t.Errorf("id[%d] = %q, want %q", i, id, expected[i]) + } + } +} + +func TestResetRegistry(t *testing.T) { + resetRegistry() + + Register("temp", func() []decision.Rule { return nil }) + if GetRules("temp") == nil { + t.Fatal("expected registered agent") + } + + resetRegistry() + + if GetRules("temp") != nil { + t.Error("expected nil after reset") + } + if len(RegisteredIDs()) != 0 { + t.Error("expected empty registry after reset") + } +} From 47e169a5b9125f8e742083df823d4d5d01b888f7 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 8 Apr 2026 23:03:15 +0000 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20cerrar=20issue=200028=20=E2=80=94?= =?UTF-8?q?=20desacoplar=20launcher=20del=20registro=20estatico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mover issue a completed/ y actualizar README con estado completado. Co-Authored-By: Claude Opus 4.6 (1M context) --- dev/issues/README.md | 1 + .../completed/0028-decouple-launcher.md | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 dev/issues/completed/0028-decouple-launcher.md diff --git a/dev/issues/README.md b/dev/issues/README.md index 235ad36..d16e387 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -36,3 +36,4 @@ afectados y notas de implementacion. | 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 | | 25 | Catálogo cron + scaffolder | [0025-cron-scaffolder.md](completed/0025-cron-scaffolder.md) | completado | +| 28 | Desacoplar launcher del registro | [0028-decouple-launcher.md](completed/0028-decouple-launcher.md) | completado | diff --git a/dev/issues/completed/0028-decouple-launcher.md b/dev/issues/completed/0028-decouple-launcher.md new file mode 100644 index 0000000..ef3af62 --- /dev/null +++ b/dev/issues/completed/0028-decouple-launcher.md @@ -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//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