Arquitectura de Workers Dinámicos

Fase 26: Registro Dinámico de Workers. Reemplaza el flujo hardcodeado Consultant/Developer de la Fase 24.5. Última actualización: 2026-04-06

Resumen

La Fase 26 reemplaza la lógica binaria hardcodeada de Consultant/Developer con un sistema de registro dinámico de workers.

Los workers se declaran en config/workers_registry.json. Cada worker define: id, label, icon, type (chat/terminal), model, max_turns, tools, system_prompt_skill, prompt_style (history/gsd), output_format, focus_dirs, log_category, builtin.

Workers integrados:

El CEO elige el worker via comandos de Telegram. Los specs son el puente entre workers: el Consultant los crea, el CEO los aprueba, el Developer los ejecuta.

CEO (Telegram)
 │
 ├── /c <mensaje>  ──► Subprocess Consultant (Claude de solo lectura)
 │                      │
 │                      ├── Analiza código, busca en la web
 │                      └── Genera bloques SPEC
 │                           │
 │                           ▼
 │                      spec_queue.json (status: "draft")
 │
 ├── /approve <id>  ──► estado del spec → "approved"
 │                           │
 │                           ▼
 ├── /d <mensaje>   ──► Developer (loop GSD principal)
 │                      │
 │                      ├── Lee specs aprobados de la cola
 │                      ├── Implementa cambios
 │                      └── Marca spec → "done"
 │
 ├── /reject <id>   ──► estado del spec → "rejected"
 │
 └── /specs         ──► Lista todos los specs con estado

Modelo de Datos

Interfaz Spec

interface Spec {
  id: string;            // nanoid(8), ej. "a1b2c3d4"
  title: string;         // Título de una línea del bloque SPEC
  problem: string;       // Qué issue resuelve
  approach: string;      // Enfoque técnico (2-3 oraciones)
  acceptance: string[];  // Lista de criterios de aceptación
  files: string[];       // Rutas de archivos afectados
  complexity: "low" | "medium" | "high";
  status: SpecStatus;
  createdAt: string;     // ISO 8601
  updatedAt: string;     // ISO 8601
  consultantThread?: string; // ID del hilo que produjo este spec
}

type SpecStatus = "draft" | "approved" | "rejected" | "executing" | "done";

Máquina de Estados

  IDEA ──► CONSULTA ──► BORRADOR SPEC ──► REVISIÓN ──► APROBADO ──► EJECUTANDO ──► HECHO
                             ↑                              │
                             +──── RECHAZADO ───────────────+

Transiciones:

Desde Hacia Disparador
draft Consultant genera un bloque SPEC
draft approved CEO envía /approve <id>
draft rejected CEO envía /reject <id>
rejected draft Consultant revisa tras feedback
approved executing Developer inicia implementación
executing done Developer completa todos los criterios de aceptación

Endpoints de la API

Todos los endpoints son internos (enrutamiento del master bot), no HTTP API.

Endpoint Método Auth Descripción
/c <msg> Telegram Solo CEO Enrutar mensaje al subprocess Consultant
/d <msg> Telegram Solo CEO Enrutar mensaje al Developer (loop GSD)
/approve <id> Telegram Solo CEO Aprobar un spec borrador
/reject <id> Telegram Solo CEO Rechazar un spec borrador con feedback opcional
/specs Telegram Solo CEO Listar todos los specs con filtro de estado
/role Telegram Solo CEO Mostrar rol activo actual
por defecto Telegram Solo CEO Enrutar al rol activo (consultant o developer)

Enrutamiento de Mensajes

El master bot handleMessage() enruta los mensajes de Telegram entrantes:

function handleMessage(text: string, chatId: number) {
  if (text.startsWith("/c "))        → callWorker("consultant", text.slice(3))
  if (text.startsWith("/d "))        → callWorker("developer", text.slice(3))
  if (text.startsWith("/w:"))        → callWorker(parsedWorkerId, parsedText)
  if (text.startsWith("/approve "))  → approveSpec(text.slice(9).trim())
  if (text.startsWith("/reject "))   → rejectSpec(text.slice(8).trim())
  if (text === "/specs")             → listSpecs()
  if (text === "/role")              → showActiveRole()
  else                               → callWorker(activeWorkerId, text)
}

Dispatch unificado: getWorkerConfig(workerId)callWorker(workerId, text, options). La configuración del worker determina modelo, tools, estilo de prompt y categoría de log. El enrutamiento por defecto usa active_role.json. Por defecto inicial: developer.

Componentes del Frontend

WorkerPanel (Fase 26)

Dispatcher genérico de panel. Renderiza ChatPanelView o TerminalPanelView según worker.type.

ChatPanelView

Muestra conversaciones de workers de tipo chat (Consultant, UI/UX Designer, etc.).

Aspecto Detalle
Fuente de datos /api/sse/logs/:name?category=${worker.log_category}
Visualización Burbujas de mensajes estilo chat (CEO a la derecha, Worker a la izquierda)
Detección de SPEC Parsea bloques ### SPEC:, los renderiza como tarjetas con botones de aprobar/rechazar
Input Barra de entrada completa con selector de modelo, menú de tools, entrada de voz, adjuntos de archivos
Resultado Developer Botón para ver la última respuesta del terminal del developer

TerminalPanelView

Muestra la salida de workers de tipo terminal (Developer, etc.).

Aspecto Detalle
Fuente de datos /api/sse/logs/:name?category=${worker.log_category}
Visualización Entradas de log en monoespaciado con iconos de tools, indicadores de pensamiento, estado de procesamiento
Input Barra de comandos simple con botón Enviar

Hook useSSEStream

Hook SSE unificado para ambos tipos de panel. Gestiona la deduplicación (seenIds), detección de estado de procesamiento y transformación de entradas según worker.type.

Barra de Workers

Barra superior que muestra todos los workers registrados como pills de toggle. Al hacer clic se añaden/eliminan paneles. El layout persiste en localStorage por proyecto (citadel-workspace-active-${project.name}).

SpecPanel

Tablero estilo Kanban que muestra el ciclo de vida de specs.

Columna Specs mostrados
Borrador status: "draft" — con botones de aprobar/rechazar
Aprobado status: "approved" — esperando al developer
En Progreso status: "executing" — developer trabajando
Hecho status: "done" — completado
Rechazado status: "rejected" — contraído por defecto

RoleToggle

Indicador visual en el header del pod del proyecto que muestra el rol/worker activo.

Al hacer clic en el toggle se envía información del comando /role; el cambio real de rol es solo via comandos de Telegram (seguridad: sin mutaciones desde el frontend).

Modelo de Seguridad

Restricciones de Tools de Workers

Las tools de los workers se declaran en workers_registry.json. La función callWorker() lee worker.tools y pasa --allowedTools a Claude CLI:

Esto garantiza que los workers de tipo chat (Consultant, UI Designer) NO PUEDAN:

Aislamiento de Workers

Estructura de Almacenamiento

Todos los archivos de estado se guardan en el directorio de trabajo del proyecto bajo state/:

state/
├── active_role.json          # { "role": "developer"|"consultant", "since": ISO8601 }
├── consultant_thread.json    # { "threadId": "...", "startedAt": ISO8601 }
├── spec_queue.json           # array Spec[] — todos los specs con estado
└── logs/
    ├── consultant-YYYY-MM-DD.log   # conversación JSONL del consultant
    └── specs-YYYY-MM-DD.log        # transiciones de estado JSONL de specs

active_role.json

{
  "role": "developer",
  "since": "2026-04-05T10:00:00Z"
}

consultant_thread.json

{
  "threadId": "thread_abc123",
  "startedAt": "2026-04-05T10:00:00Z",
  "messageCount": 12
}

spec_queue.json

[
  {
    "id": "a1b2c3d4",
    "title": "Add rate limiting to CRM endpoints",
    "problem": "CRM API has no rate limiting, vulnerable to abuse",
    "approach": "Add sliding window rate limiter middleware using in-memory Map with IP-based tracking",
    "acceptance": [
      "Rate limiter middleware applied to all /api/crm/* routes",
      "100 requests per minute per IP",
      "429 response with Retry-After header"
    ],
    "files": ["shared/crm-routes.ts", "shared/rate-limiter.ts"],
    "complexity": "medium",
    "status": "approved",
    "createdAt": "2026-04-05T10:05:00Z",
    "updatedAt": "2026-04-05T10:12:00Z"
  }
]

Registro de Workers

Todos los workers declarados en config/workers_registry.json:

{
  "version": 1,
  "workers": [
    {
      "id": "consultant",
      "label": "Consultant",
      "icon": "\ud83d\udcac",
      "type": "chat",
      "model": "claude-sonnet-4-5",
      "max_turns": 5,
      "tools": ["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
      "system_prompt_skill": "consultant_system.md",
      "prompt_style": "history",
      "output_format": "text",
      "focus_dirs": [],
      "log_category": "consultant",
      "builtin": true
    }
  ]
}
Campo Tipo Descripción
id string Identificador único del worker (usado en enrutamiento, almacenamiento, SSE)
label string Nombre visible en la UI
icon string Icono emoji para pill/header
type "chat" | "terminal" Tipo de panel: burbujas de chat vs logs monoespaciados
model string ID del modelo Claude
max_turns number Máximo de turnos agentivos por invocación
tools string[] | "all" Lista blanca de tools permitidas, o "all" para acceso completo
system_prompt_skill string | null Ruta al prompt de sistema en el directorio skills/
prompt_style "history" | "gsd" Estilo de conversación (añadir historial vs prompt GSD)
output_format "text" | "stream-json" Modo de manejo de salida
focus_dirs string[] Directorios a enfocar (inyectados en el prompt)
log_category string Categoría de log JSONL (mapea al parámetro SSE ?category=)
builtin boolean Si este worker viene incluido con Arc OS

Configuración de Roles

Configuración de rol por proyecto en config/project_roles.json:

{
  "version": 1,
  "defaults": {
    "activeRole": "developer",
    "specAutoApprove": false,
    "maxDraftSpecs": 20
  }
}

Logging

Todas las interacciones del consultant y las transiciones de specs se registran en formato JSONL.

Entradas consultant-*.log

{"ts":"2026-04-05T10:05:00Z","role":"ceo","text":"How should we add caching?"}
{"ts":"2026-04-05T10:05:15Z","role":"consultant","text":"Based on analysis...","specs":["a1b2c3d4"]}

Entradas specs-*.log

{"ts":"2026-04-05T10:05:15Z","event":"created","specId":"a1b2c3d4","title":"Add caching layer"}
{"ts":"2026-04-05T10:12:00Z","event":"approved","specId":"a1b2c3d4","by":"ceo"}
{"ts":"2026-04-05T10:30:00Z","event":"executing","specId":"a1b2c3d4"}
{"ts":"2026-04-05T11:00:00Z","event":"done","specId":"a1b2c3d4"}

Mantenido por Rick (Orquestrador). Fase 26 — Workers Dinámicos.