From 899108e3f873ed5c033c037b7c906604a7392ce1 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:24:35 +0000 Subject: [PATCH 1/7] feat: crear robot test-bot para validar pipeline de creacion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Robot command-only (type: robot) sin LLM. Scaffold via create-full.sh, registrado en Matrix con E2EE verificado. Config minimo basado en template_robot con encryption habilitada. Archivos: - agents/test-bot/agent.go — Rules() retorna nil (robot) - agents/test-bot/config.yaml — tipo robot, sin LLM - cmd/launcher/main.go — blank import añadido Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/test-bot/agent.go | 18 +++++++++ agents/test-bot/config.yaml | 80 +++++++++++++++++++++++++++++++++++++ cmd/launcher/main.go | 1 + 3 files changed, 99 insertions(+) create mode 100644 agents/test-bot/agent.go create mode 100644 agents/test-bot/config.yaml diff --git a/agents/test-bot/agent.go b/agents/test-bot/agent.go new file mode 100644 index 0000000..eaa2b79 --- /dev/null +++ b/agents/test-bot/agent.go @@ -0,0 +1,18 @@ +// Package test es un agente plantilla (no lanzable). +// Sirve como referencia canonica para crear nuevos agentes. +// Al crear un nuevo agente, new-agent.sh reemplaza test y test-bot. +package test + +import ( + "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/pkg/decision" +) + +func init() { + agents.Register("test-bot", Rules) +} + +// Rules devuelve las reglas de este agente (vacio para el template). +func Rules() []decision.Rule { + return nil +} diff --git a/agents/test-bot/config.yaml b/agents/test-bot/config.yaml new file mode 100644 index 0000000..827bca2 --- /dev/null +++ b/agents/test-bot/config.yaml @@ -0,0 +1,80 @@ +# ============================================ +# TEST-BOT — Robot de prueba (command-only, sin LLM) +# ============================================ +# Robot para validar el pipeline de creacion de bots y E2E tests. +# Solo responde a comandos (!xxx). Mensajes normales se ignoran. + +agent: + id: test-bot + name: "Test Bot" + version: "0.1.0" + type: robot + enabled: true + template: false + description: "Robot de prueba para validar el pipeline de creacion de bots" + tags: [test, robot] + +# ============================================ +# PERSONALIDAD (minima para robots) +# ============================================ +personality: + prefix: "" + language: es + +# ============================================ +# MATRIX +# ============================================ +matrix: + homeserver: "${MATRIX_HOMESERVER}" + user_id: "@test-bot:${MATRIX_SERVER_NAME}" + access_token_env: MATRIX_TOKEN_TEST_BOT + device_id: "HXINOYBBUW" + + encryption: + enabled: true + store_path: "./agents/test-bot/data/crypto/" + pickle_key_env: PICKLE_KEY_TEST_BOT + trust_mode: tofu + recovery_key_env: SSSS_RECOVERY_KEY_TEST_BOT + + rooms: + listen: [] + respond: [] + admin: [] + + filters: + command_prefix: "!" + mention_respond: false + dm_respond: false + ignore_bots: true + ignore_users: [] + unauthorized_response: silent + min_power_level: 0 + + threads: + enabled: true + auto_thread: false + +# ============================================ +# SEGURIDAD +# ============================================ +security: + audit: + enabled: false + log_file: "" + log_to_room: "" + include: [] + + secrets: + provider: env + + sanitize: + enabled: false + mode: warn + min_severity: medium + disabled_patterns: [] + + tool_rate_limit: + enabled: false + max_calls_per_min: 10 + cleanup_interval_s: 60 diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 678b04b..d7927dc 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -33,6 +33,7 @@ import ( _ "github.com/enmanuel/agents/agents/assistant-bot" _ "github.com/enmanuel/agents/agents/asistente-2" _ "github.com/enmanuel/agents/agents/meteorologo" + _ "github.com/enmanuel/agents/agents/test-bot" ) func main() { From 235c5ff827fcd23565c7bf01fc0e8984947f7346 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:25:41 +0000 Subject: [PATCH 2/7] feat: implementar comandos custom !echo y !dice para test-bot - !echo : repite el texto recibido (util para assertions exactas) - !dice / !dado: lanza un dado aleatorio (1-6) - Registro en cmd/launcher/main.go via testbot.Commands() Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/test-bot/commands.go | 47 +++++++++++++++++++++++++++++++++++++ cmd/launcher/main.go | 9 ++++++- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 agents/test-bot/commands.go diff --git a/agents/test-bot/commands.go b/agents/test-bot/commands.go new file mode 100644 index 0000000..51160aa --- /dev/null +++ b/agents/test-bot/commands.go @@ -0,0 +1,47 @@ +package test + +import ( + "context" + "fmt" + "math/rand" + "strings" + + "github.com/enmanuel/agents/pkg/command" + "github.com/enmanuel/agents/pkg/decision" +) + +// CommandEntry pairs a spec with its handler. +type CommandEntry struct { + Spec command.Spec + Handler func(ctx context.Context, msgCtx decision.MessageContext) string +} + +// Commands returns the custom command specs and handlers for test-bot. +func Commands() []CommandEntry { + return []CommandEntry{ + { + Spec: command.Spec{ + Name: "echo", + Description: "Repite el texto recibido", + Usage: "!echo ", + }, + Handler: func(_ context.Context, msgCtx decision.MessageContext) string { + if len(msgCtx.Args) == 0 { + return "Uso: !echo " + } + return strings.Join(msgCtx.Args, " ") + }, + }, + { + Spec: command.Spec{ + Name: "dice", + Aliases: []string{"dado"}, + Description: "Lanza un dado (1-6)", + Usage: "!dice", + }, + Handler: func(_ context.Context, _ decision.MessageContext) string { + return fmt.Sprintf("%d", rand.Intn(6)+1) + }, + }, + } +} diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index d7927dc..c2caf0d 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -33,7 +33,7 @@ import ( _ "github.com/enmanuel/agents/agents/assistant-bot" _ "github.com/enmanuel/agents/agents/asistente-2" _ "github.com/enmanuel/agents/agents/meteorologo" - _ "github.com/enmanuel/agents/agents/test-bot" + testbot "github.com/enmanuel/agents/agents/test-bot" ) func main() { @@ -181,6 +181,13 @@ func main() { agentCleanup() continue } + // Register agent-specific commands for robots + if cfg.Agent.ID == "test-bot" { + for _, cmd := range testbot.Commands() { + robot.RegisterCommand(cmd.Spec, cmd.Handler) + } + } + runner = robot agentLogger.Info("created robot", "id", cfg.Agent.ID) } else { From 184d7ca0aed134fd5e22f98ffe3ee409a56b5bd9 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:33:43 +0000 Subject: [PATCH 3/7] fix: permitir robots sin llm.primary.provider + command_prefix vacio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - internal/config/loader.go: skip validacion de LLM provider cuando agent.type es "robot" — los robots no usan LLM - agents/test-bot/config.yaml: usar command_prefix: "" para que acepte comandos sin prefijo ! (feature del issue 0033) Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/test-bot/config.yaml | 2 +- internal/config/loader.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/test-bot/config.yaml b/agents/test-bot/config.yaml index 827bca2..cc4ad7e 100644 --- a/agents/test-bot/config.yaml +++ b/agents/test-bot/config.yaml @@ -43,7 +43,7 @@ matrix: admin: [] filters: - command_prefix: "!" + command_prefix: "" # sin prefijo — todo mensaje es un posible comando mention_respond: false dm_respond: false ignore_bots: true diff --git a/internal/config/loader.go b/internal/config/loader.go index 0f2275a..4bf891f 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -94,7 +94,7 @@ func validate(cfg *AgentConfig) error { if cfg.Matrix.UserID == "" { return fmt.Errorf("matrix.user_id is required") } - if cfg.LLM.Primary.Provider == "" { + if cfg.Agent.Type != "robot" && cfg.LLM.Primary.Provider == "" { return fmt.Errorf("llm.primary.provider is required") } return nil From 7edbbad6b35bbaece5035be22190dbcd0940d291 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:35:44 +0000 Subject: [PATCH 4/7] feat: scripts para automatizar creacion de robots y notificar developers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevos scripts: - convert-to-robot.sh: convierte scaffold de agente a robot (config minimo, agent.go con nil Rules, sin prompts, command_prefix vacio) - notify-developer.sh: envia DM a los developers (DEVELOPER_MATRIX_USERS) al crear un bot o agente, presentandose con nombre y tipo Mejorado: - create-full.sh: acepta --type robot para pipeline completo de robots (scaffold → build → register → verify → convert → notify) - .env.example: añade DEVELOPER_MATRIX_USERS para lista de developers Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 4 + dev-scripts/agent/convert-to-robot.sh | 132 ++++++++++++++++++++++++++ dev-scripts/agent/create-full.sh | 104 ++++++++++++++++---- dev-scripts/agent/notify-developer.sh | 95 ++++++++++++++++++ 4 files changed, 316 insertions(+), 19 deletions(-) create mode 100755 dev-scripts/agent/convert-to-robot.sh create mode 100755 dev-scripts/agent/notify-developer.sh diff --git a/.env.example b/.env.example index 6bfdc60..51960d2 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,10 @@ STAGING_HOST=10.0.2.10 MONITORING_HOST=10.0.3.10 BASTION_HOST=bastion.example.com +# ── Desarrolladores (notificación al crear bots/agentes) ───── +# Lista separada por comas de usernames Matrix (sin @ ni :server) +DEVELOPER_MATRIX_USERS=egutierrez + # ── Matrix rooms (opcionales — el assistant-bot opera en DMs) ─ MATRIX_ROOM_DEVOPS= MATRIX_ROOM_ALERTS= diff --git a/dev-scripts/agent/convert-to-robot.sh b/dev-scripts/agent/convert-to-robot.sh new file mode 100755 index 0000000..7a21ac2 --- /dev/null +++ b/dev-scripts/agent/convert-to-robot.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# convert-to-robot.sh — convierte un scaffold de agente a robot +# +# Uso: +# ./dev-scripts/agent/convert-to-robot.sh +# +# Cambios: +# 1. Reescribe config.yaml desde _template_robot (manteniendo Matrix/E2EE) +# 2. Simplifica agent.go (Rules() retorna nil) +# 3. Elimina prompts/ (robots no necesitan system prompt) +# 4. Pone command_prefix: "" por defecto (sin prefijo) + +source "$(dirname "$0")/../_common.sh" +load_env + +need_arg "${1:-}" + +ID="$1" +NORM="$(normalize_id "$ID")" +PACKAGE="$(echo "$ID" | tr '-' '_' | sed 's/_bot//')" +DIR="agents/$ID" +DEVICE_ID="" + +[[ -d "$DIR" ]] || fail "No existe agents/$ID — ejecuta create-full.sh primero" + +info "Convirtiendo $ID a robot..." + +# ── Extraer device_id del config actual ────────────────────────────────── +if [[ -f "$DIR/config.yaml" ]]; then + DEVICE_ID="$(grep -m1 'device_id:' "$DIR/config.yaml" | awk '{print $2}' | tr -d '"')" +fi + +# ── Reescribir config.yaml ─────────────────────────────────────────────── +cat > "$DIR/config.yaml" << YAML +# ============================================ +# ${ID} — Robot (command-only, sin LLM) +# ============================================ +agent: + id: ${ID} + name: "${2:-$ID}" + version: "0.1.0" + type: robot + enabled: true + template: false + description: "" + tags: [robot] + +personality: + prefix: "" + language: es + +matrix: + homeserver: "\${MATRIX_HOMESERVER}" + user_id: "@${ID}:\${MATRIX_SERVER_NAME}" + access_token_env: MATRIX_TOKEN_${NORM} + device_id: "${DEVICE_ID}" + + encryption: + enabled: true + store_path: "./agents/${ID}/data/crypto/" + pickle_key_env: PICKLE_KEY_${NORM} + trust_mode: tofu + recovery_key_env: SSSS_RECOVERY_KEY_${NORM} + + rooms: + listen: [] + respond: [] + admin: [] + + filters: + command_prefix: "" + mention_respond: false + dm_respond: false + ignore_bots: true + ignore_users: [] + unauthorized_response: silent + min_power_level: 0 + + threads: + enabled: true + auto_thread: false + +security: + audit: + enabled: false + log_file: "" + log_to_room: "" + include: [] + secrets: + provider: env + sanitize: + enabled: false + mode: warn + min_severity: medium + disabled_patterns: [] + tool_rate_limit: + enabled: false + max_calls_per_min: 10 + cleanup_interval_s: 60 +YAML + +ok "config.yaml reescrito como robot (command_prefix: \"\", sin LLM)" + +# ── Simplificar agent.go ───────────────────────────────────────────────── +cat > "$DIR/agent.go" << GO +package ${PACKAGE} + +import ( + "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/pkg/decision" +) + +func init() { + agents.Register("${ID}", Rules) +} + +// Rules returns nil — robots only respond to commands. +func Rules() []decision.Rule { + return nil +} +GO + +ok "agent.go simplificado (Rules() retorna nil)" + +# ── Eliminar prompts/ ──────────────────────────────────────────────────── +if [[ -d "$DIR/prompts" ]]; then + rm -rf "$DIR/prompts" + ok "prompts/ eliminado (robots no necesitan system prompt)" +fi + +echo "" +ok "$ID convertido a robot" diff --git a/dev-scripts/agent/create-full.sh b/dev-scripts/agent/create-full.sh index 311aef7..dcbd918 100755 --- a/dev-scripts/agent/create-full.sh +++ b/dev-scripts/agent/create-full.sh @@ -1,17 +1,16 @@ #!/usr/bin/env bash -# create-full.sh — pipeline completo para crear un agente funcional +# create-full.sh — pipeline completo para crear un agente o robot funcional # -# Ejecuta en orden: scaffold → build → register → verify E2EE +# Ejecuta en orden: scaffold → build → register → verify E2EE → [convert robot] → [notify dev] # NO arranca el agente — primero personalizar agent.go, config.yaml y prompts/system.md # # Uso: -# ./dev-scripts/agent/create-full.sh "Display Name" -# -# Ejemplo: -# ./dev-scripts/agent/create-full.sh monitor-bot "Monitor Agent" +# ./dev-scripts/agent/create-full.sh "Display Name" # agente (default) +# ./dev-scripts/agent/create-full.sh "Display Name" --type robot # robot # # Requisitos en .env: # MATRIX_ADMIN_TOKEN, MATRIX_HOMESERVER, MATRIX_SERVER_NAME +# DEVELOPER_MATRIX_USERS (opcional, para notificación al developer) source "$(dirname "$0")/../_common.sh" load_env @@ -20,17 +19,47 @@ need_arg "${1:-}" ID="$1" DISPLAYNAME="${2:-$ID}" +TYPE="agent" NORM="$(normalize_id "$ID")" SCRIPT_DIR="$(dirname "$0")" +# Parse --type flag +shift 2 2>/dev/null || shift 1 2>/dev/null || true +while [[ $# -gt 0 ]]; do + case "$1" in + --type) + TYPE="${2:-agent}" + shift 2 + ;; + --type=*) + TYPE="${1#--type=}" + shift + ;; + *) + shift + ;; + esac +done + +if [[ "$TYPE" == "robot" ]]; then + TYPE_LABEL="robot" + TYPE_EMOJI="🤖" +else + TYPE_LABEL="agente" + TYPE_EMOJI="🧠" +fi + echo "" echo -e "${BLU}═══════════════════════════════════════════════════════${RST}" -echo -e "${BLU} Creando agente: ${GRN}$ID${BLU} ($DISPLAYNAME)${RST}" +echo -e "${BLU} Creando ${TYPE_LABEL}: ${GRN}$ID${BLU} ($DISPLAYNAME) ${TYPE_EMOJI}${RST}" echo -e "${BLU}═══════════════════════════════════════════════════════${RST}" echo "" # ── Paso 1: Scaffold ───────────────────────────────────────────────────── -info "Paso 1/4 — Scaffold (agent.go, config.yaml, prompts, launcher)" +TOTAL_STEPS=5 +[[ "$TYPE" == "robot" ]] && TOTAL_STEPS=6 + +info "Paso 1/${TOTAL_STEPS} — Scaffold (agent.go, config.yaml, prompts, launcher)" echo "" "$SCRIPT_DIR/new-agent.sh" "$ID" "$DISPLAYNAME" @@ -38,7 +67,7 @@ echo "" echo "" # ── Paso 2: Verificar compilación ───────────────────────────────────────── -info "Paso 2/4 — Verificando compilación..." +info "Paso 2/${TOTAL_STEPS} — Verificando compilación..." if "$GO" build -tags goolm ./... 2>&1; then ok "Compilación exitosa" @@ -49,7 +78,7 @@ fi echo "" # ── Paso 3: Registrar en Matrix ────────────────────────────────────────── -info "Paso 3/4 — Registrando en Matrix..." +info "Paso 3/${TOTAL_STEPS} — Registrando en Matrix..." echo "" # Reload .env in case new-agent.sh or previous steps changed it @@ -60,7 +89,7 @@ load_env echo "" # ── Paso 4: Verificar E2EE ─────────────────────────────────────────────── -info "Paso 4/4 — Verificación E2EE (cross-signing + recovery key)..." +info "Paso 4/${TOTAL_STEPS} — Verificación E2EE (cross-signing + recovery key)..." echo "" # Reload .env to pick up token, password, pickle key from register.sh @@ -70,15 +99,44 @@ load_env echo "" +# ── Paso 5 (robots): Convertir a robot ─────────────────────────────────── +if [[ "$TYPE" == "robot" ]]; then + info "Paso 5/${TOTAL_STEPS} — Convirtiendo a robot..." + echo "" + + "$SCRIPT_DIR/convert-to-robot.sh" "$ID" "$DISPLAYNAME" + + # Rebuild after conversion + info "Recompilando tras conversión..." + "$GO" build -tags goolm ./... 2>&1 || fail "Error de compilación tras conversión a robot" + ok "Recompilación exitosa" + + echo "" +fi + +# ── Paso final: Notificar al developer ─────────────────────────────────── +NOTIFY_STEP=$TOTAL_STEPS +info "Paso ${NOTIFY_STEP}/${TOTAL_STEPS} — Notificando a desarrolladores..." +echo "" + +# Reload .env (verify.sh may have added recovery key) +load_env + +"$SCRIPT_DIR/notify-developer.sh" "$ID" "$TYPE" "$DISPLAYNAME" || true + +echo "" + # ── Resumen ────────────────────────────────────────────────────────────── echo -e "${GRN}═══════════════════════════════════════════════════════${RST}" -echo -e "${GRN} ✓ Agente $ID creado exitosamente${RST}" +echo -e "${GRN} ✓ ${TYPE_LABEL^} $ID creado exitosamente ${TYPE_EMOJI}${RST}" echo -e "${GRN}═══════════════════════════════════════════════════════${RST}" echo "" echo -e " ${BLU}Archivos creados:${RST}" echo -e " agents/$ID/agent.go" echo -e " agents/$ID/config.yaml" -echo -e " agents/$ID/prompts/system.md" +if [[ "$TYPE" != "robot" ]]; then + echo -e " agents/$ID/prompts/system.md" +fi echo "" echo -e " ${BLU}Variables en .env:${RST}" echo -e " MATRIX_TOKEN_${NORM}" @@ -87,15 +145,23 @@ echo -e " PICKLE_KEY_${NORM}" echo -e " SSSS_RECOVERY_KEY_${NORM}" echo "" echo -e " ${BLU}Launcher actualizado:${RST}" -echo -e " cmd/launcher/main.go (import + rulesRegistry)" +echo -e " cmd/launcher/main.go (import)" echo "" echo -e "${YLW}Siguiente paso:${RST}" echo "" -echo -e " 1. Personalizar los archivos del agente:" -echo -e " ${DIM}agents/$ID/agent.go${RST} — reglas de decisión" -echo -e " ${DIM}agents/$ID/config.yaml${RST} — LLM, tools, personalidad" -echo -e " ${DIM}agents/$ID/prompts/system.md${RST} — system prompt" +if [[ "$TYPE" == "robot" ]]; then + echo -e " 1. Añadir comandos custom:" + echo -e " ${DIM}agents/$ID/commands.go${RST}" + echo "" + echo -e " 2. Registrar comandos en el launcher:" + echo -e " ${DIM}cmd/launcher/main.go${RST}" +else + echo -e " 1. Personalizar los archivos del agente:" + echo -e " ${DIM}agents/$ID/agent.go${RST} — reglas de decisión" + echo -e " ${DIM}agents/$ID/config.yaml${RST} — LLM, tools, personalidad" + echo -e " ${DIM}agents/$ID/prompts/system.md${RST} — system prompt" +fi echo "" -echo -e " 2. Arrancar:" +echo -e " Arrancar:" echo -e " ${DIM}./dev-scripts/server/start.sh${RST}" echo "" diff --git a/dev-scripts/agent/notify-developer.sh b/dev-scripts/agent/notify-developer.sh new file mode 100755 index 0000000..9e0abd6 --- /dev/null +++ b/dev-scripts/agent/notify-developer.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# notify-developer.sh — envía DM a los desarrolladores al crear un bot/agente +# +# Uso: +# ./dev-scripts/agent/notify-developer.sh +# +# Requisitos en .env: +# DEVELOPER_MATRIX_USERS — lista separada por comas de usernames Matrix +# Ejemplo: DEVELOPER_MATRIX_USERS=egutierrez,admin +# MATRIX_TOKEN_ — token del bot recién creado +# MATRIX_HOMESERVER, MATRIX_SERVER_NAME + +source "$(dirname "$0")/../_common.sh" +load_env + +ID="${1:-}" +TYPE="${2:-agent}" +DISPLAYNAME="${3:-$ID}" +NORM="$(normalize_id "$ID")" + +[[ -z "$ID" ]] && { warn "notify-developer: se necesita agent-id"; exit 0; } + +# ── Obtener token del bot ──────────────────────────────────────────────── +TOKEN_VAR="MATRIX_TOKEN_${NORM}" +TOKEN="${!TOKEN_VAR:-}" + +if [[ -z "$TOKEN" ]]; then + warn "notify-developer: $TOKEN_VAR no encontrado en .env — saltando notificación" + exit 0 +fi + +# ── Obtener lista de desarrolladores ───────────────────────────────────── +if [[ -z "${DEVELOPER_MATRIX_USERS:-}" ]]; then + warn "notify-developer: DEVELOPER_MATRIX_USERS no definido en .env — saltando" + exit 0 +fi + +# ── Construir mensaje ──────────────────────────────────────────────────── +if [[ "$TYPE" == "robot" ]]; then + EMOJI="🤖" + TYPE_LABEL="Robot" + COMMANDS_MSG="Mis comandos: help, ping, status, info, version" +else + EMOJI="🧠" + TYPE_LABEL="Agente" + COMMANDS_MSG="Escríbeme directamente o usa !help para ver mis comandos" +fi + +MSG="${EMOJI} ¡Hola! Soy **${DISPLAYNAME}** (${TYPE_LABEL}). Acabo de ser creado. ${COMMANDS_MSG}." + +# ── Enviar DM a cada desarrollador ─────────────────────────────────────── +IFS=',' read -ra DEVS <<< "$DEVELOPER_MATRIX_USERS" + +for dev in "${DEVS[@]}"; do + dev="$(echo "$dev" | xargs)" # trim spaces + [[ -z "$dev" ]] && continue + + USER_ID="@${dev}:${MATRIX_SERVER_NAME}" + info "Enviando DM de $ID a $USER_ID..." + + # Crear DM room (o reutilizar existente) + ROOM_RESP=$(curl -sf -X POST "${MATRIX_HOMESERVER}/_matrix/client/v3/createRoom" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"is_direct\": true, + \"invite\": [\"${USER_ID}\"], + \"preset\": \"trusted_private_chat\" + }" 2>&1) || { + warn " No se pudo crear DM room con $USER_ID" + continue + } + + ROOM_ID=$(echo "$ROOM_RESP" | grep -o '"room_id":"[^"]*"' | cut -d'"' -f4) + if [[ -z "$ROOM_ID" ]]; then + warn " Respuesta inesperada al crear room: $ROOM_RESP" + continue + fi + + # Enviar mensaje + TXN_ID="notify-$(date +%s%N)" + SEND_RESP=$(curl -sf -X PUT \ + "${MATRIX_HOMESERVER}/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"msgtype\": \"m.text\", + \"body\": \"${MSG}\" + }" 2>&1) || { + warn " No se pudo enviar mensaje a $USER_ID" + continue + } + + ok "DM enviado a $USER_ID" +done From 9e6d831ea9e0eba7651d1d7659a17ff006ddd4f7 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:36:45 +0000 Subject: [PATCH 5/7] docs: actualizar skills /create-bot y /create-agent con nuevos scripts - /create-bot: usa create-full.sh --type robot (pipeline de 6 pasos) - /create-agent: documenta --type robot y notificacion a developers - Referencia actualizada: agents/test-bot/ como ejemplo de robot Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/create-agent/SKILL.md | 10 +++- .claude/skills/create-bot/SKILL.md | 72 ++++++++-------------------- 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/.claude/skills/create-agent/SKILL.md b/.claude/skills/create-agent/SKILL.md index e7d64d3..05714e6 100644 --- a/.claude/skills/create-agent/SKILL.md +++ b/.claude/skills/create-agent/SKILL.md @@ -37,15 +37,23 @@ Si `$ARGUMENTS` contiene el agent-id, usarlo directamente: `$0` = agent-id, `$1` ### Paso 2: Ejecutar pipeline de scaffold +Para **agentes** (con LLM): ```bash ./dev-scripts/agent/create-full.sh "" ``` -Este script ejecuta 4 etapas: +Para **robots** (command-only, sin LLM): +```bash +./dev-scripts/agent/create-full.sh "" --type robot +``` + +El script ejecuta automaticamente: 1. **Scaffold**: copia `_template/`, personaliza archivos, actualiza launcher 2. **Build**: compila con `go build -tags goolm ./...` 3. **Register**: crea usuario Matrix, genera token + password + pickle key 4. **Verify E2EE**: genera cross-signing keys, recovery key +5. **(robots)** **Convert**: convierte a robot (config minimo, sin prompts, `command_prefix: ""`) +6. **Notify**: envia DM a los developers (`DEVELOPER_MATRIX_USERS` en `.env`) presentandose Si alguna etapa falla, revisar el error y corregir antes de continuar. diff --git a/.claude/skills/create-bot/SKILL.md b/.claude/skills/create-bot/SKILL.md index dca50ac..4adc2fe 100644 --- a/.claude/skills/create-bot/SKILL.md +++ b/.claude/skills/create-bot/SKILL.md @@ -2,7 +2,7 @@ name: create-bot description: > Crear un nuevo robot Matrix (command-only, sin LLM). Ejecuta el pipeline - scaffold + build + register + verify, luego personaliza config.yaml y + scaffold + build + register + verify + convert + notify, luego personaliza comandos custom segun los inputs del usuario. allowed-tools: Bash Read Write Edit Grep Glob Agent argument-hint: " [display-name]" @@ -34,60 +34,27 @@ Si `$ARGUMENTS` contiene el bot-id, usarlo directamente: `$0` = bot-id, `$1` = d 2. Verificar que no existe `agents//` 3. Si faltan inputs, preguntar al usuario -### Paso 2: Ejecutar pipeline de scaffold +### Paso 2: Ejecutar pipeline completo ```bash -./dev-scripts/agent/create-full.sh "" +./dev-scripts/agent/create-full.sh "" --type robot ``` -Este script ejecuta 4 etapas: scaffold → build → register Matrix → verify E2EE. +Este script ejecuta 6 etapas automaticamente: +1. **Scaffold**: crea agent.go, config.yaml, prompts/ desde template +2. **Build**: verifica compilacion con `-tags goolm` +3. **Register**: registra usuario Matrix, genera token + password + pickle key +4. **Verify E2EE**: genera cross-signing keys + recovery key +5. **Convert**: convierte a robot (config minimo, sin prompts, sin LLM, `command_prefix: ""`) +6. **Notify**: envia DM a los developers (DEVELOPER_MATRIX_USERS) presentandose + Si alguna etapa falla, revisar el error y corregir antes de continuar. -### Paso 3: Convertir a robot +### Paso 3: Personalizar config -El scaffold crea un agente por defecto. Convertirlo a robot: - -#### 3.1 Reemplazar `agents//agent.go` - -```go -package // sin guiones ni _bot: "ping-bot" → package ping - -import ( - "github.com/enmanuel/agents/agents" - "github.com/enmanuel/agents/pkg/decision" -) - -func init() { - agents.Register("", Rules) -} - -// Rules returns nil — robots don't use decision rules. -// All behavior is via RegisterCommand in the launcher. -func Rules() []decision.Rule { - return nil -} -``` - -Package name = bot-id sin guiones ni `_bot` (ej: `ping-bot` → `package ping`). - -#### 3.2 Reemplazar `agents//config.yaml` - -Consultar [templates/config.yaml.md](templates/config.yaml.md) para el template base. - -Ajustes obligatorios: -- `agent.id`: debe coincidir con el nombre del directorio -- `agent.type: robot` (CRITICO — sin esto se lanza como agent completo) +Editar `agents//config.yaml`: - `agent.description`: la descripcion del usuario -- `personality.prefix`: emoji representativo del bot -- Env vars: normalizar bot-id → mayusculas, guiones → underscores (NUNCA eliminar sufijos) - -#### 3.3 Eliminar `agents//prompts/system.md` - -Los robots no necesitan system prompt. Eliminar el directorio prompts/ completo: - -```bash -rm -rf agents//prompts/ -``` +- `personality.prefix`: emoji representativo del bot (opcional) ### Paso 4: Crear comandos custom (si el usuario los pidio) @@ -133,7 +100,7 @@ Luego registrar en `cmd/launcher/main.go` despues de `agents.NewRobot()`: ```go if cfg.Agent.ID == "" { for _, cmd := range .Commands() { - r.RegisterCommand(cmd.Spec, cmd.Handler) + robot.RegisterCommand(cmd.Spec, cmd.Handler) } } ``` @@ -153,7 +120,7 @@ Verificar y reportar al usuario: - [ ] `go build -tags goolm ./...` compila sin errores - [ ] `agents//agent.go` exporta `Rules()` que retorna `nil` - [ ] `agents//config.yaml` tiene `agent.type: robot` y `agent.id` coincide con directorio -- [ ] `cmd/launcher/main.go` tiene blank import del paquete del bot +- [ ] `cmd/launcher/main.go` tiene import del paquete del bot - [ ] `.env` contiene las 4 env vars: `MATRIX_TOKEN_`, `MATRIX_PASSWORD_`, `PICKLE_KEY_`, `SSSS_RECOVERY_KEY_` - [ ] No existe `agents//prompts/` (robots no necesitan system prompt) - [ ] Si tiene comandos custom, estan registrados en el launcher @@ -163,11 +130,11 @@ Informar al usuario: Robot creado. Para arrancar: ./dev-scripts/server/start.sh -Comandos built-in: !help, !ping, !status, !info, !version +Comandos built-in: help, ping, status, info, version Comandos custom: +(Sin prefijo ! — el robot acepta comandos directamente) Archivos a revisar: - agents//agent.go — reglas (nil para robots) agents//config.yaml — configuracion agents//commands.go — comandos custom (si aplica) ``` @@ -183,6 +150,7 @@ Archivos a revisar: | Reglas | Si (agent.go con Rules) | nil | | Tools | Opcionales | No | | Memoria/Knowledge | Opcionales | No | +| Prefijo comandos | `!` (obligatorio) | `""` (sin prefijo por defecto) | | Comandos built-in | help, ping, tools, tool, status, info, clear, prompts, version | help, ping, status, info, version | ## Notas importantes @@ -191,5 +159,5 @@ Archivos a revisar: - **Nunca commitear tokens ni passwords** — van en `.env` - **Homeserver**: `https://matrix-af2f3d.organic-machine.com` - **Server name**: `matrix-af2f3d.organic-machine.com` -- Referencia de robot existente: `agents/_template_robot/` +- Referencia de robot existente: `agents/test-bot/` - El bot-id DEBE coincidir entre directorio, config.yaml y `agents.Register()` From f3581721b1000599003d022137c47b5f0104eed6 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:43:24 +0000 Subject: [PATCH 6/7] test: E2E tests para robot test-bot y pipeline de creacion Tests funcionales (test-bot.spec.ts): - help lista built-in + custom commands sin prefijo ! - ping funciona sin prefijo y con ! (retrocompatible) - echo repite texto exacto (assertion estricta) - dice devuelve numero 1-6 - comando desconocido muestra error descriptivo - sin errores E2EE en el timeline Tests de pipeline (create-bot-pipeline.spec.ts): - Valida estructura: agent.go, config.yaml, commands.go - Verifica type: robot, command_prefix vacio, encryption habilitada - Confirma ausencia de prompts/system.md - Verifica import en launcher Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/tests/create-bot-pipeline.spec.ts | 62 +++++++++++++++++++ e2e/tests/test-bot.spec.ts | 87 +++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 e2e/tests/create-bot-pipeline.spec.ts create mode 100644 e2e/tests/test-bot.spec.ts diff --git a/e2e/tests/create-bot-pipeline.spec.ts b/e2e/tests/create-bot-pipeline.spec.ts new file mode 100644 index 0000000..04ca47b --- /dev/null +++ b/e2e/tests/create-bot-pipeline.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "@playwright/test"; +import * as fs from "fs"; +import * as path from "path"; + +const REPO_ROOT = path.resolve(__dirname, "../.."); +const AGENT_DIR = path.join(REPO_ROOT, "agents/test-bot"); +const LAUNCHER = path.join(REPO_ROOT, "cmd/launcher/main.go"); + +test.describe("create-bot pipeline (validacion estructural)", () => { + test("agents/test-bot/agent.go existe y exporta Rules()", () => { + const agentGo = path.join(AGENT_DIR, "agent.go"); + expect(fs.existsSync(agentGo)).toBe(true); + + const content = fs.readFileSync(agentGo, "utf-8"); + expect(content).toContain("func Rules()"); + expect(content).toContain('agents.Register("test-bot"'); + expect(content).toContain("return nil"); + }); + + test("agents/test-bot/config.yaml tiene type: robot", () => { + const configYaml = path.join(AGENT_DIR, "config.yaml"); + expect(fs.existsSync(configYaml)).toBe(true); + + const content = fs.readFileSync(configYaml, "utf-8"); + expect(content).toMatch(/type:\s*robot/); + expect(content).toMatch(/id:\s*test-bot/); + expect(content).toMatch(/enabled:\s*true/); + }); + + test("agents/test-bot NO tiene prompts/system.md", () => { + const systemPrompt = path.join(AGENT_DIR, "prompts/system.md"); + expect(fs.existsSync(systemPrompt)).toBe(false); + }); + + test("agents/test-bot/commands.go existe con Commands()", () => { + const commandsGo = path.join(AGENT_DIR, "commands.go"); + expect(fs.existsSync(commandsGo)).toBe(true); + + const content = fs.readFileSync(commandsGo, "utf-8"); + expect(content).toContain("func Commands()"); + expect(content).toContain('"echo"'); + expect(content).toContain('"dice"'); + }); + + test("cmd/launcher/main.go tiene import de test-bot", () => { + const content = fs.readFileSync(LAUNCHER, "utf-8"); + expect(content).toContain("agents/test-bot"); + }); + + test("config.yaml tiene command_prefix vacio (sin prefijo !)", () => { + const configYaml = path.join(AGENT_DIR, "config.yaml"); + const content = fs.readFileSync(configYaml, "utf-8"); + expect(content).toMatch(/command_prefix:\s*""/); + }); + + test("config.yaml tiene encryption habilitada", () => { + const configYaml = path.join(AGENT_DIR, "config.yaml"); + const content = fs.readFileSync(configYaml, "utf-8"); + // encryption.enabled should be true + expect(content).toMatch(/encryption:[\s\S]*?enabled:\s*true/); + }); +}); diff --git a/e2e/tests/test-bot.spec.ts b/e2e/tests/test-bot.spec.ts new file mode 100644 index 0000000..3b84bf3 --- /dev/null +++ b/e2e/tests/test-bot.spec.ts @@ -0,0 +1,87 @@ +import { test, expect, handleElementDialogs } from "../fixtures/persistent-context"; +import { + goToRoom, + sendMessage, + waitForBotReply, + assertNoDecryptionErrors, +} from "../fixtures/matrix-room"; + +test.describe("test-bot (robot)", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await handleElementDialogs(page); + await goToRoom(page, "Test Bot"); + }); + + test("help lista comandos built-in y custom", async ({ page }) => { + await sendMessage(page, "help"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBeTruthy(); + expect(reply.toLowerCase()).toContain("help"); + expect(reply.toLowerCase()).toContain("ping"); + expect(reply.toLowerCase()).toContain("echo"); + expect(reply.toLowerCase()).toContain("dice"); + }); + + test("ping responde sin prefijo !", async ({ page }) => { + await sendMessage(page, "ping"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBeTruthy(); + }); + + test("!ping tambien funciona (retrocompatible)", async ({ page }) => { + await sendMessage(page, "!ping"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBeTruthy(); + }); + + test("echo repite el texto exacto", async ({ page }) => { + await sendMessage(page, "echo hello world"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBe("hello world"); + }); + + test("dice devuelve un numero entre 1 y 6", async ({ page }) => { + await sendMessage(page, "dice"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBeTruthy(); + const num = parseInt(reply.trim(), 10); + expect(num).toBeGreaterThanOrEqual(1); + expect(num).toBeLessThanOrEqual(6); + }); + + test("comando desconocido muestra error", async ({ page }) => { + await sendMessage(page, "unknowncommand"); + + const reply = await waitForBotReply(page, { + timeout: 10_000, + sender: "Test Bot", + }); + expect(reply).toBeTruthy(); + expect(reply.toLowerCase()).toMatch(/desconocido|unknown|no reconozco/); + }); + + test("no hay errores de E2EE en el timeline", async ({ page }) => { + await assertNoDecryptionErrors(page); + }); +}); From 43c70e904b84dc193d78d7003524cdbdc59de537 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 20:45:17 +0000 Subject: [PATCH 7/7] docs: cerrar issue 0034 y actualizar CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Añadir test-bot a tabla de agentes en CLAUDE.md - Añadir nuevos specs E2E a la lista de tests - Mover issue a completed, actualizar README Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 3 ++- dev/issues/README.md | 2 +- dev/issues/{ => completed}/0034-e2e-create-bot-skill.md | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename dev/issues/{ => completed}/0034-e2e-create-bot-skill.md (100%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 98f21eb..8b80b8f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -89,7 +89,7 @@ cp e2e/.env.example e2e/.env # configurar credenciales ``` - **Fixtures**: `e2e/fixtures/` — login E2EE (`element-auth.ts`), helpers de room (`matrix-room.ts`) -- **Tests**: `e2e/tests/` — login, assistant-bot, asistente-2 +- **Tests**: `e2e/tests/` — login, assistant-bot, asistente-2, test-bot, create-bot-pipeline - **Assertions flexibles** para respuestas LLM (no-deterministicas), estrictas para commands (`!help`, `!ping`) - Documentacion completa: `e2e/README.md` @@ -115,6 +115,7 @@ Templates: `agents/_template/` (agent) y `agents/_template_robot/` (robot). |----|------|-----|-------------| | assistant-bot | agent | GPT-4o | Asistente general, DMs | | asistente-2 | agent | GPT-4o | Asistente con tools | +| test-bot | robot | — | Robot de prueba (E2E tests pipeline) | ## Build diff --git a/dev/issues/README.md b/dev/issues/README.md index 51573e0..196d55b 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -44,5 +44,5 @@ afectados y notas de implementacion. | 31 | Expandir file tools (write, list, append, delete) | [0031-expand-file-tools.md](completed/0031-expand-file-tools.md) | completado | | 32 | E2E: verificar skill /create-agent | [0032-e2e-create-agent-skill.md](0032-e2e-create-agent-skill.md) | pendiente | | 33 | Comandos de robots sin prefijo ! | [0033-bot-commands-no-prefix.md](completed/0033-bot-commands-no-prefix.md) | completado | -| 34 | E2E: verificar skill /create-bot | [0034-e2e-create-bot-skill.md](0034-e2e-create-bot-skill.md) | pendiente | +| 34 | E2E: verificar skill /create-bot | [0034-e2e-create-bot-skill.md](completed/0034-e2e-create-bot-skill.md) | completado | | 35 | Audit trail + comando !metrics | [0035-audit-trail-metrics.md](completed/0035-audit-trail-metrics.md) | completado | diff --git a/dev/issues/0034-e2e-create-bot-skill.md b/dev/issues/completed/0034-e2e-create-bot-skill.md similarity index 100% rename from dev/issues/0034-e2e-create-bot-skill.md rename to dev/issues/completed/0034-e2e-create-bot-skill.md