Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 580238b32e | |||
| ed767360c1 | |||
| 5bac05ce13 | |||
| d0ceea6f3d | |||
| 0f905b78e0 |
@@ -27,6 +27,8 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
|
||||
|
||||
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
|
||||
|
||||
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
|
||||
|
||||
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
|
||||
|
||||
---
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../projects/aurgi/.claude/commands
|
||||
@@ -1,121 +1,36 @@
|
||||
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
|
||||
|
||||
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
|
||||
|
||||
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
|
||||
|
||||
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
|
||||
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
|
||||
3. `dev/autonomous_protected_paths.json` existe.
|
||||
4. `master` local up-to-date con `origin/master`.
|
||||
5. Branch `auto/<issue_id>` NO existe ya.
|
||||
6. `gh auth status` OK (necesario para PR draft al converger).
|
||||
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
|
||||
|
||||
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
|
||||
|
||||
---
|
||||
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
|
||||
---
|
||||
|
||||
## Argumento
|
||||
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
|
||||
|
||||
`$ARGUMENTS` — `<issue_id>` o `<task_spec_path>` + flags opcionales.
|
||||
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
|
||||
|
||||
```
|
||||
/autonomous-task 0070
|
||||
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
|
||||
/autonomous-task 0070 --auto-apply-proposals safe
|
||||
/autonomous-task 0070 --dry-run
|
||||
/autonomous-task path/to/spec.yaml --branch auto/custom-name
|
||||
```
|
||||
## Por que deprecado
|
||||
|
||||
Flags:
|
||||
- `--max-iterations N` tope de iteraciones (default 10)
|
||||
- `--max-minutes M` timeout total (default 60)
|
||||
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
|
||||
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
|
||||
- `--dry-run` simula, NO aplica
|
||||
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
|
||||
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
|
||||
- Detector issue vs flow.
|
||||
- Reporte estructurado al humano post-delegate.
|
||||
- Self-Q&A migrado a fn-orquestador.
|
||||
|
||||
---
|
||||
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
|
||||
|
||||
## Comportamiento
|
||||
## Sustitucion 1:1
|
||||
|
||||
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
|
||||
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
|
||||
- `issue_id` o `task_spec`
|
||||
- flags resueltos
|
||||
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
|
||||
3. **El subagente:**
|
||||
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
|
||||
- Persiste estado en `task_runs` (operations.db del app target o repo root).
|
||||
- Despacha por fases a los 5 subagentes especializados.
|
||||
- Aplica proposals filtradas por `--auto-apply-proposals`.
|
||||
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
|
||||
4. **Reportar resultado al humano** con:
|
||||
- `status`, `iterations / max`, `duration / max`
|
||||
- `branch`, `worktree`, `PR draft url` si converged
|
||||
- `proposals creadas / aplicadas`
|
||||
- `last run_id` y status
|
||||
- Resumen iter-por-iter del `progress_json`
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
|
||||
|
||||
---
|
||||
## Modo debug
|
||||
|
||||
## Reglas duras (no negociables)
|
||||
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
|
||||
|
||||
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
|
||||
- No merge automatico — PR draft siempre.
|
||||
- No `--no-verify`, no `--force`, no skip hooks.
|
||||
- Paths protegidos via `dev/autonomous_protected_paths.json`.
|
||||
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
|
||||
- Auditoria total en `task_runs.progress_json`.
|
||||
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
|
||||
## Migration deadline
|
||||
|
||||
---
|
||||
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
|
||||
|
||||
## Integracion con call_monitor (issue 0085)
|
||||
|
||||
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
|
||||
|
||||
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
|
||||
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
|
||||
- Loggear sus invocaciones via el hook PostToolUse (automatico).
|
||||
|
||||
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
|
||||
|
||||
---
|
||||
|
||||
## Tipos NO soportados
|
||||
|
||||
- Diseño arquitectura nuevo (humano decide).
|
||||
- Decisiones UX subjetivas.
|
||||
- Cambios BD productiva.
|
||||
- Cualquier cosa que toque secrets/credenciales.
|
||||
- Self-modification del propio orquestador.
|
||||
|
||||
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
|
||||
|
||||
---
|
||||
|
||||
## Output canonico
|
||||
|
||||
```
|
||||
=== /autonomous-task: 0070 ===
|
||||
status: converged
|
||||
iterations: 7 / 10
|
||||
duration: 23 min / 60
|
||||
branch: auto/0070
|
||||
worktree: /tmp/fn_orq_0070_1731612345
|
||||
PR draft: https://github.com/.../pull/123
|
||||
proposals: 3 creadas, 2 auto-aplicadas
|
||||
last run_id: e2e_run_abc123 (status: pass)
|
||||
|
||||
Iter:
|
||||
1. construir → ok (2 funciones nuevas)
|
||||
2. ejecutar → ok
|
||||
3. analizar → fail (2/8 checks)
|
||||
4. mejorar → 3 proposals (2 auto-applicadas)
|
||||
5. construir → ok (re-build tras patches)
|
||||
6. analizar → pass
|
||||
7. recopilador → ok (operations.db integra)
|
||||
|
||||
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
|
||||
```
|
||||
Ver `.claude/commands/autopilot.md` para spec completa.
|
||||
|
||||
+132
-260
@@ -1,238 +1,68 @@
|
||||
---
|
||||
name: autopilot
|
||||
description: Modo full-auto self-Q&A. Toma issue o flow con DoD definido, valida readiness, ejecuta hasta cerrarlo sin interaccion humana. Ante cada decision se autoformula la pregunta, se autoresponde con razonamiento explicito, y avanza. Spawnea subagentes. Para.
|
||||
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
|
||||
---
|
||||
|
||||
# /autopilot — Modo autonomo end-to-end con self-Q&A
|
||||
# /autopilot — Comando autonomo unificado
|
||||
|
||||
Ejecuta un issue o flow **hasta cierre** sin intervencion humana. Ante cada decision, Claude **se formula la pregunta a si mismo y se la responde** (self-Q&A) con razonamiento trazable, en vez de abortar. Auto-prefiere la opcion **Recomendada** cuando exista; cuando no, decide en base a evidencia del codigo, registry, y reglas. Cada Q&A queda persistido en `task_runs.events_json[]` para auditoria. Spawnea subagentes en paralelo y persiste estado en `task_runs`.
|
||||
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
|
||||
|
||||
Diferencia con comandos relacionados:
|
||||
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
|
||||
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
|
||||
|
||||
| Comando | Que hace |
|
||||
|---|---|
|
||||
| `/autonomous-task <issue>` | Wrapper directo de `fn-orquestador` (registry-bound, bucle 5 fases) |
|
||||
| `/fix-issue <issue>` | Flujo guiado humano: rama + tasks + tests + version + close (pregunta cuando duda) |
|
||||
| `/flow run <NNNN>` | Runner manual de flows (fase 2 — no implementado) |
|
||||
| `/autopilot <target>` | **Meta-dispatcher**. Detecta issue vs flow, valida DoD, dispatch correcto, auto-defaults SIEMPRE |
|
||||
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
|
||||
|
||||
---
|
||||
## Por que solo delegar
|
||||
|
||||
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
|
||||
|
||||
`fn-orquestador` ahora soporta dos `task_type`:
|
||||
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
|
||||
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
|
||||
/autopilot issue:<NNNN> # issue NNNN explicito
|
||||
/autopilot i:<NNNN> # alias issue
|
||||
/autopilot issue:<NNNN> # issue explicito
|
||||
/autopilot i:<NNNN> # alias
|
||||
/autopilot flow:<NNNN> # flow NNNN
|
||||
/autopilot f:<NNNN> # alias flow
|
||||
/autopilot check <target> # solo audita DoD readiness, no ejecuta
|
||||
/autopilot <target> --max-iterations N --max-minutes M
|
||||
/autopilot <target> --dry-run
|
||||
/autopilot f:<NNNN> # alias
|
||||
/autopilot check <target> # solo audita DoD readiness, no delega
|
||||
/autopilot <target> --max-iterations N --max-minutes M --dry-run
|
||||
```
|
||||
|
||||
Detector:
|
||||
- `^\d{4}[a-z]?$` -> issue (sin prefijo = issue por defecto).
|
||||
- `^(issue|i):\d{4}[a-z]?$` -> issue.
|
||||
- `^(flow|f):\d{4}$` -> flow.
|
||||
- Otra cosa -> ABORT con error de sintaxis.
|
||||
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
|
||||
- `^(issue|i):\d{4}[a-z]?$` → issue.
|
||||
- `^(flow|f):\d{4}$` → flow.
|
||||
- Otra cosa → ABORT con error de sintaxis.
|
||||
|
||||
---
|
||||
## Pre-flight DoD readiness check (OBLIGATORIO)
|
||||
|
||||
## Pre-flight: DoD readiness check (OBLIGATORIO)
|
||||
|
||||
Sin DoD claro, autopilot no arranca. La verificacion es STOP-gate, no se rellena por inferencia.
|
||||
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
|
||||
|
||||
### Issue (`dev/issues/<NNNN>-*.md`)
|
||||
|
||||
Lee el .md. Debe cumplir **todos** estos:
|
||||
|
||||
1. Archivo existe en `dev/issues/` (no en `completed/`).
|
||||
2. Frontmatter valido (`status`, `priority`).
|
||||
3. **Al menos UNA** de:
|
||||
- Seccion `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
|
||||
- Seccion `## Acceptance` con checkboxes `[ ]`.
|
||||
- Seccion `## Tests` + `## Tareas` ambas no vacias.
|
||||
4. Tipo soportado por `/autonomous-task` si va a delegar a orquestador (`feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`). Si no entra en estos, autopilot intenta ruta `/fix-issue` simplificada (registry-only changes sin rama).
|
||||
5. **NO** contiene criterios no-verificables: "queda bonito", "es intuitivo", "UX mejor". Heuristica grep simple — si match -> warning + ABORT.
|
||||
|
||||
Si falla -> ABORT con tabla:
|
||||
|
||||
```
|
||||
=== autopilot check 0107c ===
|
||||
status: NOT READY
|
||||
gaps:
|
||||
- Sin seccion DoD/Acceptance/Tests
|
||||
- Frontmatter sin priority
|
||||
fix:
|
||||
- Anadir `## DoD` con 3-5 bullets verificables programaticamente
|
||||
- Anadir `priority: medium` al frontmatter
|
||||
```
|
||||
2. Frontmatter con `status`, `priority`.
|
||||
3. Al menos UNA de:
|
||||
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
|
||||
- `## Acceptance` con checkboxes `[ ]`.
|
||||
- `## Tests` + `## Tareas` ambas no vacias.
|
||||
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
|
||||
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
|
||||
|
||||
### Flow (`dev/flows/<NNNN>-*.md`)
|
||||
|
||||
1. Archivo existe en `dev/flows/` (no en `completed/`).
|
||||
1. Archivo existe en `dev/flows/`.
|
||||
2. Frontmatter valido.
|
||||
3. Seccion `## Acceptance` con >=1 checkbox `[ ]` (o ya `[x]` — significa parcialmente progresado).
|
||||
4. Seccion `## Flow` no vacia.
|
||||
5. Pre-requisitos declarados (incluso si vacio explicito).
|
||||
6. Tabla de funciones recomendadas presente — sin `FALTA: crear <id>` no resuelto (si hay `FALTA`, ABORT con lista de funciones a crear primero).
|
||||
3. `## Acceptance` con >=1 checkbox.
|
||||
4. `## Flow` no vacio.
|
||||
5. Pre-requisitos declarados.
|
||||
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
|
||||
|
||||
Si `FALTA` esta presente: opcionalmente autopilot ofrece spawnear `fn-constructor` para cada FALTA — pero ESO solo si `--allow-construct-missing`. Por defecto ABORT y reportar.
|
||||
|
||||
---
|
||||
|
||||
## Modo autonomo (reglas duras de comportamiento)
|
||||
|
||||
Durante toda la ejecucion de `/autopilot`:
|
||||
|
||||
1. **NO invocar `AskUserQuestion` al humano**. En su lugar, **self-Q&A loop**: cuando surja una decision, Claude la formaliza como `Question -> Options -> Reasoning -> Choice` y persiste el bloque en `task_runs.events_json[]`. Ver seccion "Self-Q&A loop" mas abajo. Solo ABORTA con `status=needs_human` si la decision toca: (a) destructivo sin rollback (`--force`, `git reset --hard`, `DROP TABLE`), (b) credenciales/secrets, (c) paths protegidos, (d) contradice DoD explicito del issue. En esos casos, NUNCA self-answer — escala al humano.
|
||||
2. **Auto-pick "Recommended"** en cualquier flag con opciones (mocks vs prod, default branch, etc.) — usar primer item etiquetado como recomendado en el archivo / convencion del proyecto. Si no hay marcado, self-Q&A con justificacion.
|
||||
3. **Acciones destructivas prohibidas sin flag explicito**: `git reset --hard`, `git push --force`, `rm -rf` fuera de `/tmp/`, `DROP TABLE`, `--no-verify`, `--force`. Si una accion las requiere -> ABORT.
|
||||
4. **Hooks NO se saltan**. Si pre-commit falla, fix raiz; si excede scope, ABORT (NO `--no-verify`).
|
||||
5. **Paths protegidos** de `dev/autonomous_protected_paths.json` se respetan exactamente.
|
||||
6. **Rama dedicada + worktree aislado SIEMPRE — sin excepciones**. Autopilot opera en `worktrees/auto-<NNNN>-<slug>/` (rama `auto/<NNNN>-<slug>`) para NO bloquear el working tree principal del humano. Aplica a issues, flows, registry-only y apps. Master NUNCA recibe commits directos en modo autopilot. Pre-flight obligatorio desde el working tree principal:
|
||||
```bash
|
||||
git fetch origin master
|
||||
git -C <main_repo> rev-parse --is-clean # tolerante: solo confirmar master rebased
|
||||
WT=worktrees/auto-<NNNN>-<slug>
|
||||
git worktree add -b auto/<NNNN>-<slug> "$WT" master
|
||||
cd "$WT" # todo el trabajo posterior aqui
|
||||
```
|
||||
Path del worktree:
|
||||
- **Dentro del repo**: `worktrees/auto-<NNNN>-<slug>/` (gitignored). Permite que herramientas con `FN_REGISTRY_ROOT` apunten al worktree y no a master.
|
||||
- Alternativa `/tmp/fn_autopilot_<NNNN>_<ts>/` si la app necesita aislamiento total del filesystem del repo (raro).
|
||||
Reanudacion idempotente: si el worktree ya existe -> `cd` a el + `git rebase master`. Si la rama `auto/<NNNN>-<slug>` existe pero el worktree no -> `git worktree add "$WT" auto/<NNNN>-<slug>`. Conflicto en rebase -> ABORT.
|
||||
Cierre exitoso: merge `--no-ff` a master desde el repo principal (`git -C <main> merge --no-ff auto/<NNNN>-<slug>`) solo tras tests verde + DoD 100%. Luego `git worktree remove <WT>` + `git branch -d auto/<NNNN>-<slug>`.
|
||||
Cierre fallido: worktree y rama quedan vivos para inspeccion humana. Master intacto.
|
||||
**Garantia**: el humano puede seguir editando en el repo principal mientras autopilot avanza — sin colisiones de checkout, sin index lockings, sin "uncommitted changes blocks branch switch".
|
||||
7. **Watchdog**: si la metrica de progreso (`acceptance_done / acceptance_total` para flows; `tests_pass / tests_total` o `checks_pass / checks_total` para issues) NO sube en 3 iteraciones consecutivas -> ABORT con `status=stalled`.
|
||||
8. **Timeout** default 60 min. Override con `--max-minutes`.
|
||||
9. **Idempotencia**: re-lanzar `/autopilot <target>` sobre el mismo target reanuda desde el ultimo `task_run` exitoso (lookup por `issue_id` o `flow_id` en `task_runs`).
|
||||
10. **No self-modification**: NUNCA tocar `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`.
|
||||
11. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary, auto_choice, self_qa?}`.
|
||||
|
||||
---
|
||||
|
||||
## Self-Q&A loop (corazon del modo)
|
||||
|
||||
Cuando aparece una decision sin Recomendado explicito, **NO abortar y NO preguntar al humano**. Claude:
|
||||
|
||||
1. **Formula la pregunta** en una frase. Una sola pregunta por bloque, especifica, contestable.
|
||||
2. **Lista opciones** (2-4). Misma forma que `AskUserQuestion` interno: `label + description`. Si solo hay una opcion viable, indicalo (`Options: [A] only viable`).
|
||||
3. **Razona en 1-3 lineas** apoyandote en: registry (`mcp__registry__fn_search`), reglas (`.claude/rules/`), tests previos, archivos del repo, convenciones del proyecto.
|
||||
4. **Elige** y marca `confidence: high|med|low`. Si `low` y la accion no es trivialmente reversible -> ABORT con `status=needs_human` y adjunta el bloque Q&A.
|
||||
5. **Persiste** en `events_json[]` con shape:
|
||||
```json
|
||||
{
|
||||
"ts": "...",
|
||||
"agent": "autopilot",
|
||||
"action": "self_qa",
|
||||
"self_qa": {
|
||||
"question": "Crear el flag enabled=false o ya enabled=true?",
|
||||
"options": [
|
||||
{"label": "enabled=false", "rationale": "TBD doctrina (feature_flags.md): merge codigo terminado pero NO expuesto"},
|
||||
{"label": "enabled=true", "rationale": "feature ya tiene tests verde y DoD 100%"}
|
||||
],
|
||||
"choice": "enabled=false",
|
||||
"confidence": "high",
|
||||
"reasoning": "feature_flags.md regla: 'cuando se activa: cambiar enabled:true y rellenar enabled_at'. Activar va en commit posterior."
|
||||
}
|
||||
}
|
||||
```
|
||||
6. **Avanza** sin esperar.
|
||||
|
||||
**Tope de self-Q&A**: `--max-self-answers` (default 20). Si se excede -> ABORT `status=overdeliberating` con dump de todas las Q&A. Una iteracion del bucle que necesita >5 Q&A es señal de DoD vago — abortar.
|
||||
|
||||
**Cuando NO usar self-Q&A (ABORT en vez de auto-responder)**:
|
||||
|
||||
| Caso | Razon |
|
||||
|---|---|
|
||||
| Destructivo sin rollback (`git reset --hard`, `rm -rf` fuera `/tmp/`, `DROP TABLE`, `--no-verify`, `--force`) | Coste de error infinito |
|
||||
| Credenciales/tokens/secrets | Riesgo de exfiltracion |
|
||||
| Paths protegidos (`dev/autonomous_protected_paths.json`) | Regla dura del orquestador |
|
||||
| Contradiccion explicita con DoD del issue | DoD es contrato |
|
||||
| Decision arquitectonica multi-app (renombrar tabla compartida, romper API publica) | Blast radius > 1 artefacto |
|
||||
| `confidence: low` + accion no reversible | Self-Q&A no garantiza acierto sin oraculo |
|
||||
|
||||
---
|
||||
|
||||
## Dispatch logic
|
||||
|
||||
### Path A: issue compatible con `fn-orquestador`
|
||||
|
||||
Si el issue declara o se infiere tipo en `(feature_app_simple, bugfix_with_repro, refactor_safe, add_e2e_check)` Y toca apps/modules/framework:
|
||||
|
||||
- Delega a `fn-orquestador` via `Agent(subagent_type="fn-orquestador", ...)` pasando:
|
||||
- `issue_id`, `--auto-apply-proposals safe`, `--max-iterations`, `--max-minutes`, paths protegidos.
|
||||
- Espera resultado, reenvia `task_run_id` + PR draft URL al humano.
|
||||
|
||||
### Path B: issue registry-only (functions/types/docs/rules)
|
||||
|
||||
- **Rama + worktree `worktrees/auto-<NNNN>-<slug>/`** desde master actualizado (regla dura 6). Humano sigue trabajando en el repo principal en paralelo.
|
||||
- Politica `apps_tbd.md` permite push directo a master para registry-only en modo humano, pero `/autopilot` NO lo usa: el aislamiento por rama+worktree es la garantia de rollback + paralelismo en modo autonomo.
|
||||
- Plan inline con TaskCreate:
|
||||
1. Pre-flight worktree (`git fetch` + `git worktree add -b auto/<NNNN>-<slug> worktrees/auto-<NNNN>-<slug> master` + `cd` a el).
|
||||
2. Leer issue + extraer DoD.
|
||||
3. Search registry para piezas existentes (registry-first).
|
||||
4. Si falta funcion -> spawn `fn-constructor` paralelo.
|
||||
5. `fn index`.
|
||||
6. Tests (`go test`, `pytest`, `bash -n`, segun stack).
|
||||
7. Si toco modulos/framework -> `/version` correspondiente.
|
||||
8. Mover `dev/issues/<NNNN>-*.md` a `dev/issues/completed/`.
|
||||
9. Actualizar `dev/issues/README.md` (si existe).
|
||||
10. Commit atomico por bloque logico en la rama.
|
||||
11. Solo si TODOS los tests pasan + DoD 100%: merge `--no-ff` a master + push + delete rama. Si algo falla -> rama queda viva, master intacto.
|
||||
- Verificacion final: DoD checkboxes -> todos marcados.
|
||||
|
||||
### Path C: flow
|
||||
|
||||
Runner inline (fase 2 manual, mientras `/flow run` no exista):
|
||||
|
||||
1. **Rama + worktree `worktrees/auto-flow-<NNNN>-<slug>/`** desde master (regla dura 6) — incluso para flows que solo ejecutan funciones sin escribir codigo, asi los side-effects en `dev/flows/<NNNN>-*.md` (checkboxes) y `dev/flows/runs/*.jsonl` se commitean en rama, no en master. Humano puede seguir editando el repo principal en paralelo.
|
||||
2. Parsea `## Flow` del .md.
|
||||
3. Cada paso tipo `function: <id>` -> `./fn run <id> [args]`.
|
||||
4. Cada paso tipo `cmd: <bash>` -> Bash tool (con guardas destructivas).
|
||||
5. Paso "MANUAL: ..." -> si tiene equivalente automatico (ej. "abrir Chrome y loguearse" vs `cdp_extract_recipe`) usa el automatico; si no tiene equivalente -> ABORT con `status=needs_human` y razon.
|
||||
6. Tras cada paso, evalua `## Acceptance` checkboxes via heuristicas:
|
||||
- "X runs en data_factory" -> `sqlite3` count.
|
||||
- "DAG corre 2 veces consecutivas" -> consultar `dag_engine` logs.
|
||||
- Otros -> dejar como `[ ]` y reportar.
|
||||
7. Persiste run en `dev/flows/runs/<NNNN>-<ts>.jsonl`.
|
||||
8. Si todos `[ ]` -> `[x]` -> commit en rama + merge `--no-ff` a master + `/flow done <NNNN>` + delete rama.
|
||||
|
||||
---
|
||||
|
||||
## Output canonico
|
||||
|
||||
```
|
||||
=== /autopilot 0107c ===
|
||||
target: issue 0107c (refactor data_table)
|
||||
path: B (registry-only)
|
||||
status: done
|
||||
iterations: 3 / 10
|
||||
duration: 18 min / 60
|
||||
dod_checks: 5/5 pass
|
||||
proposals: 2 creadas, 1 auto-aplicada
|
||||
self_qa: 7 (6 high / 1 med / 0 low)
|
||||
agents_spawned: fn-constructor x2, fn-recopilador x1
|
||||
commits: 4 (3 feat + 1 refactor)
|
||||
branch: master (registry-only, push directo)
|
||||
|
||||
Trace:
|
||||
1. construir → ok (2 funciones nuevas, 1 split)
|
||||
2. tests → ok (43/43)
|
||||
3. version → /version modules/data_table major "..."
|
||||
4. close → mv to completed/, push
|
||||
|
||||
Siguiente: ningun paso humano requerido. Verificar con: fn doctor modules
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sub-comando: `/autopilot check <target>`
|
||||
|
||||
Solo audita readiness — **no** ejecuta nada.
|
||||
Si falla:
|
||||
|
||||
```
|
||||
=== /autopilot check 0125 ===
|
||||
@@ -240,12 +70,10 @@ status: NOT READY
|
||||
target: issue 0125 (skill-tree-dashboard-panel)
|
||||
gaps:
|
||||
- Sin seccion DoD/Acceptance
|
||||
- Frontmatter sin priority
|
||||
non_verifiable_criteria:
|
||||
- "UX intuitiva" (linea 47)
|
||||
- "UX intuitiva" linea 47 — no verificable
|
||||
fix:
|
||||
- Anadir ## DoD con 3-5 bullets programaticamente verificables
|
||||
- Reemplazar "UX intuitiva" por criterio medible
|
||||
- Reemplazar criterios subjetivos por mediciones concretas
|
||||
```
|
||||
|
||||
Si OK:
|
||||
@@ -255,86 +83,130 @@ Si OK:
|
||||
status: READY
|
||||
target: issue 0107c (refactor data_table)
|
||||
dod_items: 5 checkboxes
|
||||
path_inferred: B (registry-only — modules/)
|
||||
task_type: refactor_safe
|
||||
estimated_iter: 3-5
|
||||
```
|
||||
|
||||
---
|
||||
## Dispatch a fn-orquestador
|
||||
|
||||
Tras pre-flight OK, ejecuta:
|
||||
|
||||
```
|
||||
Agent(
|
||||
subagent_type="fn-orquestador",
|
||||
prompt="""
|
||||
Issue/Flow: <path al .md>
|
||||
Modo: REAL (o --dry-run)
|
||||
task_type: <issue|flow>
|
||||
Pre-condiciones verificadas: 7/7 verde
|
||||
Master: <sha> sync con origin
|
||||
Working tree principal: limpio (baseline)
|
||||
Max iter: N
|
||||
Max min: M
|
||||
Auto-apply proposals: safe
|
||||
Token Gitea: pass gitea/dataforge-git-token
|
||||
DB task_runs: apps/deploy_server/operations.db (schema task_id)
|
||||
Reglas duras: autonomous_loop.md (11 reglas)
|
||||
""",
|
||||
run_in_background=true
|
||||
)
|
||||
```
|
||||
|
||||
Cuando termine, reporta al humano con output canonico del orquestador:
|
||||
|
||||
```
|
||||
=== /autopilot 0121b ===
|
||||
target: issue 0121b (fn doctor e2e-coverage)
|
||||
delegated_to: fn-orquestador
|
||||
status: converged
|
||||
iterations: 1 / 8
|
||||
duration: 4 min / 30
|
||||
task_run_id: task_d285372493cce2e6
|
||||
branch: auto/0121b-orquestador
|
||||
worktree: /tmp/fn_orq_0121b_1779147778
|
||||
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
|
||||
|
||||
Siguiente: revisar PR, mergear, mover issue a completed/
|
||||
```
|
||||
|
||||
## Reglas duras (autopilot-level)
|
||||
|
||||
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
|
||||
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
|
||||
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
|
||||
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
|
||||
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
|
||||
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Que hace |
|
||||
|---|---|---|
|
||||
| `--max-iterations N` | 10 | Tope de iteraciones del bucle |
|
||||
| `--max-minutes M` | 60 | Timeout total |
|
||||
| `--dry-run` | off | Plan + dispatch simulado, no aplica cambios |
|
||||
| `--allow-construct-missing` | off | Si flow tiene `FALTA: crear <id>`, spawn fn-constructor antes |
|
||||
| `--auto-apply-proposals` | `safe` | Pasado a fn-orquestador en Path A |
|
||||
| `--max-self-answers N` | 20 | Tope de bloques Self-Q&A por run. Excedido -> ABORT `overdeliberating` |
|
||||
|
||||
---
|
||||
| `--max-iterations N` | 10 | Pasado al orquestador |
|
||||
| `--max-minutes M` | 60 | Pasado al orquestador |
|
||||
| `--dry-run` | off | Pasado al orquestador |
|
||||
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
|
||||
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
|
||||
|
||||
## Errores canonicos
|
||||
|
||||
| Codigo | Significado | Accion |
|
||||
|---|---|---|
|
||||
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
|
||||
| `needs_human` | Decision sin Recomendado | Humano resuelve y relanza |
|
||||
| `stalled` | 3 iteraciones sin progreso | Humano revisa `events_json` |
|
||||
| `timeout` | Excedido `--max-minutes` | Aumentar timeout o partir issue |
|
||||
| `aborted_protected_path` | Cambio en path protegido | Humano revisa intent |
|
||||
| `iterations_exhausted` | Excedido `--max-iterations` | Humano evalua si vale subir tope |
|
||||
| `sandbox_breach` | Diff fuera del worktree | ABORT critico, audit |
|
||||
| `overdeliberating` | Excedido `--max-self-answers` | DoD probablemente vago — humano refina criterios |
|
||||
| `low_confidence_abort` | Self-Q&A devolvio `confidence: low` en accion no reversible | Humano valida la decision concreta |
|
||||
|
||||
---
|
||||
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
|
||||
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
|
||||
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| `/autopilot` sin pre-check DoD | Trabajar sin criterio de exito = bucle infinito |
|
||||
| Auto-relleno de DoD inventada | Criterios falsos -> falso "done" |
|
||||
| Merge a master sin tests verde | Master no deployable |
|
||||
| `AskUserQuestion` al humano | Rompe el contrato autonomo — usa self-Q&A loop |
|
||||
| Self-Q&A sin razonamiento explicito | Decision opaca, no auditable |
|
||||
| Self-Q&A con `confidence: high` en accion destructiva sin oraculo | Confianza injustificada — escalar |
|
||||
| Salto de hooks (`--no-verify`) | Encubre bugs reales |
|
||||
| Tocar mas issues que el target | Scope creep silencioso |
|
||||
| Borrar archivos sin backup en events_json | Pierde auditoria |
|
||||
|
||||
---
|
||||
|
||||
## Relacion con otras reglas
|
||||
|
||||
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog).
|
||||
- [[apps_tbd]] — politica TBD por tipo de cambio.
|
||||
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
|
||||
- [[feature_flags]] — codigo incompleto detras de flag OFF.
|
||||
- [[registry_calls]] — invocaciones canonicas (MCP / fn run / heredoc).
|
||||
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador como gate de Path A.
|
||||
- [[delegation]] — spawn fn-constructor antes que escribir inline.
|
||||
|
||||
---
|
||||
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
|
||||
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
|
||||
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
|
||||
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
|
||||
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```bash
|
||||
# Issue registry-only con DoD claro
|
||||
# Issue con DoD claro
|
||||
/autopilot 0107c
|
||||
/autopilot i:0107c # equivalente con prefijo explicito
|
||||
|
||||
# Issue app que requiere orquestador
|
||||
/autopilot issue:0070 --max-iterations 15 --max-minutes 90
|
||||
|
||||
# Flow con piezas faltantes — autoriza creacion antes
|
||||
/autopilot flow:0008 --allow-construct-missing
|
||||
|
||||
# Solo audit, no ejecutar
|
||||
# Solo audit
|
||||
/autopilot check 0125
|
||||
/autopilot check flow:0008
|
||||
|
||||
# Dry run
|
||||
/autopilot 0107c --dry-run
|
||||
```
|
||||
|
||||
## Relacion con otras reglas
|
||||
|
||||
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
|
||||
- [[apps_tbd]] — politica TBD por tipo de cambio.
|
||||
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
|
||||
- [[feature_flags]] — codigo incompleto detras de flag OFF.
|
||||
- [[registry_calls]] — invocaciones canonicas.
|
||||
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
|
||||
- [[delegation]] — spawn fn-constructor antes que escribir inline.
|
||||
|
||||
## Migracion desde `/autonomous-task`
|
||||
|
||||
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
|
||||
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
|
||||
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
|
||||
|
||||
## Historico
|
||||
|
||||
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
|
||||
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
|
||||
---
|
||||
|
||||
# /commands — Catalogo de slash commands del repo
|
||||
|
||||
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/commands # listado completo agrupado por namespace
|
||||
/commands <substring> # filtra por substring en nombre o descripcion
|
||||
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
|
||||
/commands --json # salida JSON para agentes
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
|
||||
CMD_DIR="$ROOT/.claude/commands"
|
||||
|
||||
# Recolecta: ns|name|description
|
||||
collect() {
|
||||
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
|
||||
rel="${f#$CMD_DIR/}"
|
||||
case "$rel" in
|
||||
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
|
||||
*) ns="global"; name="${rel%.md}" ;;
|
||||
esac
|
||||
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
|
||||
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
|
||||
done | sort
|
||||
}
|
||||
|
||||
collect | awk -F'|' '
|
||||
{
|
||||
if ($1 != prev_ns) {
|
||||
if (prev_ns) print ""
|
||||
if ($1 == "global") print "## global (/<cmd>)"
|
||||
else print "## " $1 " (/" $1 ":<cmd>)"
|
||||
prev_ns = $1
|
||||
}
|
||||
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
|
||||
}'
|
||||
```
|
||||
|
||||
Filtros:
|
||||
|
||||
- Substring: `grep -i "<substring>"` sobre stdout.
|
||||
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
|
||||
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
|
||||
|
||||
## Salida (formato humano)
|
||||
|
||||
```
|
||||
## global (/<cmd>)
|
||||
- /app — Crear, configurar y desplegar apps del registry
|
||||
- /autopilot — Modo full-auto...
|
||||
- /commands — Catalogo de slash commands del repo
|
||||
...
|
||||
|
||||
## aurgi (/aurgi:<cmd>)
|
||||
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
|
||||
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
|
||||
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
|
||||
```
|
||||
|
||||
## Cuando usarlo
|
||||
|
||||
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
|
||||
- Antes de inventar logica inline: comprobar si ya existe un command.
|
||||
- Auditoria: verificar que los projects exponen sus commands correctamente.
|
||||
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
|
||||
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
|
||||
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
|
||||
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
|
||||
@@ -37,3 +37,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
|
||||
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
|
||||
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
|
||||
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
## Slash commands por project (namespaced)
|
||||
|
||||
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
|
||||
|
||||
### Patron canonico
|
||||
|
||||
```
|
||||
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
|
||||
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
|
||||
```
|
||||
|
||||
Resultado:
|
||||
|
||||
| cwd | Invocacion |
|
||||
|---|---|
|
||||
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
|
||||
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
|
||||
|
||||
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
|
||||
|
||||
### Como anadir un project nuevo
|
||||
|
||||
1. `mkdir -p projects/<p>/.claude/commands/`.
|
||||
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
|
||||
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
|
||||
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
|
||||
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
|
||||
|
||||
### Reglas
|
||||
|
||||
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
|
||||
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
|
||||
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
|
||||
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
|
||||
|
||||
### Listado actual
|
||||
|
||||
| Project | Symlink | Commands disponibles desde fn_registry |
|
||||
|---|---|---|
|
||||
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
|
||||
|
||||
Anadir filas aqui al introducir un project nuevo con commands.
|
||||
|
||||
### Catalogo dinamico
|
||||
|
||||
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
|
||||
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
|
||||
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
|
||||
Submodule
+1
Submodule cpp/apps/chart_demo added at 026f514bb7
Submodule
+1
Submodule cpp/apps/shaders_lab added at dc9a970aff
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: "0123"
|
||||
title: "/flow run + fn-meta-orquestador: ejecutar flows + paralelo issues autonomos"
|
||||
title: "fn-meta-orquestador + fn-priorizador + fn doctor issues/flows"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
@@ -15,10 +15,13 @@ related:
|
||||
- "0069"
|
||||
- "0102"
|
||||
created: 2026-05-18
|
||||
updated: 2026-05-18
|
||||
tags: [flows, runner, meta-orquestador, paralelo, priorizador]
|
||||
updated: 2026-05-19
|
||||
tags: [meta-orquestador, paralelo, priorizador, doctor]
|
||||
---
|
||||
|
||||
**REVISION 2026-05-19:** `/flow run` y `/fix-flow` se ELIMINAN del scope. Absorbidos por `/autopilot` v2 (delega a fn-orquestador que ganara modo `task_type=flow`). Issue queda con 3 piezas: meta-orquestador + priorizador + doctor.
|
||||
|
||||
|
||||
# 0123 — Flows ejecutables + meta-orquestador paralelo
|
||||
|
||||
## Problema
|
||||
@@ -27,29 +30,32 @@ tags: [flows, runner, meta-orquestador, paralelo, priorizador]
|
||||
2. `parallel-fix-issues` lanza N agentes Claude vanilla en worktrees. `fn-orquestador` lanza 1 issue autonomo en worktree. NO existe combinacion: N issues autonomos coordinados respetando dep-graph.
|
||||
3. `/work today` prioriza con regla fija (prio+deps+DoD%). NO usa errores e2e, blast radius ni huerfanas para reordenar.
|
||||
|
||||
## Decision
|
||||
## Decision (revision 2026-05-19)
|
||||
|
||||
Tres piezas:
|
||||
|
||||
1. **`/flow run <NNNN>`**: ejecuta Acceptance checkboxes como steps. Cada step = `./fn run <id>` o subagent call. Logea en `data_factory.runs` + `e2e_runs`.
|
||||
2. **`/fix-flow <NNNN>`**: simetrico a `/fix-issue`. Cierra DoD del flow ejecutando Acceptance + abriendo issues si falla algun step.
|
||||
3. **`fn-meta-orquestador`** (subagente nuevo): lee `dev/issues/` con `status=pendiente` + dep-graph + telemetria. Spawn N `fn-orquestador` en worktrees paralelos respetando deps. Reusa `parallel-fix-issues/scripts/setup-worktrees.sh`.
|
||||
4. **`fn-priorizador`** (subagente nuevo): lee issues + telemetria call_monitor (error_rate, blast radius, huerfanas, violations). Output: top-N reordenado para `/work today`.
|
||||
5. **`fn doctor issues` + `fn doctor flows`**: valida TAXONOMY allowlist + DoD presente + user-facing surface declarada.
|
||||
1. **`fn-meta-orquestador`** (subagente nuevo): lee `dev/issues/` con `status=pendiente` + dep-graph + telemetria. Spawn N `fn-orquestador` en worktrees paralelos respetando deps. Reusa `parallel-fix-issues/scripts/setup-worktrees.sh`.
|
||||
2. **`fn-priorizador`** (subagente nuevo): lee issues + telemetria call_monitor (error_rate, blast radius, huerfanas, violations). Output: top-N reordenado para `/work today`.
|
||||
3. **`fn doctor issues` + `fn doctor flows`**: valida TAXONOMY allowlist + DoD presente + user-facing surface declarada.
|
||||
|
||||
**ELIMINADAS del scope original (absorbidas por `/autopilot` v2):**
|
||||
- `/flow run <NNNN>` — ahora `/autopilot flow:<NNNN>` delega a `fn-orquestador` con `task_type=flow`.
|
||||
- `/fix-flow <NNNN>` — mismo, fusion en `/autopilot`.
|
||||
|
||||
Implica que `fn-orquestador` necesita ganar soporte `task_type=flow` (parsear `## Flow` + ejecutar steps). Sub-tarea trackeada en 0123 reducido o issue propio (decidir).
|
||||
|
||||
## Tareas
|
||||
|
||||
1. Implementar `/flow run` en `.claude/commands/flow.md` + parser de Acceptance + dispatcher de steps.
|
||||
2. Implementar `/fix-flow` espejando `/fix-issue` adaptado al frontmatter de flows.
|
||||
3. Escribir `.claude/agents/fn-meta-orquestador/SKILL.md` con dep-graph resolver + spawner paralelo.
|
||||
4. Escribir `.claude/agents/fn-priorizador/SKILL.md` que consulta `call_monitor.operations.db` + `task_runs`.
|
||||
5. Anadir subcomandos `fn doctor issues` + `fn doctor flows` con funciones auxiliares via `fn-constructor`.
|
||||
6. Test: lanzar `/fix-flow 0001` (hn-top-stories) end-to-end + verificar acceptance.
|
||||
1. Anadir soporte `task_type=flow` a `fn-orquestador/SKILL.md` (parser `## Flow` + ejecutor steps + evaluator `## Acceptance` checkboxes via heuristicas).
|
||||
2. Escribir `.claude/agents/fn-meta-orquestador/SKILL.md` con dep-graph resolver + spawner paralelo.
|
||||
3. Escribir `.claude/agents/fn-priorizador/SKILL.md` que consulta `call_monitor.operations.db` + `task_runs`.
|
||||
4. Anadir subcomandos `fn doctor issues` + `fn doctor flows` con funciones auxiliares via `fn-constructor`.
|
||||
5. Test: lanzar `/autopilot flow:0001` (hn-top-stories) end-to-end + verificar acceptance.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] `/flow run 0001` ejecuta cada step y reporta pass/fail por step.
|
||||
- [ ] `/fix-flow 0001` cierra DoD verde y mueve a `dev/flows/completed/`.
|
||||
- [ ] `/autopilot flow:0001` ejecuta cada step y reporta pass/fail por step (delegado a fn-orquestador con task_type=flow).
|
||||
- [ ] `/autopilot flow:0001` cierra DoD verde y mueve a `dev/flows/completed/`.
|
||||
- [ ] `fn-meta-orquestador` lanza N orquestadores paralelos sobre issues sin dep entre si.
|
||||
- [ ] `fn-priorizador` output incluye senal de telemetria (no solo prio+deps).
|
||||
- [ ] `fn doctor issues --json` detecta drift TAXONOMY.
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MCPHTTPAuthFunc validates an incoming HTTP request and returns an enriched
|
||||
// context (e.g. with user_id) or an error. When it returns an error the
|
||||
// handler replies 401 Unauthorized without invoking the tool handler.
|
||||
// If nil, no auth is performed.
|
||||
type MCPHTTPAuthFunc func(r *http.Request) (context.Context, error)
|
||||
|
||||
// MCPHTTPOpts configures the Streamable HTTP MCP handler.
|
||||
type MCPHTTPOpts struct {
|
||||
Name string // server name reported to the client in initialize
|
||||
Version string // server version reported to the client in initialize
|
||||
Tools []MCPToolDef // reuses MCPToolDef from mcp_server_stdio.go
|
||||
Handler MCPToolHandler // reuses MCPToolHandler from mcp_server_stdio.go
|
||||
Auth MCPHTTPAuthFunc // optional; if nil, no auth
|
||||
Logger io.Writer // optional log sink; discards when nil
|
||||
}
|
||||
|
||||
const mcpHTTPBodyLimit = 1 << 20 // 1 MiB
|
||||
|
||||
// MCPHTTPHandler returns an http.Handler that implements the Streamable HTTP
|
||||
// MCP transport (spec 2025-03-26).
|
||||
//
|
||||
// Mount at any single path (e.g. /mcp). Handles POST for client→server
|
||||
// JSON-RPC 2.0 requests. GET and DELETE return 405 Method Not Allowed (SSE
|
||||
// server→client streaming is not implemented — see Gotchas in the .md).
|
||||
//
|
||||
// The handler is safe for concurrent use; it carries no shared mutable state.
|
||||
func MCPHTTPHandler(opts MCPHTTPOpts) http.Handler {
|
||||
logf := func(format string, args ...any) {
|
||||
if opts.Logger != nil {
|
||||
fmt.Fprintf(opts.Logger, "[mcp-http] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
// handled below
|
||||
case http.MethodGet, http.MethodDelete:
|
||||
// SSE server→client and session close not implemented yet.
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Optional auth: validate request and (optionally) enrich context.
|
||||
ctx := r.Context()
|
||||
if opts.Auth != nil {
|
||||
enriched, err := opts.Auth(r)
|
||||
if err != nil {
|
||||
logf("auth rejected: %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
ctx = enriched
|
||||
}
|
||||
|
||||
// Read body with size limit (anti-DoS).
|
||||
limitedBody := http.MaxBytesReader(w, r.Body, mcpHTTPBodyLimit)
|
||||
body, err := io.ReadAll(limitedBody)
|
||||
if err != nil {
|
||||
// MaxBytesReader wraps the error; treat any read failure as 413.
|
||||
logf("body read error: %v", err)
|
||||
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
logf("recv: %s", body)
|
||||
|
||||
// Parse JSON-RPC request. On parse failure respond HTTP 200 with
|
||||
// JSON-RPC error -32700 (per MCP spec — not HTTP 400).
|
||||
var req jsonrpcRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
logf("json parse error: %v", err)
|
||||
writeJSONRPCError(w, nil, -32700, "parse error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Notifications (no "id" key in raw JSON) → 202 Accepted, no body.
|
||||
isNotification := !jsonHasKey(body, "id")
|
||||
if isNotification {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch method.
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
result := map[string]any{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{},
|
||||
},
|
||||
"serverInfo": map[string]any{
|
||||
"name": opts.Name,
|
||||
"version": opts.Version,
|
||||
},
|
||||
}
|
||||
writeJSONRPCResult(w, req.ID, result)
|
||||
|
||||
case "initialized":
|
||||
// Should not arrive as a non-notification, but handle gracefully.
|
||||
writeJSONRPCResult(w, req.ID, map[string]any{})
|
||||
|
||||
case "tools/list":
|
||||
tools := opts.Tools
|
||||
if tools == nil {
|
||||
tools = []MCPToolDef{}
|
||||
}
|
||||
writeJSONRPCResult(w, req.ID, map[string]any{"tools": tools})
|
||||
|
||||
case "tools/call":
|
||||
var p mcpCallParams
|
||||
if err := json.Unmarshal(req.Params, &p); err != nil {
|
||||
writeJSONRPCError(w, req.ID, -32602, "invalid params: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
args := p.Arguments
|
||||
if args == nil {
|
||||
args = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
toolResult, isErr, handlerErr := opts.Handler(ctx, p.Name, args)
|
||||
if handlerErr != nil {
|
||||
logf("handler error for %q: %v", p.Name, handlerErr)
|
||||
writeJSONRPCError(w, req.ID, -32603, handlerErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resultText, _ := json.Marshal(toolResult)
|
||||
callResult := map[string]any{
|
||||
"content": []map[string]any{
|
||||
{
|
||||
"type": "text",
|
||||
"text": string(resultText),
|
||||
},
|
||||
},
|
||||
"isError": isErr,
|
||||
}
|
||||
writeJSONRPCResult(w, req.ID, callResult)
|
||||
|
||||
case "ping":
|
||||
writeJSONRPCResult(w, req.ID, map[string]any{})
|
||||
|
||||
default:
|
||||
logf("unknown method %q", req.Method)
|
||||
writeJSONRPCError(w, req.ID, -32601, "method not found: "+req.Method)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// writeJSONRPCResult writes a JSON-RPC 2.0 success response.
|
||||
func writeJSONRPCResult(w http.ResponseWriter, id any, result any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(jsonrpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
// writeJSONRPCError writes a JSON-RPC 2.0 error response with HTTP 200.
|
||||
// Per the MCP Streamable HTTP spec, protocol errors still use HTTP 200 so
|
||||
// the client can parse the JSON-RPC error object (not HTTP status codes).
|
||||
func writeJSONRPCError(w http.ResponseWriter, id any, code int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(jsonrpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &jsonrpcError{Code: code, Message: message},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: mcp_server_http
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MCPHTTPHandler(opts MCPHTTPOpts) http.Handler"
|
||||
description: "Devuelve un http.Handler que implementa el Streamable HTTP transport del protocolo MCP (spec 2025-03-26). Acepta POST con un mensaje JSON-RPC 2.0 unico y despacha initialize/tools/list/tools/call/ping al handler del usuario. Soporta auth opcional via MCPHTTPAuthFunc que enriquece el context antes de invocar el handler de tools."
|
||||
tags: [mcp, http, rpc, json-rpc, tools, server, protocol, claude, backends]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- context
|
||||
- encoding/json
|
||||
- fmt
|
||||
- io
|
||||
- net/http
|
||||
tested: true
|
||||
tests:
|
||||
- "Initialize retorna serverInfo con Name y Version correctos"
|
||||
- "ToolsList retorna las tools registradas"
|
||||
- "ToolsCall invoca handler y retorna content[0].text con el resultado"
|
||||
- "BadAuth retorna 401 cuando opts.Auth devuelve error"
|
||||
- "BodyTooLarge retorna 413 cuando el body supera 1 MiB"
|
||||
- "ParseError retorna HTTP 200 con error JSON-RPC -32700 para body invalido"
|
||||
- "Notification retorna 202 Accepted sin body cuando falta el campo id"
|
||||
- "MethodNotAllowed retorna 405 para GET y DELETE"
|
||||
test_file_path: "functions/infra/mcp_server_http_test.go"
|
||||
file_path: "functions/infra/mcp_server_http.go"
|
||||
params:
|
||||
- name: opts.Name
|
||||
desc: "Nombre del servidor reportado al cliente en la respuesta de initialize (serverInfo.name)."
|
||||
- name: opts.Version
|
||||
desc: "Version del servidor reportada al cliente en initialize (serverInfo.version)."
|
||||
- name: opts.Tools
|
||||
desc: "Lista de MCPToolDef (nombre, descripcion, JSON Schema del input) que el servidor expone. Mismo tipo que MCPServerOpts de mcp_server_stdio_go_infra."
|
||||
- name: opts.Handler
|
||||
desc: "Dispatcher unico para todas las tools. Recibe ctx (posiblemente enriquecido por Auth), nombre de la tool y arguments JSON crudo. Devuelve result, isError y err."
|
||||
- name: opts.Auth
|
||||
desc: "Funcion opcional de autenticacion. Recibe el *http.Request, devuelve un context enriquecido (p.ej. con user_id) o un error. Si devuelve error, el handler responde 401 sin invocar Handler. Si es nil no se hace auth."
|
||||
- name: opts.Logger
|
||||
desc: "Writer opcional para log de debug (p.ej. os.Stderr). Si es nil los mensajes se descartan."
|
||||
output: "http.Handler listo para montarse en un mux. El handler es seguro para uso concurrente (no tiene estado mutable compartido)."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"fn-registry/functions/infra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tools := []infra.MCPToolDef{
|
||||
{
|
||||
Name: "echo",
|
||||
Description: "Devuelve el mensaje tal cual",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {"msg": {"type": "string"}},
|
||||
"required": ["msg"]
|
||||
}`),
|
||||
},
|
||||
}
|
||||
|
||||
toolHandler := func(ctx context.Context, name string, input json.RawMessage) (any, bool, error) {
|
||||
if name == "echo" {
|
||||
var args struct{ Msg string `json:"msg"` }
|
||||
if err := json.Unmarshal(input, &args); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return map[string]string{"result": args.Msg}, false, nil
|
||||
}
|
||||
return nil, true, fmt.Errorf("unknown tool: %s", name)
|
||||
}
|
||||
|
||||
// AuthFunc Bearer simple: extrae el token, busca en DB, inyecta user_id en ctx.
|
||||
// El kanban lo implementa asi, buscando en la tabla mcp_tokens.
|
||||
authFn := func(r *http.Request) (context.Context, error) {
|
||||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("missing token")
|
||||
}
|
||||
// ... validar token en DB ...
|
||||
ctx := context.WithValue(r.Context(), "user_id", "u_123")
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
mcpH := infra.MCPHTTPHandler(infra.MCPHTTPOpts{
|
||||
Name: "my-app-mcp",
|
||||
Version: "1.0.0",
|
||||
Tools: tools,
|
||||
Handler: toolHandler,
|
||||
Auth: authFn,
|
||||
Logger: os.Stderr,
|
||||
})
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/mcp", mcpH)
|
||||
|
||||
fmt.Println("MCP HTTP server en :8300/mcp")
|
||||
if err := http.ListenAndServe(":8300", mux); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Para un cliente MCP HTTP, la configuracion en `.mcp.json` usa `type: http`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-app": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:8300/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites exponer tools MCP a Claude o a un agente via HTTP en lugar de via stdio — es decir, cuando el servidor MCP es un proceso separado (no un subproceso del cliente) o cuando varios clientes deben compartir el mismo servidor. Tipico en apps con backend Go ya existente (kanban, sqlite_api, registry_api) que quieren aceptar llamadas MCP sin lanzar un subproceso nuevo por sesion.
|
||||
|
||||
Usa `mcp_server_stdio_go_infra` si el cliente lanza el servidor como subproceso (Claude Desktop, `claude -p`). Usa esta funcion si el servidor ya esta corriendo y el cliente se conecta via URL.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Sin sesiones MCP (Mcp-Session-Id)**: la spec 2025-03-26 define un header `Mcp-Session-Id` para multiplexar sesiones sobre el mismo endpoint. Esta implementacion no lo emite ni lo exige — cada POST es independiente. TODO: implementar sesiones cuando se necesiten mas de un cliente concurrente con estado de sesion separado.
|
||||
- **GET (SSE server→client) no implementado**: POST cubre el 100% de los casos de uso actuales (tools call). El canal SSE (server-initiated notifications) se puede anadir montando un segundo handler GET en el mismo path. Devuelve 405 hasta entonces.
|
||||
- **DELETE (close session) no implementado**: 405. Sin sesiones, no hay nada que cerrar.
|
||||
- **Body limit 1 MiB**: requests mas grandes reciben 413. Si tus tools reciben inputs grandes (imagenes en base64, JSONs voluminosos), ajusta `mcpHTTPBodyLimit` en el .go o recibe los datos por referencia (URL/path) en vez de inline.
|
||||
- **CORS no incluido**: monta `http_cors_middleware_go_infra` en el mux antes del handler si el cliente MCP es un frontend web o viene de origen diferente.
|
||||
- **Errores de protocolo usan HTTP 200**: segun el spec MCP, los errores JSON-RPC se devuelven con HTTP 200 para que el cliente pueda parsear el objeto error. Solo `401` y `413` son codigos HTTP de error.
|
||||
@@ -0,0 +1,201 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
|
||||
func newTestMCPHandler(auth MCPHTTPAuthFunc) http.Handler {
|
||||
tools := []MCPToolDef{
|
||||
{
|
||||
Name: "greet",
|
||||
Description: "Returns a greeting",
|
||||
InputSchema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`),
|
||||
},
|
||||
}
|
||||
handler := func(_ context.Context, name string, _ json.RawMessage) (any, bool, error) {
|
||||
if name == "greet" {
|
||||
return map[string]string{"hello": "world"}, false, nil
|
||||
}
|
||||
return nil, true, errors.New("unknown tool")
|
||||
}
|
||||
return MCPHTTPHandler(MCPHTTPOpts{
|
||||
Name: "test-server",
|
||||
Version: "0.0.1",
|
||||
Tools: tools,
|
||||
Handler: handler,
|
||||
Auth: auth,
|
||||
})
|
||||
}
|
||||
|
||||
func postMCP(h http.Handler, body string) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
// --- tests ------------------------------------------------------------------
|
||||
|
||||
func TestMCPHTTPHandler_Initialize(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
w := postMCP(h, `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp jsonrpcResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if resp.Error != nil {
|
||||
t.Fatalf("unexpected error: %+v", resp.Error)
|
||||
}
|
||||
|
||||
result, ok := resp.Result.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("result is not map: %T", resp.Result)
|
||||
}
|
||||
if _, ok := result["protocolVersion"]; !ok {
|
||||
t.Error("missing protocolVersion in result")
|
||||
}
|
||||
si, ok := result["serverInfo"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("missing serverInfo")
|
||||
}
|
||||
if si["name"] != "test-server" {
|
||||
t.Errorf("serverInfo.name = %v, want test-server", si["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_ToolsList(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
w := postMCP(h, `{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp jsonrpcResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Error != nil {
|
||||
t.Fatalf("unexpected rpc error: %+v", resp.Error)
|
||||
}
|
||||
|
||||
result := resp.Result.(map[string]any)
|
||||
tools, ok := result["tools"].([]any)
|
||||
if !ok || len(tools) == 0 {
|
||||
t.Fatalf("expected non-empty tools array, got %v", result["tools"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_ToolsCall(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
w := postMCP(h, `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"greet","arguments":{}}}`)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp jsonrpcResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Error != nil {
|
||||
t.Fatalf("unexpected rpc error: %+v", resp.Error)
|
||||
}
|
||||
|
||||
result := resp.Result.(map[string]any)
|
||||
content, ok := result["content"].([]any)
|
||||
if !ok || len(content) == 0 {
|
||||
t.Fatalf("expected content array, got %v", result["content"])
|
||||
}
|
||||
first := content[0].(map[string]any)
|
||||
if first["text"] != `{"hello":"world"}` {
|
||||
t.Errorf("content[0].text = %q, want {\"hello\":\"world\"}", first["text"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_BadAuth(t *testing.T) {
|
||||
auth := func(_ *http.Request) (context.Context, error) {
|
||||
return nil, errors.New("bad token")
|
||||
}
|
||||
h := newTestMCPHandler(auth)
|
||||
w := postMCP(h, `{"jsonrpc":"2.0","id":4,"method":"initialize","params":{}}`)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_BodyTooLarge(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
big := strings.Repeat("x", mcpHTTPBodyLimit+1)
|
||||
body := `{"jsonrpc":"2.0","id":5,"method":"initialize","params":{"x":"` + big + `"}}`
|
||||
r := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("expected 413, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_ParseError(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
w := postMCP(h, `not valid json`)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected HTTP 200 for parse error, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp jsonrpcResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Error == nil {
|
||||
t.Fatal("expected JSON-RPC error, got nil")
|
||||
}
|
||||
if resp.Error.Code != -32700 {
|
||||
t.Errorf("error code = %d, want -32700", resp.Error.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_Notification(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
// A notification has no "id" key at all.
|
||||
w := postMCP(h, `{"jsonrpc":"2.0","method":"initialized","params":{}}`)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Fatalf("expected 202 for notification, got %d", w.Code)
|
||||
}
|
||||
if w.Body.Len() != 0 {
|
||||
t.Errorf("expected empty body for notification, got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPHTTPHandler_MethodNotAllowed(t *testing.T) {
|
||||
h := newTestMCPHandler(nil)
|
||||
|
||||
for _, method := range []string{http.MethodGet, http.MethodDelete} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
r := httptest.NewRequest(method, "/mcp", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("%s: expected 405, got %d", method, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user