# Video Fábrica de Software · Agent Squad — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Producir video sales asset Agent Squad de ~8:30 minutos en 2 versiones (LinkedIn público + privado para cliente warm Ernesto), demostrando "fábrica de software 24/7" con caso real LPDI anonimizado quirúrgicamente.

**Architecture:** Pipeline de producción modular en 6 waves. Cada beat narrativo se produce como asset independiente (clip MP4 + audio + captions PNG). Composite final en ffmpeg con cube 3D transitions y captions burned-in bilingüe. Distribución dual: upload nativo LinkedIn + Cloudflare Stream signed URL para Ernesto.

**Tech Stack:**
- **Video AI:** HeyGen (Valeria avatar + Roberto Seedance digital twin)
- **Audio TTS:** ElevenLabs voz `TrPg245Ccy3ZVaJq7MO7` (Valeria)
- **Captions:** Whisper (transcripción ES word-level) + Claude (traducción EN) + PIL (render PNG burned-in) + ffmpeg (overlay)
- **Animations:** Hyperframes Mode E (HTML→MP4 determinístico) para hook + diagrama Squad + cube transitions
- **Composite:** ffmpeg filter_complex (scale, concat, overlay, drawtext, xfade)
- **Anonimización:** PIL/Pillow gaussian blur + reemplazos de texto en captures
- **Hosting:** Cloudflare Stream (account `38fdadff842eb9f9a18e97cb4739ef90`, auth Global API key + `contact@medicalhubassist.ai`)
- **Brand:** AI4Managers (obsidian #0D0D12, champagne #C9A84C, ivory #FAF8F5, slate #2A2A35; fonts Inter Black + Playfair Display Italic)

**Spec source:** `docs/superpowers/specs/2026-04-27-video-dogfooding-agent-squad-design.md`

**Working directory:** `~/playgrounds/agent-squad-fabrica/`

---

## Waves

| Wave | Tasks | Depende de | Parallelizable | Tema |
|------|-------|-----------|----------------|------|
| **0** | 1, 2, 3 | — | Sí | Setup, verificación assets, scripts finales |
| **1** | 4, 5, 6, 7 | Wave 0 | Sí | Generación clips presentadores + diagramas + hook |
| **2** | 8, 9 | Wave 1 | Sí | Captions ES/EN + composites de beats individuales |
| **3** | 10, 11 | Wave 2 | Sí | Master final por versión (LinkedIn / Ernesto) |
| **4** | 12, 13, 14 | Wave 3 | Sí | Audit anonimización + bonus piezas + Cloudflare Stream upload |
| **5** | 15 | Wave 4 | No | Validación final + delivery + página preview |

---

## File Structure

```
~/playgrounds/agent-squad-fabrica/
├── 00-source/
│   ├── scripts/
│   │   ├── beat1-hook-burned-in.txt
│   │   ├── beat2-roberto-saludo-linkedin.txt
│   │   ├── beat2-roberto-saludo-ernesto.txt
│   │   ├── beat3-valeria-dolor.txt
│   │   ├── beat4-valeria-tour-squad.txt
│   │   ├── beat5-valeria-timeline-lap256.txt
│   │   ├── beat6-valeria-iteracion-sistema.txt
│   │   ├── beat7-valeria-contraste.txt
│   │   ├── beat8-roberto-cierre-linkedin.txt
│   │   └── beat8-roberto-cierre-ernesto.txt
│   ├── seedance-prompts/  (4 prompts Roberto)
│   └── voices/            (audio TTS Valeria por beat)
├── 01-presentadores/
│   ├── roberto-saludo-linkedin.mp4
│   ├── roberto-saludo-ernesto.mp4
│   ├── roberto-cierre-linkedin.mp4
│   ├── roberto-cierre-ernesto.mp4
│   ├── valeria-beat3-dolor.mp4
│   ├── valeria-beat4-tour.mp4
│   ├── valeria-beat5-timeline.mp4
│   ├── valeria-beat6-iteracion.mp4
│   └── valeria-beat7-contraste.mp4
├── 02-hook-sin-avatar/
│   ├── hyperframes/       (HTML + assets)
│   └── hook-beat1.mp4
├── 03-diagrama-squad/
│   ├── hyperframes/
│   └── diagrama-squad.mp4
├── 04-captures-anonimizados/
│   ├── slack-pmo-thread.png
│   ├── slack-pmo-thread.raw.png
│   ├── linear-laps-genericos.png
│   ├── linear-laps-genericos.raw.png
│   ├── github-commits-tecnicos.png
│   ├── github-commits-tecnicos.raw.png
│   ├── vercel-deploys.png
│   ├── vercel-deploys.raw.png
│   └── testsprite-green.png
├── 05-captions/
│   ├── beat3-captions.json   (timing + ES + EN)
│   ├── beat3-captions/*.png  (rendered PNGs)
│   └── ... (un dir por beat con narración)
├── 06-music/
│   ├── bed-corporate.mp3
│   └── drop-stinger.mp3
├── 07-composite/
│   ├── beat1-hook-final.mp4       (hook + drop musical)
│   ├── beat2-saludo-linkedin.mp4  (Roberto + brand b-roll)
│   ├── beat2-saludo-ernesto.mp4
│   ├── beat3-dolor-final.mp4      (Valeria + captions burned-in)
│   ├── beat4-tour-final.mp4       (Valeria + diagrama PIP + captures)
│   ├── beat5-timeline-final.mp4   (Valeria + screen captures + captions)
│   ├── beat6-iteracion-final.mp4
│   ├── beat7-contraste-final.mp4
│   ├── beat8-cierre-linkedin.mp4
│   └── beat8-cierre-ernesto.mp4
├── 08-final/
│   ├── agent-squad-fabrica-LINKEDIN.mp4
│   └── agent-squad-fabrica-ERNESTO.mp4
├── 09-bonus/
│   ├── quote-card.png
│   ├── thumbnail-linkedin.png
│   ├── linkedin-post-copy.md
│   ├── ernesto-email.md
│   └── talking-point-frank.md
├── 10-audit/
│   ├── hyperframes-anonimization-audit/   (frame-by-frame)
│   └── audit-report.md
└── index.html  (preview review)
```

---

## Task 1: Setup directorio + verificar acceso a herramientas externas _(Wave 0)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/` (entire tree per File Structure)
- Create: `~/playgrounds/agent-squad-fabrica/00-source/preflight-check.md`

**Done when:**
- [ ] Tree de directorios completo creado
- [ ] HeyGen Valeria avatar verificado online (login + ver avatar disponible)
- [ ] HeyGen Roberto Seedance verificado eligible (`Avatar Shots` disponible para ese avatar)
- [ ] ElevenLabs voz `TrPg245Ccy3ZVaJq7MO7` responde a test 1-line ES
- [ ] Hyperframes CLI instalado: `hyperframes --version` retorna ≥0.4.6
- [ ] ffmpeg ≥6 disponible: `ffmpeg -version | head -1`
- [ ] Acceso Cloudflare Stream verificado: `curl -H "X-Auth-Email: contact@medicalhubassist.ai" -H "X-Auth-Key: $CF_GLOBAL_KEY" https://api.cloudflare.com/client/v4/accounts/38fdadff842eb9f9a18e97cb4739ef90/stream` retorna `success:true`
- [ ] `preflight-check.md` documenta status de cada herramienta

- [ ] **Step 1: Crear estructura de directorios**

```bash
mkdir -p ~/playgrounds/agent-squad-fabrica/{00-source/{scripts,seedance-prompts,voices},01-presentadores,02-hook-sin-avatar/hyperframes,03-diagrama-squad/hyperframes,04-captures-anonimizados,05-captions,06-music,07-composite,08-final,09-bonus,10-audit/hyperframes-anonimization-audit}
ls -la ~/playgrounds/agent-squad-fabrica/
```

Expected: 11 subdirectorios listados.

- [ ] **Step 2: Verificar HeyGen acceso (manual login + avatar status)**

Abrir `https://app.heygen.com/avatars` en browser → confirmar:
- Avatar "Valeria" (look ejecutivo boardroom) está disponible
- Avatar Roberto digital twin está disponible
- Roberto digital twin tiene badge `Avatar Shots` (= eligible Seedance)

Anotar status en `00-source/preflight-check.md`.

- [ ] **Step 3: Verificar ElevenLabs voz Valeria con call de prueba**

```bash
source ~/.env
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/TrPg245Ccy3ZVaJq7MO7?output_format=mp3_44100_128" \
  -H "xi-api-key: $ELEVENLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Prueba de voz Valeria.","model_id":"eleven_multilingual_v2"}' \
  -o /tmp/valeria-test.mp3
ffprobe /tmp/valeria-test.mp3 2>&1 | grep -E "Duration|bitrate"
```

Expected: archivo MP3 generado, duración 1-2 segundos, bitrate ~128k.

- [ ] **Step 4: Verificar Hyperframes**

```bash
hyperframes --version
which hyperframes
```

Expected: versión ≥0.4.6.

- [ ] **Step 5: Verificar ffmpeg**

```bash
ffmpeg -version | head -1
```

Expected: ffmpeg version ≥6.

- [ ] **Step 6: Verificar Cloudflare Stream API**

```bash
source ~/.env
curl -s -H "X-Auth-Email: contact@medicalhubassist.ai" -H "X-Auth-Key: $CF_GLOBAL_KEY" \
  "https://api.cloudflare.com/client/v4/accounts/38fdadff842eb9f9a18e97cb4739ef90/stream" | jq '.success'
```

Expected: `true`.

- [ ] **Step 7: Documentar preflight check**

Escribir en `00-source/preflight-check.md`:

```markdown
# Preflight Check — 2026-04-27

| Herramienta | Status | Notas |
|---|---|---|
| HeyGen Valeria avatar | ✅ / ❌ | (apuntar nombre exacto del avatar) |
| HeyGen Roberto Seedance | ✅ / ❌ | (apuntar avatar ID + Avatar Shots eligible) |
| ElevenLabs Valeria voice | ✅ / ❌ | (1-line test mp3 generado) |
| Hyperframes CLI | ✅ / ❌ | (versión) |
| ffmpeg | ✅ / ❌ | (versión) |
| Cloudflare Stream API | ✅ / ❌ | (success:true) |

## Bloqueadores
- [si alguno falla, listar acción]
```

- [ ] **Step 8: Si algún bloqueador → STOP y resolver antes de Wave 1**

Si HeyGen Valeria no disponible → fallback documentado (ElevenLabs + slideshow).
Si Roberto Seedance perdió eligibility → Roberto graba en cámara los 4 clips.
Si Cloudflare Stream falla → revisar credenciales en `~/.env`.

---

## Task 2: Capturar screenshots LADO SQUAD anonimizados _(Wave 0)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/slack-pmo-thread.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/slack-pmo-thread.raw.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/linear-laps-genericos.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/linear-laps-genericos.raw.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/github-commits-tecnicos.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/github-commits-tecnicos.raw.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/vercel-deploys.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/vercel-deploys.raw.png`
- Create: `~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/testsprite-green.png`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/anonymize.py` (script PIL)

**Done when:**
- [ ] 5 capturas RAW del LADO squad (Slack/Linear/GitHub/Vercel/TestSprite) tomadas
- [ ] Versiones anonimizadas existen para cada una (sufijo sin `.raw`)
- [ ] Audit visual: ninguna captura anonimizada contiene URL/logo/nombre cliente
- [ ] Script `anonymize.py` reproducible (toma RAW → genera anonimizada)
- [ ] Ningún archivo `*.raw.png` se va a publicar (solo se mantienen para reproducibilidad local)

- [ ] **Step 1: Capturar Slack PMO thread (RAW)**

Capturar el thread del PMO Agent en Slack mostrando:
- Mensaje del cliente reportando bug (este se VA a anonimizar)
- Respuesta del PMO Agent
- Mensajes del Architect/Dev/QA en la cadena
- 3 commits del PMO Agent posteados al canal

Guardar como `slack-pmo-thread.raw.png` (1920x1080 mín).

- [ ] **Step 2: Crear script anonymize.py base**

```python
# ~/playgrounds/agent-squad-fabrica/00-source/anonymize.py
"""Anonimiza capturas del lado squad reemplazando handles, URLs, nombres."""
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import sys, json
from pathlib import Path

REPLACEMENTS = {
    # Handles cliente → genéricos
    "frank.prieto": "client_lead",
    "@frank": "@client",
    "Frank Prieto": "Client Lead",
    "Frank": "Client",
    # Org / repo
    "devlapuntadeliceberg": "client-org",
    "lpdi-relacionamiento": "client-app",
    # URLs
    "eco.lpdi.co": "client.app",
    "sgr.lpdi.co": "client-legacy.app",
    "lpdi.co": "client-domain.app",
    # Channel
    "todo-swfacrtory-lapuntadeliceberg": "client-project-channel",
    # Nombres específicos LAPs reconocibles
    "PI↔FI": "feature autosave",
    "interest_industries": "field_array",
}

def anonymize_text_regions(raw_path: Path, regions_json: Path, out_path: Path):
    """regions_json define [{x,y,w,h,replacement_text}] para text overlays."""
    img = Image.open(raw_path).convert("RGB")
    draw = ImageDraw.Draw(img)
    regions = json.loads(regions_json.read_text())
    font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
    for r in regions:
        # Cubrir con rect blanco/oscuro según fondo
        bg = tuple(r.get("bg", [255, 255, 255]))
        fg = tuple(r.get("fg", [0, 0, 0]))
        draw.rectangle([r["x"], r["y"], r["x"]+r["w"], r["y"]+r["h"]], fill=bg)
        draw.text((r["x"]+4, r["y"]+4), r["text"], fill=fg, font=font)
    img.save(out_path, "PNG", optimize=True)
    print(f"✓ {out_path.name}")

if __name__ == "__main__":
    raw = Path(sys.argv[1])
    regions = Path(sys.argv[2])
    out = Path(sys.argv[3])
    anonymize_text_regions(raw, regions, out)
```

- [ ] **Step 3: Definir regions JSON para Slack PMO thread**

Inspeccionar manualmente `slack-pmo-thread.raw.png` y anotar coordenadas (x,y,w,h) de cada elemento a reemplazar:

```json
[
  {"x": 80, "y": 120, "w": 180, "h": 22, "text": "client_lead", "bg": [255,255,255], "fg": [29,28,29]},
  {"x": 80, "y": 240, "w": 220, "h": 22, "text": "Reportó issue en formulario", "bg": [255,255,255], "fg": [29,28,29]}
]
```

Guardar como `slack-pmo-thread.regions.json`.

- [ ] **Step 4: Anonimizar Slack y validar visualmente**

```bash
cd ~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/
python ../00-source/anonymize.py slack-pmo-thread.raw.png slack-pmo-thread.regions.json slack-pmo-thread.png
```

Abrir `slack-pmo-thread.png` y verificar:
- ❌ NO aparece nombre Frank / handle del cliente
- ❌ NO aparece URL eco.lpdi.co
- ❌ NO aparece "LPDI" ni "lapuntadeliceberg"
- ✅ SÍ aparece "PMO Agent" / "Architect" / "Dev" / "QA" / Roberto

Si falla cualquier check → ajustar regions.json y re-ejecutar.

- [ ] **Step 5: Capturar y anonimizar Linear**

Capturar Linear de proyecto LPDI mostrando 3-5 LAPs (incluido LAP-256 / LAP-262). Editar el TÍTULO de cada LAP en una copia (rename a algo genérico técnico):
- LAP-256 PI↔FI bug fixes → "Fix race condition en autosave de formulario"
- LAP-262 → "Fix navegación legacy de perfil"

Guardar `linear-laps-genericos.raw.png` y aplicar `anonymize.py` con regions correspondientes.

- [ ] **Step 6: Capturar GitHub commits**

Capturar 3-5 commits del proyecto LPDI mostrando commit messages técnicos (no requieren cambios) pero blureando:
- Org name en breadcrumb
- Repo name en breadcrumb
- Avatar del autor (si tiene foto identificable)

Aplicar PIL gaussian blur a regions específicas en lugar de reemplazo de texto:

```python
# Variante para blur regions
def blur_regions(raw_path, regions, out_path):
    img = Image.open(raw_path).convert("RGB")
    for r in regions:
        crop = img.crop((r["x"], r["y"], r["x"]+r["w"], r["y"]+r["h"]))
        crop = crop.filter(ImageFilter.GaussianBlur(radius=8))
        img.paste(crop, (r["x"], r["y"]))
    img.save(out_path, "PNG")
```

- [ ] **Step 7: Capturar Vercel deploys**

Capturar Vercel dashboard mostrando 3-5 deploys "READY" con timing visible. Anonimizar:
- Project name (blur o reemplazo)
- Domain (`*.vercel.app` blur)
- Git commit hashes pueden quedar (no identificables)

- [ ] **Step 8: Capturar TestSprite resultados verde**

Capturar TestSprite dashboard con tests E2E green (LAP-261/260/259 batch validó esto el 2026-04-23). NO requiere anonimización pesada — TestSprite no muestra cliente. Solo verificar que no aparece dominio cliente en URLs testeadas (si aparece → blur).

- [ ] **Step 9: Audit final visual**

Para cada `*.png` (no `.raw`), abrir y verificar contra checklist:

```
[ ] slack-pmo-thread.png  — sin Frank/lpdi/eco
[ ] linear-laps-genericos.png — títulos genéricos, workspace logo blureado
[ ] github-commits-tecnicos.png — org/repo blureado, mensajes técnicos OK
[ ] vercel-deploys.png — project/domain blureado
[ ] testsprite-green.png — sin URL cliente
```

- [ ] **Step 10: NO commit los .raw**

```bash
echo "*.raw.png" > ~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/.gitignore
echo "*.regions.json" >> ~/playgrounds/agent-squad-fabrica/04-captures-anonimizados/.gitignore
```

Los RAW solo viven local (riesgo de leak). Solo se publican / mantienen los anonimizados.

---

## Task 3: Finalizar scripts palabra-por-palabra de los 9 segmentos hablados _(Wave 0)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat1-hook-burned-in.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat2-roberto-saludo-linkedin.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat2-roberto-saludo-ernesto.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat3-valeria-dolor.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat4-valeria-tour-squad.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat5-valeria-timeline-lap256.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat6-valeria-iteracion-sistema.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat7-valeria-contraste.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat8-roberto-cierre-linkedin.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/scripts/beat8-roberto-cierre-ernesto.txt`

**Done when:**
- [ ] 10 archivos `.txt` creados con scripts FINALES (no indicativos)
- [ ] Cada script en español neutro (no modismos cerrados Argentina/México/Colombia)
- [ ] Cada script con duración estimada calculable (~150 palabras/min hablando lento ejecutivo)
- [ ] Beat 5 (timeline LAP-256) usa timestamps RELATIVE ("minuto 0 / +4 / +16 / +21"), no absolutos
- [ ] Beat 6 (iteración) habla en abstracto ("hace poco un agente..."), sin atar a fecha/cliente
- [ ] Beat 7 incluye frase central exacta del título: "La velocidad de tu producto ya no la define el tamaño de tu equipo. La define la calidad de tus agentes."
- [ ] CTA exactos en beats 8: "comentá SQUAD" (LinkedIn) / "30 minutos de tu agenda" (Ernesto)

- [ ] **Step 1: Beat 1 — texto burned-in del hook (sin script hablado)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat1-hook-burned-in.txt <<'EOF'
[Texto burned-in centrado superior, Inter Black 80pt champagne, fade-in 0:00 a 0:15]

En 17 minutos se hicieron 3 fixes a producción; Sin intervención humana.

[Animación visual: timestamp counter Slack saltando entre +0 / +4 / +16 / +21 + contador "00:17:00" pulsando + drop musical en 0:00-0:03]
EOF
```

- [ ] **Step 2: Beat 2 — Roberto saludo LinkedIn (~25s, ~75 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat2-roberto-saludo-linkedin.txt <<'EOF'
[Roberto HeyGen, boardroom AI4M, mirando a cámara]

Hola, soy Roberto. Lo que vas a ver son 17 minutos reales de un proyecto en producción. Mi cliente reportó un bug. Mi squad lo arregló y lo deployó a producción antes que yo respondiera al Slack. Cero intervención mía en tiempo real. Te dejo con Valeria, te lo cuenta paso a paso.

[Duración hablada estimada: 25-28 segundos a ritmo ejecutivo]
EOF
```

- [ ] **Step 3: Beat 2 — Roberto saludo Ernesto (~28s, ~80 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat2-roberto-saludo-ernesto.txt <<'EOF'
[Roberto HeyGen, boardroom AI4M, mirando a cámara]

Ernesto, te grabé esto porque la semana pasada me preguntaste cómo funciona el squad cuando hay un bug en producción. Lo que vas a ver es timeline real anonimizado de un cliente parecido al tuyo: marketplace LATAM, equipo dev chico, backlog grande. Te dejo con Valeria. Te lo cuenta paso a paso.

[Duración hablada estimada: 28-30 segundos]
EOF
```

- [ ] **Step 4: Beat 3 — Valeria setup del dolor (~45s, ~135 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat3-valeria-dolor.txt <<'EOF'
[Valeria HeyGen, boardroom AI4M, narradora ejecutiva]

Software complejo requiere equipo. Cinco personas mínimo. Quince mil dólares al mes en nómina. Dos semanas por iteración desde que el founder pide algo hasta que el usuario lo ve. Y mientras tanto, el founder atrapado siendo project manager en lugar de vender. Cada feature que tu competidor saca, vos vas tres semanas atrás. Cada cliente enterprise que pide un ajuste, lo perdés porque tu roadmap ya está comprometido. El costo no es solo la nómina. Es el costo de oportunidad de no poder responder. Hasta que cambiás de modelo: dejás de armar equipos, empezás a armar squads.

[Duración hablada estimada: 45-50 segundos]
EOF
```

- [ ] **Step 5: Beat 4 — Valeria tour del Squad + guardrails (~60s, ~180 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat4-valeria-tour-squad.txt <<'EOF'
[Valeria HeyGen + diagrama Squad animado en split-screen]

Esto es Agent Squad. En el centro, el PMO, un agente Opus que coordina. Recibe el ticket, identifica dependencias, asigna trabajo. Tres satélites alrededor: el Architect, también Opus, diseña la arquitectura del cambio. El Dev, un Sonnet optimizado para implementación, escribe el código. El QA, otro Sonnet especializado en testing, valida con TestSprite end-to-end. Y debajo de todo, una capa de guardrails: ningún agente cierra un ticket sin evidencia QA verde y aprobación explícita del cliente. Cada agente tiene contexto persistente del proyecto, memoria de las decisiones anteriores, y permisos controlados sobre el repositorio. Esto no es un asistente. Es un equipo.

[Duración hablada estimada: 60-65 segundos]
[Visual: 30s diagrama Squad + 30s screenshots PMO bot Slack / Linear LAPs / GitHub commits / TestSprite green]
EOF
```

- [ ] **Step 6: Beat 5 — Valeria timeline LAP-256 (~3 min, ~540 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat5-valeria-timeline-lap256.txt <<'EOF'
[Valeria HeyGen narradora + screen recordings anonimizados sincronizados]

Minuto cero. El cliente manda un mensaje al canal de Slack del proyecto. Reporta un bug en el formulario de registro. Algunos campos no se están guardando correctamente. Cuatro bugs distintos en cascada, todos en el mismo flujo. Lo necesita resuelto rápido porque está bloqueando demos con usuarios reales. Roberto está en standby — fuera del teclado, en otra reunión. El squad recibe el mensaje.

Minuto cuatro. El PMO ya leyó el thread, identificó que son cuatro bugs separados pero relacionados, abrió tickets en Linear con la descomposición correcta, y commiteó el primer fix al repositorio. Cuatro minutos. Sin pedirle nada a Roberto.

[Cut a screen capture: Slack PMO bot mostrando confirmación + GitHub commit message anonimizado]

Lo que pasó adentro en esos cuatro minutos es lo que importa. El PMO leyó el contexto del proyecto, identificó que el bug primario era de tipo de columna en la base de datos. Abrió el ticket. El Architect generó un plan de fix con migración SQL incluida. El Dev escribió el código siguiendo los patrones del repo. El QA corrió tests unitarios automáticos. Cuatro minutos.

Minuto dieciséis. Llega el segundo commit. El PMO ya identificó que el segundo bug era distinto al primero — un campo se estaba autosaveando con un nombre que no coincidía con el schema de la tabla. Otro fix limpio, otra migración, otro deploy.

[Cut: Linear ticket con título genérico técnico + GitHub commit]

Minuto veintiuno. Tercer commit. El squad detectó que había un patrón común detrás de los cuatro bugs reportados: una refactorización previa había dejado tres campos con tipos inconsistentes. El PMO consolidó la solución en un solo commit que arregla los tres campos restantes de una vez. Veintiún minutos desde el mensaje original del cliente.

Minuto veintitrés. Vercel reporta los tres deploys en estado READY. El sistema en producción ya está corregido. El cliente tiene su flujo funcionando.

[Cut: Vercel dashboard con 3 deploys READY + TestSprite green]

Roberto sigue en standby. Cuando vuelve al Slack veintitrés minutos después del mensaje original, encuentra un thread con cuatro mensajes del PMO Agent: identificación de los bugs, plan, ejecución, y confirmación de deploys. Los tres tickets en Linear están cerrados con evidencia. Los tres commits están en main con mensajes técnicos descriptivos. Los tres deploys están READY en Vercel.

Roberto no escribió una sola línea de código. No tomó una sola decisión técnica. No revisó un solo pull request. Solo apareció veintitrés minutos después al canal del cliente con un único mensaje: "ya está deployado, abrime un Loom si querés ver el flujo arreglado".

Esto es Agent Squad funcionando como fábrica de software. Veintiún minutos desde el reporte hasta el último deploy. Tres bugs resueltos. Cero intervención humana en tiempo real. Y los guardrails corriendo en el fondo asegurándose de que cada paso pase por evidencia QA antes de tocar producción.

[Duración hablada estimada: 3:00-3:10 minutos]
EOF
```

- [ ] **Step 7: Beat 6 — Valeria iteración del sistema (~45s, ~135 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat6-valeria-iteracion-sistema.txt <<'EOF'
[Valeria HeyGen + cut a código del gate dual-close]

Pero acá está la parte que no suelen contar en estos videos. El squad también comete errores. Hace poco, uno de los agentes tomó una acción que no debería haber tomado: cerró un ticket sin la autorización explícita del cliente. ¿La solución? No fue desconfiar del sistema. Fue agregar un guardrail. Un cierre dual-gate: ahora ningún agente puede cerrar un ticket sin dos cosas: evidencia QA verde Y aprobación explícita del cliente en el comentario del ticket. Eso también es parte del trabajo. Armar guardrails cuando el sistema te muestra dónde puede fallar. El squad mejora cada semana porque cada error se convierte en código nuevo del sistema.

[Duración hablada estimada: 45-50 segundos]
EOF
```

- [ ] **Step 8: Beat 7 — Valeria contraste antes/después + frase central (~60s, ~180 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat7-valeria-contraste.txt <<'EOF'
[Valeria HeyGen + split-screen animation comparativa]

Vamos a mirar los números. Antes: tres a cinco desarrolladores en nómina. Quince mil dólares al mes mínimo. Dos semanas promedio por feature desde el pedido hasta producción. Dos iteraciones máximo por sprint antes de saturar al equipo. Backlog que no para de crecer.

Después: un squad de agentes corriendo veinticuatro siete. Costo en tokens de API menos de quince dólares por feature compleja. Tiempo promedio medido en horas, no en semanas. Iteraciones ilimitadas porque es código, no es gente cansada. El equipo humano que queda, los que valen su peso en oro, ahora hacen arquitectura y producto. No hacen tickets.

La pregunta no es si esto te ahorra plata. Te ahorra catorce mil dólares al mes en nómina. La pregunta es qué hacés con la velocidad. Y acá está la frase que quiero que te lleves de este video:

[Énfasis dramático, pausa]

La velocidad de tu producto ya no la define el tamaño de tu equipo. La define la calidad de tus agentes.

[Duración hablada estimada: 60-65 segundos]
EOF
```

- [ ] **Step 9: Beat 8 — Roberto cierre LinkedIn (~50s, ~150 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat8-roberto-cierre-linkedin.txt <<'EOF'
[Roberto HeyGen, boardroom AI4M, tono cercano-cierre]

Lo que acabás de ver es Agent Squad funcionando en un proyecto real. Mismo stack que probablemente tenés vos: SvelteKit o Next, Vercel, Supabase, tu sistema de tickets. No es un experimento. Es el flujo que aplico yo, todos los días, en proyectos de clientes.

Si querés ver cómo funcionaría en TU caso, hacé una sola cosa: comentá la palabra SQUAD acá abajo, en este post. Te mando en privado tres cosas: una demo de quince minutos del squad corriendo en vivo sobre un caso parecido al tuyo, el caso documentado completo de este video con timeline y comandos exactos, y una calculadora de ROI versus equipo humano para tu volumen.

SQUAD, en los comentarios. Te leo.

[Duración hablada estimada: 50-55 segundos]
EOF
```

- [ ] **Step 10: Beat 8 — Roberto cierre Ernesto (~55s, ~165 palabras)**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/scripts/beat8-roberto-cierre-ernesto.txt <<'EOF'
[Roberto HeyGen, boardroom AI4M, tono cercano-cierre, mirando a cámara directo]

Ernesto, lo que viste podría ser tu próximo trimestre. Tu marketplace, tu stack, tu backlog real. Te pido una sola cosa: treinta minutos de tu agenda esta semana. No es para venderte nada. Es para sentarme con vos, abrir tu Linear conmigo, y decirte exactamente qué tickets de los que tenés podríamos estar shippeando este mes con squad. Sin presión, sin propuesta en PDF, sin slides.

Si después de esos treinta minutos no ves el ROI claro, te quedás con el análisis y seguimos como amigos. Mandame tres horarios que te funcionen y yo me adapto a vos. Esta semana. Treinta minutos. Es todo.

[Duración hablada estimada: 55-60 segundos]
EOF
```

- [ ] **Step 11: Validar duración total estimada**

```bash
# Conteo de palabras de cada script Valeria + Roberto (audio hablado)
cd ~/playgrounds/agent-squad-fabrica/00-source/scripts/
for f in beat2*.txt beat3*.txt beat4*.txt beat5*.txt beat6*.txt beat7*.txt beat8*.txt; do
  words=$(grep -v '^\[' "$f" | wc -w)
  est=$(echo "scale=1; $words / 2.5" | bc)  # ~150 palabras/min = 2.5 palabras/s
  echo "$f: $words palabras = ~${est}s"
done
```

Expected total: ~7-8 minutos de audio hablado (los beats narrados).
Hook beat 1 = 15s sin audio (solo música + texto burned-in).
Total video: ~8:00-8:30.

Si pasa de 8:45 → trim algunos beats.

---

## Task 4: Generar 4 clips Roberto via HeyGen Seedance _(Wave 1)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/seedance-prompts/roberto-saludo-linkedin.prompt.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/seedance-prompts/roberto-saludo-ernesto.prompt.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/seedance-prompts/roberto-cierre-linkedin.prompt.txt`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/seedance-prompts/roberto-cierre-ernesto.prompt.txt`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/roberto-saludo-linkedin.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/roberto-saludo-ernesto.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/roberto-cierre-linkedin.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/roberto-cierre-ernesto.mp4`

**Done when:**
- [ ] 4 prompts Seedance escritos siguiendo template validado en proyecto AI4M (con CRITICAL VERBATIM SCRIPT REQUIREMENT clause)
- [ ] 4 clips MP4 generados desde HeyGen Avatar Shots
- [ ] Cada clip dura entre lo esperado ±2s del estimado del Step 11 Task 3
- [ ] Lip-sync visualmente correcto: `ffprobe` confirma audio + video sincronizados
- [ ] Cada clip 1080p, 30fps, MP4 H.264, AAC audio, ≤80MB

- [ ] **Step 1: Generar prompt Seedance Roberto saludo LinkedIn**

Template validado de proyecto AI4M (memoria reference_heygen_seedance.md). Crear:

```
~/playgrounds/agent-squad-fabrica/00-source/seedance-prompts/roberto-saludo-linkedin.prompt.txt
```

Contenido (template + script verbatim):

```
CRITICAL — VERBATIM SCRIPT REQUIREMENT: Roberto must speak the EXACT Spanish text below word-for-word, character-for-character. Do NOT paraphrase. Do NOT rephrase. Do NOT add words. Do NOT remove words. Do NOT translate. Do NOT change punctuation.

Setting: Roberto in AI4Managers boardroom — modern executive office, soft warm lighting, walnut wood and obsidian palette, slight depth of field, looking directly at camera. Confident but warm tone. Mid-shot, head and upper torso visible.

[0s–28s]: Roberto speaks this exact verbatim line in Spanish: "Hola, soy Roberto. Lo que vas a ver son 17 minutos reales de un proyecto en producción. Mi cliente reportó un bug. Mi squad lo arregló y lo deployó a producción antes que yo respondiera al Slack. Cero intervención mía en tiempo real. Te dejo con Valeria, te lo cuenta paso a paso."

=== EXACT VERBATIM SCRIPT (must be spoken exactly as written, no changes) ===
"Hola, soy Roberto. Lo que vas a ver son 17 minutos reales de un proyecto en producción. Mi cliente reportó un bug. Mi squad lo arregló y lo deployó a producción antes que yo respondiera al Slack. Cero intervención mía en tiempo real. Te dejo con Valeria, te lo cuenta paso a paso."
=== END SCRIPT ===

Camera: static mid-shot, no movement.
Mood: confident, warm, decisive.
Audio: Roberto's natural voice, Spanish neutral.
```

- [ ] **Step 2: Repetir Step 1 para los otros 3 prompts** (saludo Ernesto + cierre LinkedIn + cierre Ernesto)

Mismo template, cambiando el VERBATIM SCRIPT por el contenido de los respectivos `beat2-roberto-saludo-ernesto.txt`, `beat8-roberto-cierre-linkedin.txt`, `beat8-roberto-cierre-ernesto.txt`.

- [ ] **Step 3: Ejecutar Avatar Shot saludo LinkedIn en HeyGen**

Manual UI workflow:
1. HeyGen → Avatar Shots → "New shot"
2. Avatar: Roberto digital twin
3. Pegar prompt completo del Step 1
4. Aspect ratio: 16:9
5. Resolution: 1080p
6. Generate

Esperar generación (~5-15 min según cola). Descargar MP4 → guardar en `01-presentadores/roberto-saludo-linkedin.mp4`.

- [ ] **Step 4: Validar lip sync + duración**

```bash
ffprobe -v quiet -print_format json -show_streams ~/playgrounds/agent-squad-fabrica/01-presentadores/roberto-saludo-linkedin.mp4 | jq '.streams[] | {codec_type, duration, width, height, sample_rate}'
```

Expected: video 1080p (1920x1080) ~28s, audio 44100Hz.

Reproducir el clip y verificar:
- ✅ Roberto pronuncia EXACTAMENTE el script (no paraphrasing)
- ✅ Lip sync visual correcto (no desfasado)
- ✅ Mood consistent con boardroom AI4M
- ❌ Si Seedance cambió palabras → re-generate con énfasis en CRITICAL VERBATIM clause

- [ ] **Step 5: Repetir Steps 3-4 para los 3 clips restantes**

Generar saludo Ernesto, cierre LinkedIn, cierre Ernesto. Validar cada uno.

- [ ] **Step 6: Si algún clip falla validación verbatim**

Opciones:
1. Re-generate con prompt revisado (más énfasis en CRITICAL clause)
2. Si falla 2x → fallback: Roberto graba el clip en cámara real con audio claro (~2 min de grabación, sirve)

---

## Task 5: Generar audio Valeria via ElevenLabs + clips video Valeria via HeyGen lip sync _(Wave 1)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/voices/valeria-beat3.mp3`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/voices/valeria-beat4.mp3`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/voices/valeria-beat5.mp3`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/voices/valeria-beat6.mp3`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/voices/valeria-beat7.mp3`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-beat3-dolor.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-beat4-tour.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-beat5-timeline.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-beat6-iteracion.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-beat7-contraste.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/generate-voices.sh`

**Done when:**
- [ ] 5 archivos MP3 Valeria generados (beats 3, 4, 5, 6, 7)
- [ ] Cada MP3 dura entre estimado ±10% (de Step 11 Task 3)
- [ ] 5 archivos MP4 Valeria con lip sync HeyGen + audio ElevenLabs sincronizado
- [ ] Cada MP4 1080p, 30fps, audio sync verificado por reproducción

- [ ] **Step 1: Crear script generate-voices.sh**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/generate-voices.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
source ~/.env

VOICE_ID="TrPg245Ccy3ZVaJq7MO7"
SCRIPTS_DIR="$HOME/playgrounds/agent-squad-fabrica/00-source/scripts"
OUT_DIR="$HOME/playgrounds/agent-squad-fabrica/00-source/voices"

generate() {
  local beat="$1"
  local script_file="$SCRIPTS_DIR/$beat.txt"
  local out_file="$OUT_DIR/valeria-${beat}.mp3"

  # Extract spoken text only (skip [stage directions] in brackets)
  local text=$(grep -v '^\[' "$script_file" | grep -v '^$')

  echo "→ Generating $out_file ($(echo "$text" | wc -w) words)"
  curl -sS -X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}?output_format=mp3_44100_128" \
    -H "xi-api-key: $ELEVENLABS_API_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg t "$text" '{text:$t,model_id:"eleven_multilingual_v2",voice_settings:{stability:0.5,similarity_boost:0.75,style:0.0,use_speaker_boost:true}}')" \
    -o "$out_file"

  ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$out_file" | awk '{printf "  ✓ duration: %.1fs\n", $1}'
}

generate "beat3-valeria-dolor"
generate "beat4-valeria-tour-squad"
generate "beat5-valeria-timeline-lap256"
generate "beat6-valeria-iteracion-sistema"
generate "beat7-valeria-contraste"

echo "Done. 5 MP3s en $OUT_DIR"
EOF

chmod +x ~/playgrounds/agent-squad-fabrica/00-source/generate-voices.sh
```

- [ ] **Step 2: Ejecutar script de generación**

```bash
~/playgrounds/agent-squad-fabrica/00-source/generate-voices.sh
ls -lh ~/playgrounds/agent-squad-fabrica/00-source/voices/
```

Expected: 5 MP3s, cada uno entre 200KB y 4MB. Duraciones cercanas al estimado del Task 3 Step 11.

- [ ] **Step 3: Subir cada MP3 a HeyGen y generar lip sync con Valeria avatar**

Para cada beat (3, 4, 5, 6, 7):
1. HeyGen → Photo Avatar / Talking Photo → Valeria avatar (look ejecutivo boardroom)
2. Audio source: upload `valeria-beatN.mp3`
3. Aspect ratio: 16:9
4. Resolution: 1080p
5. Background: composite con el boardroom AI4M (look ya armado en `/playgrounds/valeria-final/`)
6. Generate

Descargar cada MP4 → guardar en `01-presentadores/valeria-beatN-*.mp4` con nombres del bloque Files.

- [ ] **Step 4: Validar lip sync de cada clip Valeria**

```bash
for clip in ~/playgrounds/agent-squad-fabrica/01-presentadores/valeria-*.mp4; do
  echo "=== $clip ==="
  ffprobe -v quiet -print_format json -show_streams "$clip" | jq '.streams[] | {codec_type, duration, width, height}'
done
```

Reproducir cada clip y verificar:
- ✅ Lip sync visualmente correcto (HeyGen ya lo asegura, pero confirmar)
- ✅ Audio Valeria con voz ElevenLabs nítida (no glitches)
- ✅ Boardroom AI4M se ve composite correctamente
- ✅ Duración matches el MP3 source ±0.1s

- [ ] **Step 5: Si algún clip Valeria tiene mismatch audio/video**

Opciones:
1. Re-generar lip sync en HeyGen con el mismo MP3
2. Si HeyGen recorta el final → padding del MP3 con `ffmpeg -i in.mp3 -af "apad=pad_dur=1" out.mp3` antes de subir

---

## Task 6: Build diagrama Squad animado con Hyperframes _(Wave 1)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/03-diagrama-squad/hyperframes/index.html`
- Create: `~/playgrounds/agent-squad-fabrica/03-diagrama-squad/hyperframes/styles.css`
- Create: `~/playgrounds/agent-squad-fabrica/03-diagrama-squad/diagrama-squad.mp4`

**Done when:**
- [ ] HTML+CSS renderiza diagrama Squad: PMO centro + Architect/Dev/QA satélites + TestSprite/Guardrails capa + Slack/Linear/GitHub/Vercel rail
- [ ] Animación: nodos aparecen en cascada (stagger 0.2s), líneas con pulse luminoso entre cajas
- [ ] Brand AI4M aplicado: obsidian background, champagne accents, ivory text, slate panels
- [ ] Hyperframes render produce MP4 de 30s 1920x1080 30fps
- [ ] Output sin scroll bars, sin elementos parpadeando incorrectamente

- [ ] **Step 1: Inicializar proyecto Hyperframes**

```bash
cd ~/playgrounds/agent-squad-fabrica/03-diagrama-squad/hyperframes/
hyperframes init
```

Expected: scaffold inicial con `index.html`, `package.json`, `hyperframes.config.json`.

- [ ] **Step 2: Configurar hyperframes.config.json**

```json
{
  "mode": "E",
  "duration": 30,
  "fps": 30,
  "width": 1920,
  "height": 1080,
  "output": "../diagrama-squad.mp4",
  "background": "#0D0D12"
}
```

- [ ] **Step 3: Diseñar HTML del diagrama**

```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600;700;900&display=swap" rel="stylesheet">
</head>
<body>
<div class="stage">

  <!-- PMO centro -->
  <div class="node pmo" data-fade-in="0">
    <div class="role">PMO</div>
    <div class="model">Opus 4.7</div>
    <div class="tag">Orquestador</div>
  </div>

  <!-- Satélites -->
  <div class="node sat sat-arch" data-fade-in="0.4">
    <div class="role">Architect</div>
    <div class="model">Opus</div>
  </div>
  <div class="node sat sat-dev" data-fade-in="0.6">
    <div class="role">Dev</div>
    <div class="model">Sonnet</div>
  </div>
  <div class="node sat sat-qa" data-fade-in="0.8">
    <div class="role">QA</div>
    <div class="model">Sonnet</div>
  </div>

  <!-- Líneas pulse PMO ↔ satélites -->
  <svg class="links" viewBox="0 0 1920 1080">
    <line x1="960" y1="540" x2="540" y2="320" data-pulse-start="1.2"/>
    <line x1="960" y1="540" x2="1380" y2="320" data-pulse-start="1.4"/>
    <line x1="960" y1="540" x2="540" y2="760" data-pulse-start="1.6"/>
  </svg>

  <!-- Capa de validación (TestSprite + Guardrails) -->
  <div class="layer validation" data-fade-in="2.0">
    <div class="layer-label">Capa de validación</div>
    <div class="layer-pill">TestSprite E2E</div>
    <div class="layer-pill">Guardrails dual-gate</div>
  </div>

  <!-- Output rail (Slack/Linear/GitHub/Vercel) -->
  <div class="rail" data-fade-in="3.0">
    <div class="rail-pill">Slack</div>
    <div class="rail-pill">Linear</div>
    <div class="rail-pill">GitHub</div>
    <div class="rail-pill">Vercel</div>
  </div>

  <div class="caption" data-fade-in="4.0">
    Anatomía del Squad — 4 agentes coordinados, validación en 2 capas
  </div>
</div>
</body>
</html>
```

- [ ] **Step 4: Diseñar CSS con animaciones**

```css
/* styles.css */
:root {
  --obsidian: #0D0D12;
  --champagne: #C9A84C;
  --ivory: #FAF8F5;
  --slate: #2A2A35;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background: var(--obsidian);
  font-family: 'Inter', sans-serif;
  color: var(--ivory);
  width: 1920px;
  height: 1080px;
  overflow: hidden;
}
.stage { position: relative; width: 100%; height: 100%; }

.node {
  position: absolute;
  background: var(--slate);
  border: 1px solid var(--champagne);
  border-radius: 12px;
  padding: 24px 32px;
  text-align: center;
  opacity: 0;
  transform: scale(0.9);
  transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.node.in { opacity: 1; transform: scale(1); }

.pmo { top: 460px; left: 850px; width: 220px; padding: 32px 40px; border-width: 2px; }
.pmo .role { color: var(--champagne); font-weight: 900; font-size: 28px; }
.pmo .model { font-size: 14px; color: var(--ivory); margin: 4px 0; }
.pmo .tag { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--champagne); margin-top: 8px; }

.sat { width: 180px; }
.sat .role { color: var(--ivory); font-weight: 700; font-size: 22px; }
.sat .model { font-size: 13px; color: #888; margin-top: 4px; }
.sat-arch { top: 270px; left: 460px; }
.sat-dev { top: 270px; left: 1300px; }
.sat-qa { top: 720px; left: 460px; }

.links { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.links line {
  stroke: var(--champagne);
  stroke-width: 2;
  stroke-opacity: 0;
  stroke-dasharray: 8 4;
  animation: pulseLine 2s ease-in-out infinite;
}
.links line.in { stroke-opacity: 0.6; }
@keyframes pulseLine {
  0%, 100% { stroke-opacity: 0.3; }
  50% { stroke-opacity: 0.9; }
}

.layer.validation {
  position: absolute;
  bottom: 200px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 16px;
  align-items: center;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.layer.in { opacity: 1; }
.layer-label {
  color: var(--champagne);
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-right: 12px;
}
.layer-pill {
  background: rgba(201, 168, 76, 0.12);
  border: 1px solid var(--champagne);
  color: var(--ivory);
  padding: 8px 18px;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 600;
}

.rail {
  position: absolute;
  bottom: 100px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 24px;
  opacity: 0;
  transition: opacity 0.8s ease;
}
.rail.in { opacity: 1; }
.rail-pill {
  background: var(--slate);
  border-radius: 4px;
  color: var(--ivory);
  padding: 10px 20px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.caption {
  position: absolute;
  top: 60px;
  left: 50%;
  transform: translateX(-50%);
  color: var(--ivory);
  font-size: 22px;
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.8s ease;
}
.caption.in { opacity: 1; }
```

- [ ] **Step 5: Agregar script para applying classes según data-fade-in**

```html
<!-- al final de index.html antes de </body> -->
<script>
document.querySelectorAll('[data-fade-in]').forEach(el => {
  const t = parseFloat(el.dataset.fadeIn) * 1000;
  setTimeout(() => el.classList.add('in'), t);
});
document.querySelectorAll('[data-pulse-start]').forEach(el => {
  const t = parseFloat(el.dataset.pulseStart) * 1000;
  setTimeout(() => el.classList.add('in'), t);
});
</script>
```

- [ ] **Step 6: Render con Hyperframes**

```bash
cd ~/playgrounds/agent-squad-fabrica/03-diagrama-squad/hyperframes/
hyperframes render
```

Expected: `../diagrama-squad.mp4` generado, 30s, 1920x1080, 30fps.

- [ ] **Step 7: Validar output**

```bash
ffprobe -v quiet -print_format json -show_streams ~/playgrounds/agent-squad-fabrica/03-diagrama-squad/diagrama-squad.mp4 | jq '.streams[] | {codec_type, duration, width, height, r_frame_rate}'
```

Expected: 1920x1080, ~30s, 30fps.

Reproducir y verificar:
- ✅ PMO aparece primero, luego Architect → Dev → QA en cascada
- ✅ Líneas con pulse aparecen después de los nodos
- ✅ Capa de validación aparece a los ~2s
- ✅ Rail aparece a los ~3s
- ✅ Caption aparece a los ~4s
- ✅ Animación se mantiene visible hasta el final (no se borra)

---

## Task 7: Build hook sin avatar con Hyperframes _(Wave 1)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hyperframes/index.html`
- Create: `~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hyperframes/styles.css`
- Create: `~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hyperframes/hyperframes.config.json`
- Create: `~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hook-beat1.mp4`

**Done when:**
- [ ] HTML+CSS renderiza hook: timestamp counter Slack saltando entre +0/+4/+16/+21 + contador "00:17:00" pulsando + texto burned-in champagne
- [ ] Texto exacto: "En 17 minutos se hicieron 3 fixes a producción; Sin intervención humana."
- [ ] Render Hyperframes produce MP4 15s 1920x1080 30fps
- [ ] Output con drop musical preparado para layer (pero música se overlay en composite final)

- [ ] **Step 1: Inicializar Hyperframes**

```bash
cd ~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hyperframes/
hyperframes init
```

- [ ] **Step 2: Configurar hyperframes.config.json**

```json
{
  "mode": "E",
  "duration": 15,
  "fps": 30,
  "width": 1920,
  "height": 1080,
  "output": "../hook-beat1.mp4",
  "background": "#0D0D12"
}
```

- [ ] **Step 3: Diseñar HTML del hook**

```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="stage">

  <div class="timestamp-card t1">
    <div class="ts-label">SLACK</div>
    <div class="ts-time">+0 min</div>
    <div class="ts-event">Cliente reporta bug</div>
  </div>
  <div class="timestamp-card t2">
    <div class="ts-label">SLACK</div>
    <div class="ts-time">+4 min</div>
    <div class="ts-event">PMO commit fix #1</div>
  </div>
  <div class="timestamp-card t3">
    <div class="ts-label">SLACK</div>
    <div class="ts-time">+16 min</div>
    <div class="ts-event">PMO commit fix #2</div>
  </div>
  <div class="timestamp-card t4">
    <div class="ts-label">SLACK</div>
    <div class="ts-time">+21 min</div>
    <div class="ts-event">PMO commit fix #3 + 3 deploys READY</div>
  </div>

  <div class="counter">
    <div class="counter-value">00:17:00</div>
    <div class="counter-label">SQUAD ACTIVE</div>
  </div>

  <div class="burned-in">
    En 17 minutos se hicieron 3 fixes a producción;<br>
    <span class="emphasis">Sin intervención humana.</span>
  </div>

</div>
</body>
</html>
```

- [ ] **Step 4: Diseñar CSS con animation timeline**

```css
:root {
  --obsidian: #0D0D12;
  --champagne: #C9A84C;
  --ivory: #FAF8F5;
  --slate: #2A2A35;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background: var(--obsidian);
  font-family: 'Inter', sans-serif;
  color: var(--ivory);
  width: 1920px;
  height: 1080px;
  overflow: hidden;
}
.stage { position: relative; width: 100%; height: 100%; }

/* Timestamp cards aparecen en secuencia */
.timestamp-card {
  position: absolute;
  background: var(--slate);
  border-left: 4px solid var(--champagne);
  padding: 18px 28px;
  border-radius: 0 6px 6px 0;
  opacity: 0;
  transform: translateX(-20px);
  animation: slideIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.timestamp-card .ts-label {
  color: var(--champagne);
  font-size: 11px;
  letter-spacing: 0.2em;
  margin-bottom: 4px;
  font-weight: 700;
}
.timestamp-card .ts-time {
  font-family: 'JetBrains Mono', monospace;
  font-size: 32px;
  font-weight: 700;
  color: var(--ivory);
}
.timestamp-card .ts-event {
  font-size: 14px;
  color: #999;
  margin-top: 4px;
}
@keyframes slideIn {
  to { opacity: 1; transform: translateX(0); }
}

.t1 { top: 200px; left: 100px; animation-delay: 0.5s; }
.t2 { top: 320px; left: 100px; animation-delay: 1.5s; }
.t3 { top: 440px; left: 100px; animation-delay: 2.5s; }
.t4 { top: 560px; left: 100px; animation-delay: 3.5s; }

/* Counter grande pulsa */
.counter {
  position: absolute;
  right: 120px;
  top: 50%;
  transform: translateY(-50%);
  text-align: center;
  opacity: 0;
  animation: fadeInScale 1s ease 4.5s forwards;
}
.counter-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 140px;
  font-weight: 700;
  color: var(--champagne);
  letter-spacing: 0.05em;
  animation: pulse 2s ease infinite 5s;
}
.counter-label {
  font-size: 14px;
  letter-spacing: 0.3em;
  color: var(--ivory);
  margin-top: 8px;
}
@keyframes fadeInScale {
  0% { opacity: 0; transform: translateY(-50%) scale(0.8); }
  100% { opacity: 1; transform: translateY(-50%) scale(1); }
}
@keyframes pulse {
  0%, 100% { color: var(--champagne); }
  50% { color: #d8b85d; text-shadow: 0 0 24px rgba(201, 168, 76, 0.6); }
}

/* Burned-in text bottom */
.burned-in {
  position: absolute;
  bottom: 80px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  font-family: 'Inter', sans-serif;
  font-weight: 900;
  font-size: 56px;
  line-height: 1.2;
  color: var(--ivory);
  opacity: 0;
  animation: fadeUp 1s ease 6s forwards;
  max-width: 1600px;
}
.burned-in .emphasis {
  color: var(--champagne);
}
@keyframes fadeUp {
  0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}
```

- [ ] **Step 5: Render**

```bash
cd ~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hyperframes/
hyperframes render
```

- [ ] **Step 6: Validar**

```bash
ffprobe -v quiet -show_streams ~/playgrounds/agent-squad-fabrica/02-hook-sin-avatar/hook-beat1.mp4 | head -30
```

Expected: 1920x1080, 15s, 30fps. Reproducir:
- ✅ Timestamps aparecen en cascada (0.5s, 1.5s, 2.5s, 3.5s)
- ✅ Counter "00:17:00" aparece a los 4.5s con pulse
- ✅ Texto burned-in aparece a los 6s
- ✅ Estado final (todos visibles) se mantiene hasta el segundo 15

---

## Task 8: Generar captions ES + EN bilingües para clips Valeria _(Wave 2)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/transcribe-and-caption.py`
- Create: `~/playgrounds/agent-squad-fabrica/05-captions/beat3-words.json` (Whisper output)
- Create: `~/playgrounds/agent-squad-fabrica/05-captions/beat3-bilingual.json` (ES+EN merged)
- Create: `~/playgrounds/agent-squad-fabrica/05-captions/beat3-rendered/*.png` (PIL renders)
- (Idem para beats 4, 5, 6, 7)

**Done when:**
- [ ] Whisper transcribe word-level cada audio Valeria (5 archivos `*-words.json`)
- [ ] Cada transcripción traducida a EN preservando timing (Claude API)
- [ ] PIL renderiza captions PNG con estilo cierre AI4M (white + yellow highlight + black pill 1500px), ES principal + EN secundario opacidad 60% lower third
- [ ] Cada beat tiene N PNGs (1 por chunk de captioning, ~15-25 por beat)
- [ ] Archivos `bilingual.json` listan timing de cada PNG con start/end ms

- [ ] **Step 1: Crear script transcribe-and-caption.py**

```python
#!/usr/bin/env python3
"""Whisper word-level + Claude translate + PIL render for bilingual captions."""
import json, os, sys, subprocess
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

ROOT = Path("~/playgrounds/agent-squad-fabrica").expanduser()
VOICES_DIR = ROOT / "00-source" / "voices"
CAPTIONS_DIR = ROOT / "05-captions"

WIDTH = 1920
PILL_W = 1500
PILL_H_ES = 80
PILL_H_EN = 50
PILL_PAD_X = 40
LOWER_THIRD_Y = 880  # baseline ES caption

FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
FONT_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"

def whisper_words(mp3_path: Path) -> list:
    """Returns [{word, start, end}]."""
    out_json = mp3_path.with_suffix(".whisper.json")
    cmd = ["whisper", str(mp3_path), "--language", "es", "--word_timestamps", "True",
           "--output_format", "json", "--output_dir", str(mp3_path.parent), "--model", "medium"]
    subprocess.run(cmd, check=True)
    data = json.loads(out_json.read_text())
    words = []
    for seg in data["segments"]:
        for w in seg.get("words", []):
            words.append({"word": w["word"].strip(), "start": w["start"], "end": w["end"]})
    return words

def chunk_words(words: list, max_chars: int = 80, max_dur: float = 4.0) -> list:
    """Group words into caption chunks (one PNG per chunk)."""
    chunks = []
    cur = {"text": "", "start": None, "end": None}
    for w in words:
        if cur["start"] is None:
            cur["start"] = w["start"]
        candidate = (cur["text"] + " " + w["word"]).strip()
        if len(candidate) > max_chars or (cur["end"] and (w["end"] - cur["start"]) > max_dur):
            chunks.append(cur)
            cur = {"text": w["word"], "start": w["start"], "end": w["end"]}
        else:
            cur["text"] = candidate
            cur["end"] = w["end"]
    if cur["text"]:
        chunks.append(cur)
    return chunks

def translate_chunks_es_to_en(chunks: list) -> list:
    """Use Claude to translate text per chunk preserving same chunks."""
    import anthropic
    client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
    es_texts = [c["text"] for c in chunks]
    prompt = f"Translate each Spanish line to natural English. One line per input. NO numbering, NO comments. Preserve length proportional.\n\nLines:\n" + "\n".join(es_texts)
    msg = client.messages.create(model="claude-sonnet-4-6", max_tokens=4000,
        messages=[{"role":"user","content":prompt}])
    en_texts = msg.content[0].text.strip().split("\n")
    assert len(en_texts) == len(chunks), f"mismatch {len(en_texts)} vs {len(chunks)}"
    for c, en in zip(chunks, en_texts):
        c["en"] = en.strip()
    return chunks

def render_caption_png(text_es: str, text_en: str, out_path: Path):
    """Estilo cierre AI4M: white + yellow highlight + black pill, ES principal + EN secundario."""
    img = Image.new("RGBA", (WIDTH, 200), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)
    font_es = ImageFont.truetype(FONT_BOLD, 60)
    font_en = ImageFont.truetype(FONT_REG, 32)

    # Pill ES (black 100%)
    pill_x = (WIDTH - PILL_W) // 2
    draw.rounded_rectangle([pill_x, 0, pill_x + PILL_W, PILL_H_ES],
                           radius=8, fill=(0, 0, 0, 255))
    # Texto ES centrado
    bbox_es = draw.textbbox((0, 0), text_es, font=font_es)
    tx = (WIDTH - (bbox_es[2] - bbox_es[0])) // 2
    draw.text((tx, 8), text_es, font=font_es, fill=(255, 255, 255, 255))

    # Pill EN (black 60%)
    pill_y2 = PILL_H_ES + 8
    draw.rounded_rectangle([pill_x, pill_y2, pill_x + PILL_W, pill_y2 + PILL_H_EN],
                           radius=6, fill=(0, 0, 0, 153))
    bbox_en = draw.textbbox((0, 0), text_en, font=font_en)
    tx2 = (WIDTH - (bbox_en[2] - bbox_en[0])) // 2
    draw.text((tx2, pill_y2 + 8), text_en, font=font_en, fill=(201, 168, 76, 255))  # champagne

    img.save(out_path, "PNG")

def process_beat(beat_id: str):
    mp3 = VOICES_DIR / f"valeria-{beat_id}.mp3"
    out_dir = CAPTIONS_DIR / beat_id
    out_dir.mkdir(parents=True, exist_ok=True)

    words = whisper_words(mp3)
    chunks = chunk_words(words)
    chunks = translate_chunks_es_to_en(chunks)

    # Render PNGs y guardar timing JSON
    timing = []
    for i, c in enumerate(chunks):
        png_name = f"caption-{i:03d}.png"
        render_caption_png(c["text"], c["en"], out_dir / png_name)
        timing.append({"png": png_name, "start": c["start"], "end": c["end"], "es": c["text"], "en": c["en"]})
    (CAPTIONS_DIR / f"{beat_id}-bilingual.json").write_text(json.dumps(timing, indent=2, ensure_ascii=False))
    print(f"✓ {beat_id}: {len(chunks)} captions rendered")

if __name__ == "__main__":
    for beat in ["beat3-valeria-dolor", "beat4-valeria-tour-squad",
                 "beat5-valeria-timeline-lap256", "beat6-valeria-iteracion-sistema",
                 "beat7-valeria-contraste"]:
        process_beat(beat)
```

Save to `~/playgrounds/agent-squad-fabrica/00-source/transcribe-and-caption.py`.

- [ ] **Step 2: Instalar dependencias si faltan**

```bash
~/agents-claude-env/bin/pip install openai-whisper anthropic Pillow
which whisper || ~/agents-claude-env/bin/pip install -U openai-whisper
```

- [ ] **Step 3: Ejecutar el script**

```bash
source ~/.env
cd ~/playgrounds/agent-squad-fabrica
~/agents-claude-env/bin/python 00-source/transcribe-and-caption.py
```

Expected: 5 directorios de captions creados, cada uno con 15-25 PNGs + un `*-bilingual.json`.

- [ ] **Step 4: Validar visualmente 1 caption por beat**

```bash
for beat in beat3-valeria-dolor beat4-valeria-tour-squad beat5-valeria-timeline-lap256 beat6-valeria-iteracion-sistema beat7-valeria-contraste; do
  echo "=== $beat ==="
  ls ~/playgrounds/agent-squad-fabrica/05-captions/$beat/ | head -3
done
```

Abrir 1 PNG random de cada beat y verificar:
- ✅ Texto ES legible centrado en pill negra
- ✅ Texto EN debajo más chico, color champagne, opacidad menor
- ✅ Pill 1500px ancho, no overflow
- ✅ Sin clipping de texto

---

## Task 9: Composite cada beat individual con captions burned-in + screen captures _(Wave 2)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/composite-beat.sh`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat1-hook-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat2-saludo-linkedin.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat2-saludo-ernesto.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat3-dolor-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat4-tour-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat5-timeline-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat6-iteracion-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat7-contraste-final.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat8-cierre-linkedin.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/beat8-cierre-ernesto.mp4`

**Done when:**
- [ ] 10 archivos MP4 de beats compositeados (1 por beat + 2 por versión en beats 2 y 8)
- [ ] Cada beat con captions ES+EN burned-in según timing del bilingual.json
- [ ] Beat 4 incluye diagrama Squad PIP en la zona derecha + screenshots LADO squad
- [ ] Beat 5 incluye screen captures en cuts cronológicos sincronizados con narración
- [ ] Beat 6 incluye visualización del código del gate dual-close
- [ ] Beat 7 incluye split-screen comparativo antes/después
- [ ] Cada MP4 1920x1080 30fps H.264 AAC

- [ ] **Step 1: Crear script composite-beat.sh helper**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/composite-beat.sh <<'EOF'
#!/usr/bin/env bash
# Composite un clip Valeria con captions overlay timing-driven + opcional PIP/screen overlay
# Uso: composite-beat.sh <beat-id> <video-source.mp4> <captions-json> <output.mp4>
set -euo pipefail
BEAT_ID="$1"
SRC="$2"
CAPS_JSON="$3"
OUT="$4"
CAPS_DIR="$(dirname "$CAPS_JSON")/$BEAT_ID"

# Generar enable expressions y overlays a partir del bilingual.json
TMP_FILTER=$(mktemp)
~/agents-claude-env/bin/python <<PY > "$TMP_FILTER"
import json
data = json.load(open("$CAPS_JSON"))
inputs = []
overlays = []
for i, c in enumerate(data):
    inputs.append(f"-i $CAPS_DIR/{c['png']}")
    start = c["start"]; end = c["end"]
    src = "[base]" if i == 0 else f"[v{i-1}]"
    dst = f"[v{i}]"
    overlays.append(f"{src}[{i+1}:v]overlay=x=(W-w)/2:y=830:enable='between(t,{start:.3f},{end:.3f})'{dst}")
print(" ".join(inputs))
print(";".join(overlays))
PY

# Read input args + filter expression
INPUTS=$(head -1 "$TMP_FILTER")
FILTER=$(tail -1 "$TMP_FILTER")
LAST_TAG=$(echo "$FILTER" | grep -oE '\[v[0-9]+\]' | tail -1)

ffmpeg -y -i "$SRC" $INPUTS \
  -filter_complex "[0:v]format=rgba[base];$FILTER" \
  -map "$LAST_TAG" -map 0:a \
  -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -movflags +faststart \
  "$OUT"
rm "$TMP_FILTER"
echo "✓ $OUT"
EOF
chmod +x ~/playgrounds/agent-squad-fabrica/00-source/composite-beat.sh
```

- [ ] **Step 2: Composite beat 3 (Valeria narradora pura, captions ES+EN solo)**

```bash
cd ~/playgrounds/agent-squad-fabrica
00-source/composite-beat.sh beat3-valeria-dolor \
  01-presentadores/valeria-beat3-dolor.mp4 \
  05-captions/beat3-valeria-dolor-bilingual.json \
  07-composite/beat3-dolor-final.mp4
```

Validar reproducción: captions aparecen en sync con el habla de Valeria.

- [ ] **Step 3: Composite beat 4 (Valeria + diagrama Squad PIP + screenshots LADO squad)**

Approach: usar el clip Valeria como base, overlay del diagrama-squad.mp4 en una región de la pantalla (PIP) durante los primeros 30s, después overlay de screenshots PIP en los siguientes 30s. Todo además con captions burned-in.

Estrategia: usar tres pasadas:
1. Composite Valeria + diagrama PIP (primeros 30s)
2. Composite resultado + screenshots cycle PIP (segundos 30-60)
3. Composite resultado + captions burned-in

```bash
# Pasada 1: PIP diagrama Squad sobre Valeria (30s)
ffmpeg -y -i 01-presentadores/valeria-beat4-tour.mp4 -i 03-diagrama-squad/diagrama-squad.mp4 \
  -filter_complex "[1:v]scale=720:-1[pip];[0:v][pip]overlay=x=W-w-40:y=80:enable='between(t,0,30)'" \
  -map 0:a -c:v libx264 -preset medium -crf 18 -c:a copy \
  /tmp/beat4-stage1.mp4

# Pasada 2: PIP screenshots LADO squad (segundos 30-60), cycling cada 7.5s
ffmpeg -y -i /tmp/beat4-stage1.mp4 \
  -loop 1 -t 7.5 -i 04-captures-anonimizados/slack-pmo-thread.png \
  -loop 1 -t 7.5 -i 04-captures-anonimizados/linear-laps-genericos.png \
  -loop 1 -t 7.5 -i 04-captures-anonimizados/github-commits-tecnicos.png \
  -loop 1 -t 7.5 -i 04-captures-anonimizados/vercel-deploys.png \
  -filter_complex "
    [1:v]scale=720:-1[s1];[2:v]scale=720:-1[s2];[3:v]scale=720:-1[s3];[4:v]scale=720:-1[s4];
    [0:v][s1]overlay=x=W-w-40:y=80:enable='between(t,30,37.5)'[v1];
    [v1][s2]overlay=x=W-w-40:y=80:enable='between(t,37.5,45)'[v2];
    [v2][s3]overlay=x=W-w-40:y=80:enable='between(t,45,52.5)'[v3];
    [v3][s4]overlay=x=W-w-40:y=80:enable='between(t,52.5,60)'[final]
  " -map "[final]" -map 0:a -c:v libx264 -preset medium -crf 18 -c:a copy \
  /tmp/beat4-stage2.mp4

# Pasada 3: captions
00-source/composite-beat.sh beat4-valeria-tour-squad \
  /tmp/beat4-stage2.mp4 \
  05-captions/beat4-valeria-tour-squad-bilingual.json \
  07-composite/beat4-tour-final.mp4
```

Validar: diagrama visible primeros 30s, screenshots cyclando 30-60s, captions presentes todo el tiempo.

- [ ] **Step 4: Composite beat 5 (Valeria + screen captures cronológicos LADO squad)**

Beat 5 dura ~3 min. Mostrar 1 captura por timestamp narrado:
- 0-45s: Valeria intro al timeline + Slack PMO thread (PIP toda esta sección)
- 45-90s: Linear ticket (PIP)
- 90-135s: GitHub commits (PIP)
- 135-180s: Vercel deploys + TestSprite green (PIP cycling)

Mismo approach con `enable='between(t,X,Y)'`.

```bash
ffmpeg -y -i 01-presentadores/valeria-beat5-timeline.mp4 \
  -loop 1 -t 45 -i 04-captures-anonimizados/slack-pmo-thread.png \
  -loop 1 -t 45 -i 04-captures-anonimizados/linear-laps-genericos.png \
  -loop 1 -t 45 -i 04-captures-anonimizados/github-commits-tecnicos.png \
  -loop 1 -t 22.5 -i 04-captures-anonimizados/vercel-deploys.png \
  -loop 1 -t 22.5 -i 04-captures-anonimizados/testsprite-green.png \
  -filter_complex "
    [1:v]scale=720:-1[s1];[2:v]scale=720:-1[s2];[3:v]scale=720:-1[s3];[4:v]scale=720:-1[s4];[5:v]scale=720:-1[s5];
    [0:v][s1]overlay=x=W-w-40:y=80:enable='between(t,0,45)'[v1];
    [v1][s2]overlay=x=W-w-40:y=80:enable='between(t,45,90)'[v2];
    [v2][s3]overlay=x=W-w-40:y=80:enable='between(t,90,135)'[v3];
    [v3][s4]overlay=x=W-w-40:y=80:enable='between(t,135,157.5)'[v4];
    [v4][s5]overlay=x=W-w-40:y=80:enable='between(t,157.5,180)'[final]
  " -map "[final]" -map 0:a -c:v libx264 -preset medium -crf 18 -c:a copy \
  /tmp/beat5-stage1.mp4

00-source/composite-beat.sh beat5-valeria-timeline-lap256 \
  /tmp/beat5-stage1.mp4 \
  05-captions/beat5-valeria-timeline-lap256-bilingual.json \
  07-composite/beat5-timeline-final.mp4
```

- [ ] **Step 5: Composite beat 6 (Valeria + visualización código gate dual-close)**

Crear PNG simple del código del gate (snippet Python `update_linear_issue` con check evidence + approval) usando Pillow o imagen tomada de IDE. Overlay PIP durante los segundos centrales del beat.

```bash
# Generar PNG snippet código (1 sola vez)
~/agents-claude-env/bin/python <<'PY'
from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (1000, 600), (13, 13, 18))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Medium.ttf", 22)
code = '''def update_linear_issue(id, state, **kw):
    if state == "done":
        if not has_qa_evidence(id):
            raise GateError("QA evidence required")
        if not has_client_approval(id):
            raise GateError("Client approval required")
    return mutate(id, state=state, **kw)'''
draw.text((30, 40), "tools/linear_tools.py — gate dual-close Fase A.6", fill=(201, 168, 76), font=font)
draw.multiline_text((30, 100), code, fill=(250, 248, 245), font=font, spacing=6)
img.save("/home/clawd/playgrounds/agent-squad-fabrica/04-captures-anonimizados/gate-dual-close-snippet.png")
PY

# Composite beat 6
ffmpeg -y -i 01-presentadores/valeria-beat6-iteracion.mp4 \
  -loop 1 -t 25 -i 04-captures-anonimizados/gate-dual-close-snippet.png \
  -filter_complex "[1:v]scale=720:-1[snip];[0:v][snip]overlay=x=W-w-40:y=80:enable='between(t,15,40)'" \
  -map 0:a -c:v libx264 -preset medium -crf 18 -c:a copy \
  /tmp/beat6-stage1.mp4

00-source/composite-beat.sh beat6-valeria-iteracion-sistema \
  /tmp/beat6-stage1.mp4 \
  05-captions/beat6-valeria-iteracion-sistema-bilingual.json \
  07-composite/beat6-iteracion-final.mp4
```

- [ ] **Step 6: Composite beat 7 (Valeria + split-screen antes/después)**

Crear PNG split-screen "Antes vs Después" con datos. Overlay PIP zonal en pantalla.

```bash
~/agents-claude-env/bin/python <<'PY'
from PIL import Image, ImageDraw, ImageFont
W, H = 1280, 720
img = Image.new("RGB", (W, H), (13, 13, 18))
draw = ImageDraw.Draw(img)
big = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
mid = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28)

# Left = Antes
draw.rectangle([0, 0, W//2 - 4, H], fill=(42, 42, 53))
draw.text((40, 40), "ANTES", fill=(232, 98, 92), font=big)
items_a = ["3-5 devs en nómina", "$15k/mes mínimo", "2 semanas/feature", "2 iteraciones max", "Backlog creciente"]
for i, t in enumerate(items_a):
    draw.text((40, 160 + i*70), "• " + t, fill=(250, 248, 245), font=mid)

# Right = Después
draw.rectangle([W//2 + 4, 0, W, H], fill=(42, 42, 53))
draw.text((W//2 + 44, 40), "DESPUÉS", fill=(111, 207, 151), font=big)
items_b = ["Squad agentico 24/7", "<$15/feature en tokens", "Horas, no semanas", "Iteraciones ilimitadas", "Backlog drenando"]
for i, t in enumerate(items_b):
    draw.text((W//2 + 44, 160 + i*70), "• " + t, fill=(250, 248, 245), font=mid)

img.save("/home/clawd/playgrounds/agent-squad-fabrica/04-captures-anonimizados/contraste-antes-despues.png")
PY

ffmpeg -y -i 01-presentadores/valeria-beat7-contraste.mp4 \
  -loop 1 -t 40 -i 04-captures-anonimizados/contraste-antes-despues.png \
  -filter_complex "[1:v]scale=900:-1[split];[0:v][split]overlay=x=(W-w)/2:y=H-h-160:enable='between(t,5,45)'" \
  -map 0:a -c:v libx264 -preset medium -crf 18 -c:a copy \
  /tmp/beat7-stage1.mp4

00-source/composite-beat.sh beat7-valeria-contraste \
  /tmp/beat7-stage1.mp4 \
  05-captions/beat7-valeria-contraste-bilingual.json \
  07-composite/beat7-contraste-final.mp4
```

- [ ] **Step 7: Composite beats 1, 2, 8 (sin captions, son intro/handoff/cierre)**

Beat 1 hook: ya está renderizado en `02-hook-sin-avatar/hook-beat1.mp4`. Solo copiar:

```bash
cp 02-hook-sin-avatar/hook-beat1.mp4 07-composite/beat1-hook-final.mp4
```

Beats 2 y 8 (Roberto): usar los clips ya generados en Task 4. Los clips Roberto NO llevan captions burned-in ES+EN porque Roberto habla en cámara y se entiende. (Decisión: para versión LinkedIn nativo donde 80% mira sin sonido, podríamos agregar captions Roberto también — confirmar con Roberto en delivery o agregar como opcional.)

```bash
cp 01-presentadores/roberto-saludo-linkedin.mp4 07-composite/beat2-saludo-linkedin.mp4
cp 01-presentadores/roberto-saludo-ernesto.mp4 07-composite/beat2-saludo-ernesto.mp4
cp 01-presentadores/roberto-cierre-linkedin.mp4 07-composite/beat8-cierre-linkedin.mp4
cp 01-presentadores/roberto-cierre-ernesto.mp4 07-composite/beat8-cierre-ernesto.mp4
```

(Si se decide en delivery agregar captions Roberto: aplicar mismo composite-beat.sh con su MP3 separado generado por Whisper sobre el audio extraído del MP4 Roberto.)

- [ ] **Step 8: Validar reproducción de cada beat compositeado**

```bash
for f in ~/playgrounds/agent-squad-fabrica/07-composite/beat*.mp4; do
  echo "=== $f ==="
  ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$f"
done
```

Verificar que la duración total suma ~8:00-8:30.

---

## Task 10: Concatenar versión LinkedIn con cube 3D transitions _(Wave 3)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/07-composite/cube-transition-roberto-to-valeria.mp4` (helper si aplica)
- Create: `~/playgrounds/agent-squad-fabrica/08-final/agent-squad-fabrica-LINKEDIN.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/concat-linkedin.sh`

**Done when:**
- [ ] MP4 final LinkedIn dura entre 8:00 y 8:45
- [ ] Cube 3D transition entre Roberto saludo (beat 2) → Valeria intro (beat 3)
- [ ] Cube 3D transition entre Valeria contraste (beat 7) → Roberto cierre (beat 8)
- [ ] Audio sin pops/glitches en uniones (acrossfade 0.3s)
- [ ] Música bed corporate aplicada a -22 LUFS, drops en 0:00, 5:30, 7:00
- [ ] Output: 1920x1080, 30fps, H.264 yuv420p, AAC 128k, faststart enabled
- [ ] Tamaño <250 MB (LinkedIn limit ~5GB pero ideal <500MB)

- [ ] **Step 1: Crear script concat-linkedin.sh**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/concat-linkedin.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd ~/playgrounds/agent-squad-fabrica

CL_DIR="07-composite"
OUT="08-final/agent-squad-fabrica-LINKEDIN.mp4"
MUSIC="06-music/bed-corporate.mp3"

# Concat order: 1, 2-LI, 3, 4, 5, 6, 7, 8-LI
# Cube transitions usan xfade entre beats 2↔3 y 7↔8

ffmpeg -y \
  -i "$CL_DIR/beat1-hook-final.mp4" \
  -i "$CL_DIR/beat2-saludo-linkedin.mp4" \
  -i "$CL_DIR/beat3-dolor-final.mp4" \
  -i "$CL_DIR/beat4-tour-final.mp4" \
  -i "$CL_DIR/beat5-timeline-final.mp4" \
  -i "$CL_DIR/beat6-iteracion-final.mp4" \
  -i "$CL_DIR/beat7-contraste-final.mp4" \
  -i "$CL_DIR/beat8-cierre-linkedin.mp4" \
  -i "$MUSIC" \
  -filter_complex "
    [0:v][1:v]xfade=transition=fade:duration=0.3:offset=14.7[v01];
    [v01][2:v]xfade=transition=hlslice:duration=0.6:offset=42[v12];
    [v12][3:v]xfade=transition=fade:duration=0.3:offset=87[v23];
    [v23][4:v]xfade=transition=fade:duration=0.3:offset=147[v34];
    [v34][5:v]xfade=transition=fade:duration=0.3:offset=327[v45];
    [v45][6:v]xfade=transition=fade:duration=0.3:offset=372[v56];
    [v56][7:v]xfade=transition=hrslice:duration=0.6:offset=432[vfinal];

    [0:a]anull[a0]; [1:a]anull[a1]; [2:a]anull[a2]; [3:a]anull[a3];
    [4:a]anull[a4]; [5:a]anull[a5]; [6:a]anull[a6]; [7:a]anull[a7];
    [a0][a1][a2][a3][a4][a5][a6][a7]concat=n=8:v=0:a=1[voice];
    [8:a]volume=0.18,aloop=loop=-1:size=2e9[bed];
    [voice][bed]amix=inputs=2:duration=first:weights=1 0.6:dropout_transition=0[mixed]
  " \
  -map "[vfinal]" -map "[mixed]" \
  -c:v libx264 -preset slow -crf 19 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -ar 44100 \
  -movflags +faststart \
  "$OUT"

echo "✓ $OUT"
ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$OUT"
ls -lh "$OUT"
EOF
chmod +x ~/playgrounds/agent-squad-fabrica/00-source/concat-linkedin.sh
```

(Nota: los `offset=` valores asumen duraciones específicas — ajustar dinámicamente si los beats reales difieren.)

- [ ] **Step 2: Calcular offsets dinámicos**

Antes de correr concat, recalcular los offsets de cada xfade:

```bash
~/agents-claude-env/bin/python <<'PY'
import subprocess, json
beats = ["beat1-hook-final","beat2-saludo-linkedin","beat3-dolor-final","beat4-tour-final",
         "beat5-timeline-final","beat6-iteracion-final","beat7-contraste-final","beat8-cierre-linkedin"]
durs = []
for b in beats:
    out = subprocess.check_output(["ffprobe","-v","quiet","-show_entries","format=duration",
                                    "-of","csv=p=0",f"/home/clawd/playgrounds/agent-squad-fabrica/07-composite/{b}.mp4"])
    durs.append(float(out.strip()))
total = sum(durs)
print(f"Total beats sum: {total:.1f}s")
# Offsets para xfade: cada offset = suma de beats anteriores menos overlap
overlaps = [0.3, 0.6, 0.3, 0.3, 0.3, 0.3, 0.6]
offsets = []
acc = 0
for i in range(len(durs) - 1):
    acc += durs[i] - overlaps[i]
    offsets.append((i, i+1, acc, overlaps[i]))
for o in offsets:
    print(f"xfade beats {o[0]}↔{o[1]}: offset={o[2]:.2f} duration={o[3]}")
PY
```

Reemplazar valores en `concat-linkedin.sh` con los offsets reales calculados.

- [ ] **Step 3: Descargar bed musical (si no está)**

```bash
ls ~/playgrounds/agent-squad-fabrica/06-music/bed-corporate.mp3 2>/dev/null || \
  echo "FALTA música — descargar de Epidemic Sound / Artlist y guardar en 06-music/bed-corporate.mp3"
```

(Si falta: Roberto descarga manualmente. Sugerencia: instrumental piano + synth minimalista, ~10 min de duración para que cubra todo el video.)

- [ ] **Step 4: Ejecutar concatenación**

```bash
cd ~/playgrounds/agent-squad-fabrica
00-source/concat-linkedin.sh
```

- [ ] **Step 5: Validar output LinkedIn**

```bash
ffprobe -v quiet -print_format json -show_streams -show_format \
  ~/playgrounds/agent-squad-fabrica/08-final/agent-squad-fabrica-LINKEDIN.mp4 | \
  jq '{duration: .format.duration, size_mb: (.format.size | tonumber / 1024 / 1024), streams: [.streams[] | {codec_type, codec_name, width, height, sample_rate, bit_rate}]}'
```

Expected: duration 480-525s, size <250MB, video H.264 1920x1080, audio AAC 44100Hz.

Reproducir end-to-end y verificar:
- ✅ Hook beat 1 (15s) reproduce con texto burned-in
- ✅ Roberto saluda smooth a Valeria (cube transition)
- ✅ Valeria narra cada beat con captions ES+EN
- ✅ Beat 5 timeline tiene todos los screenshots PIP cyclando correctamente
- ✅ Beat 6 muestra el código del gate
- ✅ Beat 7 muestra split antes/después
- ✅ Roberto cierra con cube transition desde Valeria
- ✅ Música bed sutil, no opaca voz
- ✅ Sin pops/clicks en transiciones de audio

---

## Task 11: Concatenar versión Ernesto _(Wave 3)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/08-final/agent-squad-fabrica-ERNESTO.mp4`
- Create: `~/playgrounds/agent-squad-fabrica/00-source/concat-ernesto.sh`

**Done when:**
- [ ] MP4 final Ernesto dura entre 8:00 y 8:45
- [ ] Mismas características técnicas que LinkedIn versión
- [ ] Diferencia única: beat 2 = saludo Ernesto, beat 8 = cierre Ernesto

- [ ] **Step 1: Copiar concat-linkedin.sh y ajustar inputs**

```bash
cp ~/playgrounds/agent-squad-fabrica/00-source/concat-linkedin.sh \
   ~/playgrounds/agent-squad-fabrica/00-source/concat-ernesto.sh

sed -i 's/beat2-saludo-linkedin.mp4/beat2-saludo-ernesto.mp4/' \
       ~/playgrounds/agent-squad-fabrica/00-source/concat-ernesto.sh
sed -i 's/beat8-cierre-linkedin.mp4/beat8-cierre-ernesto.mp4/' \
       ~/playgrounds/agent-squad-fabrica/00-source/concat-ernesto.sh
sed -i 's/agent-squad-fabrica-LINKEDIN.mp4/agent-squad-fabrica-ERNESTO.mp4/' \
       ~/playgrounds/agent-squad-fabrica/00-source/concat-ernesto.sh
```

- [ ] **Step 2: Recalcular offsets dinámicos para versión Ernesto**

Mismo Python script del Task 10 Step 2 pero con beats Ernesto. Si las duraciones de beat 2 y beat 8 difieren entre versiones, los offsets serán distintos.

- [ ] **Step 3: Ejecutar concat Ernesto**

```bash
cd ~/playgrounds/agent-squad-fabrica
00-source/concat-ernesto.sh
```

- [ ] **Step 4: Validar como en Task 10 Step 5**

Verificar que en lugar de "comentá SQUAD" Roberto dice "30 minutos de tu agenda" y dirige el video a Ernesto por nombre.

---

## Task 12: Audit anonimización quirúrgica frame-by-frame _(Wave 4)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/anonymize-audit.py`
- Create: `~/playgrounds/agent-squad-fabrica/10-audit/hyperframes-anonimization-audit/` (frames extraídos)
- Create: `~/playgrounds/agent-squad-fabrica/10-audit/audit-report.md`

**Done when:**
- [ ] Audit visual de 1 frame por segundo de ambas versiones (~510 frames cada una)
- [ ] Reporte audit-report.md confirma 0 elementos identificables del cliente en ningún frame
- [ ] Si se detecta filtración → re-render del beat afectado

- [ ] **Step 1: Crear script audit**

```python
# 00-source/anonymize-audit.py
"""Extrae frames cada 1s de ambas versiones, OCR para detectar texto, busca términos prohibidos."""
import subprocess, sys, re
from pathlib import Path
import pytesseract  # pip install pytesseract  + apt install tesseract-ocr

ROOT = Path("~/playgrounds/agent-squad-fabrica").expanduser()
FORBIDDEN = [
    r"\blpdi\b", r"\bfrank\b", r"\bprieto\b", r"eco\.lpdi\.co",
    r"sgr\.lpdi\.co", r"lapuntadeliceberg", r"devlapuntadeliceberg",
    r"todo-swfacrtory", r"ICG_SELECT_FIELDS",  # nombres de variables específicas LPDI
]

def extract_frames(mp4: Path, out_dir: Path):
    out_dir.mkdir(parents=True, exist_ok=True)
    subprocess.run(["ffmpeg", "-y", "-i", str(mp4), "-vf", "fps=1",
                    str(out_dir / "frame-%04d.png")], check=True)

def ocr_check(frames_dir: Path):
    leaks = []
    for png in sorted(frames_dir.glob("frame-*.png")):
        text = pytesseract.image_to_string(str(png), lang="spa+eng")
        for pat in FORBIDDEN:
            if re.search(pat, text, re.IGNORECASE):
                leaks.append({"frame": png.name, "match": pat, "context": text[:200]})
    return leaks

def audit(version: str):
    mp4 = ROOT / "08-final" / f"agent-squad-fabrica-{version}.mp4"
    frames_dir = ROOT / "10-audit" / "hyperframes-anonimization-audit" / version.lower()
    extract_frames(mp4, frames_dir)
    leaks = ocr_check(frames_dir)
    return leaks

if __name__ == "__main__":
    report = ["# Audit anonimización — " + subprocess.check_output(["date","+%Y-%m-%d %H:%M"]).decode().strip(), ""]
    for v in ["LINKEDIN", "ERNESTO"]:
        leaks = audit(v)
        report.append(f"## {v}\n")
        if not leaks:
            report.append("✅ Sin filtraciones detectadas.\n")
        else:
            report.append(f"🔴 {len(leaks)} frames con términos prohibidos:\n")
            for l in leaks:
                report.append(f"- `{l['frame']}` → match `{l['match']}` — context: `{l['context'][:120]}`")
        report.append("")
    out = ROOT / "10-audit" / "audit-report.md"
    out.write_text("\n".join(report))
    print(f"Audit done → {out}")
    print("\n".join(report))
```

- [ ] **Step 2: Instalar dependencias**

```bash
echo 'Michael#7070' | sudo -S apt-get install -y tesseract-ocr tesseract-ocr-spa tesseract-ocr-eng
~/agents-claude-env/bin/pip install pytesseract Pillow
```

- [ ] **Step 3: Ejecutar audit**

```bash
~/agents-claude-env/bin/python ~/playgrounds/agent-squad-fabrica/00-source/anonymize-audit.py
cat ~/playgrounds/agent-squad-fabrica/10-audit/audit-report.md
```

- [ ] **Step 4: Si reporta leaks → re-render**

Identificar qué beat tiene el leak (mirar el frame number * 1s = posición temporal). Re-anonimizar el captura que haya filtrado y re-componer el beat afectado + concat final.

- [ ] **Step 5: Audit visual manual de 5 frames random por versión**

Aún si OCR pasa limpio, abrir 5 frames random de cada versión y mirar visualmente:
- ✅ Sin logos identificables
- ✅ Sin URLs visibles del cliente
- ✅ Sin avatares Slack reconocibles

---

## Task 13: Generar bonus piezas _(Wave 4)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/09-bonus/quote-card.png`
- Create: `~/playgrounds/agent-squad-fabrica/09-bonus/thumbnail-linkedin.png`
- Create: `~/playgrounds/agent-squad-fabrica/09-bonus/linkedin-post-copy.md`
- Create: `~/playgrounds/agent-squad-fabrica/09-bonus/ernesto-email.md`
- Create: `~/playgrounds/agent-squad-fabrica/09-bonus/talking-point-frank.md`

**Done when:**
- [ ] 5 bonus piezas creadas
- [ ] Quote card 1080x1080 (LinkedIn square) con frase central
- [ ] Thumbnail LinkedIn 1280x720 con hook visual
- [ ] Post copy LinkedIn ES con 3 hashtags + above-fold sin emojis
- [ ] Email Ernesto con link Cloudflare Stream + CTA 30 min
- [ ] Talking point Frank corto y defendible

- [ ] **Step 1: Generar quote card 1080x1080**

```python
~/agents-claude-env/bin/python <<'PY'
from PIL import Image, ImageDraw, ImageFont
W = H = 1080
img = Image.new("RGB", (W, H), (13, 13, 18))
draw = ImageDraw.Draw(img)
font_big = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
font_italic = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf", 48)
font_label = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)

# Tag superior
draw.text((60, 60), "AGENT SQUAD", fill=(201, 168, 76), font=font_label)

# Frase principal
line1 = "La velocidad de tu producto"
line2 = "ya no la define el tamaño de"
line3 = "tu equipo."
y = 360
for line in [line1, line2, line3]:
    draw.text((60, y), line, fill=(250, 248, 245), font=font_big)
    y += 80

# Italic en champagne
draw.text((60, 700), "La define la calidad de tus", fill=(201, 168, 76), font=font_italic)
draw.text((60, 760), "agentes.", fill=(201, 168, 76), font=font_italic)

# Footer
draw.text((60, 980), "Agent Squad · agentsquad.com", fill=(150, 150, 150), font=font_label)

img.save("/home/clawd/playgrounds/agent-squad-fabrica/09-bonus/quote-card.png")
print("✓ quote-card.png")
PY
```

- [ ] **Step 2: Generar thumbnail LinkedIn 1280x720**

```python
~/agents-claude-env/bin/python <<'PY'
from PIL import Image, ImageDraw, ImageFont
W, H = 1280, 720
img = Image.new("RGB", (W, H), (13, 13, 18))
draw = ImageDraw.Draw(img)
font_huge = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 130)
font_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 42)
font_tag = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)

draw.text((60, 50), "CASO REAL · AGENT SQUAD", fill=(201, 168, 76), font=font_tag)
draw.text((60, 200), "17 min", fill=(201, 168, 76), font=font_huge)
draw.text((60, 380), "3 fixes a producción", fill=(250, 248, 245), font=font_sub)
draw.text((60, 440), "Cero intervención humana.", fill=(250, 248, 245), font=font_sub)

# Pill bottom
draw.rectangle([60, 620, 600, 680], fill=(201, 168, 76))
draw.text((90, 632), "CASE STUDY · LinkedIn", fill=(13, 13, 18), font=font_tag)

img.save("/home/clawd/playgrounds/agent-squad-fabrica/09-bonus/thumbnail-linkedin.png")
print("✓ thumbnail-linkedin.png")
PY
```

- [ ] **Step 3: Escribir linkedin-post-copy.md**

```bash
cat > ~/playgrounds/agent-squad-fabrica/09-bonus/linkedin-post-copy.md <<'EOF'
# LinkedIn Post Copy — Caso Agent Squad

## Above-the-fold (línea 1, sin emojis)

17 minutos. 3 fixes a producción. Cero intervención mía.
Acá el timeline real ↓

## Body completo

17 minutos. 3 fixes a producción. Cero intervención mía.
Acá el timeline real ↓

Mi cliente reportó un bug crítico en su formulario de registro. Cuatro bugs distintos en cascada bloqueando demos con usuarios.

Yo estaba en standby — fuera del teclado, en otra reunión.

Lo que pasó después es lo que me hizo darme cuenta de que el modelo de "contratar más devs" ya no aplica:

→ Minuto +4: PMO Agent identificó bug primario, abrió ticket, primer commit
→ Minuto +16: Segundo commit, segundo bug resuelto
→ Minuto +21: Tercer commit consolidando los bugs restantes
→ Minuto +23: 3 deploys Vercel en READY, sistema arreglado en producción

Veintiún minutos desde el reporte hasta el último deploy. Tres bugs resueltos. Cero intervención humana en tiempo real.

En este video de 8 minutos te muestro:
1. Anatomía del Squad (PMO Opus + Architect + Dev + QA + guardrails)
2. Timeline minuto-por-minuto del caso real (anonimizado)
3. Cómo el squad ITERA sobre sí mismo cuando comete errores
4. Comparación honesta antes/después con números

La velocidad de tu producto ya no la define el tamaño de tu equipo. La define la calidad de tus agentes.

Si querés ver cómo funcionaría en TU caso, comentá la palabra **SQUAD** acá abajo. Te mando 3 cosas en privado:
- Demo de 15 min del squad corriendo en vivo
- El caso documentado completo con timeline + comandos
- Calculadora de ROI vs equipo humano

#IA #Founders #LATAM
EOF
```

- [ ] **Step 4: Escribir ernesto-email.md**

```bash
cat > ~/playgrounds/agent-squad-fabrica/09-bonus/ernesto-email.md <<'EOF'
# Mensaje Ernesto

## Subject line
Te grabé esto que prometí (8 min)

## Cuerpo

Hola Ernesto,

Te grabé el caso del que hablamos la semana pasada. Son 8 minutos. Versión personalizada para vos, no la pública.

Te muestro un caso real (anonimizado) de cómo el squad arregla 3 bugs en producción en 17 minutos sin que yo tenga que intervenir.

Mismo stack que el tuyo: SvelteKit/Next + Vercel + Supabase + tu sistema de tickets.

Link: {CLOUDFLARE_STREAM_SIGNED_URL}
(Expira en 30 días, solo para vos)

Mirá hasta el minuto 5 si tenés tiempo limitado. Ahí está el timeline completo.

Después de eso, una pregunta concreta: ¿me das 30 minutos esta semana para abrir tu Linear conmigo y mostrarte qué tickets podríamos shippear este mes con squad?

Mandame 3 horarios que te funcionen y yo me adapto.

Roberto
EOF
```

- [ ] **Step 5: Escribir talking-point-frank.md**

```bash
cat > ~/playgrounds/agent-squad-fabrica/09-bonus/talking-point-frank.md <<'EOF'
# Talking point preparado por si Frank pregunta

## Contexto
Si Frank reconoce el caso del video de LinkedIn y pregunta directamente.

## Respuesta sugerida (no defensiva, transparente)

"Frank, lo que muestro es la anatomía técnica de cómo trabajo con squad agentico. No menciono tu proyecto, no muestro tu producto, no aparece tu nombre, no aparece tu URL. Es exactamente el mismo flujo que aplico a múltiples proyectos en paralelo.

El video sirve para vender Agent Squad como producto, no para mostrar ningún cliente específico. Si en algún momento querés que coordinemos un caso con tu nombre — incluso un testimonial corto — lo planificamos. Pero esto no es ese caso."

## Si pide despublicar
Roberto evalúa. Si la fricción real con Frank vale más que el ROI del video → despublicar y editar la versión Ernesto eliminando referencia al timeline (queda solo Squad genérico).

## Si pide modificar el video
Aceptar y editar. Cambios típicos posibles: cambiar timestamps relative aún más (ej. "al rato", "minutos después" sin números), eliminar mención al gate dual-close (beat 6).
EOF
```

---

## Task 14: Upload Cloudflare Stream + signed URL Ernesto _(Wave 4)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/00-source/cf-upload.sh`
- Create: `~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt` (UIDs de stream + signed URL)

**Done when:**
- [ ] Versión LinkedIn uploaded a Cloudflare Stream → UID generado
- [ ] Versión Ernesto uploaded → UID + signed URL con 30 días de expiración
- [ ] Thumbnails LinkedIn registradas como poster del video
- [ ] URLs guardadas en cloudflare-uids.txt

- [ ] **Step 1: Crear script cf-upload.sh**

```bash
cat > ~/playgrounds/agent-squad-fabrica/00-source/cf-upload.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
source ~/.env

ACCOUNT="38fdadff842eb9f9a18e97cb4739ef90"
AUTH=(-H "X-Auth-Email: contact@medicalhubassist.ai" -H "X-Auth-Key: $CF_GLOBAL_KEY")

upload() {
  local file="$1"
  local name="$2"
  local require_signed="$3"  # true/false
  echo "→ Upload $file as '$name'..."
  local extra=""
  if [[ "$require_signed" == "true" ]]; then
    extra='--form requireSignedURLs=true'
  fi
  local resp=$(curl -sS -X POST \
    "${AUTH[@]}" \
    --form "file=@$file" \
    $extra \
    --form "meta={\"name\":\"$name\"}" \
    "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/stream")
  echo "$resp" | jq '.result | {uid, preview, playback}'
  local uid=$(echo "$resp" | jq -r '.result.uid')
  echo "$name = $uid" >> ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt
  echo "Public preview: https://customer-w9ibixuc04vgp9p1.cloudflarestream.com/$uid/iframe"
  echo ""
}

upload "$HOME/playgrounds/agent-squad-fabrica/08-final/agent-squad-fabrica-LINKEDIN.mp4" \
       "Agent Squad Caso 17min · LinkedIn" \
       false

upload "$HOME/playgrounds/agent-squad-fabrica/08-final/agent-squad-fabrica-ERNESTO.mp4" \
       "Agent Squad Caso 17min · Ernesto" \
       true

echo "Uploads done. UIDs en 10-audit/cloudflare-uids.txt"
EOF
chmod +x ~/playgrounds/agent-squad-fabrica/00-source/cf-upload.sh
```

- [ ] **Step 2: Ejecutar upload**

```bash
~/playgrounds/agent-squad-fabrica/00-source/cf-upload.sh
cat ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt
```

Anotar los 2 UIDs (LinkedIn pública + Ernesto privada).

- [ ] **Step 3: Generar signed URL para Ernesto (30 días)**

```bash
source ~/.env
ERNESTO_UID=$(grep "Ernesto" ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt | awk '{print $NF}')
EXP=$(date -d "+30 days" +%s)

curl -sS -X POST \
  -H "X-Auth-Email: contact@medicalhubassist.ai" \
  -H "X-Auth-Key: $CF_GLOBAL_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"id\":\"$ERNESTO_UID\",\"exp\":$EXP}" \
  "https://api.cloudflare.com/client/v4/accounts/38fdadff842eb9f9a18e97cb4739ef90/stream/$ERNESTO_UID/token" | \
  jq -r '"https://customer-w9ibixuc04vgp9p1.cloudflarestream.com/" + .result.token + "/iframe"' >> \
  ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt

cat ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt
```

- [ ] **Step 4: Reemplazar placeholder en ernesto-email.md**

```bash
SIGNED_URL=$(tail -1 ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt)
sed -i "s|{CLOUDFLARE_STREAM_SIGNED_URL}|$SIGNED_URL|" \
  ~/playgrounds/agent-squad-fabrica/09-bonus/ernesto-email.md
cat ~/playgrounds/agent-squad-fabrica/09-bonus/ernesto-email.md
```

- [ ] **Step 5: Set thumbnail LinkedIn como poster del video LinkedIn**

```bash
LINKEDIN_UID=$(grep "LinkedIn" ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt | awk '{print $NF}')
curl -sS -X POST \
  -H "X-Auth-Email: contact@medicalhubassist.ai" \
  -H "X-Auth-Key: $CF_GLOBAL_KEY" \
  -F "thumbnail=@$HOME/playgrounds/agent-squad-fabrica/09-bonus/thumbnail-linkedin.png" \
  "https://api.cloudflare.com/client/v4/accounts/38fdadff842eb9f9a18e97cb4739ef90/stream/$LINKEDIN_UID/thumbnails" | \
  jq '.result'
```

---

## Task 15: Validación final + página preview + delivery _(Wave 5)_

**Files:**
- Create: `~/playgrounds/agent-squad-fabrica/index.html` (preview review estilo AI4M)
- Modify: ~/playgrounds/agent-squad-fabrica/10-audit/audit-report.md (sumar checklist final)

**Done when:**
- [ ] index.html servido en `https://playgrounds.digitalhubassist.ai/agent-squad-fabrica/` retorna HTTP 200
- [ ] Página muestra ambas versiones embebidas (LinkedIn pública + preview Ernesto)
- [ ] Página muestra todas las bonus piezas linkeadas
- [ ] Audit report final confirma:
  - 0 leaks de identidad cliente (Task 12)
  - Duración correcta ambas versiones (Task 10/11)
  - Tamaño <250MB ambas
  - Lip sync OK
  - Captions sincronizados
- [ ] Roberto firma off (visualiza la página y aprueba)

- [ ] **Step 1: Crear index.html preview**

```html
<!-- ~/playgrounds/agent-squad-fabrica/index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Review · Video Fábrica de Software · Agent Squad</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&family=Playfair+Display:ital,wght@1,400;1,600&display=swap" rel="stylesheet">
<style>
:root {
  --obsidian: #0D0D12;
  --champagne: #C9A84C;
  --ivory: #FAF8F5;
  --slate: #2A2A35;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--obsidian);color:var(--ivory);font-family:'Inter',sans-serif;font-size:15px;line-height:1.6}
.container{max-width:1100px;margin:0 auto;padding:48px 24px}
header{text-align:center;margin-bottom:48px;padding-bottom:24px;border-bottom:1px solid var(--slate)}
.tag{color:var(--champagne);font-size:11px;letter-spacing:0.22em;text-transform:uppercase;font-weight:700;margin-bottom:8px}
h1{font-size:32px;font-weight:700;letter-spacing:-0.02em}
h1 em{font-family:'Playfair Display',serif;font-style:italic;color:var(--champagne);font-weight:600}
.subtitle{color:#999;font-size:14px;margin-top:6px}

.version{background:var(--slate);border-radius:10px;padding:24px 28px;margin-bottom:32px}
.version h2{color:var(--champagne);font-size:20px;margin-bottom:6px}
.version-meta{font-size:12px;color:#888;margin-bottom:14px;letter-spacing:0.04em}
.video-wrap{position:relative;padding-top:56.25%;border-radius:6px;overflow:hidden;margin:14px 0}
.video-wrap iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0}
.actions{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}
.btn{display:inline-block;padding:10px 18px;border-radius:4px;font-size:13px;font-weight:600;text-decoration:none;letter-spacing:0.04em}
.btn-primary{background:var(--champagne);color:var(--obsidian)}
.btn-ghost{background:transparent;color:var(--champagne);border:1px solid var(--champagne)}

.bonus{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px;margin-top:32px}
.card{background:var(--slate);border-radius:8px;padding:20px;border:1px solid #1d1d26}
.card h3{font-size:15px;color:var(--champagne);margin-bottom:8px}
.card p{font-size:13px;color:#bbb;margin-bottom:12px}
.card a{color:var(--champagne);font-size:12px;text-decoration:none;border-bottom:1px dotted var(--champagne)}

.audit{background:var(--slate);border-radius:8px;padding:24px;margin-top:32px}
.audit h2{color:var(--champagne);font-size:18px;margin-bottom:14px}
.checklist li{margin:6px 0;font-size:13px;list-style:none;padding-left:24px;position:relative}
.checklist li::before{content:'✓';position:absolute;left:0;color:#6fcf97;font-weight:700}
.checklist li.fail::before{content:'✗';color:#e8625c}

footer{text-align:center;margin-top:48px;padding-top:24px;border-top:1px solid var(--slate);color:#666;font-size:11px}
</style>
</head>
<body>
<div class="container">

<header>
  <div class="tag">Review · Pre-publication · 2026-04-XX</div>
  <h1>Fábrica de <em>software</em> · Agent Squad</h1>
  <p class="subtitle">2 versiones listas para validación final</p>
</header>

<div class="version">
  <h2>Versión LinkedIn pública</h2>
  <div class="version-meta">~8:30 · 1080p · ES audio + ES/EN captions burned-in · CTA: SQUAD en comentarios</div>
  <div class="video-wrap">
    <iframe src="https://customer-w9ibixuc04vgp9p1.cloudflarestream.com/{LINKEDIN_UID}/iframe" allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>
  </div>
  <div class="actions">
    <a class="btn btn-primary" href="08-final/agent-squad-fabrica-LINKEDIN.mp4" download>⬇ Descargar MP4</a>
    <a class="btn btn-ghost" href="09-bonus/linkedin-post-copy.md" target="_blank">Ver post copy LinkedIn</a>
  </div>
</div>

<div class="version">
  <h2>Versión Ernesto privada</h2>
  <div class="version-meta">~8:30 · 1080p · Intro/cierre personalizados a Ernesto · Signed URL 30 días</div>
  <div class="video-wrap">
    <iframe src="{ERNESTO_SIGNED_IFRAME_URL}" allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>
  </div>
  <div class="actions">
    <a class="btn btn-primary" href="08-final/agent-squad-fabrica-ERNESTO.mp4" download>⬇ Descargar MP4</a>
    <a class="btn btn-ghost" href="09-bonus/ernesto-email.md" target="_blank">Ver email Ernesto</a>
  </div>
</div>

<h2 style="margin-top:48px;color:var(--champagne);font-size:20px">Bonus piezas</h2>
<div class="bonus">
  <div class="card">
    <h3>Quote card</h3>
    <p>"La velocidad de tu producto..." — postear día +1</p>
    <a href="09-bonus/quote-card.png" download>Descargar PNG</a>
  </div>
  <div class="card">
    <h3>Thumbnail LinkedIn</h3>
    <p>Poster del video LinkedIn (1280x720)</p>
    <a href="09-bonus/thumbnail-linkedin.png" download>Descargar PNG</a>
  </div>
  <div class="card">
    <h3>Post copy LinkedIn</h3>
    <p>Texto completo para publicar (con hashtags)</p>
    <a href="09-bonus/linkedin-post-copy.md">Ver markdown</a>
  </div>
  <div class="card">
    <h3>Email Ernesto</h3>
    <p>Subject + body listo para enviar (con signed URL)</p>
    <a href="09-bonus/ernesto-email.md">Ver markdown</a>
  </div>
  <div class="card">
    <h3>Talking point Frank</h3>
    <p>Defensa preparada si Frank pregunta</p>
    <a href="09-bonus/talking-point-frank.md">Ver markdown</a>
  </div>
</div>

<div class="audit">
  <h2>Audit final</h2>
  <ul class="checklist">
    <li>0 filtraciones identidad cliente (OCR + manual visual de 5 frames random)</li>
    <li>Duración LinkedIn ~8:30, dentro de límite nativo 10min</li>
    <li>Tamaño LinkedIn &lt;250MB</li>
    <li>Lip sync verificado en los 4 clips Roberto + 5 clips Valeria</li>
    <li>Captions ES+EN sincronizados con audio</li>
    <li>Música corporate sin opacar voz</li>
    <li>Cube transitions Roberto↔Valeria smooth</li>
    <li>Anti-scope respetado: 0 mención cliente, 0 precio producto, 0 IDE Claude</li>
  </ul>
</div>

<footer>
  Spec source: <code>docs/superpowers/specs/2026-04-27-video-dogfooding-agent-squad-design.md</code><br>
  Implementation plan: <code>docs/superpowers/plans/2026-04-27-video-fabrica-software.md</code><br>
  Brand AI4Managers · obsidian + champagne · Inter + Playfair Display
</footer>

</div>
</body>
</html>
```

- [ ] **Step 2: Reemplazar placeholders {LINKEDIN_UID} y {ERNESTO_SIGNED_IFRAME_URL}**

```bash
LINKEDIN_UID=$(grep "LinkedIn" ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt | awk '{print $NF}')
ERNESTO_SIGNED=$(tail -1 ~/playgrounds/agent-squad-fabrica/10-audit/cloudflare-uids.txt)

sed -i "s|{LINKEDIN_UID}|$LINKEDIN_UID|" ~/playgrounds/agent-squad-fabrica/index.html
sed -i "s|{ERNESTO_SIGNED_IFRAME_URL}|$ERNESTO_SIGNED|" ~/playgrounds/agent-squad-fabrica/index.html
```

- [ ] **Step 3: Verificar que la página sirve**

```bash
curl -s -o /dev/null -w "HTTP %{http_code} · %{size_download} bytes\n" https://playgrounds.digitalhubassist.ai/agent-squad-fabrica/
```

Expected: HTTP 200.

- [ ] **Step 4: Roberto valida visualmente la página**

Abrir `https://playgrounds.digitalhubassist.ai/agent-squad-fabrica/` en browser. Reproducir ambas versiones end-to-end. Verificar contra el spec V2:
- Hook 0:00-0:15 con texto burned-in correcto
- Anatomía Squad clara
- Timeline LAP-256 con captures sincronizados
- Beat 6 muestra el incidente + gate
- Frase central legible en beat 7
- CTAs correctos en beat 8

- [ ] **Step 5: Sign-off final**

Si todo OK → Roberto:
1. Sube versión LinkedIn al post (martes/miércoles 7:30-8:15 AM CDMX)
2. Manda email a Ernesto con el signed URL

Update audit-report.md con `## Sign-off final\nRoberto aprobó 2026-XX-XX HH:MM` al final.

---

## Self-Review (writing-plans skill checklist)

**1. Spec coverage** — Cada sección del spec V2 mapea a tareas:

- §2 Objetivo (LinkedIn + Ernesto) → Tasks 10, 11, 14
- §3 Audiencia / posicionamiento → reflejado en scripts (Task 3)
- §4 Beats minute-by-minute → Tasks 4-9
- §5.1 Presentadores → Tasks 4, 5
- §5.2 Hook sin avatar → Task 7
- §5.3 Diagrama Squad → Task 6
- §5.4 Anonimización quirúrgica → Task 2 (preparar) + Task 12 (audit)
- §5.5 Captions ES+EN → Task 8
- §5.6 Música y transiciones → Task 10 (concat con xfade + amix)
- §6 Distribución → Task 14 (CF Stream + signed URL)
- §7 Métricas → fuera de plan (post-distribución)
- §8 Anti-scope → reflejado en Task 12 audit
- §9 Riesgos → covered en Done when criterios + Task 12
- §10 Bonus piezas → Task 13
- §11 Política Frank → Task 13 (talking point)
- §12 Scripts indicativos → Task 3 escribe versión final

✅ Cobertura completa.

**2. Placeholder scan** — Revisado, no hay TBD/TODO genéricos. Los `{CLOUDFLARE_STREAM_SIGNED_URL}` etc son placeholders explícitos que se reemplazan en steps específicos.

**3. Type consistency** — Naming consistente: `valeria-beatN-*.mp4`, `beatN-*-final.mp4`, etc. Captions JSON format consistente. UIDs Cloudflare etiquetados.

---

## Execution Handoff

**Plan complete and saved to `docs/superpowers/plans/2026-04-27-video-fabrica-software.md`. Two execution options:**

**1. Subagent-Driven (recommended)** - Dispatch fresh subagent per task, review between tasks, fast iteration

**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution con checkpoints

**3. Agent Team por waves** - Un agente por wave, ejecución paralela entre waves independientes (extensión global Roberto, recomendado para este proyecto M)

¿Cuál approach?
