# Super Skills (agent-squad-app) · Parte 1 de 2

> Secciones 1-4: qué es, inventario, cadena end-to-end, registro y descubrimiento. · Parte 2: https://playgrounds.digitalhubassist.ai/superskills-arquitectura-parte2.md

> Generado: 2026-07-25 · Verificado en código (6 lectores + cazador de gaps) · Versión interactiva: https://playgrounds.digitalhubassist.ai/superskills-arquitectura.html

## 1. Qué es un Super Skill aquí

Un Super Skill es la copia 1:1 del `draft` de un `plan_draft` compuesto por Nova (shape `{template, constraints}`), promovida con nombre propio a un artefacto reutilizable. Vive como fila JSONB en la tabla Postgres `superskills` del substrate (puerto 5433, migración `db/substrate/migrations/0007_superskills.sql`), con ciclo de vida insert → archive (soft delete, sin UPDATE de contenido). Nace cuando el usuario guarda un plan propuesto por Nova vía `POST /api/workspaces/:id/superskills` con `{draft_id, name}` (`apps/api/src/routes/superskills.ts:57-129`), que valida el plan contra `OPERATION_CATALOG` antes del INSERT. Su "input schema" son las claves `{{intent.constraints.X}}` extraídas del template (`constraintKeysOf`) menos la denylist `gate_timeout_ms`. Se ejecuta con `POST /api/workspaces/:id/superskills/:skillId/launch`, que fusiona overrides permitidos y delega en `launchCompiledPlan` (`apps/api/src/substrate/launch-plan.ts:98-144`): crea Intent + Plan compilado + Trace y emite el evento Inngest `plan.compiled` directo al motor, saltando `intent.declared` a propósito. Los skills promovidos se reinyectan al prompt de Nova (cap 20) para que futuros pedidos hagan `match_custom`.

## 2. Inventario de componentes

| Componente | Archivo | Entrypoint | Inputs | Outputs | Dependencias |
|---|---|---|---|---|---|
| Tabla `superskills` (DDL) | `db/substrate/migrations/0007_superskills.sql:23-44` | `CREATE TABLE superskills` | ninguno | Columnas id, workspace_id, name, description, plan JSONB, est_cost_usd, source_draft_id, created_at, archived_at; índices `idx_superskills_workspace` y único parcial `uq_superskills_source_active` | Postgres substrate:5433; sin FK a plan_drafts (deliberado) |
| Store substrate de superskills | `apps/api/src/substrate/superskills.ts:26-147` | `insertSuperskill` (l.47), `listSuperskills` (l.85), `getSuperskill` (l.104), `archiveSuperskill` (l.126), `constraintKeysOf` (l.144) | plan `{template, constraints}` copiado 1:1 del draft; workspace_id, name | `SuperskillRow`; `constraintKeysOf` = claves de constraint menos denylist `gate_timeout_ms` | `validatePlanAgainstCatalog` de `@agent-squad/substrate-spec` ANTES del INSERT; `extractConstraintKeys` de nova-compose; list/get filtran `archived_at IS NULL` |
| Ruta promote | `apps/api/src/routes/superskills.ts:57-129` | `POST /api/workspaces/:id/superskills` | `{draft_id uuid, name 3..80, description? ≤500}`; Bearer `SUBSTRATE_API_TOKEN` | 201 con skill plano + constraint_keys/values; 409 `already_promoted` (SELECT previo + catch 23505), `draft_not_promotable`, `invalid_plan` | `getPlanDraft` (plan-drafts.ts), `insertSuperskill`; NO muta el draft, NO emite eventos, sin rate limit |
| Ruta list | `apps/api/src/routes/superskills.ts:137-153` | `GET /api/workspaces/:id/superskills` | Params uuid; Bearer | 200 `{skills:[...]}` con constraint_keys y constraint_values pre-llenados | `listSuperskills` (solo activos, `created_at DESC`) |
| Ruta launch | `apps/api/src/routes/superskills.ts:171-215` | `POST /api/workspaces/:id/superskills/:skillId/launch` | `{constraints? (solo claves en constraintKeysOf, resto SILENCIADO l.189-194), declared_by? (default 'user:anonymous')}` | 201 `{launched, intent_id, plan_id, trace_id}`; 404 `skill_not_found`; 409 `invalid_skill`; 502 `launch_failed` | `getSuperskill`, `launchCompiledPlan`; rate limit 10/min por workspace (`apps/api/src/index.ts:111,144-150`); montaje en `index.ts:207` |
| `launchCompiledPlan` (maquinaria compartida) | `apps/api/src/substrate/launch-plan.ts:98-144` (gate l.69-96, denylist l.27) | `launchCompiledPlan({workspaceId, subjectRef, subjectLabel, templateId, template, constraints, declaredBy})` | template con id reescrito a `skill-<skillId>` (o `adhoc-<draftId>`); constraints fusionadas | `{intent_id, plan_id, trace_id}`; throw `invalid_plan` si falla re-validación o contrato human_gate (fallback 'fail', gate terminal) | intents/plans/traces; `compilePlanFromTemplate` (plans.ts:18-119); emite `plan.compiled` `{intent_id, plan_id, template_id, workspace_id}` (l.133-141), nunca `intent.declared` |
| nova-compose (módulo puro) | `apps/api/src/substrate/nova-compose.ts` (prompt 522-641, parser 688-698, Zod 707-733, `interpretNovaText` 829-1098, `extractConstraintKeys` 808-816) | `buildComposeSystemPrompt` / `buildComposeUserPrompt` / `interpretNovaText` | texto crudo del LLM + customSkills del workspace + forgedOps | `NovaOutcome`: match / match_custom / plan (template enriquecido, máx 16 steps, 4 LLM) / cannot / invalid | `OPERATION_CATALOG`, `EVALUATOR_CATALOG`, zod; sin I/O |
| Ruta compose (origen del draft) | `apps/api/src/routes/compose.ts:68-296` (launch 306-358, discard 361-369) | `POST /api/workspaces/:id/compose` (+ `/:draftId/launch`, `/:draftId/discard`) | `{request 10..1000, attachments? ≤3}`; timeout LLM 80s + 1 reintento con feedback | fila `plan_drafts` (proposed/matched/rejected) + 201 `{draft_id, estimated_cost_usd, steps, agents}` | `generateLLMText`, plan-drafts, launch-plan, `listSuperskills` cap 20 fail-soft; montaje `index.ts:203` |
| Adapter LLM + ModelPlane | `apps/api/src/inngest/llm.ts:215-254` (CLI 274-383, cliEnv 261-272) + `apps/api/src/model-plane/policy.ts:19-27`, `resolve.ts:54-76` | `generateLLMText({modelClass:'compose',...})` / `resolveModelClass` | system prompt ~10KB + user prompt | `{text, usage, provider, reportedCostUsd}` | Default `claude-cli` + sesión OAuth Max (borra `ANTHROPIC_API_KEY` del child); fallback opt-in `LLM_API_FALLBACK=true`; precedencia env > `model_routing_policy` (DB) > default, fail-open |
| Store `plan_drafts` | `apps/api/src/substrate/plan-drafts.ts:24-70`; DDL 0005/0008 | `insertPlanDraft` / `getPlanDraft` / `flipPlanDraftStatus` | draft jsonb, status, first_pass | fila con estados proposed/launched/discarded/rejected/matched; flips optimistas (WHERE status=from) | Postgres 5433 vía `substrate/db.ts` |
| Motor Inngest (`substrate-execute-plan` + serve) | funciones registradas en `apps/api/src/inngest/functions/index.ts` (10 FUNCTIONS); ruta del archivo execute-plan por confirmar | trigger `plan.compiled`, concurrency 2, retries 0; serve handler en `/api/inngest` (fuera del bearer) | evento `plan.compiled` | ejecución topológica de steps con `runWithRetry` + `runStepWithTimeout`, dos fases en `step_executions`; gates con `step.waitForEvent` (`approval.received` / `task.completed`); `completeTrace` + `trace.completed` → `substrate-notify-dispatch` | Executor self-hosted Docker (127.0.0.1:8288) con `--sdk-url http://host.docker.internal:4000/api/inngest --poll-interval 5` |
| Proxies BFF SvelteKit | `apps/web/src/routes/api/substrate/superskills/+server.ts:14-44` y `superskills/launch/+server.ts:14-34` | GET/POST `/api/substrate/superskills`, POST `/api/substrate/superskills/launch` | sesión (gate doble `locals.user` + `locals.accessAuthorized`); JSON con draft_id/name o skillId/constraints | GET fail-soft 200 `{skills}`; POST propaga status del motor; launch devuelve traceId/intentId/planId | `$lib/server/substrate`; consumidores: `NovaModal.svelte:182,233`, `NovaOfficeDock.svelte:455`, `SkillLaunchModal.svelte:51` |
| Cliente server-side del motor | `apps/web/src/lib/server/substrate.ts` (`readSubstrateConfig` 19-31, compose 702-757, `fetchSuperskills` 932-954, `promoteSuperskill` 887-922, `launchSuperskill` 1000-1032) | funciones homónimas | env `SUBSTRATE_API_URL/TOKEN/WORKSPACE_ID`; payloads de cada op | fetch fail-soft a `[]`; promote/launch propagan status; timeouts 2.5s/8s/10s/88s (compose) | Bearer en cada llamada; el browser nunca ve token ni baseUrl |
| SSR + helpers UI | `apps/web/src/routes/workflow-library/+page.server.ts:10-20`; `apps/web/src/lib/superskills/defaultName.ts:18-28`; `matchInput.ts:10,20-27` | `load` (PageServerLoad), `defaultSkillName`, `matchInputReady` | `locals.accessAuthorized`; request del usuario; `LiveWorkflowMeta` | `{canLaunch, mySkills}` por SSR; nombre default ≤60 chars; gate client-side de longitud/formato (NO matching semántico) | `fetchSuperskills`; catálogo estático `LIVE_WORKFLOWS` (`$lib/library/launchable.ts:19-26`) |

## 3. Cadena de invocación end-to-end

1. **Usuario**: en `/workflow-library` (skills listados por SSR) o en Nova (`NovaModal` / `NovaOfficeDock`); para un skill custom o de la library pulsa lanzar (`SkillLaunchModal.svelte:51`, `NovaModal.svelte:182`, `NovaOfficeDock.svelte:455`).
2. **UI → BFF**: el browser hace `POST /api/substrate/superskills/launch` con `{skillId, constraints?}`; el proxy SvelteKit (`apps/web/src/routes/api/substrate/superskills/launch/+server.ts:14-34`) exige sesión (gate doble `locals.user` + `locals.accessAuthorized`, 403 si falta).
3. **BFF → API**: `launchSuperskill` (`substrate.ts:1000-1032`) hace `POST ${SUBSTRATE_API_URL}/api/workspaces/${SUBSTRATE_WORKSPACE_ID}/superskills/${skillId}/launch` con `Authorization: Bearer ${SUBSTRATE_API_TOKEN}`, timeout 10s.
4. **API Hono**: `superskillsRoute.post(...launch)` (`routes/superskills.ts:171-215`), tras `protectExposed` (`index.ts:77-78`) y rate limit 10/min por workspace (`index.ts:144-150`), carga el skill activo con `getSuperskill` y fusiona `skill.plan.constraints` + overrides permitidos (`constraintKeysOf`; claves no permitidas se silencian).
5. **launchCompiledPlan** (`launch-plan.ts:98-144`): re-valida el template contra `OPERATION_CATALOG` con id `skill-<skillId>`, fuerza `assertHumanGatePresent` (≥1 `human_gate.approve`, todos con fallback `'fail'`, ≥1 gate terminal) y borra `gate_timeout_ms` (denylist).
6. **Intent + Plan**: `createIntent` (kind `execute_action`, subject_label `'superskill'`, subject_ref skillId, acceptance_criteria_ref `eval.intent.nova_adhoc@1`) → status `planning` → `compilePlanFromTemplate` (`plans.ts:18-119`, sustituye `{{intent.constraints.X}}`) escribe filas en `plans`, `steps`, `plan_edges`.
7. **Trace**: `createTrace` (status `queued`) → intent pasa a `running`.
8. **Evento**: `inngest.send('plan.compiled', {intent_id, plan_id, template_id, workspace_id})` DIRECTO, saltando `intent.declared` (el default-throw de `handle-intent-declared`, que no conoce `'superskill'`, actúa de guard). La ruta responde 201 síncrono y el BFF mapea `{trace_id, intent_id, plan_id}` a `{traceId, intentId, planId}` para reconciliar la escena.
9. **Inngest**: el executor self-hosted (Docker, 127.0.0.1:8288, sdk-url `host.docker.internal:4000/api/inngest`) despacha a `substrate-execute-plan` (trigger `plan.compiled`, concurrency 2, retries 0).
10. **Steps**: execute-plan carga el plan compilado, ordena topológicamente y ejecuta cada step con `runWithRetry` + `runStepWithTimeout` (presupuesto `steps.timeout_ms`), ciclo de vida en dos fases sobre `step_executions`; los human gates suspenden con `step.waitForEvent` (`approval.received` filtrado por artifact_id con `gate.timeout_ms`; `human_task` espera `task.completed` por step_execution_id, fallback siempre fail).
11. **Cierre**: `completeTrace` fija verdict y cost_actual en `traces`, el intent pasa a `succeeded`/`failed` y se emite `trace.completed`.
12. **Notify**: `substrate-notify-dispatch` consume `trace.completed` y despacha la notificación.

## 4. Cómo se registran y descubren hoy

**Registro de funciones Inngest.** Las 10 funciones viven en `FUNCTIONS` (`apps/api/src/inngest/functions/index.ts`) y se sirven por el serve handler de Hono en `/api/inngest`, deliberadamente fuera del bearer. El executor Inngest self-hosted (Docker) las descubre por polling: corre con `--sdk-url http://host.docker.internal:4000/api/inngest` y `--poll-interval 5` (`substrate-infra/inngest/docker-compose.yml:59-60,69-70`), es decir re-lee la topología cada 5 segundos sin necesidad de un `PUT /api/inngest` explícito. El `PUT` de sync existe como mecanismo pero el auto-deploy no lo ejecuta (`substrate-infra/scripts/auto-deploy-api.sh:49-57` solo cURLea `/health`). El guard `inngest-serve-guard.ts:38-43` valida `INNGEST_SERVE_HOST` al boot (fatal en prod), pero valida topología estática, no registro real. La única verificación del path completo es el canary (`apps/api/src/inngest/functions/canary.ts:13-20` + `scripts/slo-alert.ts:119-128`, cron cada 10 minutos).

**Descubrimiento de skills por workspace (DB).** `GET /api/workspaces/:id/superskills` lista los activos (`archived_at IS NULL`, `created_at DESC`) con `constraint_keys` y `constraint_values`. La UI los recibe por SSR en el load de `/workflow-library` (`+page.server.ts:14`, fail-soft a lista vacía). Además, cada compose reinyecta los skills custom al prompt de Nova (cap 20, fail-soft): si Nova responde `match` con `custom:<uuid>`, el server valida el uuid contra la DB y devuelve la ficha desde el registro, nunca del texto del LLM.

**Rate limits.** Launch de superskills: 10/min por workspace, key `superskills-launch:<workspaceId>` (`index.ts:111,144-150`). Intents clásicos: 10/min global, key `intents:global` (`index.ts:133-136`). Promote y list no tienen rate limit propio (sin LLM involucrado). Todo `/api/workspaces/*` va tras el bearer compartido `SUBSTRATE_API_TOKEN`.
