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

> Secciones 5-7: gaps verificados, checklist de inicialización pre-workflow y snippets. · Parte 1: https://playgrounds.digitalhubassist.ai/superskills-arquitectura-parte1.md

## 5. Gaps de inicialización (verificados)

**(a) PARCIAL — El auto-deploy no re-sincroniza ni verifica el registro de funciones tras el restart.** `auto-deploy-api.sh:49-57` gatea solo con `curl /health` 200 y actualiza el marker; jamás hace `PUT /api/inngest` ni consulta al executor. Mitigado por el polling de 5s del executor (`docker-compose.yml:59-60,69-70`), pero en el caso infeliz (executor caído, signing key rotada) el deploy se declara exitoso con el motor async muerto y el primer aviso llega hasta 10 minutos después por el canary.

**(b) CONFIRMADO — `/health` valida DB y tracing pero NO Inngest ni el schema.** `apps/api/src/routes/health.ts:9-39`: checks = `substrate_db` (`SELECT 1`) + `tracing`. Ningún fetch a `INNGEST_BASE_URL` (127.0.0.1:8288), ningún check de que la tabla `superskills` exista. Agravante: las migraciones están repartidas entre `db/substrate/migrations/` (0005/0007/0008) y `apps/api/db/substrate/migrations/` (0021+, 0035); un bootstrap parcial es invisible para `/health`.

**(c) CONFIRMADO — Sin validación al boot de env vars críticas.** `apps/api/src/env.ts`: `INNGEST_EVENT_KEY`, `INNGEST_SIGNING_KEY` (l.8-9), `ANTHROPIC_API_KEY` (l.24), `SUBSTRATE_API_TOKEN` (l.32) y `NOTIFY_EMAIL_OPS` (l.117) son todos `optional`; solo `SUBSTRATE_DB_URL` es obligatoria. Los guards fatales de `index.ts:64-65` cubren solo serve-host y tracing. Sin `INNGEST_EVENT_KEY` el fallo aparece recién en el primer `inngest.send`; sin `NOTIFY_EMAIL_OPS` los `trace_failed` se descartan en silencio.

**(d) PARCIAL — Launch con Inngest caído devuelve 502 (correcto) pero deja huérfanos; con función sin registrar devuelve 201 sin ejecución.** El `inngest.send` es el último paso de `launch-plan.ts:133-141` y su throw propaga a 502 (`routes/superskills.ts:207-213`), pero para entonces ya existen un intent `'running'` y un trace `'queued'` sin rollback, que el reaper no caza (solo caza `step_executions` running). Y si el executor está vivo pero la función no está registrada (ventana post-deploy), el send acepta el evento → 201 `{launched:true}` y silencio total hasta el canary.

**(e) CONFIRMADO — Sin orden de arranque garantizado.** `agent-squad-api.service:3-4` ordena contra el daemon Docker (`After=docker.service`), no contra los contenedores `substrate-postgres` (5433) ni `substrate-inngest` (8288), que son compose-managed. La conexión Postgres es lazy y el boot no la prueba (`index.ts:396-407` banner sin tocar DB). En un reboot del box Hetzner el API puede quedar listening antes que sus dependencias.

## 6. Plan de inicialización pre-workflow

Checklist ordenado para garantizar superskills activos ANTES de cualquier workflow:

1. **[Boot] Validar env completo en prod**: agregar `assertProdEnvComplete()` junto a los guards existentes de `index.ts:64-65`, exigiendo `INNGEST_EVENT_KEY` + `INNGEST_SIGNING_KEY`, `SUBSTRATE_API_TOKEN`, `NOTIFY_EMAIL_OPS` (o WARN ruidoso) y al menos un path LLM viable (binario `claude` con sesión, o `LLM_API_FALLBACK=true` + `ANTHROPIC_API_KEY`). Cierra el gap (c).
2. **[Boot] Verificar schema**: `SELECT to_regclass(...)` para `superskills`, `plan_drafts`, `intents`, `plans`, `traces`, `step_executions` al arrancar; fatal si falta alguna. Cubre el riesgo de los dos directorios de migraciones (gap b).
3. **[Boot] Fail-fast de DB**: un `SELECT 1` real antes del banner de listening (hoy la conexión es lazy, gap e).
4. **[systemd] Orden de arranque**: `ExecStartPre` en el drop-in de `agent-squad-api.service` con espera activa de `pg_isready -h 127.0.0.1 -p 5433` y TCP a 127.0.0.1:8288, acotado por `TimeoutStartSec` (subir desde 90s si hace falta). Cierra el gap (e).
5. **[Deploy] Re-sync Inngest post-restart**: en `auto-deploy-api.sh`, tras el health 200, forzar `curl -X PUT http://127.0.0.1:4000/api/inngest` para no depender solo del polling de 5s. Cierra la primera mitad del gap (a).
6. **[Deploy] Verificar registro de funciones**: consultar la API del executor (endpoint tipo `http://127.0.0.1:8288/v1/apps`, forma exacta por confirmar contra la versión del executor) y comprobar que la app reporta las 10 funciones de `FUNCTIONS`; si no coincide, NO actualizar el marker y loguear ERROR. Cierra la segunda mitad del gap (a).
7. **[API] Readiness compuesto**: nuevo `GET /ready` separado de `/health`: DB + schema (`to_regclass`) + fetch a `INNGEST_BASE_URL` + conteo de funciones registradas vs `FUNCTIONS.length` + env crítico. Cierra el gap (b) sin abaratar el `/health` existente.
8. **[Deploy] Gatear el marker con `/ready`**, no con `/health`.
9. **[Deploy] Warm-up/smoke**: disparar el canary existente (`canary.ts`) inmediatamente post-deploy en lugar de esperar el cron de 10 min, y exigir su round-trip antes de declarar el deploy OK.
10. **[Launch] Compensación de huérfanos**: en `launch-plan.ts`, envolver el `inngest.send` en try/catch con `updateIntentStatus(intent.id,'failed')` + trace failed (o invertir el orden: send primero, estados `running` después). Cierra la primera mitad del gap (d).
11. **[Launch] Gate de registro en el endpoint**: antes de aceptar un launch, verificar (con cache corto) que `substrate-execute-plan` figura registrada en el executor; si no, 503 `engine_not_ready` en vez de 201 fantasma. Cierra la segunda mitad del gap (d). Ver snippet (c).
12. **[Continuo] Mantener el canary cada 10 min** como red post-hoc (ya existe: `slo-alert.ts:119-128`), ahora como segunda línea y no como única detección.

## 7. Snippets

Pseudo-código listo para adaptar (rutas y nombres reales del repo; el endpoint exacto de la API del executor Inngest queda por confirmar contra la versión desplegada).

### (a) Readiness endpoint compuesto (DB + schema + Inngest + env) en Hono

```ts
// apps/api/src/routes/ready.ts — montar en index.ts junto a healthRoute
import { Hono } from 'hono';
import { sql } from '../substrate/db';
import { env } from '../env';
import { FUNCTIONS } from '../inngest/functions'; // las 10 funciones

const REQUIRED_TABLES = ['superskills', 'plan_drafts', 'intents', 'plans', 'traces', 'step_executions'];

export const readyRoute = new Hono().get('/ready', async (c) => {
  const checks: Record<string, boolean | string> = {};

  // 1. DB viva
  try { await sql`SELECT 1 AS ok`; checks.substrate_db = true; }
  catch { checks.substrate_db = false; }

  // 2. Schema aplicado (cubre los DOS directorios de migraciones)
  try {
    const rows = await sql`
      SELECT unnest(${REQUIRED_TABLES}::text[]) AS t,
             to_regclass('public.' || unnest(${REQUIRED_TABLES}::text[])) IS NOT NULL AS present`;
    const missing = rows.filter(r => !r.present).map(r => r.t);
    checks.schema = missing.length === 0 ? true : `missing: ${missing.join(',')}`;
  } catch { checks.schema = false; }

  // 3. Executor Inngest alcanzable + funciones registradas
  try {
    const base = env.INNGEST_BASE_URL; // http://127.0.0.1:8288
    const ping = await fetch(base, { signal: AbortSignal.timeout(2000) });
    checks.inngest_reachable = ping.ok;
    // endpoint por confirmar según versión del executor (p.ej. /v1/apps)
    const apps = await fetch(`${base}/v1/apps`, { signal: AbortSignal.timeout(2000) }).then(r => r.json());
    const registered = apps?.data?.find((a: any) => a.name?.includes('substrate'))?.functions_count ?? 0;
    checks.inngest_functions = registered >= FUNCTIONS.length ? true : `registered ${registered}/${FUNCTIONS.length}`;
  } catch { checks.inngest_reachable = false; }

  // 4. Env crítico (espejo de assertProdEnvComplete)
  checks.env = Boolean(env.INNGEST_EVENT_KEY && env.SUBSTRATE_API_TOKEN && env.NOTIFY_EMAIL_OPS)
    || 'missing critical env';

  const ready = Object.values(checks).every(v => v === true);
  return c.json({ ready, checks }, ready ? 200 : 503);
});
```

### (b) systemd/deploy con re-sync y gate de readiness

```ini
# /etc/systemd/system/agent-squad-api.service.d/robustness.conf (extender el drop-in existente)
[Service]
# Espera activa a las dependencias reales, no solo al daemon Docker (gap e)
ExecStartPre=/bin/sh -c 'until pg_isready -q -h 127.0.0.1 -p 5433; do sleep 2; done'
ExecStartPre=/bin/sh -c 'until (echo > /dev/tcp/127.0.0.1/8288) 2>/dev/null; do sleep 2; done'
TimeoutStartSec=180
```

```bash
# substrate-infra/scripts/auto-deploy-api.sh — reemplaza el bloque de las líneas 49-57
systemctl restart agent-squad-api.service

# 1. Gate de readiness compuesto (NO el /health barato)
for i in $(seq 1 30); do
  code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4000/ready) && [ "$code" = "200" ] && break
  sleep 2
done
[ "$code" = "200" ] || { echo "ERROR: /ready=$code — marker NO actualizado" >&2; exit 1; }

# 2. Forzar re-registro en el executor (no depender del polling de 5s)
curl -sf -X PUT http://127.0.0.1:4000/api/inngest > /dev/null \
  || { echo "ERROR: PUT /api/inngest falló" >&2; exit 1; }

# 3. Verificar registro real de las 10 funciones (endpoint por confirmar)
fns=$(curl -sf http://127.0.0.1:8288/v1/apps | jq '[.data[] | select(.name|test("substrate")) | .functions_count] | add // 0')
[ "$fns" -ge 10 ] || { echo "ERROR: solo $fns/10 funciones registradas — marker NO actualizado" >&2; exit 1; }

# 4. Smoke: disparar el canary ya, sin esperar el cron de 10 min
curl -sf -X POST http://127.0.0.1:4000/api/dev/canary-fire > /dev/null || true  # ruta por confirmar

# 5. Solo ahora, actualizar el marker
git -C "$REPO" rev-parse HEAD > "$MARKER"
```

### (c) Guard en el endpoint de launch: verificar registro de la función antes de aceptar

```ts
// apps/api/src/substrate/engine-ready.ts — guard con cache corto para no penalizar cada launch
let cache: { ok: boolean; at: number } = { ok: false, at: 0 };
const TTL_MS = 15_000;

export async function assertExecutePlanRegistered(): Promise<void> {
  if (Date.now() - cache.at < TTL_MS && cache.ok) return;
  try {
    // endpoint por confirmar según versión del executor self-hosted
    const res = await fetch(`${env.INNGEST_BASE_URL}/v1/apps`, { signal: AbortSignal.timeout(1500) });
    const apps = await res.json();
    const fns: string[] = apps?.data?.flatMap((a: any) => a.functions?.map((f: any) => f.id) ?? []) ?? [];
    cache = { ok: fns.some(id => id.includes('substrate-execute-plan')), at: Date.now() };
  } catch {
    cache = { ok: false, at: Date.now() };
  }
  if (!cache.ok) throw new Error('engine_not_ready');
}

// apps/api/src/routes/superskills.ts — dentro del handler de launch, ANTES de crear nada
try {
  await assertExecutePlanRegistered();
} catch {
  return c.json({ error: 'engine_not_ready' }, 503); // en vez del 201 fantasma del gap (d)
}
// ... getSuperskill + merge de overrides + launchCompiledPlan como hoy ...

// Y en launch-plan.ts, la compensación (mitad 1 del gap d):
try {
  await inngest.send({ name: 'plan.compiled', data: { intent_id, plan_id, template_id, workspace_id } });
} catch (err) {
  await updateIntentStatus(intent.id, 'failed');
  await completeTrace(trace.id, { verdict: 'failed', reason: 'event_send_failed' }); // firma real por confirmar
  throw err; // la ruta lo mapea a 502 launch_failed como hoy, pero ya sin huérfanos 'running'
}
```