Architecture des workers dynamiques
Phase 26 : Registry de workers dynamiques. Remplace le flux binaire Consultant/Developer codé en dur de la Phase 24.5. Dernière mise à jour : 2026-04-06
Vue d'ensemble
La Phase 26 remplace la logique binaire codée en dur Consultant/Developer par un système de registry de workers dynamiques.
Les workers sont déclarés dans config/workers_registry.json. Chaque worker définit : 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 intégrés :
- Consultant (chat, sonnet) — analyse en lecture seule, brainstorm, création de spec. Routé via
/cou/w:consultant. - Developer (terminal, opus) — implémentation complète. Routé via
/dou/w:developer. - UI/UX Designer (chat, sonnet) — analyse frontend, suggestions de design. Routé via
/w:ui-designer.
Le CEO choisit le worker via les commandes Telegram. Les specs font le pont entre workers : le Consultant les crée, le CEO les approuve, le Developer les exécute.
CEO (Telegram)
│
├── /c <message> ──► Sous-processus Consultant (Claude en lecture seule)
│ │
│ ├── Analyse le code, recherche sur le web
│ └── Produit des blocs SPEC
│ │
│ ▼
│ spec_queue.json (status: "draft")
│
├── /approve <id> ──► statut spec → "approved"
│ │
│ ▼
├── /d <message> ──► Developer (boucle GSD principale)
│ │
│ ├── Lit les specs approuvées depuis la file
│ ├── Implémente les changements
│ └── Marque la spec → "done"
│
├── /reject <id> ──► statut spec → "rejected"
│
└── /specs ──► Lister toutes les specs avec statut
Modèle de données
Interface Spec
interface Spec {
id: string; // nanoid(8), ex. "a1b2c3d4"
title: string; // Titre d'une ligne depuis le bloc SPEC
problem: string; // Quel problème ça résout
approach: string; // Approche technique (2-3 phrases)
acceptance: string[]; // Liste des critères d'acceptation
files: string[]; // Chemins des fichiers concernés
complexity: "low" | "medium" | "high";
status: SpecStatus;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
consultantThread?: string; // ID du thread qui a produit cette spec
}
type SpecStatus = "draft" | "approved" | "rejected" | "executing" | "done";
Machine d'états
IDÉE ──► CONSULTATION ──► SPEC DRAFT ──► REVUE ──► APPROUVÉE ──► EN EXÉCUTION ──► TERMINÉE
↑ │
+──── REJETÉE ────────────+
Transitions :
| De | Vers | Déclencheur |
|---|---|---|
| — | draft |
Le Consultant produit un bloc SPEC |
draft |
approved |
Le CEO envoie /approve <id> |
draft |
rejected |
Le CEO envoie /reject <id> |
rejected |
draft |
Le Consultant révise après feedback |
approved |
executing |
Le Developer commence l'implémentation |
executing |
done |
Le Developer remplit tous les critères d'acceptation |
Endpoints API
Tous les endpoints sont internes (routage master bot), pas HTTP API.
| Endpoint | Méthode | Auth | Description |
|---|---|---|---|
/c <msg> |
Telegram | CEO uniquement | Router le message vers le sous-processus Consultant |
/d <msg> |
Telegram | CEO uniquement | Router le message vers le Developer (boucle GSD) |
/approve <id> |
Telegram | CEO uniquement | Approuver une spec draft |
/reject <id> |
Telegram | CEO uniquement | Rejeter une spec draft avec feedback optionnel |
/specs |
Telegram | CEO uniquement | Lister toutes les specs avec filtre de statut |
/role |
Telegram | CEO uniquement | Afficher le rôle actif en cours |
| default | Telegram | CEO uniquement | Router vers le rôle actif (consultant ou developer) |
Routage des messages
Le master bot handleMessage() route les messages Telegram entrants :
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 unifié : getWorkerConfig(workerId) → callWorker(workerId, text, options). La config du worker détermine le modèle, les outils, le style de prompt et la catégorie de log. Le routage par défaut utilise active_role.json. Défaut initial : developer.
Composants frontend
WorkerPanel (Phase 26)
Dispatcher de panel générique. Affiche ChatPanelView ou TerminalPanelView selon worker.type.
ChatPanelView
Affiche les conversations des workers de type chat (Consultant, UI/UX Designer, etc.).
| Aspect | Détail |
|---|---|
| Source de données | /api/sse/logs/:name?category=${worker.log_category} |
| Affichage | Bulles de messages style chat (CEO à droite, Worker à gauche) |
| Détection SPEC | Parse les blocs ### SPEC:, les affiche sous forme de cartes avec boutons approve/reject |
| Entrée | Barre de saisie complète avec sélecteur de modèle, menu outils, entrée vocale, pièces jointes |
| Résultat Dev | Bouton pour voir la dernière réponse de terminal du developer |
TerminalPanelView
Affiche la sortie des workers de type terminal (Developer, etc.).
| Aspect | Détail |
|---|---|
| Source de données | /api/sse/logs/:name?category=${worker.log_category} |
| Affichage | Entrées de log monospace avec icônes d'outils, indicateurs de réflexion, état de traitement |
| Entrée | Barre de commande simple avec bouton Envoyer |
Hook useSSEStream
Hook SSE unifié pour les deux types de panel. Gère la déduplication (seenIds), la détection d'état de traitement et la transformation des entrées selon worker.type.
Barre de workers
Barre supérieure affichant tous les workers enregistrés sous forme de pills à bascule. Cliquer pour ajouter/retirer des panels. La disposition persiste dans localStorage par projet (citadel-workspace-active-${project.name}).
SpecPanel
Tableau de bord style Kanban affichant le cycle de vie des specs.
| Colonne | Specs affichées |
|---|---|
| Draft | status: "draft" — avec boutons approve/reject |
| Approuvée | status: "approved" — en attente du developer |
| En cours | status: "executing" — developer en train de travailler |
| Terminée | status: "done" — complétée |
| Rejetée | status: "rejected" — réduite par défaut |
RoleToggle
Indicateur visuel dans l'en-tête du pod projet affichant le rôle/worker actif.
Cliquer sur la bascule envoie les infos de commande /role ; le vrai changement de rôle se fait uniquement via les commandes Telegram (sécurité : pas de mutations depuis le frontend).
Modèle de sécurité
Restrictions d'outils par worker
Les outils des workers sont déclarés dans workers_registry.json. La fonction callWorker() lit worker.tools et passe --allowedTools au CLI Claude :
"tools": ["Read", "Glob", ...]— liste blanche explicite"tools": "all"— accès complet (Developer uniquement)
Ceci garantit que les workers de type chat (Consultant, UI Designer) NE PEUVENT PAS :
- Écrire ou modifier des fichiers
- Exécuter des commandes shell
- Déployer, committer ou pousser
Isolation des workers
- Chaque worker s'exécute dans son propre processus/thread Claude
- L'historique de conversation des workers est suivi dans la Map
workerThreads(clé par ID de worker) - Compat rétrograde : tableau
consultantThreadsynchronisé en parallèle avecworkerThreads - Seul le chatId CEO peut émettre les commandes
/approveet/reject
Structure de stockage
Tous les fichiers d'état sont stockés dans le répertoire de travail du projet sous state/ :
state/
├── active_role.json # { "role": "developer"|"consultant", "since": ISO8601 }
├── consultant_thread.json # { "threadId": "...", "startedAt": ISO8601 }
├── spec_queue.json # Tableau Spec[] — toutes les specs avec statut
└── logs/
├── consultant-YYYY-MM-DD.log # JSONL conversation consultant
└── specs-YYYY-MM-DD.log # JSONL transitions d'état des 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"
}
]
Registry des workers
Tous les workers déclarés dans 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
}
]
}
| Champ | Type | Description |
|---|---|---|
id |
string | Identifiant unique du worker (utilisé dans le routage, le stockage, le SSE) |
label |
string | Nom d'affichage dans l'UI |
icon |
string | Icône emoji pour pill/header |
type |
"chat" | "terminal" |
Type de panel : bulles chat vs logs monospace |
model |
string | ID du modèle Claude |
max_turns |
number | Nombre max de tours agentiques par invocation |
tools |
string[] | "all" |
Liste blanche d'outils autorisés, ou "all" pour l'accès complet |
system_prompt_skill |
string | null | Chemin vers le system prompt dans le répertoire skills/ |
prompt_style |
"history" | "gsd" |
Style de conversation (append history vs prompt GSD) |
output_format |
"text" | "stream-json" |
Mode de gestion des sorties |
focus_dirs |
string[] | Répertoires sur lesquels se concentrer (injectés dans le prompt) |
log_category |
string | Catégorie de log JSONL (correspond au paramètre SSE ?category=) |
builtin |
boolean | Si ce worker est livré avec Arc OS |
Configuration des rôles
Configuration de rôle par projet dans config/project_roles.json :
{
"version": 1,
"defaults": {
"activeRole": "developer",
"specAutoApprove": false,
"maxDraftSpecs": 20
}
}
Logging
Toutes les interactions consultant et transitions de specs sont enregistrées en format JSONL.
Entrées 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"]}
Entrées 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"}
Maintenu par Rick (Orchestrateur). Phase 26 — Workers Dynamiques.