# Agent Squad Substrate — el viaje completo

> Versión Markdown de **https://playgrounds.digitalhubassist.ai/substrate-journey-motor-v3.html**
>
> Generado automáticamente desde el HTML. La web es la fuente canónica: los 16 diagramas SVG (pipeline, DAGs, arquitectura) no tienen equivalente en Markdown plano y están marcados en el texto.

---

*Convergence Hub · agents-platform · 2026-05-19*

## Un substrato *simbólico* para agentes que *compongan*

Cómo en una sola sesión pasamos del insight de un speaker — “control planes, no Photoshops” — a tener cuatro workflows productivos corriendo con LLM real sobre un grafo de claims tipados en Hetzner.

| | |
|---|---|
| **10** | Primitives |
| **16** | Operations |
| **4** | Templates |
| **$0.06** | Smoke total |
| **28/28** | Tests verdes |

*Antes del viaje · La pregunta obvia*

## ¿Qué es un *substrato*?

Si llegás a esto por primera vez, probablemente esa palabra es lo primero que te detiene. Cinco minutos acá y el resto del documento se lee distinto.

Un **substrato** es la capa por debajo de los agentes y los workflows donde *todo lo que producen queda tipado y consultable*. No es una base de datos. No es un knowledge base con embeddings. Es un grafo donde cada decisión, cada output, cada juicio humano y cada inferencia que un agente afirma se deposita como un nodo con relaciones explícitas a los demás.

La diferencia con un asistente conversacional es directa: el asistente es *amnésico*, el substrato es *coherente con su propio pasado*. Cada respuesta nueva puede consultar todo lo que se decidió antes y razonar contra eso. La aprobación que diste ayer es contexto disponible para el agente que arranca mañana.

**Sin substrato** — Cada workflow es amnésico. Lo que produce se pierde.

```
**Workflow A** → output al usuario 
 ↓ (nada queda registrado) 
 **Workflow B** arranca sin contexto 
 → repite preguntas, contradice decisiones previas
```

- Cada conversación empieza de cero, incluso entre agentes del mismo equipo
- El usuario hace de memoria del sistema (le pegás contexto en cada prompt)
- Las decisiones se evaporan: nadie sabe quién aprobó qué cosa, ni por qué
- Imposible auditar afirmaciones: el modelo dijo X, nadie sabe de dónde lo sacó

**Con substrato** — Cada workflow deposita inferencia trazable. Lo que produce compone valor.

```
**Workflow A** → outputs + Claims tipados 
 ↓ (grafo crece) 
 **Workflow B** lee Claims de A antes de empezar 
 → razona contra decisiones previas, no contra cero
```

- Cada decisión humana se materializa como Claim consultable por workflows futuros
- El equipo de agentes acumula memoria estructurada — no solo logs, conocimiento tipado
- Trazabilidad nativa: cualquier afirmación tiene provenance (qué Trace la produjo, qué humano la validó)
- Network effects: cada workflow extra agrega contexto a todos los siguientes

> *[Diagrama SVG — ver la versión web]*

*Capítulo uno · El origen*

## Empezó con una *charla* en Stanford

Llegaste a la sesión con la transcripción de Chamath Palihapitiya en el Stanford AI Club. Una frase puntual — *"I would rather build MS-DOS and Windows, not Adobe Photoshop"* — fue la palanca que dio vuelta el problema.

> *[Diagrama SVG — ver la versión web]*

Las herramientas se desgastan; los substratos compuestan. Lo que Chamath nombraba como *control plane* no era un producto sino una capa por debajo de los agentes donde cada decisión, claim, intent y artifact queda trazado y disponible para futuras inferencias. Eso es lo que hace que un equipo de agentes mejore con el uso.

El punto era simple y duro a la vez: si construyo el consumer Agent Squad sin substrato, repito el patrón de cada SaaS de IA — los usuarios producen pixels pero el sistema no aprende. Si construyo el substrato *primero*, cada workflow que después monto encima compone valor en el grafo. Agent Squad consumer pasa a ser *la primera ontology overlay*, no el producto final.

Y entonces vino la pieza estratégica. Chamath estaba conectando control plane con network effect: el grafo de Claims tipados es lo que crea el efecto compuesto cross-empresa, no la app que se monta arriba.

Decidimos no construir un Photoshop. El producto es el substrato. La app que Miles está terminando es la primera demostración pública de lo que ese substrato permite. La idea de fondo: `{ Intent, Plan, Trace, Claim, Artifact }` son los primitives que cualquier workflow agentic puede compilar a una DAG ejecutable, y cualquier decisión humana se vuelve contexto tipado para la próxima.

*Capítulo dos · Las siete bifurcaciones*

## Siete decisiones *estratégicas* tomadas con velocidad

Antes de tocar una sola línea de código, alineamos siete forks. Cada uno cierra una rama del árbol que abre otra. Todos se decidieron en la misma sesión.

*Capítulo tres · El vocabulario*

## Diez *primitives* y dos reglas duras

El substrato entero está expresado en diez tipos. No hay nada más. Si algo no entra acá, no es del substrato — es ontology overlay que se construye encima.

- **Intent** — Lo que alguien (humano o agente) quiere que pase. Tiene kind, subject, constraints, acceptance_criteria_ref.
- **Plan** — DAG de Steps compilado desde un Intent. Inmutable una vez compilado; nueva versión = nuevo Plan.
- **Step** — Nodo del Plan que referencia una Operation, con inputs específicos y schema de output esperado.
- **Operation** — Capability del catálogo (ej. text.compose_brief@1.0.0). Declara knowledge_access para Rule B.
- **Trace** — Ejecución concreta de un Plan. Cada Step ejecutado genera un StepExecution con inputs/outputs/cost.
- **Claim** — Triple semántico (subject, predicate, object) con provenance y confidence. Ejemplo: approvedBy → human:roberto.
- **Artifact** — Output materializado de un Trace. Content-addressed, con status (pending_review, approved, rejected) y embedding.
- **Evaluator** — Función pura sobre el output de un Step. Emite verdict {pass|fail, score, rationale}. Vive en el spec.
- **Ontology Overlay** — Vocabulario y patrones de uso de un dominio (ej. Agent Squad consumer). Se construye encima del substrato.
- **Workspace Manifest** — Snapshot cacheado del estado del workspace que los agentes leen al arrancar. Materializado nightly.

> **Regla A · Inviolable: WorkspaceManifest es *primitive*, no convención**
>
> Cada agente al arrancar lee el manifest cacheado, no el grafo completo. Si el manifest no existe, el agente se rehúsa a ejecutar. Esto fuerza pre-cómputo nightly y elimina queries inestables sobre el grafo en hot-path.

> **Regla B · Inviolable: Manifest-first contract: `knowledge_access.requires_vector`**
>
> Cada Operation declara si necesita vector search. El runtime rechaza ejecución si una Op intenta vector lookup sin haberlo declarado. Reduce queries pgvector ~70% y vuelve auditable qué Operations son caras de leer.

*Capítulo cuatro · El stack tecnológico*

## Doce decisiones de *tech* tomadas en una hora

Cada capa tiene una razón concreta. No hay tecnología elegida por inercia ni por hype. La pregunta por defecto fue: ¿qué cosa madura y barata cubre esto sin pintarnos en una esquina?

| Capa | Tech | Por qué |
|---|---|---|
| Spec | JSON Schema + Zod + TypeScript | Spec abierta, no DSL propio. Cualquiera con JSON Schema válida outputs sin tocar el motor. |
| Motor | v2 event-sourced + Inngest OSS | El compilador produce graph_nodes; reducer puro + aplicar-evento proyectan runs/run_events; un worker consume work_items. Inngest conserva eventos y crons durables. execute-plan es el executor v1 en deprecación. |
| Substrate DB | Postgres 16 + pgvector + pgvectorscale | DiskANN para 3072-dim embeddings (HNSW topa a 2000). Dedicada en Hetzner, NO en InsForge. |
| Workspace DB | InsForge | Sigue siendo para la app consumer (auth, user data, outputs feed). Separado del substrato por diseño. |
| LLM | Max CLI (claude-cli, plan Max; sin API de Anthropic) | Sonnet 4.5 y Opus para composers y scorers; Grok para synthesis. El path Vercel AI SDK + @ai-sdk/anthropic quedó como gateway dormido (Fase 1). |
| Embedding | OpenAI text-embedding-3-large (3072d) | Mejor precision/recall para retrieval semántico. Backup a R2 desde día uno por si migramos vector DB. |
| Reranker | Cohere Rerank v3.5 | Capa de relevancia sobre top-K. Activable cuando el grafo crezca. Hoy aún no lo necesita. |
| Evaluators | Propios en packages/substrate-spec | No LangSmith. Evaluators son código testeado que vive con el spec, no servicio externo. |
| Observability | Langfuse OSS v3 self-hosted | Web + worker + ClickHouse + MinIO + Redis + Postgres. Tracing por trace_id del substrato. |
| API runtime | Hono sobre Bun 1.3 | Cold-start nulo, footprint mínimo. Una sola Hono app expone /api/intents, /api/approvals, /api/health. |
| API contract | tRPC interno · REST + OpenAPI 3.1 externo | Internamente tipos compartidos. Externamente OpenAPI para que cualquier cliente implemente. |
| Compute | Apps en Vercel · Motor + DB + Obs en Hetzner | Hetzner 4CPU/8GB ya pagado. El motor stateful vive donde tenemos disco; las apps stateless siguen en Vercel. |

*Capítulo cinco · Infraestructura levantada*

## Una caja Hetzner, todos los servicios *encendidos*

No es la nube, no es Kubernetes, no es serverless. Es una sola máquina con systemd y tres stacks de Docker Compose convivendo bien. Memoria total usada: aproximadamente 1.7 GB sobre 8 disponibles.

El backup corre nightly a las 3 AM vía cron sobre `~/substrate-infra/scripts/backup-substrate-db.sh`. Genera dump a `~/backups/substrate/` y, si existe el remote rclone `r2-substrate:`, también empuja a Cloudflare R2. Las migraciones SQL del substrato viven en `agent-squad-app/db/substrate/migrations/0001_init.sql`: 15 tablas, índices DiskANN sobre vectores de Claims y Artifacts, partitioning por fecha en `traces` y `step_executions`.

Pensalo así: imaginá que en una pared tenés colgado el brief que aprobaste ayer. A su lado van apareciendo *post-its* que el sistema escribe automáticamente. Cada uno de esos post-its es **un Claim**.

| Pieza | Qué responde | Ejemplos |
|---|---|---|
| sujeto | A qué cosa se pega el post-it | un brief · tu voz · un proyecto · una decisión · un prospecto |
| predicado | Qué dice sobre eso | `approvedBy` · `hasKind` · `voiceTone` · `fitScore` · `producedByOp` |
| objeto | Qué valor lleva | `"human:roberto"` · `"doc"` · `"directo sin rodeos"` · `0.75` |
| provenance | De dónde vino (auditoría nativa) | qué Trace lo emitió · qué Step · qué humano lo confirmó |

> *[Diagrama SVG — ver la versión web]*

## Cuatro alternativas *conocidas* y dónde rompen para este caso

### Vanilla RAG

*retrieval-augmented generation*

**Da bien.** Recuperar chunks de documentos que *se parecen* a tu query usando similitud vectorial. Sirve para Q&A sobre un corpus estático (manuales, knowledge bases).

**Rompe acá.** No puede responder *"¿qué decidió Roberto sobre este topic?"* — solo encuentra documentos que *mencionan* el topic. Cero auditoría: el LLM cita un chunk pero no sabés si fue una decisión, una opinión rechazada, o una hipótesis vieja. Sin estructura, cada respuesta puede alucinar sobre afirmaciones que nunca se hicieron.

> RAG te dice **qué se parece**. El substrato te dice **qué fue afirmado, por quién, y cuándo**.

### LangChain · LlamaIndex

*orchestration libraries*

**Da bien.** Componer cadenas de prompts y llamadas a LLM con una API consistente. Útil para prototipar pipelines lineales rápidamente en un notebook.

**Rompe acá.** El estado vive en memoria del proceso. Si el server crashea a mitad de la cadena, arrancás de cero. *No* tiene human-in-the-loop durable, *no* tiene retries por step con backoff persistido, *no* tiene grafo de afirmaciones — solo es pegamento entre prompts. Para producción con flujos de >2 minutos se vuelve frágil.

> LangChain es **el pegamento entre prompts**. El substrato es **el motor durable + la memoria estructurada**.

### AutoGPT · BabyAGI · CrewAI

*autonomous agent frameworks*

**Da bien.** Demos virales: agentes que se prompten a sí mismos en loop, eligen tools, descomponen tareas. Espectaculares en YouTube.

**Rompe acá.** El LLM decide el próximo paso en cada iteración → costo y latencia impredecibles, runs infinitos cuando se confunde. *Sin* DAG explícito no podés auditar qué va a hacer ni acotar el blast radius. *Sin* memoria compartida entre runs, cada conversación empieza de cero. Funciona en demos, no en operación.

> Los agent frameworks son **improvisación**. El substrato es **partitura con margen de improvisación dentro de cada Step**.

### Workflow engine solo

*Temporal · Inngest · Step Functions*

**Da bien.** Ejecución durable, retries, human-in-the-loop, observabilidad. Es la base de lo que estamos usando — Inngest mismo cae en esta categoría.

**Rompe acá.** Es *solo orquestación*. No tiene noción semántica de qué se está afirmando. Cada Step ejecuta lógica suelta; el output queda como blob en una tabla genérica. Sin la capa de Claims tipados arriba, dos workflows del mismo dominio no pueden compartir afirmaciones de manera estructurada — solo eventos.

> El workflow engine es **el verbo**. El substrato agrega **el sustantivo (Claims tipados)** arriba. Inngest + Postgres + spec = substrato.

*Capítulo seis · El catálogo*

## Una librería de *capabilities* de la que cualquier Plan tira

El catálogo es público (packages/substrate-spec). Cualquier PlanTemplate nuevo agrega entre cero y cuatro Operations específicas del dominio; el resto del motor (durable execution, human-gate, lineage, claims, cost tracking) viene gratis.

- **73 Operations** — Capabilities atómicas registradas en el motor. Cada una declara inputs, outputs, knowledge_access y cost model. El dominio más grande es **research (17)**; la última en sumarse, research.audience_intelligence.
- **12 Evaluators** — Funciones puras que juzgan outputs. Emiten verdict {pass|fail, score, rationale}.
- **23 PlanTemplates** — DAGs reusables que compilan un Intent a un Plan. Los 4 canónicos (abajo) más document-extract/query, junta-yt-review, scene-production y el pipeline de video (script/assembly/publish/reel).
- **✓ Gate bloqueante** — Sin verde no se mergea ni despliega: check + unit (web/api) + e2e-criticos + e2e-fast/3d en el CI soberano. Cada Operation y PlanTemplate gatea ahí.

- research.synthesize@1.0.0
- research.keyword_expand@1.0.0
- research.reddit_search@1.0.0
- research.youtube_comments@1.0.0
- research.rank_opportunities@1.0.0
- research.audience_intelligence@1.0.0

- document.ingest@1.0.0
- document.chunk@1.0.0
- document.extract@1.0.0
- document.query@1.0.0

- media.ingest@1.0.0
- media.transcribe@1.0.0
- url.fetch_transcript@1.0.0

- video.script_draft@1.0.0
- broll.generate@1.0.0
- heygen.generate@1.0.0
- voice.tts@1.0.0

- junta.panel@1.0.0
- junta.compose_verdict@1.0.0
- junta.load_report@1.0.0

- text.compose_narrative@2.0.0
- text.compose_brief@1.0.0

- trace.query@1.0.0
- artifact.publish@1.0.0
- claim.recall_decisions@1.0.0
- claim.recall_voice@1.0.0

- evaluator.run@1.0.0

- prospect.search@1.0.0
- pricing.observe@1.0.0
- metrics.digest@1.0.0
- audience.load@1.0.0

Muestra representativa. El catálogo completo son **73 Operations** registradas en el motor (inngest/operations), con research como el dominio más denso.

*Capítulo siete · Los cuatro Plan Templates*

## Cuatro *DAGs* productivos cada uno con su propio agente

Cada PlanTemplate es un DAG topo-sorteado que se compila desde un Intent. Los colores marcan el tipo de Step: amber LLM, cian evaluator, verde publish, rosa human-gate, gris system.

> *[Diagrama SVG — ver la versión web]*

*Capítulo ocho · Human-gate suspension*

## El gate *durable* donde la persona es autoridad final

En v2 el gate no es un `step.waitForEvent`: `ACTIVAR_GATE` crea un flow durable en `artifact_approval_flows`. Revisión, aprobación/elección, acuse del executor y cierre son estados persistidos; `deadline_at` + el barrido aplican el fallback con CAS, y `required_role` define quién puede decidir.

**Bug crítico aprendido:** el match en `step.waitForEvent` debe usar `async.data.X`, no `event.data.X`. `event` referencia el trigger original (`plan.compiled`) — usarlo ahí evalúa al *tiempo de la suspension*, no del evento futuro, y matchea con null. `async.data` referencia el evento que se está esperando. Sin esto, todos los gates se quedan colgados indefinidamente.

**Lo que esto desbloquea:** el humano puede aprobar/rechazar/agregar comment, y la decisión queda como Claim tipado consultable por futuros workflows. La próxima vez que armemos un brief sobre el mismo topic, `claim.recall_decisions` ya tiene "este angle ganó" como contexto. Esto es el efecto compuesto del substrato — no es retrieval, es construcción de memoria estructurada.

*Capítulo nueve · Smoke test con LLM real*

## Cuatro templates *activados* con Sonnet 4.5 verdadero

Wireamos ANTHROPIC_API_KEY + OPENAI_API_KEY a apps/api/.env, restart systemd, y disparamos un Intent por cada template. Los cuatro alcanzaron awaiting_human con outputs reales. Costo total para los cuatro runs: seis centavos.

**Lo no trivial:** Sonnet razonó sobre el propio backlog del workspace. El narrative de standup-digest mencionó textualmente cuántos digests anteriores quedaron `queued` y `awaiting_human`, porque `trace.query` + `artifact.list_recent` + `claim.recall` le feedearon esos datos. La cadena retrieval → composer está validada end-to-end con datos reales del substrato.

*Capítulo diez · El ciclo cerrado*

## Aprobamos el *brief* y nacieron cinco Claims

El último paso de la sesión fue cerrar el ciclo del content-brief: POST /api/approvals con decision approve y un comment. El gate se reactivó, el artifact pasó a approved, y el substrato materializó cinco Claims tipados que viven para siempre como contexto consultable.

Este es el momento donde el substrato deja de ser teoría y empieza a comportarse como capital. La próxima vez que armemos un brief sobre el mismo topic, `claim.recall_decisions` va a recuperar "Hook 2 ganó sobre los otros 3" como contexto del composer. Sonnet no necesita recordarlo — el grafo se lo entrega. Eso es lo que distingue un substrato de un RAG: no busca documentos parecidos, sirve afirmaciones tipadas con provenance.

**Después de una sola sesión**, el workspace de Roberto tiene cuatro PlanTemplates productivos, diecisiete Operations atómicas, sesenta y pico de Claims acumulados como contexto compuesto, y un costo real de operación de aproximadamente dos centavos por workflow ejecutado. La pieza importante no es eso. La pieza importante es que cada uno de esos Claims va a estar acá mañana, y dentro de un año, alimentando los próximos workflows que armemos.

*Extensión · 2026-05-20 · primer estándar externo absorbido*

## El substrato *absorbe* un estándar de la comunidad

Apareció [audience.md](https://github.com/KikoPalomares/audience.md) — un estándar Markdown abierto, escrito en 2025, para definir audiencias de modo que humanos y agentes la consuman igual. La pregunta no era si reemplazaba al substrato. La pregunta era si las primitivas que ya teníamos podían absorberlo sin reescribirlo. La respuesta corta es sí. Lo que sigue es la evidencia.

*Qué es audience.md (resumen)*

Archivo Markdown plano (`AUDIENCE.md`) versionable en git con doce secciones canónicas: nombre, summary, primary audiences, jobs-to-be-done, pains, motivations, decision criteria, language and tone, anti-goals, evidence vs assumptions, open questions. Frontmatter YAML opcional con status, owners, last_reviewed. Pensado para que cualquier agente LLM lo lea como contexto base antes de operar sobre la audiencia.

*Mapping a las primitivas que ya teníamos*

### Cero primitivas nuevas. Cuatro alineaciones limpias.

Conceptualmente equivalente a cualquier otro Artifact publicado, solo que su lifecycle es "siempre presente" en lugar de "producido por una Op". Vive en filesystem, no en R2.

Define qué predicados son válidos para hablar de la audiencia (hasJob, hasPain, hasDecisionCriterion). Otros workspaces pueden tener overlays distintos.

Esta es la coincidencia perfecta. Evidence con source verificable = Claim confidence alta + source_refs poblado. Assumption = confidence baja + source_refs vacío. El estándar y el substrato describen exactamente la misma distinción.

Cumple Rule A: declarativo, versionable, durable. Idéntica filosofía al manifest del workspace.

*El wiring · brief-synthesis-v1 antes y después*

### Un step nuevo. Cero refactors al resto del DAG.

> *[Diagrama SVG — ver la versión web]*

*El archivo concreto · workspaces/ai4managers/AUDIENCE.md*

### El primer AUDIENCE.md derivado del brand blueprint de Roberto.

```
---
audience_id: ai4managers
status: stable
owners: [roberto@aguirre]
last_reviewed: 2026-05-20
source_doc: basic-memory:/sistema/reference_ai4m_brand_blueprint.md
---

# Audience: AI4Managers

## Summary
Spanish-speaking middle-to-senior managers (40–55) who built consolidated
careers before the AI shift and now sense the rules of their profession changed...

## Jobs To Be Done
- Understand what AI changes in MY specific role this quarter
- Operate AI tools and agents without becoming an engineer
- Preserve professional standing against AI-fluent juniors

## Language And Tone
**Core terms**: operar, desplegar, agentes en producción, manager, battle-tested
**Prohibited terms**: "estamos en la era de…", "imaginá si…", "lab", emojis,
  jerga ingenieril sin traducción (LLM, RAG, embeddings, fine-tuning)

## Anti-Goals
- Beginner-tutorial content
- Engineering deep-dives that require coding fluency
- English-only or US-only case studies

## Evidence
- Roberto's professional transition (Manager Senior → Head of AI Ops Engineering)
- Corporate training engagements (only currently validated monetization)
- LinkedIn outreach with ~207 unique authors, 33 batches

## Assumptions
- LATAM and Spain share enough vocabulary to be a single audience
- Status preservation > upside ambition (untested at scale)

... 4 secciones más: Motivations, Decision Criteria, Open Questions, Content Pillars
```

*Lo que cambia en el prompt de Sofia*

### El prompt ahora tiene una regla de precedencia y un guard de pillar.

El guard de pillar es la pieza interesante. Si alguien dispatcha un Intent para escribir, por ejemplo, un tutorial básico de prompt engineering — que no encaja en ninguno de los cuatro pillars del brand blueprint — Sofia no produce el brief: lo marca `PILLAR_MISMATCH` con razón. El human-gate lo recibe y Roberto decide si ampliar los pillars o descartar la pieza.

*Backward compatibility · cero breakage*

Workspaces sin `AUDIENCE.md` siguen funcionando idéntico al día anterior. El handler retorna `{ present: false, doc: null }` y Sofia hace fallback al string `target_audience` como siempre lo hizo. Los smoke tests previos (standup, lead, brief sin audience, video) pasan sin tocar nada. La extensión es opt-in al nivel de workspace, no un cambio de comportamiento global.

*El delta exacto · qué pesa esta extensión*

### 120 líneas insertadas. 6 archivos modificados. 5 archivos nuevos. 6 tests más.

**La lectura arquitectural.** Esta es la primera vez que un estándar comunitario externo mapea limpio a las primitivas del substrato sin forzar. Eso confirma una hipótesis que llevábamos asumiendo: el modelo simbólico tiene poder expresivo suficiente para absorber convenciones de la comunidad sin reescribirlas. La próxima extensión candidata es *llms.txt* (estándar de Jeremy Howard para que crawlers AI consuman docs) — si vuelve a mapear a Artifact + WorkspaceManifest, el patrón "absorb don't reinvent" se vuelve doctrina.

**La lectura operativa.** El próximo Intent `produce_artifact / content-brief` dispatched contra el workspace AI4Managers va a generar briefs alineados al brand blueprint sin que nadie tenga que pegarle el documento al prompt cada vez. La tesis, los pillars, las palabras prohibidas, los anti-goals — todo está en el grafo, y se aplica automáticamente.

*Capítulo doce · As-built · fotografía 2026-06-11 · escrito para estudiantes de ingeniería*

## La arquitectura *hoy*, pieza por pieza

Los capítulos anteriores cuentan el origen (mayo 2026: el substrato nació en una sesión). Este capítulo fotografía lo que existe **hoy, construido y verificado en producción**: qué es cada componente, para qué sirve, dónde vive y cómo se conecta. Lo que en mayo eran 16 operations y 28 tests, hoy son 73 operations, 13.776 tests y un manual de 34 pasos que un navegador re-verifica contra producción. Si vas a robarte algo de este caso, robate el criterio: *cada pieza existe por una razón operativa, ninguna por hype.*

**El sistema en una frase:** una app stateless en Vercel le declara *intenciones* a un motor en una caja Hetzner; el motor las compila a planes tipados, los ejecuta de forma durable con un LLM cuyo costo se contabiliza paso a paso, fuerza una aprobación humana al final de cada plan, y convierte cada decisión humana en memoria estructurada consultable con SQL.

Visto de afuera, un plan en ejecución parece un grafo de pasos con un nodo que consulta una base vectorial. De ahí sale la crítica fácil: *"esto es una plantilla de n8n con un nodo de vector, y ya."* Confunde el **artefacto** (un DAG en runtime) con el **proceso que lo generó**. n8n es un flujo que un humano dibuja a mano. Aquí el flujo **no se dibuja: se compone y se valida.** Tres diferencias de categoría:

Nova (un LLM) **genera** el plan desde lenguaje natural; el servidor lo **valida formalmente** contra el catálogo — refs y edges (`validatePlanAgainstCatalog`), grafo sin ciclos por Kahn, tope de 10 pasos. Un humano no dibujó este grafo; el sistema lo derivó y lo probó.

Karina, Alexa, Sofía no son procesos ni tienen capacidad: son **etiquetas de identidad**. La capacidad vive en las operations del catálogo, y Nova asigna el actor desde ahí — nunca lee el squad. Es la inversión que ningún flujo de nodos tiene: **el equipo se deriva del trabajo, no al revés.**

De 73 operations, **55 son handlers de código deterministas**; el LLM solo entra en las 18 que *componen* o *juzgan* texto. Y todo plan termina en un gate humano con `fallback:'fail'`: el modelo no tiene camino para saltárselo. No es "el LLM hace todo" ni "todo hardcoded".

La sofisticación no está donde la critican (que exista un DAG, que exista un vector) sino donde no miran: la composición-validada, la inversión catálogo↔personas y el gate estructural. Cuatro arquitectos reales — Cherny, Embiricos, Chase, Majors — sometieron este as-built a revisión: [acta de la sesión Master-Arq →](master-arq-asbuilt.html)

### El mapa de capas — quién habla con quién

- **Browser** SvelteKit 5 (runes) hidratado · oficina 3D Threlte/Three.js · parser SSE propio
- **SvelteKit server · Vercel** 13 páginas de producto + 15 endpoints · gates fail-closed por request · auth server-first
- **InsForge (BaaS)** identidad: auth.users + perfil JSONB `app_state` · `user_access` (gate) · `magic_links` (one-time)
- **Resend** SMTP transaccional (magic links)
- **Cloudflare → nginx** TLS · expone SOLO health, workspaces/*, approvals, intents · `proxy_buffering off` para SSE · Inngest NO es público
- **Hono sobre Bun · systemd :4000** 10 rutas · rate limits · validación de planes contra el catálogo · Nova · `idleTimeout: 120`
- **Inngest OSS** ejecución durable · human-gate `step.waitForEvent` ≤24h · sobrevive reinicios
- **Adapter LLM** Claude Code headless (CLI, $0 marginal) · fallback API · solo `text_delta` sale al usuario
- **Postgres 16 + pgvector/pgvectorscale** 77 tablas del substrato · DiskANN 3072d en claims y artifacts · 32 particiones mensuales
- **Langfuse v3** web + worker + ClickHouse + MinIO + Redis · traza y costo por trace_id
- **Crons + backups** pg_dump nightly 03:00 UTC → `~/backups/substrate/` (+R2 opcional) · standup digest 07:30 con preflight del CLI

Tres dominios de cómputo, cada uno donde le conviene: lo stateless en Vercel (escala solo, no guarda nada), lo stateful en una caja con disco (Postgres, Inngest, Langfuse: 3 stacks de Docker Compose + un systemd), y la identidad en un BaaS aparte — desacoplada del substrato por diseño. Memoria total observada del lado motor: ~1.7 GB de 15 disponibles.

### Las piezas, una por una

#### SvelteKit 5 + Vercel app consumer

**Qué es.** Una app full-stack con runes (reactividad compilada) servida serverless. 13 páginas de producto (welcome, onboarding, office, briefing, library, activity, outputs, squads, hire, squad-proposal, discover, deep-dive, share — aparte quedan la demo pública, el callback de OAuth y las legales) cada una con su `+page.server.ts` que carga datos reales del motor — y degrada a contenido de ejemplo etiquetado si el motor no responde.

**Para qué sirve.** Es la cara del producto: todo lo que el founder ve y toca. Stateless a propósito: no guarda nada, así Vercel puede crear y destruir instancias sin que se pierda estado.
`apps/web/src/routes/ · adapter-vercel · maxDuration 90s en chat y compose`

#### La oficina 3D Threlte + Three.js

**Qué es.** Una escena voxel renderizada en el browser: 16 agentes en 3 zonas de squad, 12 escritorios, layout calculado por una función pura (testeada con vitest, sin tocar WebGL).

**Para qué sirve.** Convierte "tenés agentes trabajando" en algo que se ve. El recap de la oficina ("N shipped · M await review") viene de un endpoint del motor que agrega trazas reales de las últimas 24h.
`apps/web/src/lib/scenes/ · OfficeScene, VoxelAgent, SquadZones, CameraRig`

#### ChatDrawer + parser SSE streaming

**Qué es.** El chat con cada agente. Un parser SSE propio (~60 líneas, puro, testeado) pinta cada fragmento de la respuesta en una burbuja transitoria; al abrir el drawer con sesión vacía, hidrata el hilo persistido en el motor.

**Para qué sirve.** Conversar con un agente sin proceso esperando: la "continuidad" del agente vive en su memoria (briefing + trabajos + hilo), que el motor arma en el instante del mensaje. Si el stream se corta antes del primer fragmento, degrada al camino clásico con un único retry.
`lib/components/chat/ChatDrawer.svelte · lib/chat/sse.ts · lib/chat/session.ts`

#### NovaModal + SkillLaunchModal puerta única

**Qué es.** La UI de "pedile a Nova": un textarea en lenguaje natural y un modal que muestra el desenlace — match con un SuperSkill existente, plan nuevo compuesto (con costo estimado y pasos en humano), o un "no puedo" honesto. Desde ahí el plan se guarda con nombre propio.

**Para qué sirve.** El usuario nunca navega un catálogo técnico: pide. Y lo que Nova compone puede promoverse a SuperSkill relanzable desde la library.
`lib/components/library/NovaModal.svelte · SkillLaunchModal.svelte`

#### InsForge BaaS de identidad

**Qué es.** Un backend-as-a-service que aporta `auth.users` con perfil JSONB passthrough, más dos tablas propias: `user_access` (gate booleano de beta privada, fail-closed) y `magic_links` (tokens CSPRNG de un solo uso, 15 min, consumo atómico con `UPDATE … WHERE consumed_at IS NULL`).

**Para qué sirve.** Toda la identidad, separada del substrato por diseño. El estado de la app del usuario (squad, onboarding, SuperSkills instalados) vive en `profile.app_state` — el perfil ES la base de datos del consumer.
`migrations/2026…create-user-access.sql · create-magic-links.sql · SDK @insforge/sdk`

#### hooks.server.ts el guardián por request

**Qué es.** El hook de SvelteKit que corre en CADA request del server: lee la cookie httpOnly, renueva el access token con el refresh si expiró (~15 min), y consulta `user_access` con la service key.

**Para qué sirve.** Fail-closed de verdad: error leyendo el gate = no autorizado. El bypass para tests E2E existe pero está doblemente gateado (`dev && CI`) — en build de producción esa rama es código muerto.
`apps/web/src/hooks.server.ts`

#### Auth server-first la lección del refresh token

**Qué es.** Login, OAuth de Google y magic links se intercambian *en el server* (`client_type=server`), nunca en el browser. Razón: el SDK de browser hace el intercambio PKCE en su constructor y jamás expone el refresh token — sin él, las sesiones morían a los 15 minutos.

**Para qué sirve.** Sesiones de 30 días con renovación silenciosa, y tokens que viven solo en cookie httpOnly/secure.
`lib/server/auth.ts · routes/api/auth/* · routes/auth/callback`

#### /api/user/state estado con disciplina

**Qué es.** El único endpoint que escribe `app_state`: hace read-merge-write del estado completo, re-sanitiza campos con pick explícito (el junk no persiste) y rechaza bodies >64KB. Del lado cliente, una *write-queue* serializa los POSTs (dos escrituras en vuelo se pisarían) con update optimista y revert compare-and-swap.

**Para qué sirve.** Multi-dispositivo sin servidor de estado: creás un squad en un browser, lo ves en otro. localStorage queda como cache write-through, con precedencia perfil > cache > derivación.
`routes/api/user/state/+server.ts · lib/stores/userState.ts · lib/squads/store.ts`

#### Cloudflare → nginx superficie mínima

**Qué es.** TLS con certificado origin de Cloudflare y un solo site de nginx que proxya 443 → 127.0.0.1:4000 exponiendo únicamente `/health`, `/api/workspaces/*`, `/api/approvals` y `/api/intents`. El endpoint de Inngest no existe para internet. Para el SSE del chat: `proxy_buffering off` y read timeout de 120s.

**Para qué sirve.** Que el motor tenga exactamente la superficie que la app necesita y ni un path más. El puerto 4000 está cerrado al público por firewall; solo loopback y el bridge de Docker llegan.
`substrate-infra/nginx/api-substrate.digitalhubassist.ai.conf`

#### Bearer + rate limiting middleware

**Qué es.** Un token bearer server-to-server comparado en tiempo constante (anti timing-attack), y un rate limiter de ventana deslizante en memoria: chat 10/min, compose 4/min, intents 10/min — montado *después* del bearer (un 401 no consume cupo), respondiendo 429 con `Retry-After`.

**Para qué sirve.** El LLM es el recurso caro; los límites protegen la sesión de un loop accidental o un script ansioso. El browser jamás ve este token: solo la app server-side lo usa.
`apps/api/src/middleware/bearer-auth.ts · rate-limit.ts`

#### Hono sobre Bun systemd :4000

**Qué es.** Una sola app HTTP con 10 archivos de ruta: `POST /api/intents`, `POST /api/approvals`, y bajo `/api/workspaces/:id/` — outputs, activity, brief (GET/PUT), manifest, chat (+history), compose (+launch/discard), superskills (+launch). Corre como unidad systemd con restart on-failure y logs a archivo.

**Para qué sirve.** Es el único punto de entrada al substrato. Detalle con cicatriz: `idleTimeout: 120` en el export del server — el default de Bun (10s) mataba el SSE cuando el modelo razonaba más de 10s en silencio. Lo cazó el manual vivo, no un test unitario.
`apps/api/src/index.ts · routes/ · .runtime/{stdout,stderr}.log`

#### El catálogo: 73 Operations la costura de sustitución

**Qué es.** Setenta y tres refs tipadas y versionadas repartidas en 33 dominios (`trace.query`, `claim.recall_decisions`, `text.compose_brief`, `video.compose`…); son 70 ids, porque tres conviven en dos versiones vivas. El dominio más grande es `research`, con 17. Todas tienen handler de runtime salvo dos — `human_gate.approve` y `human_task` — que no lo tienen *a propósito*: las ejecuta el propio executor suspendiendo el workflow.

**Para qué sirve.** La operation es la unidad con contrato (inputs tipados → outputs + costo) y la costura donde se pondría en cuarentena cualquier dependencia volátil — un framework de agentes cabe *adentro* de una operation; la inversa no.
`packages/substrate-spec/src/catalog/ · apps/api/src/inngest/operations/`

#### Compiler + 23 PlanTemplates + 12 evaluators Intent → Plan

**Qué es.** Un Intent declarado se compila a un Plan: DAG topo-sorteado de steps donde cada step referencia una operation del catálogo. Los planes que compone Nova pasan una validación server-side de 4 capas (cada operación existe en el catálogo, el grafo no tiene ciclos — Kahn —, tope de 10 steps, y la aprobación final la agrega el sistema); todo lanzamiento — de fábrica, compuesto o guardado — re-valida contra el catálogo y exige estructuralmente el gate humano terminal. Los evaluators emiten verdicts por paso.

**Para qué sirve.** Que "el modelo decidió" nunca sea la explicación de una ejecución: lo que corre es un artefacto tipado que el servidor validó, con costo estimado calculado server-side (el modelo no puede mentirlo).
`apps/api/src/substrate/plans.ts · packages/substrate-spec/`

#### Nova (compose) una llamada, tres desenlaces

**Qué es.** Una sola consulta al modelo decide: MATCH (el pedido corresponde a un SuperSkill existente — validado server-side contra la lista real, el modelo no puede inventar ids), COMPOSE (arma un plan nuevo usando SOLO operations del catálogo), o CANNOT (lo dice honesto y el pedido queda registrado como demanda).

**Para qué sirve.** Es el único lugar del sistema donde el LLM "diseña" — y por eso es el más vigilado: lo que propone entra por su validación de 4 capas y por el mismo gate estructural que todo lo demás. Cada pedido queda en `plan_drafts` como telemetría: los "no pude" priorizan qué construir.
`apps/api/src/substrate/nova-compose.ts · routes/compose.ts`

#### SuperSkills + launch-plan planes promovidos

**Qué es.** Un plan compuesto que gustó se promueve con nombre propio al catálogo del workspace: se re-valida contra el catálogo ANTES del insert, y un unique parcial en DB hace imposible el doble-promote (gana exactamente uno, el otro recibe 409). El lanzamiento usa un helper compartido con los planes de fábrica: `assertHumanGatePresent` verifica estructuralmente ≥1 gate humano terminal con fallback 'fail'.

**Para qué sirve.** Que lo que el usuario crea pidiendo tenga las mismas garantías que lo que construyó el equipo. El formulario de relanzado se deriva mecánicamente de las variables que el propio plan declara.
`apps/api/src/substrate/superskills.ts · launch-plan.ts · routes/superskills.ts`

#### Adapter LLM CLI Max + fallback API

**Qué es.** Un adapter propio con dos backends: Claude Code headless por CLI (sesión de suscripción Max — costo marginal $0) y la API oficial como fallback. Para streaming parsea NDJSON línea a línea y emite SOLO `text_delta` — el razonamiento interno del modelo jamás llega al usuario; la línea `result` final gana sobre lo acumulado.

**Para qué sirve.** Separar arquitectura de etapa: la beta corre a costo cero, y como cada paso registra su costo notional en dólares, la economía unitaria del switch a API se conoce HOY ($0.013–0.022 por digest). Prender la API es configuración, no re-arquitectura.
`apps/api/src/inngest/llm.ts · LLM_PROVIDER=claude-cli`

#### Inngest OSS ejecución durable

**Qué es.** El runtime durable self-hosted para eventos, crons y reintentos operativos. `handle-intent-declared` selecciona v2 y arranca una corrida sobre el grafo compilado; `worker-orquestacion` da cuerda al worker que consume `work_items`; los barridos recuperan leases y gates vencidos; `materialize-manifest` conserva su cron. `execute-plan` permanece únicamente como executor v1 en deprecación y canario anti-zombie.

**Para qué sirve.** Inngest hace durable la entrega y el housekeeping, pero ya no es la autoridad sobre el avance del DAG. Esa verdad vive en Postgres: `runs` + `run_events`, plegados por el reducer, y la cola transaccional que procesa el worker v2. Un deploy puede interrumpir un tick; no puede borrar lo que la corrida ya decidió.
`apps/api/src/inngest/functions/ · apps/api/src/orquestacion/ · substrate-infra/inngest/ · :8288 (no público)`

#### Postgres 16 + pgvector/pgvectorscale el substrato físico

**Qué es.** Una instancia dedicada (Docker, :5433) con 77 tablas lógicas y 32 particiones mensuales. Las dos de mayor volumen — `traces` y `step_executions` — particionadas por mes. Los embeddings (3072d, text-embedding-3-large) son una columna en `claims` y `artifacts` con índice StreamingDiskANN — HNSW topa en 2000 dimensiones, por eso pgvectorscale.

**Para qué sirve.** Memoria semántica y hechos tipados en el MISMO store: un JOIN entre similitud vectorial y claims estructurados, sin dual-write contra un vector DB externo. La consistencia es gratis porque hay una sola base.
`db/substrate/migrations/0001–0007 · substrate-infra/postgres/`

#### El grafo de provenance lineage obligatorio

**Qué es.** La cadena `intents → plans (steps, edges) → traces → step_executions → artifacts / claims`, más `lineage_edges` con una vista recursiva (profundidad máx 12, guard de ciclos). Regla dura: todo claim y artifact DEBE tener provenance `{trace_id, step_id}`.

**Para qué sirve.** Auditoría nativa: de cualquier afirmación del sistema se puede responder "¿de dónde salió esto?" caminando el grafo — qué trace la emitió, qué step, qué humano la confirmó.
`db/substrate/migrations/0001_init.sql · lineage_upstream view`

#### workspace_manifests Regla A

**Qué es.** Un snapshot navegable por workspace (squad, intents activos, artifacts recientes, decisiones, índice de navegación) materializado cada noche y on-demand por evento. Desde la migración 0006, la columna `manifest` JSONB es la fuente de verdad.

**Para qué sirve.** Que un agente arranque sabiendo dónde está parado sin listar media base: lee UN registro. Es cache derivado — se puede reconstruir entero desde el grafo.
`apps/api/src/substrate/manifests.ts · inngest/functions/materialize-manifest.ts`

#### Langfuse v3 observabilidad LLM

**Qué es.** Stack self-hosted de 6 servicios (web, worker, ClickHouse, MinIO, Redis, Postgres propio) que traza cada llamada LLM con latencia, tokens y costo, correlacionada por trace_id del substrato. Con cicatriz propia: ClickHouse capado a 2 CPUs después de un runaway que clavó 7.7 cores durante 19 horas.

**Para qué sirve.** Responder "¿qué hizo exactamente el modelo en este paso y cuánto costó?" con un click. El único Redis del sistema vive adentro de este stack — no está en el camino de la app.
`substrate-infra/langfuse/ · :3030`

#### Crons + backups la rutina nocturna

**Qué es.** Dos crons: backup nightly (03:00 UTC, `pg_dump` gzip a `~/backups/substrate/`, retención 30 días, push opcional a R2) y standup digest diario (07:30 UTC, declara un Intent real vía la API pública con bearer — con preflight que verifica la salud del CLI antes de disparar).

**Para qué sirve.** El backup es la respuesta al dominio de falla catastrófico (RPO ≤24h). El digest es el latido del producto: el sistema se usa a sí mismo todos los días. Los dumps crecen con el sistema: 680K en mayo, 1.8M hoy.
`substrate-infra/scripts/backup-substrate-db.sh · standup-digest-daily.sh`

#### La verificación como producto 13.776 tests + manual vivo

**Qué es.** Tres anillos: 13.333 tests unit y de integración (4.102 de la app, 9.058 del motor contra Postgres real, 173 del spec), 443 E2E/visuales con Playwright repartidos en 81 archivos (journeys completos + regresión de screenshots), y el anillo exterior — [un manual de 34 pasos](manual-agentsquad/) que un navegador real re-ejecuta contra producción antes de cada demo, con usuario efímero y limpieza quirúrgica del workspace.

**Para qué sirve.** El anillo exterior caza lo que los internos no ven: el bug del `idleTimeout` (SSE muerto a los 10s de silencio) pasó todos los tests unitarios y E2E — lo encontró el manual. Si un paso se rompe en producción, su sello deja de decir VERIFICADO.
`tools/manual-walkthrough/ · apps/web/tests/ · apps/api/src/**/*.test.ts`

### Anatomía de tres requests — el sistema en movimiento

La arquitectura estática es la mitad de la historia. Estos son los tres caminos que más enseñan, paso por paso:

*Flujo 1 · Ejecutar un SuperSkill (el ciclo completo)*

01. El founder toca "Ejecutar" en la library. El browser manda SOLO `{workflowId, input}` — nada más viaja.
02. El proxy de la app valida sesión + gate, valida el input contra el catálogo cerrado y construye el Intent completo server-side: `declared_by = human:<email>`. POST al motor con bearer.
03. El motor compila el Intent a un Plan (DAG tipado) y lo valida contra el catálogo; `assertHumanGatePresent` verifica estructuralmente el gate humano terminal. Si el plan no termina en aprobación humana, no existe ejecución posible.
04. Inngest camina el DAG: cada step despacha su operation, registra inputs/outputs snapshot, verdict del evaluator y costo en dólares. Si el proceso se reinicia a mitad, la ejecución continúa donde estaba.
05. El último step produce un Artifact `pending_review` y el workflow se SUSPENDE (`step.waitForEvent`, hasta 24h). No hay polling: el proceso duerme.
06. El founder aprueba con comentario en /outputs. El evento resume el workflow: artifact → `approved` y la decisión se materializa como Claims tipados (`approvedBy`, `approvalComment`…) que los próximos planes consultarán vía `claim.recall_decisions`. Si nadie decide en 24h: estado terminal `expired`, sin huérfanos.

*Flujo 2 · Una pregunta al chat, en streaming*

01. El drawer se abre con sesión vacía → hidrata el último hilo desde el motor (fail-soft: si no responde, el chat abre vacío e igual funciona). Enviar continúa el mismo `conversation_id`.
02. El motor persiste el mensaje del usuario, arma al agente EN ESE INSTANTE (persona + briefing + últimos trabajos con comentarios de aprobación + hilo) y lanza el CLI en modo stream.
03. El parser NDJSON re-emite SOLO `text_delta` como eventos SSE. nginx no bufferea (`proxy_buffering off`); la función de Vercel hace pipe del body sin acumular; el browser pinta cada delta en la burbuja.
04. La cicatriz didáctica: el default de Bun cortaba toda conexión muda >10s — y el modelo razona en silencio (sus thinking deltas se filtran). Preguntas cortas pasaban; las "de trabajo real" morían y degradaban al camino clásico. Fix: `idleTimeout: 120`. Lo cazó el manual vivo en producción.
05. El evento `done` trae la reply canónica; la burbuja transitoria desaparece y la reply se persiste — SOLO si el stream completó. No quedan respuestas a medias en el historial.

*Flujo 3 · Nova compone, el usuario lo guarda*

01. El pedido en lenguaje natural va al motor. UNA llamada al modelo decide: ¿match con un SuperSkill existente, plan nuevo, o "no puedo"? El match se valida contra la lista real del workspace — un id inventado no existe.
02. Si compone: el plan usa SOLO operations del catálogo y pasa las 4 capas de validación de Nova (existencia, sin ciclos, tope de steps, aprobación final). El costo estimado lo calcula el servidor desde el catálogo. El usuario ve los pasos en humano y el precio.
03. "Guardar como SuperSkill": el plan se re-valida y se inserta en el catálogo del workspace. Doble-guardado imposible: unique parcial en DB, la carrera la gana exactamente uno.
04. La card aparece en "Tus SuperSkills" — leída del motor, fail-soft. Nova la ve en pedidos futuros (≤20 customs en su prompt) y puede ofrecerla en vez de componer de cero.
05. Relanzar pasa por el MISMO helper que los planes de fábrica: `assertHumanGatePresent` — sin gate humano terminal, no hay launch. Las garantías no dependen de quién creó el plan.

### El modelo de datos — 15 tablas, cada una con su porqué

| Tabla | Qué guarda | El porqué |
|---|---|---|
| intents | Qué quiere lograr quién, en qué workspace | El punto de partida auditado de TODA ejecución; soporta árbol (parent_intent_id) |
| plans + steps + plan_edges | El DAG ejecutable compilado del Intent | Versionado por intent; cada step lleva operation_ref, retry policy, timeout y human_gate opcional |
| operations | El catálogo: metadatos de cada operación, versionados | Deprecar sin borrar; el plan referencia `id@version` |
| traces | Cada ejecución concreta de un plan (estado, costo real, verdict) | Particionada por mes; entrada al lineage |
| step_executions | Cada step ejecutado: inputs/outputs snapshot, error, verdict, costo | La auditoría fina; particionada igual que traces |
| claims | Hechos tipados (sujeto · predicado · objeto) + provenance + confidence + embedding 3072d | La memoria del equipo; recallable por SQL y por similitud (DiskANN) |
| artifacts | Lo producido (docs, videos, digests…) content-addressable (sha256), con status machine | pending_review → approved/rejected/expired; dedup por contenido |
| lineage_edges | Aristas de provenance entre claims y artifacts | Vista recursiva (profundidad 12, cycle guard): "¿de dónde salió esto?" |
| evaluators | Catálogo de cómo evaluar (deterministic / llm_judge / human) con threshold | Los verdicts por paso salen de acá |
| ontology_overlays | Relabels por vertical sobre el mismo grafo | Workspace A dice "video reel", workspace B "blueprint" — mismos primitives |
| workspace_manifests | El snapshot navegable por workspace (Regla A) | Cache derivado, reconstruible; columna manifest JSONB como SSOT |
| chat_messages | El hilo founder ↔ agente por conversación | El motor lee los últimos 20 para armar el prompt; la app hidrata al recargar |
| plan_drafts | Cada pedido a Nova con su desenlace (proposed/matched/rejected/launched) | Telemetría de demanda: los "no pude" priorizan el roadmap |
| superskills | Planes promovidos con nombre, por workspace | Unique parcial anti doble-promote; archive en vez de delete |
| + en InsForge (identidad, separada del substrato): `user_access` (gate fail-closed de beta privada) · `magic_links` (tokens one-time con consumo atómico) · `profile.app_state` (el estado del consumer) |  |  |

Detalle que un estudiante debería notar: `workspace_id` está en todas las tablas de *datos* del workspace desde la migración 0001 (intents, traces, claims, artifacts, lineage_edges, manifests, chat, drafts, superskills); las de definición (plans, steps) lo heredan vía su intent, y las de catálogo (operations, evaluators, overlays) son globales por diseño. El modelo de datos es multi-tenant desde el día uno — el aislamiento pendiente está en la capa de routing, no en los datos.

### Siete decisiones para robarse

### Las cifras as-built

Medidas contra el repo el **2026-09-03**, no estimadas: el catálogo se cuenta importando `OPERATION_CATALOG`, y los tests corriendo las suites. Cualquiera de estos números que quede viejo es un bug del documento.

| | |
|---|---|
| **73** | Operations |
| **23** | PlanTemplates |
| **12** | Evaluators |
| **77** | tablas (+32 particiones) |
| **13.776** | tests (13.333 unit + 443 E2E) |
| **34** | pasos verificados en prod |
| **~1.7GB** | RAM del motor (de 15) |
| **$0.013–0.022** | por digest (notional) |

*Lo que NO hay todavía — la otra mitad de la honestidad*

Routing multi-tenant (el modelo de datos está listo; la beta sirve un workspace con gate de founder) · alta disponibilidad y RTO formalizado (hay durabilidad de runs y backup nightly, no hay réplica ni drill de restore documentado) · alerting proactivo con SLOs (hay observabilidad, nadie despierta a las 3am) · regresión offline de evals (el human gate ya acumula el dataset etiquetado; falta el harness en CI). Las cuatro están asumidas, clasificadas y comprometidas — el detalle vive en el [red-team de arquitectura](comite-red-team.html).

**Para cerrar el caso de estudio.** La base de este sistema son marcas que cualquier estudiante reconoce: Postgres, nginx, Vercel, Cloudflare, Anthropic, Inngest. Lo custom es una capa fina, tipada y testeada. La pregunta que ordenó cada decisión no fue "¿qué tecnología está de moda?" sino *"¿qué cosa madura y barata cubre esto sin pintarnos en una esquina?"* — y la única apuesta original de verdad, la memoria estructurada de decisiones humanas, es exactamente la pieza que ningún framework vendía. El recorrido completo del producto, con cada paso verificado contra producción, vive en el [manual del operador](manual-agentsquad/).

*Capítulo trece · La Junta de Arquitectos responde*

## La Junta *responde*

Cuatro arquitectos reales de agentes de coding tomaron las preguntas que dejaron los estudiantes y un padre de familia, y las contestan desde su lente. **Boris Cherny** (Claude Code · Anthropic), **Alexander Embiricos** (Codex · OpenAI), **Harrison Chase** (LangChain/LangGraph) y **Charity Majors** (Honeycomb). Las citas marcadas ⚠ son recaps de charlas públicas (no transcripción literal); las ✓ son verbatim de fuente propia.

- **Boris Cherny** — do the simple thing 
 IRIS es andamiaje para un cuello de botella que este sistema no tiene. Espejar todo a RAM resuelve latencia de *alto tráfico sincrónico* — cientos de miles de usuarios, 10-15 tool calls por pregunta. La carga de Agent Squad es la opuesta: pocos workflows profundos y asíncronos, un digest tarda minutos por diseño. Cuando agregás una capa, preguntate si sobrevive al próximo modelo o si la estás poniendo porque el de hoy no llega. 
 ⚠ recap: "scaffolding gains get wiped out by the next model"
- **Charity Majors** — ¿qué se rompe a las 3am? 
 El costo que IRIS te *cobra* es el que más duele en producción: una segunda copia de tus datos por CDC es un segundo sistema que mantener consistente — y la consistencia entre dos stores es exactamente lo que falla un domingo a las 3am, sin que lo hayas anticipado. No operes lo que no necesitás operar.
- **Harrison Chase** — context engineering 
 IRIS es una solución de *contexto* para alto tráfico. Acá el contexto se controla por el catálogo y los claims tipados — sabés exactamente qué entra al LLM en cada paso, que es el problema difícil de verdad. Matiz honesto: si algún día aparece un agente sincrónico de alto tráfico, el patrón de IRIS cabe adentro de una operation. Hoy no. 
 ✓ blog: "make sure the LLM has the appropriate context at each step"
- **Alexander Embiricos** — tenías razón 
 La intuición es correcta y ya se actuó sobre ella: hay una conversación inicial donde el sistema descubre *qué trabajo* necesitás, propone las capacidades y deriva el equipo de ahí. El "3" era un default de presentación, nunca una restricción. Lo que sigue es tu segunda mitad — la última milla: que el agente produzca con más autonomía, no que el humano conduzca paso a paso. 
 ⚠ recap: "you're delegating to it rather than pairing with it"
- **Boris Cherny** — las personas siguen al catálogo 
 El squad nunca fue el centro del diseño. Las personas son `etiquetas de identidad`; la capacidad vive en el catálogo de operations. El planner compone desde el catálogo y nunca lee el squad — así que el número de personas *emerge del trabajo*, no se fija de antemano. Hardcodear el tamaño habría sido el error que el estudiante temía; el sistema lo evita por construcción.
- **Harrison Chase** — composición validada 
 Lo que ves no es hardcode ni autonomía total: es un router/state-machine que *genera* el plan desde lenguaje natural y lo *valida* contra un catálogo, con guardrails. El equipo es un subproducto de qué operations entraron al plan. Esa es la diferencia entre dibujar un flujo a mano y derivarlo.
- **Boris Cherny** Que entienda *el modelo* y cómo darle herramientas y un objetivo — no que se case con un framework de moda que el próximo modelo deja obsoleto. La habilidad durable es saber cuánto andamiaje poner y cuánto borrar.
- **Alexander Embiricos** Que aprenda a *dirigir y delegar* a agentes. El cuello de botella del futuro no es la capacidad del modelo, es la velocidad del humano para encargar y revisar. Quien sepa orquestar varios agentes a la vez rinde por diez.
- **Harrison Chase** Sigue siendo *ingeniería de sistemas*. El trabajo difícil de los agentes no es el modelo: es controlar qué información entra a cada paso. Fundamentos de sistemas + "context engineering" no caducan con el próximo release.
- **Charity Majors** Que aprenda a *operar* sistemas en producción — observabilidad, "¿cómo sé que esto funciona y cómo lo arreglo cuando falle?". Los sistemas con IA son no-deterministas; entender cómo se comportan cuando se rompen es una habilidad que la IA no automatiza pronto.
- **Charity Majors** — el mejor asiento 
 En el *anillo de verificación*: el robot que recorre el producto entero contra producción cada noche. Para verificar un solo paso tenés que entender TODAS las capas que lo atraviesan — pantalla, servidor, motor, base. Es el único asiento desde donde se ve el mapa completo, y con riesgo cero: usa usuarios de prueba que se autodestruyen. Test in prod, desde el día uno. 
 ✓ "Nines don't matter if users aren't happy"
- **Alexander Embiricos** El practicante *es* el caso de uso del sistema: un trabajador con energía y sin criterio formado todavía — igual que el modelo. Las mismas barandas que contienen a la IA lo contienen a él, así que puede hacer trabajo real desde la semana uno, que es la única forma de aprender.
- **Harrison Chase** Después del anillo, dale una *Operation propia* del catálogo: una pieza determinista de punta a punta. Ahí aprende a controlar exactamente qué entra y qué sale de un paso — el músculo central.
- **Primero, el dato** — qué es Hermes Agent (verificado) 
 Framework open-source de **Nous Research** (feb 2026): un agente *personal*, self-hosted, que vive en tus mensajeros (CLI, Telegram, Discord, Slack), recuerda entre sesiones y *se auto-mejora* — tras una tarea compleja escribe una "skill" reutilizable, sin pedir permiso — y se conecta a 200+ modelos. Sus barandas son de *infraestructura* (container hardening, root de solo lectura), no de aprobación de las acciones. La pregunta no es ociosa: es de las herramientas que más crecen en 2026. [hermes-agent.org](https://hermes-agent.org)
- **Alexander Embiricos** — es mi tesis al máximo 
 Hermes lleva la autonomía hasta el final: se auto-mejora, corre largo, saca al humano del loop. Para un asistente *personal* es exactamente lo que querés — el humano es el cuello de botella, quitalo. Agent Squad pone una compuerta humana a propósito, porque sus efectos caen sobre un *negocio ajeno*: un brief que se manda a un cliente, no un script que corrés en tu máquina. Si tu caso es personal y tolera autonomía, Hermes encaja. Si alguien tiene que firmar cada entregable, esa compuerta no es burocracia. 
 ⚠ recap: "the bottleneck is the human, not the model"
- **Charity Majors** — ¿dónde está la baranda? 
 La diferencia más honesta: Hermes pone la baranda en el *contenedor* (que el agente no te rompa la máquina); Agent Squad la pone en la *acción* (que no haga nada con efecto sin firma) y suma provenance y SLOs. Un agente que aprende solo y actúa solo en tus mensajeros: ¿cómo sabés qué aprendió, cómo lo auditás cuando hace algo raro a las 3am? Para uso personal, esa opacidad es *tu* riesgo — aceptable. Para trabajo que cae sobre clientes, no. 
 ✓ "Nines don't matter if users aren't happy"
- **Harrison Chase** — misma idea, otra governance 
 Hermes auto-crea skills (orquestación *emergente*): flexible para tareas personales variadas. Agent Squad deriva un grafo tipado validado contra un catálogo (orquestación *explícita*): controlable para flujos de negocio repetibles y auditables. Y un punto de humildad: el "closed learning loop" de Hermes y nuestros *SuperSkills promovidos* son la misma idea — capturar un procedimiento para reusarlo. La diferencia es quién aprieta el gatillo: Hermes lo hace solo; nosotros, con un humano que promueve. 
 ✓ blog: "control exactly what goes into the LLM at each step"
- **Boris Cherny** — apuestas opuestas 
 Hermes es model-agnostic (200+ modelos) y trae harness: memoria, learning loop, 40+ tools. Eso es opcionalidad y features — y también andamiaje que el próximo modelo puede volver innecesario. Agent Squad apuesta a un solo modelo frontera y pone andamiaje mínimo + barandas duras. Ninguna es "mejor": son la apuesta opuesta. La pregunta que importa no es cuál tiene más, sino cuál de las dos apuestas le sirve a *tu* problema. 
 ⚠ recap: "bet on the model, not the scaffolding"

*Capítulo catorce · La sustitución del motor*

## De dos motores a *uno*

Durante semanas convivieron dos autoridades sobre una corrida. Hoy no: producción corre v2 al 100%. Inngest conserva la durabilidad de eventos y crons; el avance del trabajo pertenece al motor event-sourced.

El motor original (v1) vivía en `inngest/functions/execute-plan.ts`. Recibía `plan.compiled`, caminaba el DAG de `plans/steps` y mantenía un workflow suspendido con `step.waitForEvent` cuando encontraba un human gate. Funcionó, dejó trazas útiles y probó la tesis. También concentró demasiada autoridad dentro de una función difícil de reconstruir, inspeccionar y reemplazar.

v2 cambió la unidad de verdad. El `compilador` convierte el template en un grafo versionado de `graph_nodes`. Una corrida vive en `runs`; lo que le ocurrió vive, en orden, en `run_events`. Un `reducer` puro decide transiciones y efectos; `aplicar-evento` los persiste transaccionalmente; el worker reclama leases de `work_items`, ejecuta operaciones y devuelve nuevos eventos. Estado derivado, log reproducible, cola explícita. Esa arquitectura ganó porque se puede explicar después de un crash, no solamente mientras todo está sano.

#### Motor v1 executor en deprecación

**Qué hacía.** `execute-plan` recorría el Plan, escribía steps y traces, reintentaba operaciones y suspendía el workflow en los gates con `step.waitForEvent`.

**Por qué sale.** Su estado efectivo estaba repartido entre el historial de Inngest y las proyecciones del substrato. Reanudar, reconstruir o cambiar el motor exigía entender una ejecución viva, no plegar un log estable.
`apps/api/src/inngest/functions/execute-plan.ts · v1, inalcanzable para lanzamientos nuevos`

#### Motor v2 100% de producción

**Qué hace.** Compila templates a grafos inmutables, registra cada transición como evento y mueve trabajo mediante una cola con leases. El reducer no hace I/O: declara. `aplicar-evento` persiste. El worker ejecuta.

**Qué conserva Inngest.** Entrada durable de eventos, crons, retries operativos y el tick que da cuerda al worker. Inngest sigue en el sistema; dejó de ser el lugar donde se esconde la máquina de estados de una corrida.
`apps/api/src/orquestacion/ · graph_nodes · runs · run_events · work_items`

#### Gate bifásico durable flow propio

**Ya no existe un `waitForEvent` sosteniendo el Plan.** El efecto `ACTIVAR_GATE` crea transaccionalmente un registro en `artifact_approval_flows`: el entregable aparece para revisión, la persona aprueba y, cuando el protocolo lo requiere, elige una opción; el executor acusa recibo y recién entonces el flow completa.

`consumirHandoffV2` aplica la decisión a la corrida y encadena los sucesores. El orden de locks es una invariante, no una recomendación: `runs → artifact_approval_flows → work_items`. Evita que aprobación, timeout y worker cierren el mismo gate en órdenes incompatibles.
`migraciones 0073–0075 · reducer.ts · aplicar-evento.ts · aprobar-gate.ts`

#### Timeout, autoridad y calidad fail closed

Cada flow congela `deadline_at`, política de fallback y `required_role`. El cron `barrido-gates-vencidos` reclama solamente el estado que observó mediante CAS: `state = fase AND deadline_at <= now()`. Una aprobación que ganó la carrera no puede ser pisada por el timeout.

El gate no es la única barrera. Un evaluator configurado como bloqueante emite un veredicto hard-stop: si falla, el downstream no se despacha. La corrida no convierte una mala salida en trabajo posterior solamente porque el worker siga vivo.
`artifact_approval_flows.deadline_at · barrido-gates-vencidos · evaluator gating`

W1 
 **v1 quedó inalcanzable.** `seleccionarMotor` nunca devuelve v1: una bandera distinta de `active + v2` falla cerrado. El handler y `launchCompiledPlan` tampoco hacen fallback. Una guarda de arquitectura exige cero emisores de `plan.compiled`. Si el executor viejo recibe uno, emite el canario `orquestacion.v1_zombie_awakened` y dispara alerta SLO. 
 prod · cerrado 
 

 
 W2 
 **El kill-switch pasó a decir la verdad.** `orchestration_state = active|paused` controla admisión y está separado de `orchestration_engine`. Pausar no cambia de motor. Además, `WORKER_ORQUESTACION` quedó encendido por default y está en `on` en producción. 
 prod · cerrado 
 

 
 W3 
 **v2 dejó de depender del archivo que vamos a borrar.** Los helpers compartidos salieron de `execute-plan.ts` hacia `substrate/`. El executor viejo ya no es una biblioteca accidental del motor nuevo. 
 prod · cerrado 
 

 
 → 
 **Lo que sigue es aburrido a propósito:** período de soak observando cero actividad v1, borrar `execute-plan.ts` y después limpiar eventos, columnas y tablas legacy del schema. Primero demostrar que el cadáver no respira; después retirarlo. 
 siguiente wave 
 
 

 D Las dos cicatrices 
 
 
 El escape hatch que era mentira 
 “Volver a v1” sonaba reversible, pero no lo era. v1 no podía reanudar una corrida v2, no se ejercitaba con tráfico real y ejecutaba el template, no el grafo compilado que v2 había autorizado. Cambiar la bandera no recuperaba una corrida: creaba otra interpretación del trabajo. 
 

 
 El worker apagado que parecía seguridad 
 Con dos motores, apagar el worker v2 podía parecer una precaución. Con v2 como único motor es un footgun: acepta corridas, deja `work_items` en `ready` y nada avanza. Por eso el default ahora es `on`; apagarlo exige una decisión explícita y observable. 
 
 

 Los tres resets operativos obedecen la misma conclusión: normalizan el ruteo a `orchestration_state='active'` y `orchestration_engine='v2'` sin tocar `enabled`. Resetear el progreso no revoca permiso ni resucita un motor retirado. 
 
 

 
 
 Próximo turno · agenda viva 
 Qué *sigue* 
 El hito grande ya cayó: **el motor v2 corre al 100% en producción** y v1 quedó inalcanzable, vigilado por canario. Lo que de verdad sigue son los **compromisos vivos** que ese cambio dejó abiertos, seguidos de los pendientes reordenados por riesgo y valor. Lista revisada el 2026-08-19. 

 Prioridad — compromisos vivos 
 
 A. Completar la **deprecación de v1**: soak con cero `orquestacion.v1_zombie_awakened`, borrar `execute-plan.ts` y después limpiar el schema legacy. Las tres waves de aislamiento ya están en producción.
B. Convertir la **eval offline** en evidencia longitudinal: repetirla sobre un dataset creciente hasta sostener accuracy ≥ 0.85 y falsos positivos ≈ 0. El harness y la primera señal buena existen; n=8 todavía no autoriza relajar el gate.
C. Volver a medir la **tasa de gates sin decisión** después del gate durable con `deadline_at` y barrido CAS. El timeout/fallback ya no es deuda técnica; si la tasa sigue alta, la deuda es de atención y UX.
01. Template ejecutable **email-triage** — side-effect externo solamente con idempotencia ante retries, observabilidad del envío y gate antes de actuar fuera del substrato. Hoy existe como promesa de producto, no como workflow productivo.
02. **Aislamiento multi-tenant real** cuando se active R1: identidad por caller, membership user↔workspace y evaluación de RLS. El IDOR puntual de approvals está cerrado; esto es la frontera siguiente, no el mismo bug.
✓. **Motor v2 al 100%** — compilador a graph_nodes, reducer puro, run_events y worker sobre work_items; v1 inalcanzable y vigilado por canario.
✓. **Gate bifásico durable** — artifact_approval_flows, ACTIVAR_GATE transaccional, handoff con ACK, required_role, deadline_at y barrido con CAS.
✓. **IDOR defensivo de /api/approvals** — artifact filtrado por workspace, mismatch 404 y regresión funcional cross-workspace.
✓. **pricing-watch productivo** — template, cron, endpoint, persistencia y publicación solamente ante cambio.
✓. **TTS + video reales** — ElevenLabs vía vo.generate, composición real con video.compose@2 y escenas Hyperframes.
✓. **Reset operativo seguro** — los tres resets normalizan active + v2 sin tocar enabled.

*El viaje continúa · Agosto 2026*

## Del *ruido* a la audiencia

El motor de research empezó midiendo demanda: qué se busca, qué se pregunta, qué hueco queda entre lo que la gente pide y lo que hay. Terminó entendiendo a la persona que mira: quién es, qué la mueve, qué la frena, y las palabras exactas de su dolor.

La investigación de mercado del substrato ya era profunda. Medía la **demanda compuesta** de cada oportunidad por tres dimensiones separadas, nunca una disfrazada de otra: la búsqueda del ángulo, el mercado del tema, y el respaldo social de Reddit y de los comentarios de YouTube, atribuido contra una tabla cerrada para que el modelo nunca escriba un número que no vio. Un pase dedicado ataba la evidencia, y un ranking por frentes de Pareto ordenaba las oportunidades sin sumar peras con manzanas.

Y entonces un fundador dudó de un cero. El informe decía que no había comentarios de valor en YouTube, y él no se lo creyó: *"yo manualmente encuentro casos que no son elogios"*. La duda, insistida cuatro veces, destapó capas reales. Primero, cuatro defectos en cómo el motor **descubría** los videos: buscaba consejos genéricos ("cómo crecer"), no los videos de problema ("por qué mi canal no crece") donde la audiencia pregunta. Y un hallazgo de fondo: el clasificador léxico no podía distinguir un problema de una curiosidad, porque la diferencia era semántica; la señal limpia estaba en otra capa, en la procedencia de la búsqueda, no en el título.

Pero lo mayor llegó con un video que el fundador trajo a mano: una masterclass de instructora, cuyos comentarios eran historias personales. Ahí el valor no era demanda. Era **inteligencia de audiencia**: el perfil de quién mira, sus motivaciones, sus barreras, y los huecos que revela contando su historia. El motor lo perseguía sin verlo. Así nació un pase nuevo, r14, que lee los comentarios que el research ya muestreó y extrae cinco cosas honestas contra una tabla cerrada: **perfil, motivaciones, barreras, huecos, y frases de dolor** como cita textual literal, lista para el guion.

| | |
|---|---|
| **3** | Waves nuevas |
| **5** | Idiomas leídos |
| **100** | Comentarios por corrida |
| **r14** | Pase de audiencia |
| **v10** | Template en prod |

En una corrida real de producción, en cinco idiomas, el informe escribió lo que antes se perdía: un creador de **70 años que usa IA como asistente**, uno que se identifica como **neurodiverso** y pierde el hilo si preproduce varios videos, uno que escribe en francés *"j'espère que je vais y arriver avant que la maladie m'emporte"*. Palabras reales de personas reales, escritas en el brief, listas para volverse contenido. El cero que el fundador no se creyó nunca fue el final: era la punta del hilo.

*Soberanía y guardián · Agosto 2026*

## El *guardián* entre el motor y producción

Un motor que evoluciona rápido necesita algo que le impida llevar un cambio roto hasta los usuarios. No confianza en la intención: un guardián que solo deja pasar lo que probó que funciona.

El substrato dejó de depender de terceros para su propia integración. El código y el CI se volvieron **soberanos**: viven en una caja propia, sin pedirle permiso a nadie. Y con esa mudanza llegó la pregunta que importa de verdad: cuando el motor cambia, ¿qué es lo que decide si ese cambio sale a producción o no?

La suite de pruebas de extremo a extremo llevaba días marcada como "en rojo" y fuera de esa decisión. Al mirarla de cerca apareció algo distinto: no estaba rota, llevaba **cinco noches seguidas en verde**. El semáforo mentía. Se corrigió lo que el comentario del gate declaraba, y quedó a la vista la deuda real, la única que faltaba pagar: que esa suite volviera a **frenar un deploy malo**, no solo a mirarlo pasar.

El hallazgo que ordenó todo fue este: no hay un despliegue, hay **dos**. El de la cara visible y el del motor, cada uno por su camino. Y no se validan igual. Las pruebas de extremo a extremo corren contra un simulador del substrato, así que dicen la verdad sobre la interfaz pero no sobre el motor, que tiene su propia suite. Tratarlos como uno solo habría hecho que un tropiezo de la cara frenara al motor sin motivo. Cada guardián cuida su propia puerta.

| | |
|---|---|
| **2** | Despliegues distintos |
| **6** | Shards de prueba |
| **5** | Noches en verde |
| **✓** | Mordida: rojo bloquea |
| **0** | Dependencia externa |

Con la puerta clara, la suite de la cara pasó a bloquear su deploy: las pruebas livianas de interfaz en cada cambio, y las pesadas de la escena viva cuando el cambio toca esa escena. El motor sigue su camino, validado por lo suyo. El substrato ya no solo se construye rápido: se publica sin que un cambio roto se cuele, y esa garantía está probada a mano, mordida por mordida.

Y el guardián se afinó dos veces más. Dejó de **exponer su llave**: el token con el que publica ya no viaja en la línea de comando, donde cualquier proceso del servidor podía leerlo, sino en el entorno, fuera de la vista. Y aprendió a **no congelarse**: una vez la herramienta de publicación se colgó en una llamada de red y se quedó clavada hasta el tope del turno, cuarenta y cinco minutos, hasta que hubo que matarla a mano. Ahora cada paso de publicación tiene un límite de tiempo duro y se reintenta solo, así un cuelgue se corta a los pocos minutos en vez de trabar todo. Un guardián que además no deja su llave a la vista ni se duerme en la puerta.

*El substrato, en tus manos · demo guiado*

## Asígnale una tarea a tu equipo y *míralo* trabajar

Tres actos contra el motor real. Le das un objetivo en lenguaje natural; el equipo lo planifica y lo ejecuta; Control de calidad lo revisa *antes* de entregarlo; tú apruebas o pides cambios. Después le pides algo que tu equipo todavía no sabe hacer — y lo construye.
