---
title: |-
  Playbook *UC4*
  Spawn Peer Local
description: "Verificación: sesión existente en Mac spawnea otra sesión via runner daemon local. Spawn-plane MVP sin workspace remoto."
kicker: Playbook · UC4 · Spawn Peer Local
subtitle: |-
  Desde una sesión Claude Code en Mac (`cl minimax --proxy --channels`), ejecutar
  `SpawnPeer` para crear una **segunda sesión** gestionada por el runner local.
  Verificar que el spawn funciona, que el nuevo peer aparece en ListPeers, y que
  ambas sesiones pueden comunicarse.
strip:
  - { k: PLAYBOOK, v: UC4-SPAWN }
  - { k: REV, v: "1.0" }
  - { k: FECHA, v: 21 JUN 2026 }
  - { k: DEPENDE, v: P0 + UC3 }
meta:
  - { k: Esfuerzo, v: 20 min }
  - { k: Depende de, v: "P0 (prerequisites) + UC3 (channels)" }
  - { k: Bloquea, v: UC5 (workspace spawn E2E) }
  - { k: Veredicto, v: ⏳ pendiente }
stamps:
  - { text: Runner daemon NO configurado en Mac, tone: warn }
  - { text: Sin verificación runtime, tone: warn }
toc_legend:
  - { text: verificado, tone: ok }
  - { text: requiere setup, tone: warn }
  - { text: no implementado, tone: bad }
footer:
  left: |-
    **UC4-SPAWN · REV 1.0**
    Playbooks de verificación E2E para mks-agentics
  right: |-
    **SPAWN** local · **RUNNER** daemon · **SPAWN-PLANE**
---

## Objetivo {#objetivo}

Probar el spawn-plane en local: una sesión Claude Code spawnea otra a través del broker + runner daemon. Esto requiere que el Mac tenga un runner corriendo (`GATEWAY_MODE=runner`) que acepte spawn_orders del broker y ejecute `cliLauncher.launch()`. {.lead}

```dossier:diagram
engine: d2
source: |
  direction: right

  sessionA: "Sesión A (Mac)\ncl minimax\n--proxy --channels" {
    style: {
      fill: "#1a1a2e"
      stroke: "#e94560"
    }
    mcpA: "a2a-mcp" {
      style: {
        fill: "#16213e"
        stroke: "#e94560"
      }
    }
  }

  broker: "Broker público\nbroker.gateway.mks2508.systems" {
    style: {
      fill: "#0f3460"
      stroke: "#e94560"
    }
  }

  runner: "Runner daemon (Mac)\nGATEWAY_MODE=runner\nRUNNER_ID=mac" {
    style: {
      fill: "#1a1a2e"
      stroke: "#533483"
    }
  }

  sessionB: "Sesión B (spawneada)\nclaude --sdk-url" {
    style: {
      fill: "#16213e"
      stroke: "#533483"
    }
    mcpB: "a2a-mcp\n(embedded)" {
      style: {
        fill: "#16213e"
        stroke: "#533483"
      }
    }
  }

  # Flujo spawn
  sessionA.mcpA -> broker: "1. SpawnPeer\n{host:'mac', provider:'minimax'}" {
    style.stroke: "#e94560"
  }
  broker -> runner: "2. spawn_order WS" {
    style.stroke: "#0f3460"
  }
  runner -> sessionB: "3. Bun.spawn\nclaude --sdk-url" {
    style.stroke: "#533483"
  }
  sessionB.mcpB -> broker: "4. peer_registered" {
    style.stroke: "#533483"
  }
  broker -> sessionA: "5. spawnId + peerId" {
    style.stroke: "#e94560"
  }

  # Comunicación post-spawn
  sessionA.mcpA -> broker: "SendPeerMessage to B" {
    style.stroke-dash: 3
  }
  broker -> sessionB: "CHANNEL PUSH" {
    style.stroke-dash: 3
  }
```

```dossier:note
tone: warn
tag: PREREQUISITO — Runner daemon en Mac
body: |
  Para que UC4 funcione, el Mac necesita un **runner daemon** corriendo:

  ```bash
  cd /path/to/mks-agentics
  GATEWAY_MODE=runner \
  RUNNER_ID=mac \
  BROKER_URL=https://broker.gateway.mks2508.systems \
  BROKER_TOKEN=<JWT> \
  GATEWAY_PORT=4105 \
  bun run apps/gateway-server/src/runner.ts
  ```

  El runner se conecta al broker vía WS (`/ws/runner/mac`) y espera `spawn_order`.
  Cuando recibe uno, llama a `cliLauncher.launch()` y responde `peer_registered`.

  **Estado actual (2026-06-20):** el runner daemon NO está configurado como servicio
  permanente en Mac. Hay que arrancarlo manualmente para UC4. Esto es parte del
  trabajo pendiente de spawn-plane (0.18.D+).
```

## F1 — *Arrancar* runner daemon en Mac {#f1-runner}

```dossier:phases
items:
  - dur: "5 min"
    title: Configurar y arrancar el runner
    checks:
      - text: "`cd apps/gateway-server && bun run src/runner.ts` con env vars → arranca"
        sub: El runner inicia y se conecta al broker
      - text: "Log muestra `runner online: id=mac`"
        sub: Conexión WS al broker establecida
      - text: "`curl -s https://broker.gateway.mks2508.systems/api/runners -H 'Authorization: Bearer $JWT'` → incluye 'mac'"
        sub: El runner está registrado en el broker
      - text: "`curl -s http://localhost:4105/health` → 200 OK"
        sub: El runner expone health check local
```

### Variables de entorno para el runner

```dossier:table
caption: Env vars requeridas para el runner daemon
columns:
  - { h: Variable }
  - { h: Valor }
  - { h: Nota }
rows:
  - cells:
      - GATEWAY_MODE
      - runner
      - Activa modo runner (sin REST API pública)
  - cells:
      - RUNNER_ID
      - mac
      - Identificador único del runner
  - cells:
      - BROKER_URL
      - https://broker.gateway.mks2508.systems
      - WebSocket del broker
  - cells:
      - BROKER_TOKEN
      - (JWT válido)
      - Auth del WS (`?token=<JWT>`)
  - cells:
      - GATEWAY_PORT
      - "4105"
      - Puerto para --sdk-url de las sesiones spawneadas
  - cells:
      - GATEWAY_HOST
      - 127.0.0.1
      - Solo localhost (F-OPS-1)
  - cells:
      - CLAUDE_BINARY_PATH
      - claude
      - Binary de Claude Code (default)
```

```dossier:term
title: Arrancar runner daemon
content: |
  # En una terminal dedicada (TMUX pane o similar):

  $ cd /Volumes/KODAK1TB/REPOS\ y\ PROYECTOS/nodejs-bun/mks-agentics

  # Generar JWT para el runner:
  $ source .env.brk-extract.local
  $ JWT=$(curl -s -X POST "$OIDC_TOKEN_ENDPOINT" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=client_credentials&client_id=$MCP_STANDALONE_CLIENT_ID&client_secret=$MCP_STANDALONE_CLIENT_SECRET&scope=broker" \
      | jq -r '.access_token')

  # Arrancar runner:
  $ GATEWAY_MODE=runner \
    RUNNER_ID=mac \
    BROKER_URL=https://broker.gateway.mks2508.systems \
    BROKER_TOKEN="$JWT" \
    GATEWAY_PORT=4105 \
    GATEWAY_HOST=127.0.0.1 \
    bun run apps/gateway-server/src/runner.ts

  # Output esperado:
  → Runner started: id=mac broker=https://broker.gateway.mks2508.systems localPort=4105
  → runner online: id=mac
```

## F2 — *Arrancar* sesión A con channels {#f2-sessionA}

```dossier:phases
items:
  - dur: "3 min"
    title: Sesión existente que spawnea
    checks:
      - text: "En otra terminal: `cl minimax --proxy --channels`"
        sub: Sesión A con channels activos
      - text: "`ListPeers` → muestra peer A y posiblemente el runner"
        sub: Peer discovery funciona
      - text: "`ListPeers` NO muestra peer B todavía"
        sub: Aún no se ha spawneado nada
```

## F3 — *Spawn* peer B {#f3-spawn}

```dossier:phases
items:
  - dur: "5 min"
    title: Ejecutar SpawnPeer desde sesión A
    checks:
      - text: "En sesión A: `SpawnPeer` con `host='mac'`, `provider='minimax'`, `name='spawned-peer-B'`"
        sub: MCP tool SpawnPeer → POST /api/spawn del broker
      - text: "El broker enruta el spawn_order al runner 'mac' vía WS"
        sub: runner.ts recibe `spawn_order` y llama a cliLauncher.launch()
      - text: "La respuesta de SpawnPeer incluye `spawnId` y `status: 'pending'`"
        sub: El spawn fue aceptado por el broker
      - text: "En unos segundos, `ListPeers` muestra 3+ peers (A, B, runner)"
        sub: El peer B se registró tras spawn exitoso
      - text: "`GetPeer <peerId-B>` → status 'running'"
        sub: La sesión B está viva
```

### SpawnPeer — comandos

```dossier:term
title: Sesión A — spawnear peer B
content: |
  # Dentro de Claude Code sesión A:

  > SpawnPeer name="spawned-peer-B" provider="minimax" host="mac" cwd="/tmp"

  → {
  →   "spawnId": "spawn_abc123...",
  →   "status": "pending",
  →   "host": "mac"
  → }

  # Esperar unos segundos y verificar:

  > ListPeers
  → [
  →   {"id": "a1b2c3d4-...", "kind": "operator", "name": "Claude Code — A", "status": "running"},
  →   {"id": "e5f6g7h8-...", "kind": "operator", "name": "spawned-peer-B", "status": "running"},
  →   {"id": "mac", "kind": "runner", "name": "mac", "status": "online"}
  → ]

  > GetPeer e5f6g7h8-...
  → (card completa del peer B — spawneado exitosamente)
```

## F4 — *Comunicación* post-spawn {#f4-comms}

```dossier:phases
items:
  - dur: "3 min"
    title: Verificar mensajería entre A y B
    checks:
      - text: "`SendPeerMessage to=<peerId-B> content='hola desde A, spawneada'`"
        sub: Mensaje de A a la sesión spawneada B
      - text: "En la terminal del runner, el log muestra `peer_registered: spawnId=... peerId=... pid=...`"
        sub: Runner confirma el spawn exitoso
      - text: "El mensaje llega a B vía channel-push (si channels=true en el spawn)"
        sub: La sesión spawneada tiene channels porque el runner hereda la config
      - text: "`CheckInbox <peerId-B>` → el mensaje está en el inbox (fallback)"
        sub: Verificación alternativa si channel-push falla
```

## F5 — *Stop* peer B {#f5-stop}

```dossier:phases
items:
  - dur: "2 min"
    title: Detener la sesión spawneada
    checks:
      - text: "`StopPeer peerId=<peerId-B>` → 200 OK"
        sub: MCP tool StopPeer → broker → runner → stop_order WS
      - text: "Runner log muestra `peer_stopped: spawnId=...`"
        sub: El runner detuvo el proceso Claude CLI
      - text: "`ListPeers` → peer B ya no aparece (o status 'stopped')"
        sub: Peer desregistrado correctamente
```

```dossier:matrix
groups:
  - tone: v
    title: Debe pasar
    badge: "4 checks"
    items:
      - text: Runner se conecta al broker y aparece en /api/runners
        sub: Runner registry funciona
      - text: SpawnPeer → spawn_order → peer_registered
        sub: Spawn-plane local funciona
      - text: ListPeers muestra el peer spawneado
        sub: Peer discovery post-spawn
      - text: StopPeer detiene la sesión spawneada
        sub: "Lifecycle completo: spawn → communicate → stop"
  - tone: r
    title: Puede fallar (esperable)
    badge: "2 checks"
    items:
      - text: El runner no arranca por falta de BROKER_TOKEN
        sub: Necesita JWT válido de OIDC client_credentials
      - text: SpawnPeer falla porque el host no coincide con ningún runner
        sub: Verificar RUNNER_ID y el parámetro host en SpawnPeer
  - tone: p
    title: Si falla → bloquea UC5
    badge: "1 check"
    items:
      - text: El spawn nunca completa (queda en pending)
        sub: Runner offline, broker no enruta, o cliLauncher falla
```

```dossier:questions
items:
  - no: Q1
    title: "¿Está el runner daemon corriendo en Mac?"
    chip: { text: prerequisite, tone: hot }
    body: Verificar con `curl http://localhost:4105/health`
    branches:
      - tone: "yes"
        label: "Sí → 200 OK"
        text: El runner está listo para recibir spawn_orders.
      - tone: "no"
        label: "No → connection refused"
        text: "Arrancar el runner manualmente (F1). Si no está corriendo, el broker no tiene a quién enviar el spawn_order y SpawnPeer fallará con RUNNER_OFFLINE o se quedará en pending."
  - no: Q2
    title: "¿Aparece el peer spawneado en ListPeers?"
    chip: { text: verificación, tone: ok }
    body: Después de SpawnPeer, esperar 5-10s y ejecutar ListPeers
    branches:
      - tone: "yes"
        label: "Sí → 3+ peers"
        text: UC4 verde. Spawn-plane local funciona. Se puede proceder a UC5 (workspace spawn).
      - tone: "no"
        label: "No → solo 2 peers"
        text: "Revisar logs del runner. Posibles causas: (1) cli-launcher falló (binary no encontrado), (2) loop-guard rechazó el spawn, (3) el broker no enrutó el spawn_order al runner correcto."
```

```dossier:sources
groups:
  - title: Código relevante
    items:
      - label: runner.ts
        url: apps/gateway-server/src/runner.ts
        chip: { text: source }
      - label: spawn.routes.ts
        url: apps/gateway-broker/src/routes/spawn.routes.ts
        chip: { text: source }
      - label: broker.service.ts
        url: apps/gateway-broker/src/services/broker.service.ts
        chip: { text: source }
      - label: runner-registry.ts
        url: apps/gateway-broker/src/services/runner-registry.ts
        chip: { text: source }
  - title: Diseño
    items:
      - label: spawn-plane-design-2026-06-20.md
        url: docs/jarvis/spawn-plane-design-2026-06-20.md
        chip: { text: spec }
```
