Arquitetura de Workers Dinâmicos
Phase 26: Registry Dinâmico de Workers. Substitui o fluxo hardcoded dual-agent da Phase 24.5. Última atualização: 2026-04-06
Visão Geral
A Phase 26 substitui a lógica binária hardcoded Consultant/Developer por um sistema de registry dinâmico de workers.
Os workers são declarados em 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 nativos:
- Consultant (chat, sonnet) — análise somente leitura, brainstorm, criação de specs. Roteado via
/cou/w:consultant. - Developer (terminal, opus) — implementação completa. Roteado via
/dou/w:developer. - UI/UX Designer (chat, sonnet) — análise de frontend, sugestões de design. Roteado via
/w:ui-designer.
O CEO escolhe o worker via comandos do Telegram. As specs são uma ponte entre workers: o Consultant as cria, o CEO aprova e o Developer executa.
CEO (Telegram)
│
├── /c <message> ──► Subprocesso Consultant (Claude somente leitura)
│ │
│ ├── Analisa código, pesquisa na web
│ └── Gera blocos SPEC
│ │
│ ▼
│ spec_queue.json (status: "draft")
│
├── /approve <id> ──► status da spec → "approved"
│ │
│ ▼
├── /d <message> ──► Developer (loop principal GSD)
│ │
│ ├── Lê specs aprovadas da fila
│ ├── Implementa mudanças
│ └── Marca spec → "done"
│
├── /reject <id> ──► status da spec → "rejected"
│
└── /specs ──► Lista todas as specs com status
Modelo de Dados
Interface Spec
interface Spec {
id: string; // nanoid(8), e.g. "a1b2c3d4"
title: string; // One-line title from SPEC block
problem: string; // What issue this solves
approach: string; // Technical approach (2-3 sentences)
acceptance: string[]; // Acceptance criteria list
files: string[]; // Affected file paths
complexity: "low" | "medium" | "high";
status: SpecStatus;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
consultantThread?: string; // Thread ID that produced this spec
}
type SpecStatus = "draft" | "approved" | "rejected" | "executing" | "done";
Máquina de Estado
IDEIA ──► CONSULTA ──► RASCUNHO SPEC ──► REVISÃO ──► APROVADA ──► EXECUTANDO ──► CONCLUÍDA
↑ │
+──── REJEITADA ────────────+
Transições:
| De | Para | Gatilho |
|---|---|---|
| — | draft |
Consultant gera um bloco SPEC |
draft |
approved |
CEO envia /approve <id> |
draft |
rejected |
CEO envia /reject <id> |
rejected |
draft |
Consultant revisa após feedback |
approved |
executing |
Developer inicia a implementação |
executing |
done |
Developer conclui todos os critérios de aceite |
Endpoints de API
Todos os endpoints são internos (roteamento pelo master bot), não são HTTP API.
| Endpoint | Método | Auth | Descrição |
|---|---|---|---|
/c <msg> |
Telegram | Somente CEO | Roteia mensagem para subprocesso Consultant |
/d <msg> |
Telegram | Somente CEO | Roteia mensagem para Developer (loop GSD) |
/approve <id> |
Telegram | Somente CEO | Aprova uma spec em rascunho |
/reject <id> |
Telegram | Somente CEO | Rejeita uma spec em rascunho com feedback opcional |
/specs |
Telegram | Somente CEO | Lista todas as specs com filtro de status |
/role |
Telegram | Somente CEO | Mostra o papel ativo atual |
| padrão | Telegram | Somente CEO | Roteia para o papel ativo (consultant ou developer) |
Roteamento de Mensagens
handleMessage() do master bot roteia mensagens Telegram recebidas:
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). A configuração do worker determina model, tools, estilo de prompt e categoria de log. O roteamento padrão usa active_role.json. Padrão inicial: developer.
Componentes Frontend
WorkerPanel (Phase 26)
Dispatcher de painel genérico. Renderiza ChatPanelView ou TerminalPanelView com base em worker.type.
ChatPanelView
Exibe conversas de workers do tipo chat (Consultant, UI/UX Designer, etc.).
| Aspecto | Detalhe |
|---|---|
| Fonte de dados | /api/sse/logs/:name?category=${worker.log_category} |
| Exibição | Balões de mensagem estilo chat (CEO à direita, Worker à esquerda) |
| Detecção de SPEC | Faz parse de blocos ### SPEC:, renderiza como cards com botões aprovar/rejeitar |
| Entrada | Barra de entrada completa com seletor de modelo, menu de tools, entrada de voz, anexos de arquivo |
| Resultado do Dev | Botão para ver a última resposta do terminal do developer |
TerminalPanelView
Exibe saída de workers do tipo terminal (Developer, etc.).
| Aspecto | Detalhe |
|---|---|
| Fonte de dados | /api/sse/logs/:name?category=${worker.log_category} |
| Exibição | Entradas de log monoespaçadas com ícones de tool, indicadores de "thinking", estado de processamento |
| Entrada | Barra de comando simples com botão Enviar |
Hook useSSEStream
Hook SSE unificado para ambos os tipos de painel. Trata deduplicação (seenIds), detecção de estado de processamento e transformação de entradas com base em worker.type.
Workers Bar
Barra superior exibindo todos os workers registrados como pills alternáveis. Clique para adicionar/remover painéis. Layout persiste no localStorage por projeto (citadel-workspace-active-${project.name}).
SpecPanel
Board no estilo Kanban mostrando o ciclo de vida das specs.
| Coluna | Specs exibidas |
|---|---|
| Rascunho | status: "draft" — com botões aprovar/rejeitar |
| Aprovada | status: "approved" — aguardando o developer |
| Em Andamento | status: "executing" — developer trabalhando |
| Concluída | status: "done" — finalizada |
| Rejeitada | status: "rejected" — recolhida por padrão |
RoleToggle
Indicador visual no cabeçalho do pod do projeto mostrando o papel/worker ativo.
Clicar no toggle envia informação do comando /role; a troca de papel em si é feita apenas por comandos Telegram (segurança: sem mutações pelo frontend).
Modelo de Segurança
Restrições de Tools por Worker
As tools dos workers são declaradas em workers_registry.json. A função callWorker() lê worker.tools e passa --allowedTools para o Claude CLI:
"tools": ["Read", "Glob", ...]— whitelist explícita"tools": "all"— acesso total (somente Developer)
Isso garante que workers do tipo chat (Consultant, UI Designer) NÃO possam:
- Escrever ou editar arquivos
- Executar comandos shell
- Fazer deploy, commit ou push
Isolamento de Workers
- Cada worker roda em seu próprio processo/thread Claude
- Histórico de conversa de cada worker rastreado no Map
workerThreads(chaveado por worker ID) - Compatibilidade retroativa: array
consultantThreadsincronizado junto comworkerThreads - Apenas o chatId do CEO pode emitir comandos
/approvee/reject
Layout de Armazenamento
Todos os arquivos de estado são armazenados no diretório de trabalho do projeto sob state/:
state/
├── active_role.json # { "role": "developer"|"consultant", "since": ISO8601 }
├── consultant_thread.json # { "threadId": "...", "startedAt": ISO8601 }
├── spec_queue.json # Spec[] array — all specs with status
└── logs/
├── consultant-YYYY-MM-DD.log # JSONL consultant conversation
└── specs-YYYY-MM-DD.log # JSONL spec state transitions
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"
}
]
Registry de Workers
Todos os workers declarados em 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 | Descrição |
|---|---|---|
id |
string | Identificador único do worker (usado em roteamento, armazenamento, SSE) |
label |
string | Nome de exibição na UI |
icon |
string | Emoji para pill/cabeçalho |
type |
"chat" | "terminal" |
Tipo de painel: balões de chat vs logs monoespaçados |
model |
string | ID do modelo Claude |
max_turns |
number | Máximo de turns agênticos por invocação |
tools |
string[] | "all" |
Whitelist de tools permitidas, ou "all" para acesso total |
system_prompt_skill |
string | null | Path para o system prompt no diretório skills/ |
prompt_style |
"history" | "gsd" |
Estilo de conversa (append de histórico vs prompt GSD) |
output_format |
"text" | "stream-json" |
Modo de tratamento da saída |
focus_dirs |
string[] | Diretórios para focar (injetados no prompt) |
log_category |
string | Categoria de log JSONL (mapeia para o parâmetro SSE ?category=) |
builtin |
boolean | Indica se este worker é nativo do Arc OS |
Configuração de Papel
Configuração de papel por projeto em config/project_roles.json:
{
"version": 1,
"defaults": {
"activeRole": "developer",
"specAutoApprove": false,
"maxDraftSpecs": 20
}
}
Logging
Todas as interações do consultant e transições de spec são registradas em 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"}
Mantido por Rick (Orchestrator). Phase 26 — Workers Dinâmicos.