7913116a8e
- .claude/agents/fn-analizador/SKILL.md - .claude/agents/fn-constructor/SKILL.md - .claude/agents/fn-executor/SKILL.md - .claude/agents/fn-mejorador/SKILL.md - .claude/agents/fn-orquestador/SKILL.md - .claude/agents/fn-recopilador/SKILL.md - .claude/commands/app.md - .claude/commands/compile.md - .claude/commands/cpp-app.md - .claude/commands/create_functions.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
388 lines
13 KiB
Markdown
388 lines
13 KiB
Markdown
# Estandar de Ejecucion: YAML + Exit Codes + Hooks
|
|
|
|
Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con dag_engine y operations.db.
|
|
|
|
---
|
|
|
|
## Motivacion
|
|
|
|
Tenemos dos patrones que ya funcionan:
|
|
|
|
- **dag_engine** (`apps/dag_engine/`): orquesta DAGs con `command`/`function`, usa exit codes para decidir flujo, soporta `handlers` (init/success/failure/exit) y scheduling cron via `schedule:`.
|
|
- **script_navegador**: ejecuta pasos tipados (CDP actions) desde YAML, registra todo en operations.db, sale con 0/1.
|
|
|
|
Ambos comparten la misma idea: **YAML declara el flujo, exit code señala el resultado, logs estructurados permiten analisis posterior**. Este documento formaliza ese contrato para que cualquier app nueva lo siga desde el inicio.
|
|
|
|
---
|
|
|
|
## 1. Estructura YAML
|
|
|
|
Toda app que ejecute un flujo debe leer un YAML con esta estructura minima:
|
|
|
|
```yaml
|
|
# Cabecera obligatoria
|
|
name: nombre_del_flujo
|
|
description: "Que hace este flujo" # opcional pero recomendado
|
|
|
|
# Variables de entorno disponibles para todos los pasos
|
|
env: # opcional
|
|
KEY: value
|
|
|
|
# Pasos del flujo
|
|
steps:
|
|
- name: paso_1 # identificador unico dentro del flujo
|
|
action: tipo_de_accion # dispatch key (navigate, command, query, etc.)
|
|
# ... parametros especificos del action
|
|
continue_on_error: false # default: false
|
|
timeout_ms: 30000 # default: 30s, 0 = sin timeout
|
|
depends: [] # IDs de pasos previos (vacio = secuencial)
|
|
|
|
# Hooks opcionales
|
|
hooks:
|
|
on_success: "comando o script" # se ejecuta si exit code = 0
|
|
on_failure: "comando o script" # se ejecuta si exit code = 1
|
|
on_partial: "comando o script" # se ejecuta si exit code = 2
|
|
```
|
|
|
|
### Campos de la cabecera
|
|
|
|
| Campo | Requerido | Descripcion |
|
|
|-------|-----------|-------------|
|
|
| `name` | si | Identificador del flujo (snake_case) |
|
|
| `description` | no | Descripcion legible |
|
|
| `env` | no | Variables de entorno (mapa key-value) |
|
|
| `steps` | si | Lista de pasos a ejecutar |
|
|
| `hooks` | no | Comandos a ejecutar segun resultado |
|
|
|
|
### Campos de cada step
|
|
|
|
| Campo | Requerido | Default | Descripcion |
|
|
|-------|-----------|---------|-------------|
|
|
| `name` | si | — | ID unico del paso dentro del flujo |
|
|
| `action` | si | — | Tipo de accion (cada app define sus propios actions) |
|
|
| `continue_on_error` | no | `false` | Si `true`, un fallo no detiene el flujo |
|
|
| `timeout_ms` | no | `30000` | Timeout del paso en ms (0 = sin timeout) |
|
|
| `depends` | no | `[]` | Pasos que deben completarse antes |
|
|
|
|
Los campos adicionales dependen del `action` y los define cada app/dominio.
|
|
|
|
### Actions por dominio
|
|
|
|
Cada app registra sus actions validos. Ejemplos:
|
|
|
|
| Dominio | Actions | App de referencia |
|
|
|---------|---------|-------------------|
|
|
| navegacion | `navigate`, `click`, `type`, `wait`, `screenshot`, `evaluate`, `get_html`, `wait_load`, `sleep` | script_navegador |
|
|
| shell | `command`, `script` | dag_engine / apps genericas |
|
|
| registry | `function`, `args` | dag_engine (invoca `fn run <id>` y captura `function_id` en `dag_step_results`) |
|
|
| database | `query`, `migrate`, `backup` | futuras apps |
|
|
| http | `request`, `assert_status`, `extract` | futuras apps |
|
|
|
|
---
|
|
|
|
## 2. Exit Codes
|
|
|
|
Toda app/script que siga este estandar DEBE salir con uno de estos tres codigos:
|
|
|
|
| Codigo | Constante | Significado |
|
|
|--------|-----------|-------------|
|
|
| `0` | `EXIT_SUCCESS` | Todos los pasos completaron sin error |
|
|
| `1` | `EXIT_FAILURE` | Al menos un paso fallo y el flujo aborto |
|
|
| `2` | `EXIT_PARTIAL` | Algunos pasos fallaron pero el flujo continuo (continue_on_error) |
|
|
|
|
### Reglas
|
|
|
|
- Si todos los pasos son exitosos → `0`
|
|
- Si algun paso falla y tiene `continue_on_error: false` → `1` (aborta)
|
|
- Si algun paso falla pero todos los fallos tenian `continue_on_error: true` → `2`
|
|
- Un timeout agotado cuenta como fallo del paso
|
|
|
|
### Compatibilidad con dag_engine
|
|
|
|
dag_engine interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure, el DAG que llama a la app puede usar `continue_on.exit_code`:
|
|
|
|
```yaml
|
|
steps:
|
|
- name: run_app
|
|
command: ./mi_app --script flujo.yaml
|
|
continue_on:
|
|
exit_code: [2] # exit 2 no aborta el DAG (partial es tolerado)
|
|
- name: check_result
|
|
command: test $? -le 1 # falla solo si fue error fatal
|
|
depends: [run_app]
|
|
```
|
|
|
|
O alternativamente, la app puede configurarse con `--strict` para mapear partial → failure (exit 1).
|
|
|
|
---
|
|
|
|
## 3. Output Estructurado
|
|
|
|
Toda app DEBE escribir un **resumen JSON en stdout** al finalizar (despues de su output humano). El formato:
|
|
|
|
```json
|
|
{
|
|
"name": "nombre_del_flujo",
|
|
"status": "success|failure|partial",
|
|
"exit_code": 0,
|
|
"started_at": "2026-04-01T10:00:00Z",
|
|
"ended_at": "2026-04-01T10:00:05Z",
|
|
"duration_ms": 5000,
|
|
"steps_total": 5,
|
|
"steps_ok": 4,
|
|
"steps_failed": 1,
|
|
"steps": [
|
|
{
|
|
"name": "paso_1",
|
|
"action": "navigate",
|
|
"status": "ok",
|
|
"elapsed_ms": 1200,
|
|
"output": "optional output"
|
|
},
|
|
{
|
|
"name": "paso_2",
|
|
"action": "click",
|
|
"status": "error",
|
|
"elapsed_ms": 500,
|
|
"error": "elemento no encontrado: #btn-submit"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Separacion humano / maquina
|
|
|
|
- **stderr**: logs legibles para humanos (progreso, warnings)
|
|
- **stdout**: output JSON estructurado al final (para dag_engine, hooks, pipes)
|
|
- Flag `--json` o `--quiet`: suprime output humano, solo JSON en stdout
|
|
|
|
Esto permite que dag_engine capture el JSON:
|
|
|
|
```yaml
|
|
steps:
|
|
- name: navegacion
|
|
command: ./script_navegador --script busqueda.yaml --json
|
|
output: RESULT
|
|
- name: analizar
|
|
command: echo "$RESULT" | jq '.steps[] | select(.status=="error")'
|
|
depends: [navegacion]
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Hooks
|
|
|
|
Los hooks se ejecutan **despues** de todos los pasos, segun el exit code resultante.
|
|
|
|
### En el YAML del flujo
|
|
|
|
```yaml
|
|
hooks:
|
|
on_success: "notify-send 'Flujo completado'"
|
|
on_failure: "echo 'FALLO: {{.Name}}' >> /tmp/failures.log"
|
|
on_partial: "echo 'PARCIAL: {{.StepsFailed}} pasos fallaron' >> /tmp/warnings.log"
|
|
on_step_error: "echo 'Step {{.StepName}} fallo: {{.Error}}'" # se ejecuta por cada paso fallido
|
|
```
|
|
|
|
### Variables disponibles en hooks
|
|
|
|
| Variable | Tipo | Descripcion |
|
|
|----------|------|-------------|
|
|
| `{{.Name}}` | string | Nombre del flujo |
|
|
| `{{.Status}}` | string | success, failure, partial |
|
|
| `{{.ExitCode}}` | int | 0, 1, 2 |
|
|
| `{{.DurationMs}}` | int | Duracion total en ms |
|
|
| `{{.StepsTotal}}` | int | Total de pasos |
|
|
| `{{.StepsOk}}` | int | Pasos exitosos |
|
|
| `{{.StepsFailed}}` | int | Pasos fallidos |
|
|
| `{{.StepName}}` | string | Nombre del paso (solo en on_step_error) |
|
|
| `{{.Error}}` | string | Mensaje de error (solo en on_step_error/on_failure) |
|
|
|
|
### En dag_engine (handlers globales)
|
|
|
|
Los hooks del YAML son locales al flujo. Para orquestacion, dag_engine maneja sus propios handlers (`init/success/failure/exit`, alias `handler_on`):
|
|
|
|
```yaml
|
|
# apps/dag_engine/dags_migrated/mi_automatizacion.yaml
|
|
name: mi_automatizacion
|
|
handlers:
|
|
failure:
|
|
- name: alerta
|
|
command: echo "Fallo en mi_automatizacion" >> /var/log/fn_registry/failures.log
|
|
success:
|
|
- name: registrar
|
|
command: echo "OK $(date)" >> /var/log/fn_registry/success.log
|
|
steps:
|
|
- name: ejecutar
|
|
function: mi_pipeline_bash_pipelines
|
|
args: ["flujo.yaml"]
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Integracion con operations.db
|
|
|
|
Las apps que siguen el bucle reactivo DEBEN registrar la ejecucion en operations.db:
|
|
|
|
### Mapping del estandar a operations.db
|
|
|
|
| Concepto estandar | Tabla operations.db | Campo |
|
|
|-------------------|---------------------|-------|
|
|
| Flujo completo | `executions` | `status`: success/failure/partial |
|
|
| Cada paso | `logs` | `level`: info/error, `message`: resultado |
|
|
| Exit code | `executions.metrics` | `{"exit_code": N}` |
|
|
| Output JSON | `executions.metrics` | `{"steps": [...]}` |
|
|
| Duracion | `executions.duration_ms` | milisegundos |
|
|
| Pasos in/out | `executions.records_in/out` | total/exitosos |
|
|
|
|
### Patron de registro (referencia: script_navegador/ops.go)
|
|
|
|
```
|
|
1. initOpsDB() → abrir/crear operations.db
|
|
2. EnsureEntities() → registrar recursos involucrados
|
|
3. EnsureRelations() → registrar como se conectan
|
|
4. [ejecutar flujo]
|
|
5. RecordRun() → insertar execution con metricas
|
|
6. LogStep() por paso → insertar log por cada paso
|
|
7. UpdateRelation() → status final de la relation
|
|
```
|
|
|
|
Este patron ya esta implementado en `script_navegador/ops.go` y sirve como referencia para nuevas apps.
|
|
|
|
---
|
|
|
|
## 6. Flujo completo: App → dag_engine → Hooks
|
|
|
|
```
|
|
dag_engine (scheduler)
|
|
│
|
|
cron / manual / API
|
|
│
|
|
┌────▼────┐
|
|
│ DAG │ apps/dag_engine/dags_migrated/mi_dag.yaml
|
|
│ step │ command: ./mi_app --script flujo.yaml --json
|
|
└────┬────┘
|
|
│
|
|
┌────▼────────────────┐
|
|
│ App (mi_app) │
|
|
│ │
|
|
│ 1. Load YAML │
|
|
│ 2. Validate steps │
|
|
│ 3. Init ops.db │
|
|
│ 4. Execute steps │
|
|
│ 5. Run hooks │
|
|
│ 6. Record to ops │
|
|
│ 7. JSON → stdout │
|
|
│ 8. Exit code │
|
|
└────┬────────────────┘
|
|
│
|
|
┌──────────┼──────────┐
|
|
│ │ │
|
|
exit 0 exit 1 exit 2
|
|
success failure partial
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
engine OK engine FAIL engine FAIL*
|
|
handler handler (configurable)
|
|
```
|
|
|
|
*Con `continue_on: { exit_code: [2] }` en el step, exit 2 no aborta el DAG.
|
|
|
|
---
|
|
|
|
## 7. Implementacion: funciones del registry
|
|
|
|
### Existentes (ya en uso por script_navegador)
|
|
|
|
Todas las funciones CDP del registry (`cdp_*_go_browser`, `chrome_launch_go_browser`) ya siguen el patron de retornar error. El runner de script_navegador las compone.
|
|
|
|
### Nuevas (por construir)
|
|
|
|
| Funcion | Lang | Domain | Descripcion |
|
|
|---------|------|--------|-------------|
|
|
| `run_steps` | bash | shell | Ejecuta pasos de un YAML generico (action=command/script), reporta JSON y sale con 0/1/2 |
|
|
| `report_execution_json` | bash | shell | Genera el JSON de salida estandar dado los resultados de pasos |
|
|
| `exit_with_status` | bash | shell | Calcula exit code (0/1/2) a partir de contadores ok/fail/partial |
|
|
|
|
Estas funciones Bash permiten que cualquier script nuevo siga el estandar sin reimplementar la logica de reporte y exit codes.
|
|
|
|
---
|
|
|
|
## 8. Ejemplo completo
|
|
|
|
### Script YAML (flujo.yaml)
|
|
|
|
```yaml
|
|
name: backup_db
|
|
description: "Backup de operations.db y push a remoto"
|
|
steps:
|
|
- name: check_db
|
|
action: command
|
|
command: "test -f operations.db"
|
|
- name: backup
|
|
action: command
|
|
command: "cp operations.db backups/operations_$(date +%Y%m%d).db"
|
|
depends: [check_db]
|
|
- name: push
|
|
action: command
|
|
command: "git add backups/ && git commit -m 'backup' && git push"
|
|
depends: [backup]
|
|
continue_on_error: true
|
|
hooks:
|
|
on_failure: "echo 'Backup fallo' >> /tmp/backup_errors.log"
|
|
```
|
|
|
|
### Invocacion desde dag_engine
|
|
|
|
```yaml
|
|
name: backup_diario
|
|
schedule: "0 2 * * *"
|
|
working_dir: $HOME/fn_registry/apps/mi_app
|
|
steps:
|
|
- name: backup
|
|
function: backup_db_bash_pipelines
|
|
args: ["flujo.yaml", "--json"]
|
|
handlers:
|
|
failure:
|
|
- name: alerta
|
|
command: echo "Backup fallo $(date)" >> /var/log/fn_registry/failures.log
|
|
```
|
|
|
|
### Output esperado (stdout)
|
|
|
|
```json
|
|
{
|
|
"name": "backup_db",
|
|
"status": "partial",
|
|
"exit_code": 2,
|
|
"started_at": "2026-04-01T02:00:00Z",
|
|
"ended_at": "2026-04-01T02:00:03Z",
|
|
"duration_ms": 3000,
|
|
"steps_total": 3,
|
|
"steps_ok": 2,
|
|
"steps_failed": 1,
|
|
"steps": [
|
|
{"name": "check_db", "action": "command", "status": "ok", "elapsed_ms": 10},
|
|
{"name": "backup", "action": "command", "status": "ok", "elapsed_ms": 450},
|
|
{"name": "push", "action": "command", "status": "error", "elapsed_ms": 2540, "error": "remote rejected"}
|
|
]
|
|
}
|
|
```
|
|
|
|
Exit code: `2` (partial — push fallo pero tenia continue_on_error).
|
|
|
|
---
|
|
|
|
## Resumen del contrato
|
|
|
|
| Aspecto | Regla |
|
|
|---------|-------|
|
|
| Formato de flujo | YAML con `name` + `steps[]` |
|
|
| Exit codes | 0=success, 1=failure, 2=partial |
|
|
| Output maquina | JSON en stdout (con `--json`) |
|
|
| Output humano | stderr (progreso, warnings) |
|
|
| Error handling | `continue_on_error` por paso, hooks por resultado |
|
|
| Trazabilidad | operations.db (executions + logs) |
|
|
| Orquestacion | dag_engine como scheduler (`apps/dag_engine/`), apps/funciones del registry como ejecutores |
|
|
| Funciones | Reutilizar del registry, actions por dominio |
|