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 :

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 :

Ceci garantit que les workers de type chat (Consultant, UI Designer) NE PEUVENT PAS :

Isolation des workers

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.