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:
- Consultant (chat, sonnet) — análisis de solo lectura, brainstorming, creación de specs. Enrutado via
/co/w:consultant. - Developer (terminal, opus) — implementación completa. Enrutado via
/do/w:developer. - UI/UX Designer (chat, sonnet) — análisis de frontend, sugerencias de diseño. Enrutado via
/w:ui-designer.
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:
"tools": ["Read", "Glob", ...]— lista blanca explícita"tools": "all"— acceso completo (solo Developer)
Esto garantiza que los workers de tipo chat (Consultant, UI Designer) NO PUEDAN:
- Escribir o editar archivos
- Ejecutar comandos de shell
- Hacer deploy, commit o push
Aislamiento de Workers
- Cada worker corre en su propio proceso/hilo de Claude
- El historial de conversación de cada worker se rastrea en el Map
workerThreads(clave: ID del worker) - Compatibilidad hacia atrás: array
consultantThreadsincronizado junto aworkerThreads - Solo el chatId del CEO puede emitir los comandos
/approvey/reject
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.