merge: issue/0025-cron-scaffolder — catálogo cron + scaffolder + Fire()
This commit is contained in:
@@ -59,8 +59,10 @@ tools/ tool registry + tool implementations (subpackages)
|
||||
internal/config/ schema.go + loader.go
|
||||
cmd/launcher/ entrypoint principal (rulesRegistry)
|
||||
cmd/agentctl/ CLI de gestion
|
||||
crons/ catálogo de automatizaciones nombradas (schedule.yaml + prompts)
|
||||
dev-scripts/server/ start, stop, restart, ps, logs, dashboard
|
||||
dev-scripts/agent/ new, register, verify, avatar, remove, list
|
||||
dev-scripts/cron/ new, list, apply — gestión de automatizaciones cron
|
||||
dev-scripts/e2e/ install, run — E2E tests con Playwright
|
||||
e2e/ proyecto Node.js con Playwright (tests, fixtures, Element Web)
|
||||
```
|
||||
|
||||
@@ -23,13 +23,24 @@ Se necesita el issue objetivo. Si no se proporciona, preguntar.
|
||||
- tareas/fases
|
||||
- arquitectura y limites (pure core / impure shell)
|
||||
|
||||
3. Crear rama de trabajo usando `/git-branch`:
|
||||
3. Crear rama de trabajo (inline, sin invocar `/git-branch`):
|
||||
|
||||
```text
|
||||
/git-branch
|
||||
Verificar la rama actual:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Usar formato `issue/<NNNN>-<slug>` y nunca trabajar en `master`.
|
||||
- Si ya estamos en `issue/<NNNN>-<slug>` que coincide con el issue → continuar directamente a paso 4.
|
||||
- Si estamos en `master` o cualquier otra rama → crear la rama:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
|
||||
Nunca trabajar directamente en `master`.
|
||||
|
||||
4. Planificar con `TodoWrite`:
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# crons/ — Catálogo de automatizaciones
|
||||
|
||||
Directorio central de automatizaciones nombradas para los agentes. Cada subdirectorio es una
|
||||
automatización reutilizable que puede aplicarse a uno o más agentes.
|
||||
|
||||
## Estructura de una automatización
|
||||
|
||||
```
|
||||
crons/<nombre>/
|
||||
schedule.yaml # spec: descripción, cron por defecto, acción
|
||||
prompts/
|
||||
message.md # plantilla de mensaje (send_message)
|
||||
prompt.md # prompt para el LLM (llm_prompt)
|
||||
```
|
||||
|
||||
## Convención de `schedule.yaml`
|
||||
|
||||
```yaml
|
||||
# Metadata
|
||||
name: nombre-de-la-automatizacion
|
||||
description: "Descripción breve"
|
||||
|
||||
# Cron por defecto (el agente puede sobreescribir en su config.yaml)
|
||||
default_cron: "0 9 * * *"
|
||||
|
||||
# Acción
|
||||
action:
|
||||
kind: send_message # send_message | llm_prompt
|
||||
template: prompts/message.md # relativo a la raíz del proyecto
|
||||
|
||||
# Sala por defecto (opcional; el agente debe sobreescribir con output_room)
|
||||
default_output_room: ""
|
||||
```
|
||||
|
||||
> **Nota**: `template` es relativo a la **raíz del proyecto**, no a la carpeta de la automatización.
|
||||
> Usa siempre la ruta completa desde la raíz: `crons/<nombre>/prompts/message.md`.
|
||||
|
||||
## Automatizaciones disponibles
|
||||
|
||||
| Nombre | Tipo | Cron por defecto | Descripción |
|
||||
|--------|------|-----------------|-------------|
|
||||
| `good-morning` | `send_message` | `0 9 * * *` | Saludo de buenos días |
|
||||
| `daily-summary` | `llm_prompt` | `0 18 * * *` | Resumen diario del equipo |
|
||||
|
||||
## Scripts de gestión
|
||||
|
||||
```bash
|
||||
# Crear nueva automatización (interactivo)
|
||||
./dev-scripts/cron/new.sh
|
||||
|
||||
# Listar todas las automatizaciones con descripción
|
||||
./dev-scripts/cron/list.sh
|
||||
|
||||
# Aplicar automatización a un agente (parchea config.yaml)
|
||||
./dev-scripts/cron/apply.sh <nombre> <agent-id>
|
||||
```
|
||||
|
||||
## Cómo añadir manualmente a un agente
|
||||
|
||||
En `agents/<id>/config.yaml`:
|
||||
|
||||
```yaml
|
||||
schedules:
|
||||
- name: good-morning
|
||||
cron: "0 9 * * *"
|
||||
output_room: "!TUROOM:matrix-af2f3d.organic-machine.com"
|
||||
action:
|
||||
kind: send_message
|
||||
template: "crons/good-morning/prompts/message.md"
|
||||
```
|
||||
|
||||
Ajusta `output_room` con la sala real del agente. El campo `cron` puede sobreescribir el
|
||||
`default_cron` del catálogo.
|
||||
@@ -0,0 +1,7 @@
|
||||
Genera un breve resumen del día para el equipo. Incluye:
|
||||
|
||||
- Un saludo de cierre del día
|
||||
- Un recordatorio de mantener el ánimo y la energía para mañana
|
||||
- Una frase motivadora corta
|
||||
|
||||
Responde en español, de forma amigable y concisa (máximo 3-4 líneas).
|
||||
@@ -0,0 +1,15 @@
|
||||
# Automatización: daily-summary
|
||||
name: daily-summary
|
||||
description: "Resumen diario generado por el LLM"
|
||||
|
||||
# Cron por defecto: cada día a las 18:00
|
||||
default_cron: "0 18 * * *"
|
||||
|
||||
# Acción
|
||||
action:
|
||||
kind: llm_prompt
|
||||
# Relativo a la raíz del proyecto
|
||||
template: crons/daily-summary/prompts/prompt.md
|
||||
|
||||
# Sala de salida por defecto (vacío = el agente debe configurar output_room)
|
||||
default_output_room: ""
|
||||
@@ -0,0 +1,3 @@
|
||||
¡Buenos días! 🌅
|
||||
|
||||
Espero que tengan un excelente día. Estoy aquí si necesitan ayuda con algo.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Automatización: good-morning
|
||||
name: good-morning
|
||||
description: "Saludo de buenos días en una sala"
|
||||
|
||||
# Cron por defecto: cada día a las 9:00
|
||||
default_cron: "0 9 * * *"
|
||||
|
||||
# Acción
|
||||
action:
|
||||
kind: send_message
|
||||
# Relativo a la raíz del proyecto
|
||||
template: crons/good-morning/prompts/message.md
|
||||
|
||||
# Sala de salida por defecto (vacío = el agente debe configurar output_room)
|
||||
default_output_room: ""
|
||||
@@ -0,0 +1,45 @@
|
||||
# dev-scripts/cron/ — Gestión de automatizaciones cron
|
||||
|
||||
Scripts para crear, listar y aplicar automatizaciones del catálogo `crons/`.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `new.sh` — Scaffolder interactivo
|
||||
|
||||
Crea una nueva automatización en `crons/<nombre>/`:
|
||||
|
||||
```bash
|
||||
./dev-scripts/cron/new.sh
|
||||
```
|
||||
|
||||
Pregunta: nombre, descripción, tipo de acción (`send_message` o `llm_prompt`) y cron expression.
|
||||
Crea `schedule.yaml` y el archivo de prompt/mensaje vacío.
|
||||
Imprime el bloque YAML listo para añadir a `config.yaml`.
|
||||
|
||||
### `list.sh` — Listar automatizaciones
|
||||
|
||||
Lista todas las automatizaciones del catálogo con nombre, tipo, cron y descripción:
|
||||
|
||||
```bash
|
||||
./dev-scripts/cron/list.sh
|
||||
```
|
||||
|
||||
### `apply.sh` — Aplicar a un agente
|
||||
|
||||
Añade una automatización al `config.yaml` de un agente:
|
||||
|
||||
```bash
|
||||
./dev-scripts/cron/apply.sh <nombre> <agent-id>
|
||||
|
||||
# Ejemplo:
|
||||
./dev-scripts/cron/apply.sh good-morning assistant-bot
|
||||
```
|
||||
|
||||
Usa `yq` si está disponible para parchear el YAML directamente.
|
||||
Si `yq` no está instalado, imprime el bloque YAML para copiar a mano.
|
||||
|
||||
Recuerda editar `output_room` en `config.yaml` con la sala real del agente.
|
||||
|
||||
## Catálogo
|
||||
|
||||
Las automatizaciones viven en `crons/`. Ver `crons/README.md` para la documentación completa.
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# apply.sh <name> <agent-id>
|
||||
# Añade la automatización <name> al config.yaml del agente <agent-id>.
|
||||
# Usa yq si está disponible; en caso contrario imprime el bloque YAML para copiar a mano.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Uso: $0 <nombre-automatizacion> <agent-id>" >&2
|
||||
echo "Ejemplo: $0 good-morning assistant-bot" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
AGENT_ID="$2"
|
||||
|
||||
SCHEDULE_FILE="$REPO_ROOT/crons/$NAME/schedule.yaml"
|
||||
AGENT_CONFIG="$REPO_ROOT/agents/$AGENT_ID/config.yaml"
|
||||
|
||||
if [[ ! -f "$SCHEDULE_FILE" ]]; then
|
||||
echo "Error: no existe crons/$NAME/schedule.yaml" >&2
|
||||
echo "Usa ./dev-scripts/cron/list.sh para ver las automatizaciones disponibles." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$AGENT_CONFIG" ]]; then
|
||||
echo "Error: no existe agents/$AGENT_ID/config.yaml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse schedule.yaml fields
|
||||
kind=""
|
||||
template=""
|
||||
cron_expr=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
" kind:"*) kind="${line#*kind:}"; kind="${kind// /}" ;;
|
||||
" template:"*) template="${line#*template:}"; template="${template# }" ;;
|
||||
default_cron:*) cron_expr="${line#default_cron:}"; cron_expr="${cron_expr# }"; cron_expr="${cron_expr//\"/}" ;;
|
||||
esac
|
||||
done < "$SCHEDULE_FILE"
|
||||
|
||||
if [[ -z "$kind" || -z "$template" || -z "$cron_expr" ]]; then
|
||||
echo "Error: schedule.yaml incompleto (falta kind, template o default_cron)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build YAML block
|
||||
YAML_BLOCK=" - name: $NAME
|
||||
cron: \"$cron_expr\"
|
||||
output_room: \"\" # TODO: reemplaza con la sala real del agente
|
||||
action:
|
||||
kind: $kind
|
||||
template: \"$template\""
|
||||
|
||||
# Try yq first
|
||||
if command -v yq &>/dev/null; then
|
||||
# Check if schedules key already has this entry
|
||||
existing=$(yq ".schedules // [] | .[] | select(.name == \"$NAME\") | .name" "$AGENT_CONFIG" 2>/dev/null || true)
|
||||
if [[ -n "$existing" ]]; then
|
||||
echo "Advertencia: el agente $AGENT_ID ya tiene un schedule llamado '$NAME'. No se añade de nuevo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Append using yq
|
||||
yq -i ".schedules += [{\"name\": \"$NAME\", \"cron\": \"$cron_expr\", \"output_room\": \"\", \"action\": {\"kind\": \"$kind\", \"template\": \"$template\"}}]" "$AGENT_CONFIG"
|
||||
echo "✓ Añadido schedule '$NAME' a agents/$AGENT_ID/config.yaml"
|
||||
echo "→ Edita output_room en agents/$AGENT_ID/config.yaml para apuntar a la sala correcta."
|
||||
else
|
||||
echo "yq no está disponible. Añade manualmente el siguiente bloque a agents/$AGENT_ID/config.yaml:"
|
||||
echo ""
|
||||
echo "schedules:"
|
||||
echo "$YAML_BLOCK"
|
||||
echo ""
|
||||
echo "→ Edita output_room para apuntar a la sala correcta del agente."
|
||||
fi
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# list.sh — Lista todas las automatizaciones del catálogo crons/ con nombre, tipo y descripción.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CRONS_DIR="$REPO_ROOT/crons"
|
||||
|
||||
if [[ ! -d "$CRONS_DIR" ]]; then
|
||||
echo "No se encontró el directorio crons/." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Collect entries: name, kind, default_cron, description
|
||||
entries=()
|
||||
while IFS= read -r -d '' schedule_file; do
|
||||
name=""
|
||||
description=""
|
||||
kind=""
|
||||
cron_expr=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
name:*) name="${line#name:}"; name="${name// /}" ;;
|
||||
description:*) description="${line#description:}"; description="${description# }"; description="${description#\"}"; description="${description%\"}" ;;
|
||||
" kind:"*) kind="${line#*kind:}"; kind="${kind// /}" ;;
|
||||
default_cron:*) cron_expr="${line#default_cron:}"; cron_expr="${cron_expr# }"; cron_expr="${cron_expr//\"/}" ;;
|
||||
esac
|
||||
done < "$schedule_file"
|
||||
|
||||
if [[ -n "$name" ]]; then
|
||||
entries+=("$name|$kind|$cron_expr|$description")
|
||||
fi
|
||||
done < <(find "$CRONS_DIR" -name "schedule.yaml" -print0 | sort -z)
|
||||
|
||||
if [[ ${#entries[@]} -eq 0 ]]; then
|
||||
echo "No hay automatizaciones en crons/."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Print header
|
||||
printf "%-22s %-15s %-15s %s\n" "NOMBRE" "TIPO" "CRON" "DESCRIPCIÓN"
|
||||
printf "%-22s %-15s %-15s %s\n" "------" "----" "----" "-----------"
|
||||
|
||||
for entry in "${entries[@]}"; do
|
||||
IFS='|' read -r name kind cron_expr description <<< "$entry"
|
||||
printf "%-22s %-15s %-15s %s\n" "$name" "$kind" "$cron_expr" "$description"
|
||||
done
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# new.sh — Scaffolder interactivo para automatizaciones cron
|
||||
# Crea crons/<name>/schedule.yaml y el archivo de prompt/mensaje vacío.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
echo "=== Nueva automatización cron ==="
|
||||
echo ""
|
||||
|
||||
# Nombre
|
||||
read -rp "Nombre de la automatización (ej: weekly-report): " NAME
|
||||
NAME="${NAME// /-}"
|
||||
NAME="${NAME,,}" # lowercase
|
||||
if [[ -z "$NAME" ]]; then
|
||||
echo "Error: el nombre no puede estar vacío." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -d "$REPO_ROOT/crons/$NAME" ]]; then
|
||||
echo "Error: ya existe crons/$NAME/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Descripción
|
||||
read -rp "Descripción breve: " DESCRIPTION
|
||||
if [[ -z "$DESCRIPTION" ]]; then
|
||||
echo "Error: la descripción no puede estar vacía." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Tipo de acción
|
||||
echo ""
|
||||
echo "Tipo de acción:"
|
||||
echo " 1) send_message — envía un mensaje estático o plantilla"
|
||||
echo " 2) llm_prompt — llama al LLM con un prompt y envía la respuesta"
|
||||
read -rp "Selecciona [1/2]: " ACTION_TYPE_NUM
|
||||
case "$ACTION_TYPE_NUM" in
|
||||
1) ACTION_KIND="send_message"; PROMPT_FILE="message.md" ;;
|
||||
2) ACTION_KIND="llm_prompt"; PROMPT_FILE="prompt.md" ;;
|
||||
*)
|
||||
echo "Error: selección inválida. Usa 1 o 2." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Cron expression
|
||||
DEFAULT_CRON="0 9 * * *"
|
||||
read -rp "Cron expression [default: $DEFAULT_CRON]: " CRON_EXPR
|
||||
CRON_EXPR="${CRON_EXPR:-$DEFAULT_CRON}"
|
||||
|
||||
# Crear estructura
|
||||
CRON_DIR="$REPO_ROOT/crons/$NAME"
|
||||
PROMPTS_DIR="$CRON_DIR/prompts"
|
||||
mkdir -p "$PROMPTS_DIR"
|
||||
|
||||
# schedule.yaml
|
||||
cat > "$CRON_DIR/schedule.yaml" <<EOF
|
||||
# Automatización: $NAME
|
||||
name: $NAME
|
||||
description: "$DESCRIPTION"
|
||||
|
||||
# Cron por defecto
|
||||
default_cron: "$CRON_EXPR"
|
||||
|
||||
# Acción
|
||||
action:
|
||||
kind: $ACTION_KIND
|
||||
# Relativo a la raíz del proyecto
|
||||
template: crons/$NAME/prompts/$PROMPT_FILE
|
||||
|
||||
# Sala de salida por defecto (vacío = el agente debe configurar output_room)
|
||||
default_output_room: ""
|
||||
EOF
|
||||
|
||||
# Archivo de prompt/mensaje
|
||||
touch "$PROMPTS_DIR/$PROMPT_FILE"
|
||||
|
||||
echo ""
|
||||
echo "✓ Creado: crons/$NAME/schedule.yaml"
|
||||
echo "✓ Creado: crons/$NAME/prompts/$PROMPT_FILE"
|
||||
echo ""
|
||||
echo "Edita crons/$NAME/prompts/$PROMPT_FILE con el contenido deseado."
|
||||
echo ""
|
||||
echo "Añade esto a agents/<id>/config.yaml:"
|
||||
echo ""
|
||||
echo " schedules:"
|
||||
echo " - name: $NAME"
|
||||
echo " cron: \"$CRON_EXPR\""
|
||||
echo " output_room: \"!TUROOM:matrix-af2f3d.organic-machine.com\""
|
||||
echo " action:"
|
||||
echo " kind: $ACTION_KIND"
|
||||
echo " template: \"crons/$NAME/prompts/$PROMPT_FILE\""
|
||||
echo ""
|
||||
echo "O usa: ./dev-scripts/cron/apply.sh $NAME <agent-id>"
|
||||
@@ -5,6 +5,12 @@
|
||||
"issue": "0019",
|
||||
"description": "Hardening contra prompt injection: deny-by-default en tools, SSRF protection, path traversal, allowlists",
|
||||
"added": "2026-03-07"
|
||||
},
|
||||
"centralized-security-groups": {
|
||||
"enabled": false,
|
||||
"issue": "0024",
|
||||
"description": "Sistema centralizado de grupos de usuarios y agentes para control de acceso; elimina security.roles y allowed_users por agente",
|
||||
"added": "2026-03-08"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,9 @@ afectados y notas de implementacion.
|
||||
| 22a | E2E: Infraestructura base | [0022a-e2e-infra.md](completed/0022a-e2e-infra.md) | completado |
|
||||
| 22b | E2E: Auth fixtures y helpers | [0022b-e2e-auth-helpers.md](completed/0022b-e2e-auth-helpers.md) | completado |
|
||||
| 22c | E2E: Tests de agentes + docs | [0022c-e2e-agent-tests.md](completed/0022c-e2e-agent-tests.md) | completado |
|
||||
| 23 | Seccion de tests en dashboard | [0023-dashboard-tests.md](completed/0023-dashboard-tests.md) | completado |
|
||||
| 23 | Seccion de tests en dashboard | [0023-dashboard-tests.md](completed/0023-dashboard-tests.md) | completado |
|
||||
| 24 | Grupos y permisos centralizados | [0024-centralized-security-groups.md](0024-centralized-security-groups.md) | pendiente |
|
||||
| 24a | Security types: pkg/security/ | [0024a-security-types.md](0024a-security-types.md) | pendiente |
|
||||
| 24b | Security loader: shell/security/ | [0024b-security-loader.md](0024b-security-loader.md) | pendiente |
|
||||
| 24c | Security integration + cleanup | [0024c-security-integration.md](0024c-security-integration.md) | pendiente |
|
||||
| 25 | Catálogo cron + scaffolder | [0025-cron-scaffolder.md](completed/0025-cron-scaffolder.md) | completado |
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# 0025 — Catálogo de automatizaciones cron + scaffolder
|
||||
|
||||
## Objetivo
|
||||
|
||||
Crear un directorio `crons/` como catálogo central de automatizaciones nombradas, y un conjunto de
|
||||
scripts en `dev-scripts/cron/` para crear nuevas automatizaciones, listarlas y aplicarlas a agentes
|
||||
sin editar YAML a mano. Evolución directa de la infraestructura creada en el issue 0005.
|
||||
|
||||
## Contexto
|
||||
|
||||
- `shell/cron/` ya implementa el scheduler con `send_message` y `llm_prompt` (issue 0005)
|
||||
- Las automatizaciones se definen en cada `agents/<id>/config.yaml` bajo `schedules:`, lo que las
|
||||
dispersa y dificulta reutilizarlas entre agentes
|
||||
- No hay forma de crear una nueva automatización sin editar YAML a mano y conocer la estructura
|
||||
- No existe un catálogo centralizado ni scripts de gestión
|
||||
- Depende de: issue 0005 (completado)
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
crons/ NEW — catálogo de automatizaciones nombradas
|
||||
good-morning/
|
||||
schedule.yaml NEW — spec (description, cron, action, output_room por defecto)
|
||||
prompts/
|
||||
message.md NEW — plantilla de mensaje
|
||||
daily-summary/
|
||||
schedule.yaml NEW
|
||||
prompts/
|
||||
prompt.md NEW
|
||||
|
||||
dev-scripts/cron/ NEW — herramientas de gestión
|
||||
new.sh NEW — scaffolder interactivo
|
||||
list.sh NEW — listar automatizaciones con descripción
|
||||
apply.sh NEW — añadir automatización a config de agente
|
||||
|
||||
shell/cron/scheduler.go MODIFY — añadir Fire(name) para disparo manual en tests
|
||||
shell/cron/actions.go MODIFY — pequeñas mejoras si surgen al escribir ejemplos
|
||||
```
|
||||
|
||||
### Patrón pure core / impure shell
|
||||
|
||||
- `pkg/` — sin cambios (no hay lógica pura nueva)
|
||||
- `shell/cron/` — modificación mínima: añadir `Fire(ctx, sc)` para testing manual
|
||||
- `crons/` — datos puros (YAML + Markdown), sin código Go
|
||||
- `dev-scripts/cron/` — shell scripts impuros (leen/escriben filesystem, parchean YAML)
|
||||
|
||||
### Convención de `crons/<name>/schedule.yaml`
|
||||
|
||||
```yaml
|
||||
# Metadata
|
||||
name: good-morning
|
||||
description: "Saludo de buenos días en una sala"
|
||||
|
||||
# Schedule por defecto (el agente puede sobreescribir)
|
||||
default_cron: "0 9 * * *"
|
||||
|
||||
# Acción
|
||||
action:
|
||||
kind: send_message # send_message | llm_prompt
|
||||
template: prompts/message.md # relativo a la carpeta de la automatización
|
||||
|
||||
# Sala por defecto (opcional; el agente puede sobreescribir con output_room)
|
||||
default_output_room: ""
|
||||
```
|
||||
|
||||
Este archivo es solo **documentación + template**. El agente lo referencia en su `config.yaml`
|
||||
usando la sección `schedules:` habitual; `apply.sh` automatiza ese paso.
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Estructura `crons/` y automatizaciones de ejemplo
|
||||
|
||||
- [ ] **1.1** Crear `crons/` con un `README.md` que explique la convención
|
||||
- [ ] **1.2** Crear `crons/good-morning/schedule.yaml` + `prompts/message.md` (ejemplo `send_message`)
|
||||
- [ ] **1.3** Crear `crons/daily-summary/schedule.yaml` + `prompts/prompt.md` (ejemplo `llm_prompt`)
|
||||
|
||||
### Fase 2: Scripts de gestión en `dev-scripts/cron/`
|
||||
|
||||
- [ ] **2.1** `dev-scripts/cron/new.sh` — scaffolder interactivo:
|
||||
- Pregunta: nombre, descripción, tipo (`send_message` o `llm_prompt`), cron expression
|
||||
- Crea `crons/<name>/schedule.yaml` y el archivo de prompt/mensaje vacío
|
||||
- Imprime el bloque YAML listo para copiar en `config.yaml`
|
||||
- [ ] **2.2** `dev-scripts/cron/list.sh` — lista todas las carpetas bajo `crons/` con nombre y
|
||||
descripción extraída del `schedule.yaml`
|
||||
- [ ] **2.3** `dev-scripts/cron/apply.sh <name> <agent-id>` — añade la entrada `schedules:` a
|
||||
`agents/<agent-id>/config.yaml` con los valores por defecto del `schedule.yaml`. Usa `yq` si está
|
||||
disponible; en caso contrario imprime el bloque YAML para copiar a mano
|
||||
|
||||
### Fase 3: Mejora menor en `shell/cron/`
|
||||
|
||||
- [ ] **3.1** Exportar `Fire(ctx context.Context, sc config.ScheduleCfg)` en `scheduler.go` para
|
||||
poder disparar un schedule en tests o desde CLI sin esperar al cron
|
||||
- [ ] **3.2** Actualizar `scheduler_test.go` para usar `Fire` en lugar de `@every 100ms` donde
|
||||
sea posible (reduce tiempo de test)
|
||||
|
||||
### Fase 4: Tests
|
||||
|
||||
- [ ] **4.1** Test de `Fire` para `send_message` inline
|
||||
- [ ] **4.2** Test de `Fire` para `llm_prompt`
|
||||
- [ ] **4.3** Verificar que `go test -tags goolm ./shell/cron/...` pasa sin regresiones
|
||||
|
||||
### Fase 5: Cleanup y docs
|
||||
|
||||
- [ ] **5.1** Añadir entrada `crons/` en la tabla de estructura de `CLAUDE.md`
|
||||
- [ ] **5.2** Añadir `dev-scripts/cron/` en la misma tabla
|
||||
- [ ] **5.3** Mención en `dev-scripts/agent/README.md` o crear `dev-scripts/cron/README.md`
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```bash
|
||||
# Crear una nueva automatización
|
||||
./dev-scripts/cron/new.sh
|
||||
|
||||
# → Nombre de la automatización: weekly-report
|
||||
# → Descripción: Resumen semanal del equipo
|
||||
# → Tipo de acción [send_message/llm_prompt]: llm_prompt
|
||||
# → Cron expression [default: 0 9 * * 1]: 0 9 * * 1
|
||||
# ✓ Creado: crons/weekly-report/schedule.yaml
|
||||
# ✓ Creado: crons/weekly-report/prompts/prompt.md
|
||||
#
|
||||
# Añade esto a agents/<id>/config.yaml:
|
||||
# schedules:
|
||||
# - name: weekly-report
|
||||
# cron: "0 9 * * 1"
|
||||
# output_room: "!ROOM:server.com"
|
||||
# action:
|
||||
# kind: llm_prompt
|
||||
# template: "crons/weekly-report/prompts/prompt.md"
|
||||
|
||||
# Listar automatizaciones disponibles
|
||||
./dev-scripts/cron/list.sh
|
||||
|
||||
# → good-morning send_message "0 9 * * *" Saludo de buenos días
|
||||
# → daily-summary llm_prompt "0 18 * * *" Resumen diario del equipo
|
||||
# → weekly-report llm_prompt "0 9 * * 1" Resumen semanal del equipo
|
||||
|
||||
# Aplicar a un agente (parchea config.yaml automáticamente)
|
||||
./dev-scripts/cron/apply.sh good-morning assistant-bot
|
||||
# → Añadido schedule 'good-morning' a agents/assistant-bot/config.yaml
|
||||
# → Edita output_room en config.yaml para apuntar a la sala correcta
|
||||
```
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **`crons/` como catálogo de datos, no de código**: Los archivos `schedule.yaml` son solo
|
||||
documentación + template. No hay un registry Go nuevo; el scheduler sigue leyendo de
|
||||
`config.yaml` como hasta ahora. Esto evita añadir un pattern nuevo al proyecto.
|
||||
- **`apply.sh` opcional**: Si `yq` no está disponible, el script imprime el bloque YAML para
|
||||
copiar a mano. Sin dependencias obligatorias.
|
||||
- **`Fire()` en lugar de cron real en tests**: Los tests actuales usan `@every 100ms` y duermen
|
||||
350ms. `Fire()` los hace deterministas e instantáneos.
|
||||
- **No registry Go para crons**: Añadir un registry compilado (como `cmd/launcher`) para crons
|
||||
sería over-engineering. La gestión vía shell scripts es suficiente y más flexible.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Issue 0005 completado (scheduler en `shell/cron/` — ya está)
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **`yq` no disponible en el entorno**: `apply.sh` cae back a imprimir el bloque YAML, nunca
|
||||
falla. Sin riesgo real.
|
||||
- **Paths relativos en `schedule.yaml`**: El campo `template` en el YAML es relativo a la raíz
|
||||
del proyecto. Documentar claramente en el `README.md` del catálogo.
|
||||
- **Divergencia entre catálogo y config del agente**: Si alguien edita `schedule.yaml` después
|
||||
de aplicarlo, el agente no se actualiza. Es intencional — `apply.sh` es un helper de
|
||||
scaffolding, no sync continua.
|
||||
@@ -45,6 +45,22 @@ func New(
|
||||
}
|
||||
}
|
||||
|
||||
// Fire immediately executes the action for the given schedule, bypassing the cron timer.
|
||||
// Useful for tests and manual triggering from CLI.
|
||||
func (s *Scheduler) Fire(ctx context.Context, sc config.ScheduleCfg) {
|
||||
room := sc.OutputRoom
|
||||
if room == "" {
|
||||
s.logger.Warn("Fire: schedule has no output_room, skipping", "name", sc.Name)
|
||||
return
|
||||
}
|
||||
handler := s.buildHandler(sc)
|
||||
if handler == nil {
|
||||
s.logger.Warn("Fire: unsupported action kind", "name", sc.Name, "kind", sc.Action.Kind)
|
||||
return
|
||||
}
|
||||
handler(ctx, room)
|
||||
}
|
||||
|
||||
// Start registers all schedules and starts the cron loop.
|
||||
// It returns when ctx is cancelled, stopping the cron runner.
|
||||
func (s *Scheduler) Start(ctx context.Context) {
|
||||
|
||||
+126
-71
@@ -62,7 +62,7 @@ func waitCalls(t *testing.T, f *fakeSender, n int32) {
|
||||
t.Fatalf("expected %d call(s) to SendMarkdown, got %d", n, f.calls.Load())
|
||||
}
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────────
|
||||
// ── cron-based tests (require timer) ──────────────────────────────────────
|
||||
|
||||
func TestScheduler_SendMessage_Inline(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
@@ -171,76 +171,6 @@ func TestScheduler_LLMPrompt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_LLMPrompt_NoLLM(t *testing.T) {
|
||||
// When no LLM is configured, llm_prompt should be skipped gracefully (no panic).
|
||||
sender := &fakeSender{}
|
||||
cfg := []config.ScheduleCfg{
|
||||
{
|
||||
Name: "no-llm",
|
||||
Cron: "@every 100ms",
|
||||
OutputRoom: "!room:server.com",
|
||||
Action: config.ScheduledAction{
|
||||
Kind: "llm_prompt",
|
||||
Prompt: "hello",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := shellcron.New(cfg, sender, nil, "", newTestLogger(t))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
s.Start(ctx)
|
||||
}()
|
||||
|
||||
// Wait a bit to confirm nothing is sent
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
if sender.calls.Load() != 0 {
|
||||
t.Errorf("expected 0 calls without LLM, got %d", sender.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_SkipsInvalidSchedule(t *testing.T) {
|
||||
// Schedules without output_room or without action kind must be skipped silently.
|
||||
sender := &fakeSender{}
|
||||
cfg := []config.ScheduleCfg{
|
||||
{
|
||||
Name: "no-room",
|
||||
Cron: "@every 100ms",
|
||||
// missing OutputRoom
|
||||
Action: config.ScheduledAction{Kind: "send_message", Message: "hi"},
|
||||
},
|
||||
{
|
||||
Name: "no-kind",
|
||||
Cron: "@every 100ms",
|
||||
OutputRoom: "!room:server.com",
|
||||
// missing Action.Kind
|
||||
},
|
||||
}
|
||||
|
||||
s := shellcron.New(cfg, sender, nil, "", newTestLogger(t))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
s.Start(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
if sender.calls.Load() != 0 {
|
||||
t.Errorf("expected 0 calls for invalid schedules, got %d", sender.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_MatrixSendError(t *testing.T) {
|
||||
// If matrix.SendMarkdown returns an error, the scheduler should log it and not panic.
|
||||
cfg := []config.ScheduleCfg{
|
||||
@@ -269,3 +199,128 @@ func TestScheduler_MatrixSendError(t *testing.T) {
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
// ── Fire() tests (deterministic, no timer) ─────────────────────────────────
|
||||
|
||||
func TestFire_SendMessage_Inline(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
s := shellcron.New(nil, sender, nil, "", newTestLogger(t))
|
||||
|
||||
sc := config.ScheduleCfg{
|
||||
Name: "fire-inline",
|
||||
Cron: "0 9 * * *",
|
||||
OutputRoom: "!fireroom:server.com",
|
||||
Action: config.ScheduledAction{
|
||||
Kind: "send_message",
|
||||
Message: "buenos días via Fire",
|
||||
},
|
||||
}
|
||||
|
||||
s.Fire(context.Background(), sc)
|
||||
|
||||
if sender.calls.Load() != 1 {
|
||||
t.Fatalf("expected 1 call, got %d", sender.calls.Load())
|
||||
}
|
||||
if sender.lastRM != "!fireroom:server.com" {
|
||||
t.Errorf("unexpected room: %s", sender.lastRM)
|
||||
}
|
||||
if sender.lastMD != "buenos días via Fire" {
|
||||
t.Errorf("unexpected message: %s", sender.lastMD)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFire_LLMPrompt(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
llm := fakeLLM("respuesta del LLM via Fire")
|
||||
s := shellcron.New(nil, sender, llm, "gpt-4o", newTestLogger(t))
|
||||
|
||||
sc := config.ScheduleCfg{
|
||||
Name: "fire-llm",
|
||||
Cron: "0 18 * * *",
|
||||
OutputRoom: "!llmroom:server.com",
|
||||
Action: config.ScheduledAction{
|
||||
Kind: "llm_prompt",
|
||||
Prompt: "resume el día",
|
||||
},
|
||||
}
|
||||
|
||||
s.Fire(context.Background(), sc)
|
||||
|
||||
if sender.calls.Load() != 1 {
|
||||
t.Fatalf("expected 1 call, got %d", sender.calls.Load())
|
||||
}
|
||||
if sender.lastMD != "respuesta del LLM via Fire" {
|
||||
t.Errorf("unexpected LLM reply: %q", sender.lastMD)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFire_NoOutputRoom_Skips(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
s := shellcron.New(nil, sender, nil, "", newTestLogger(t))
|
||||
|
||||
sc := config.ScheduleCfg{
|
||||
Name: "fire-no-room",
|
||||
Cron: "0 9 * * *",
|
||||
OutputRoom: "", // intentionally empty
|
||||
Action: config.ScheduledAction{
|
||||
Kind: "send_message",
|
||||
Message: "should not send",
|
||||
},
|
||||
}
|
||||
|
||||
s.Fire(context.Background(), sc)
|
||||
|
||||
if sender.calls.Load() != 0 {
|
||||
t.Errorf("expected 0 calls when output_room is empty, got %d", sender.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFire_LLMPrompt_NoLLM_Skips(t *testing.T) {
|
||||
// When no LLM is configured, Fire with llm_prompt should not send anything.
|
||||
sender := &fakeSender{}
|
||||
s := shellcron.New(nil, sender, nil, "", newTestLogger(t))
|
||||
|
||||
sc := config.ScheduleCfg{
|
||||
Name: "fire-no-llm",
|
||||
Cron: "0 9 * * *",
|
||||
OutputRoom: "!room:server.com",
|
||||
Action: config.ScheduledAction{
|
||||
Kind: "llm_prompt",
|
||||
Prompt: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
s.Fire(context.Background(), sc)
|
||||
|
||||
if sender.calls.Load() != 0 {
|
||||
t.Errorf("expected 0 calls without LLM, got %d", sender.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_SkipsInvalidSchedule(t *testing.T) {
|
||||
// Schedules without output_room or without action kind must be skipped during Start.
|
||||
// We use Fire directly to test the skip logic without timer overhead.
|
||||
sender := &fakeSender{}
|
||||
s := shellcron.New(nil, sender, nil, "", newTestLogger(t))
|
||||
ctx := context.Background()
|
||||
|
||||
// No output_room → skip
|
||||
s.Fire(ctx, config.ScheduleCfg{
|
||||
Name: "no-room",
|
||||
Cron: "@every 100ms",
|
||||
// missing OutputRoom
|
||||
Action: config.ScheduledAction{Kind: "send_message", Message: "hi"},
|
||||
})
|
||||
|
||||
// No kind → Fire calls buildHandler which returns nil → skip
|
||||
s.Fire(ctx, config.ScheduleCfg{
|
||||
Name: "no-kind",
|
||||
Cron: "@every 100ms",
|
||||
OutputRoom: "!room:server.com",
|
||||
// missing Action.Kind
|
||||
})
|
||||
|
||||
if sender.calls.Load() != 0 {
|
||||
t.Errorf("expected 0 calls for invalid schedules, got %d", sender.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user