[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fgOUuG4HqcB3Q7zEwCm4g-vONB3sqxzSS6Zy81FyhppQ":3,"$fk1oB-e5O_BgwF2c-QwDgsx2Y7SMPhf77z9g2mrpoBuM":23},{"chapter":4,"prev":13,"next":18},{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10,"contentHtml":11,"contentHtmlEn":12},"agentic-core","03","Le cœur agentique : Atlas et les agents","The Agentic Core: Atlas and the Agents","Décrit l'architecture du moteur agentique : classification LLM des emails entrants, spawn headless de Claude Code via pseudo-TTY, injection des personas agents et orchestration post-spawn (deploy, QA, récap).","Describes the architecture of the agentic engine: LLM classification of incoming emails, headless spawning of Claude Code via pseudo-TTY, injection of agent personas, and post-spawn orchestration (deploy, QA, recap).","\u003Ch2 id=\"coeur-agentique-vue-ensemble\">Le cœur agentique : orchestrateur et agents\u003C\u002Fh2>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>À quoi sert cette section\u003C\u002Fstrong> — Elle décrit le moteur qui transforme un email entrant (ou une tâche de chantier) en action exécutée : classification d'intention par LLM, lancement d'un session de code en mode headless via un pseudo-terminal, injection de la persona de l'agent désigné, et orchestration post-exécution (déploiement préprod, contrôle qualité, email récapitulatif).\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"vue-ensemble\">1. Vue d'ensemble\u003C\u002Fh2>\n\n\u003Cp>Le cœur agentique repose sur deux briques principales :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>L'orchestrateur\u003C\u002Fstrong> (agent de direction, codename public : \u003Cstrong>Atlas\u003C\u002Fstrong>). Il n'a pas de runtime propre : « être Atlas » consiste à démarrer une session de code assisté par LLM avec un prompt système dédié. Son pilotage est assuré par un ensemble de modules Python et un script Node coordinateur.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Les agents spécialisés\u003C\u002Fstrong> (une trentaine au total, répartis en familles fonctionnelles) : des personas — identité, cadre cognitif, périmètre métier — injectées dans le contexte d'un LLM au moment d'exécuter une tâche.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Le flux principal, appelé \u003Cstrong>pipeline Atlas Inbox\u003C\u002Fstrong>, se déroule comme suit :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Email forwardé → boîte Atlas\n        │\n        ▼  (ingestion → table messages + ligne de suivi status='received')\n ┌──────────────────────────┐\n │ Classificateur d'intent  │  intent ∈ {run, chantier, question,\n │  (LLM via façade)        │           noise, negociation, conseil}\n └──────────────────────────┘  → status='classified'\n        │                        + matérialise run \u002F question \u002F\n        │                          negociation \u002F conseil selon l'intent\n        ▼\n ┌──────────────────────────┐\n │ Module de spawn          │  whitelist {run, chantier, question, negociation}\n │  → coordinateur Node     │  lance session code headless via pseudo-TTY\n │    (stream JSON)         │  + prompt système Atlas (CODE + COMMIT uniquement)\n └──────────────────────────┘\n        │  La session écrit un fichier résultat temporaire puis se termine\n        ▼\n ┌──────────────────────────┐\n │ Orchestration post-spawn │  déploiement préprod → QA Playwright →\n │                          │  email récap Atlas → UPDATE 'actioned'\n └──────────────────────────┘  (re-spawn max 3 itérations si QA échoue)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Étape d'ingestion\u003C\u002Fstrong> — La transition « email reçu → ligne de suivi » est assurée par un module de scrutation IMAP. Ce module interroge la boîte Atlas via la façade de messagerie du vaisseau-mère, effectue une recherche des messages non lus, puis pour chaque message : insère une ligne dans la table de messages (opération idempotente grâce à une contrainte d'unicité sur l'identifiant de message) et crée une ligne de suivi au statut \u003Ccode>received\u003C\u002Fcode>, avant de marquer le message comme lu. Il \u003Cstrong>enchaîne automatiquement\u003C\u002Fstrong> le scan antivirus puis la classification — c'est cet enchaînement, et non un scheduler externe désactivé, qui boucle le pipeline de bout en bout.\u003C\u002Fp>\n\n\u003Ch2 id=\"modele-agents\">2. Modèle d'agents\u003C\u002Fh2>\n\n\u003Ch3>2.1 Stockage\u003C\u002Fh3>\n\n\u003Cp>Les agents sont stockés dans une table centrale du schéma principal. Une vue de compatibilité, portant un nom hérité d'une migration antérieure (\u003Cem>pattern Strangler Fig\u003C\u002Fem>), expose les mêmes colonnes. Le code Python lit via cette vue ; l'interface web lit directement la table via l'ORM. Les deux surfaces sont strictement équivalentes.\u003C\u002Fp>\n\n\u003Ch3>2.2 Colonnes clés\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Colonne\u003C\u002Fth>\n      \u003Cth>Type\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>codename\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Clé de lookup, kebab-case unique (ex. \u003Ccode>orchestrator\u003C\u002Fcode>, \u003Ccode>backend\u003C\u002Fcode>, \u003Ccode>securite\u003C\u002Fcode>, \u003Ccode>seo-technique\u003C\u002Fcode>).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>nickname\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Alias humain public (ex. Atlas, Gauss, Mitnick, Otlet).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>role\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>Intitulé de poste (ex. « SEO Technical Officer »).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>group_name\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Famille fonctionnelle : \u003Ccode>direction\u003C\u002Fcode>, \u003Ccode>cadrage\u003C\u002Fcode>, \u003Ccode>execution\u003C\u002Fcode>, \u003Ccode>validation\u003C\u002Fcode>.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>orbite\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>Anneau d'orbite (1, 2 ou 3). ⚠️ Ne coïncide pas toujours avec l'anneau affiché visuellement (voir section 7).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>heritage\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>Ancrage géographique\u002Fhistorique de la persona (ex. « Grèce antique » pour Atlas, « États-Unis, 1963 » pour Mitnick).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>cognitive_frame\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>« Façon de penser » — bloc injecté en priorité dans le briefing (les 4 dimensions cognitives).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>personality\u003C\u002Fcode>, \u003Ccode>quote\u003C\u002Fcode>, \u003Ccode>inspiration\u003C\u002Fcode>, \u003Ccode>proof\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Identité riche (tier \u003Ccode>full\u003C\u002Fcode> uniquement).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>job_mission\u003C\u002Fcode>, \u003Ccode>job_perimeter\u003C\u002Fcode>, \u003Ccode>job_key_checks\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Scope métier (tier \u003Ccode>metier\u003C\u002Fcode> et supérieur).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>content_md\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Profil markdown long, consommé par le module d'appel agent legacy.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>active\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>1 = agent recrutable.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>prompt_domains\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>CSV de domaines de prompt (contenu, faq, cover…) pour le routage vers les agents de génération.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>auto_spawn\u003C\u002Fcode>, \u003Ccode>spawn_count\u003C\u002Fcode>, \u003Ccode>error_count\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>Compteurs runtime.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Des colonnes d'internationalisation EN sont présentes pour les champs narratifs : \u003Ccode>role_en\u003C\u002Fcode>, \u003Ccode>heritage_en\u003C\u002Fcode>, \u003Ccode>cognitive_frame_en\u003C\u002Fcode>, etc.\u003C\u002Fp>\n\n\u003Ch3>2.3 Tables satellites\u003C\u002Fh3>\n\n\u003Cp>Plusieurs tables satellites gravitent autour de la table principale des agents : activité, événements, heartbeats, relations inter-agents, compétences, outils, expérience et historique d'expérience. Certaines de ces tables disposent d'une vue de compatibilité héritée (activité, heartbeat, relations, expérience et historique) ; d'autres n'en ont pas (événements, compétences, outils). Le manifeste du domaine agents liste la surface complète des tables et routes API exposées.\u003C\u002Fp>\n\n\u003Ch3>2.4 Chargement de la persona\u003C\u002Fh3>\n\n\u003Cp>Avant qu'un LLM exécute une tâche, il « devient » l'agent désigné via un mécanisme d'assemblage de contexte en trois couches :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Identité + cadre cognitif + scope métier\u003C\u002Fstrong> issus de la table des agents.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Cicatrices pertinentes\u003C\u002Fstrong> par recherche sémantique vectorielle (RAG pgvector) sur la table des embeddings, en priorisant les cicatrices propres à l'agent concerné avant de compléter avec les cicatrices globales. Objectif explicite : éviter qu'un agent voie en priorité les leçons d'un autre.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Lettre de mission du chantier\u003C\u002Fstrong> si le chantier est résolvable depuis la tâche.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Préférences utilisateur\u003C\u002Fstrong> (profil opérateur, tranche \u003Ccode>core\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Trois \u003Cstrong>tiers de rendu\u003C\u002Fstrong> sont calibrés par budget tokens, afin de limiter la dilution d'attention et de maximiser le hit du cache prompt (TTL ~5 min) :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tier\u003C\u002Fth>\n      \u003Cth>Budget approx.\u003C\u002Fth>\n      \u003Cth>Contenu inclus\u003C\u002Fth>\n      \u003Cth>Usage typique\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Core\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~150 tokens\u003C\u002Ftd>\n      \u003Ctd>Cadre cognitif seul + identité minimale\u003C\u002Ftd>\n      \u003Ctd>Fast-path sans code, sous-agents de type Task\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Métier\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~350 tokens\u003C\u002Ftd>\n      \u003Ctd>Core + rôle + scope métier\u003C\u002Ftd>\n      \u003Ctd>Défaut pour le spawn Atlas complet et le worker de tâches\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Full\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~600 tokens\u003C\u002Ftd>\n      \u003Ctd>Métier + heritage + citation + personnalité + inspiration + preuve\u003C\u002Ftd>\n      \u003Ctd>Revue, drill, lettre de mission\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Un alias de rétrocompatibilité expose le tier \u003Cem>métier\u003C\u002Fem> sous le nom générique \u003Ccode>format_briefing\u003C\u002Fcode> pour les appels anciens.\u003C\u002Fp>\n\n\u003Ch3>2.5 Note sur le module d'appel agent legacy\u003C\u002Fh3>\n\n\u003Cp>Un module d'appel agent plus ancien permet d'invoquer un agent en subprocess et de journaliser l'échange dans le réacteur. \u003Cstrong>⚠️ Ce module porte une dette technique notable\u003C\u002Fstrong> : ses helpers de chargement des cicatrices et des doctrines se connectent encore à une base de données MySQL héritée — vestige antérieur à la migration vers PostgreSQL. Les exceptions y sont silencieusement avalées, ce qui signifie que l'appel ne plante pas mais peut retourner des profils sans cicatrices ni doctrines.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point d'attention à confirmer\u003C\u002Fstrong> — Ce chemin legacy est-il encore invoqué en production, ou a-t-il été supplanté par le pipeline de spawn Atlas associé au module de persona actuel ? Le commentaire interne du module indique une migration en cours vers la table d'agents PostgreSQL, mais les deux helpers de cicatrices\u002Fdoctrines pointent toujours l'ancienne source MySQL.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"atlas-orchestrateur-spawn\">Atlas l'orchestrateur — le moteur de spawn\u003C\u002Fh2>\n\n\u003Ch3>Rôle restreint de l'agent spawné\u003C\u002Fh3>\n\n\u003Cp>Le prompt système transmis à l'agent spawné cadre strictement son périmètre d'action : \u003Cstrong>investigation, écriture de code et commit\u003C\u002Fstrong>. L'agent ne déclenche pas de déploiement, n'envoie pas d'email, et ne met pas à jour le registre des messages entrants — ces opérations sont toutes orchestrées par le moteur Python après la fin de l'exécution de l'agent.\u003C\u002Fp>\n\n\u003Cp>Des garde-fous de périmètre sont inscrits dans le prompt système : les modifications sont autorisées uniquement dans les zones hub, back-office et tenant concerné ; jamais dans un composant partagé public.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Instruction « checkpoint tôt »\u003C\u002Fstrong> (introduite mi-2026) : le prompt système impose désormais à l'agent d'écrire un fichier de résultat intermédiaire \u003Cem>le plus tôt possible\u003C\u002Fem> — dès le premier diagnostic partiel — et de l'écraser après chaque avancée majeure, plutôt que d'attendre la fin complète du travail. La raison est simple : si l'agent est interrompu par le délai d'expiration, le dernier état persisté sert de salvage pour prévenir l'équipe. Un fichier partiel vaut mieux que rien. Cette instruction fait suite à un incident où le fichier de résultat n'était pas encore écrit au moment où le processus a été tué.\u003C\u002Fp>\n\n\u003Ch3>Sélection des candidats au spawn\u003C\u002Fh3>\n\n\u003Cp>Le moteur de sélection interroge le registre des messages entrants et retient uniquement les entrées qui réunissent les trois conditions suivantes :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>statut \u003Ccode>classified\u003C\u002Fcode> ;\u003C\u002Fli>\n  \u003Cli>intention classifiée parmi : \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode> ;\u003C\u002Fli>\n  \u003Cli>absence d'audit de spawn récent (démarré, terminé ou échoué dans la dernière heure) — garantie d'idempotence anti-double-spawn.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Constantes de sécurité appliquées :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Intents autorisés\u003C\u002Fstrong> : \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode> — les intents \u003Ccode>noise\u003C\u002Fcode> et \u003Ccode>conseil\u003C\u002Fcode> sont exclus du spawn.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Maximum de spawns par cycle\u003C\u002Fstrong> : 3, pour limiter les coûts en cas de dérive.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Délai d'expiration par spawn\u003C\u002Fstrong> : 40 minutes. Ce délai a été relevé (depuis 25 minutes) à la suite d'un incident où une investigation sur un environnement de production complexe était coupée juste avant l'écriture du livrable — le spawn était marqué échoué alors que le correctif était déjà en place. Cette correction a été déployée conjointement avec l'instruction « checkpoint tôt ».\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Kill-switch\u003C\u002Fstrong> : une variable d'environnement permet de désactiver entièrement le moteur de spawn sans toucher au code.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Modèle par défaut\u003C\u002Fstrong> : Sonnet.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note de vigilance\u003C\u002Fstrong> : le commentaire d'en-tête du module principal est périmé — il annonce encore une liste blanche à deux intents, un délai d'expiration de 10 minutes et un offset cron différent. Ce sont les \u003Cstrong>constantes effectives du code\u003C\u002Fstrong> qui font foi : quatre intents autorisés, délai de 40 minutes. Cette page utilise les valeurs réelles ; ne pas se fier au commentaire de tête du fichier.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Anti-race : verrou consultatif de base de données\u003C\u002Fh3>\n\n\u003Cp>Pour couvrir la collision entre le cron toutes les 5 minutes et une invocation manuelle simultanée, le moteur acquiert un verrou consultatif au niveau session sur l'identifiant du message traité avant tout spawn. Ce verrou est maintenu via une connexion interactive distincte et libéré automatiquement à sa fermeture ou à la mort du processus. Si le verrou est déjà tenu par une autre instance, le message est ignoré pour ce cycle et un audit \u003Ccode>spawn_skipped_lock_held\u003C\u002Fcode> est enregistré.\u003C\u002Fp>\n\n\u003Ch3>Construction du prompt\u003C\u002Fh3>\n\n\u003Cp>Le prompt transmis à l'agent est construit à partir de trois sources :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>l'email forwardé (expéditeur, sujet, corps tronqué à 6 000 caractères) ;\u003C\u002Fli>\n  \u003Cli>l'identité du tenant, résolue par heuristique de domaine (table de correspondance explicite en priorité, puis repli sur le contact enregistré) ;\u003C\u002Fli>\n  \u003Cli>la sortie du classificateur d'intents (intention, niveau de confiance, raisonnement du modèle).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>En cas d'échec du contrôle qualité (QA), un prompt de re-spawn est construit en ajoutant les commits précédents et les erreurs détectées (erreurs HTTP niveau couche applicative, erreurs navigateur niveau interface).\u003C\u002Fp>\n\n\u003Ch3>Livrable contractuel de l'agent\u003C\u002Fh3>\n\n\u003Cp>Avant de terminer son exécution, l'agent doit impérativement écrire un fichier de résultat structuré contenant : le statut (\u003Ccode>ok\u003C\u002Fcode> ou non), le tenant concerné, les routes à soumettre au QA, la liste des commits réalisés, un résumé en markdown et un brouillon de réponse à l'utilisateur. Ce fichier constitue le contrat lu par l'orchestrateur Python pour décider des actions post-spawn.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"invocation-node-pty\">Invocation programmatique via pseudo-TTY\u003C\u002Fh2>\n\n\u003Cp>Règle fondamentale du harness : un agent Claude lancé programmatiquement passe obligatoirement par \u003Cstrong>un vrai pseudo-TTY\u003C\u002Fstrong> via un wrapper Node.js dédié — jamais par un appel subprocess Python direct.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Pourquoi\u003C\u002Fstrong> : lancer \u003Ccode>claude\u003C\u002Fcode> en mode non-interactif depuis un subprocess Python provoque un \u003Cstrong>blocage silencieux\u003C\u002Fstrong> — deadlock de buffer et\u002Fou détection du mode non-TTY qui refuse le prompt en argument. Un pseudo-TTY de type \u003Ccode>xterm-color\u003C\u002Fcode> contourne les deux pièges.\u003C\u002Fp>\n\n\u003Ch3>Chaîne d'appel\u003C\u002Fh3>\n\n\u003Cp>Le moteur Python écrit le prompt et le prompt système dans des fichiers temporaires, puis délègue l'exécution au wrapper Node.js en lui passant les chemins de ces fichiers, l'identifiant du spawn, le modèle cible et le délai d'expiration. L'appel ressemble schématiquement à :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>node un composant interne \\\n  --id &lt;ID&gt; \\\n  --prompt-file &lt;fichier-prompt&gt; \\\n  --system-prompt-file &lt;fichier-système&gt; \\\n  --model sonnet \\\n  --stream-log &lt;fichier-log&gt; \\\n  --add-dir &lt;répertoire-projet&gt; \\\n  --timeout-sec 2400\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Le wrapper Node\u003C\u002Fh3>\n\n\u003Cp>Le wrapper Node.js (dépendance \u003Ccode>node-pty\u003C\u002Fcode>) gère les points suivants :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Localisation du binaire Claude sur la machine hôte.\u003C\u002Fli>\n  \u003Cli>Construction des arguments passés à Claude : mode non-interactif, répertoire de travail autorisé, sans persistance de session, format de sortie stream-JSON verbeux avec messages partiels et événements de hooks, modèle et prompt système en append.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Ordre critique des arguments\u003C\u002Fstrong> : l'option \u003Ccode>--add-dir\u003C\u002Fcode> doit impérativement être placée avant les autres flags — sinon son comportement varargs « avale » le prompt comme répertoire. Cette contrainte a coûté 30 minutes de débogage lors d'un incident passé.\u003C\u002Fli>\n  \u003Cli>Spawn du pseudo-TTY avec des dimensions généreuses (200 colonnes × 50 lignes).\u003C\u002Fli>\n  \u003Cli>Injection des variables d'environnement nécessaires aux hooks Stop de l'agent (coordonnées base de données, contexte de worker) — le mot de passe de base de données est hérité de l'environnement parent, jamais codé en dur.\u003C\u002Fli>\n  \u003Cli>Émission de chaque événement stream-JSON en JSONL sur stdout (avec tee vers un fichier de log), détection de l'événement de résultat final.\u003C\u002Fli>\n  \u003Cli>Codes de sortie : \u003Ccode>0\u003C\u002Fcode> si succès, \u003Ccode>1\u003C\u002Fcode> si erreur, \u003Ccode>2\u003C\u002Fcode> si arguments invalides. Un événement wrapper final de type \u003Ccode>node_pty_exit\u003C\u002Fcode> est émis pour le processus Python parent.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Persistance live des événements\u003C\u002Fh3>\n\n\u003Cp>Côté Python, chaque ligne JSONL reçue est parsée et persistée en temps réel dans le journal des événements de spawn. Chaque enregistrement porte : un numéro de séquence, un type d'événement (sous-typé selon qu'il s'agit d'un événement de stream, d'un événement système ou d'un résultat d'outil), le nom de l'outil éventuellement impliqué, et le payload complet en JSON.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note d'architecture\u003C\u002Fstrong> : la table de journal des événements est en réalité une \u003Cstrong>vue simplement actualisable\u003C\u002Fstrong> posée sur une table physique sous-jacente — le même pattern Strangler-Fig que pour le registre des agents. Les insertions à travers la vue fonctionnent normalement sur les colonnes exposées.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>Cette persistance en temps réel permet de diagnostiquer un blocage (le dernier événement visible en base révèle où l'agent est bloqué) et d'agréger coût, durée et tokens depuis l'événement de résultat final. Tout est également audité dans le journal d'audit central.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"classification-intents\">Classification des intents\u003C\u002Fh2>\n\n\u003Ch3>Énumération des intents\u003C\u002Fh3>\n\n\u003Cp>Le classificateur reconnaît six intents valides. Le tableau suivant décrit leur sémantique et leur effet en aval :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Intent\u003C\u002Fth>\n      \u003Cth>Sens\u003C\u002Fth>\n      \u003Cth>Effet en aval\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>run\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Tâche atomique exécutable en une seule commande\u003C\u002Ftd>\n      \u003Ctd>Création ou mise à jour d'un run dans le registre des runs\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>chantier\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Mission structurée multi-étapes (squelette + agents)\u003C\u002Ftd>\n      \u003Ctd>Création d'un brouillon de chantier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>question\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Demande d'avis ou d'analyse, réponse en brouillon\u003C\u002Ftd>\n      \u003Ctd>Création ou mise à jour d'une question dans le registre\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>noise\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Newsletter, spam, rien d'actionnable\u003C\u002Ftd>\n      \u003Ctd>Aucune action\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>negociation\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Demande commerciale entrante (prospect, devis)\u003C\u002Ftd>\n      \u003Ctd>Insertion d'une négociation dans le registre commercial\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>conseil\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Demande d'avis ou d'expertise client sur un élément externe existant\u003C\u002Ftd>\n      \u003Ctd>Insertion d'un conseil dans le registre dédié\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Chaque matérialisation est mutuellement exclusive et idempotente : des index uniques et des gardes conditionnels empêchent la création de doublons même en cas d'appel répété. L'identifiant créé est tracé dans l'enregistrement du message d'origine.\u003C\u002Fp>\n\n\u003Ch3>Moteur LLM\u003C\u002Fh3>\n\n\u003Cp>La classification s'appuie sur la \u003Cstrong>façade de fournisseur IA\u003C\u002Fstrong> — couche d'abstraction centralisée — jamais sur un appel SDK direct. Le routage du modèle est résolu dynamiquement par configuration ; le fournisseur et le modèle par défaut sont Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode>. La sortie est demandée en JSON strict, avec un délai d'expiration de 30 secondes.\u003C\u002Fp>\n\n\u003Ch3>Garde-fous\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Anti-prompt-injection\u003C\u002Fstrong> : le corps de l'email est sandwiché entre deux délimiteurs explicites et précédé d'un avertissement indiquant au modèle qu'il s'agit de données email, non d'instructions. Le corps est tronqué à 8 000 caractères.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Énumération stricte\u003C\u002Fstrong> : tout intent hors des six valeurs reconnues est rejeté, quelle que soit la réponse du modèle. Tout niveau de confiance hors de l'intervalle [0, 1] est également rejeté.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Auto-floor confiance\u003C\u002Fstrong> : une confiance inférieure à 0,5 force l'intent à \u003Ccode>noise\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Seuil d'incertitude\u003C\u002Fstrong> : une confiance inférieure à 0,7 déclenche un audit \u003Ccode>classification_uncertain\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Court-circuit pré-LLM\u003C\u002Fstrong> : si le fondateur re-forwarde un récapitulatif Atlas (sujet commençant par \u003Ccode>Fwd: [Atlas]…\u003C\u002Fcode>), l'intent est forcé à \u003Ccode>noise\u003C\u002Fcode> sans appel LLM, pour éviter un spawn coûteux à vide.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Scan antivirus en premier\u003C\u002Fstrong> : la classification est bloquée si une pièce jointe présente un verdict autre que \u003Ccode>clean\u003C\u002Fcode> — doctrine « scanner avant d'ouvrir ».\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Garde-fou faux-noise avec pièce jointe du fondateur\u003C\u002Fstrong> : si un email est classé \u003Ccode>noise\u003C\u002Fcode> mais provient du fondateur \u003Cem>et\u003C\u002Fem> comporte des pièces jointes, l'intent est reclassifié en \u003Ccode>question\u003C\u002Fcode>. Rationale : le contenu actionnable peut se trouver dans une capture d'écran que le modèle textuel ne voit pas. Un audit \u003Ccode>noise_override_founder_attachment\u003C\u002Fcode> est enregistré.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Garde-fou client existant\u003C\u002Fstrong> : si un email est classé \u003Ccode>negociation\u003C\u002Fcode> ou \u003Ccode>conseil\u003C\u002Fcode> mais que l'adresse expéditrice correspond à un tenant actif, l'intent est rabaissé en \u003Ccode>run\u003C\u002Fcode> scopé au tenant concerné. Les emails clients ne sont pas des nouvelles négociations commerciales. Un audit \u003Ccode>existing_client_guard\u003C\u002Fcode> est enregistré.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>En fin de classification, le registre des messages entrants est mis à jour avec l'intent, le niveau de confiance, le modèle utilisé, le raisonnement du classificateur et le statut \u003Ccode>classified\u003C\u002Fcode>. Un audit \u003Ccode>classified\u003C\u002Fcode> est enregistré.\u003C\u002Fp>\n\n\u003Ch3>Enrichissement OCR des pièces jointes\u003C\u002Fh3>\n\n\u003Cp>Avant l'appel LLM, si des pièces jointes sont présentes et que le verdict antivirus est \u003Ccode>clean\u003C\u002Fcode>, un module d'extraction de texte par reconnaissance optique de caractères (OCR) est invoqué sur le répertoire temporaire des pièces jointes. Le moteur utilisé est Tesseract en local — aucun appel cloud.\u003C\u002Fp>\n\n\u003Cp>Le texte extrait est injecté dans le contexte du prompt sandwich, entre ses propres délimiteurs (\u003Ccode>IMAGE_OCR_TEXT_START\u003C\u002Fcode> \u002F \u003Ccode>END\u003C\u002Fcode>), après le corps de l'email. La configuration du moteur OCR est résolue par le système de routage dynamique.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Important — données personnelles\u003C\u002Fstrong> : le texte OCR extrait n'est \u003Cem>jamais\u003C\u002Fem> persisté en base de données (risque de données personnelles — usage mémoire uniquement dans le cycle de classification). Seul un audit \u003Ccode>ocr_extracted\u003C\u002Fcode> est enregistré, portant le nombre de caractères extraits et le nombre approximatif d'images traitées.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Raison d'être\u003C\u002Fstrong> : le modèle textuel ne voit pas les captures d'écran jointes aux emails. Sans OCR, des tickets contenant des captures d'écran de problèmes étaient faussement classés \u003Ccode>noise\u003C\u002Fcode>, bloquant tout traitement automatique. L'OCR Tesseract local extrait le texte visible sans coût supplémentaire ni fuite de données vers un service tiers.\u003C\u002Fp>\n\u003Ch2 id=\"routeurs-ia-paralleles\">Les deux routeurs IA parallèles\u003C\u002Fh2>\n\n\u003Cp>Le système fait tourner \u003Cstrong>deux moteurs de routage IA multi-fournisseur distincts\u003C\u002Fstrong>, qui ne partagent ni code ni configuration. Ce n'est pas un doublon accidentel : ils servent deux environnements d'exécution différents. Il faut savoir lequel modifier selon le contexte de développement.\u003C\u002Fp>\n\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Axe\u003C\u002Fth>\n\u003Cth>Routeur Python (automates)\u003C\u002Fth>\n\u003Cth>Routeur TypeScript (hub Nuxt)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Runtime\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Automates planifiés (crons, rêve, rappel, classifieur, apprentissage…)\u003C\u002Ftd>\n\u003Ctd>Runtime Nitro\u002FNuxt du hub (points d'entrée API serveur)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Surface exposée\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>embed(…)\u003C\u002Fcode> + \u003Ccode>complete(…)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Génération de contenu + résolution du fournisseur par client\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Fournisseurs\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Embeddings : Mistral \u002F Voyage \u002F OpenAI ; complétion : Claude \u002F Mistral \u002F OpenAI\u003C\u002Ftd>\n\u003Ctd>Mistral \u002F Anthropic \u002F OpenAI\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Source du routage\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Fichier YAML de configuration de routage IA\u003C\u002Ftd>\n\u003Ctd>Par requête (fournisseur explicite) ou par configuration client\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Défaut\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Embeddings → Mistral ; complétion → Claude\u003C\u002Ftd>\n\u003Ctd>Mistral (souveraineté données FR\u002FEU)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Appel au fournisseur Claude\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Délégué au mécanisme d'invocation d'agent (sous-processus)\u003C\u002Ftd>\n\u003Ctd>Requête HTTP directe via une fonction utilitaire dédiée\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Doctrine\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Passage obligatoire par la façade de routage validée\u003C\u002Ftd>\n\u003Ctd>Service interne du module Nuxt, sans façade gardée par hook\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n\u003Cp>Le détail complet du routeur Python (embeddings, complétion, fichier YAML, relances, garde-fous) est documenté dans le chapitre Mémoire &amp; apprentissage. Cette section décrit le pendant \u003Cstrong>TypeScript\u003C\u002Fstrong>, symétriquement.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>La fonction de génération — point d'entrée unique du runtime Nuxt\u003C\u002Fh3>\n\n\u003Cp>La fonction de génération de contenu est le \u003Cstrong>point d'entrée unique\u003C\u002Fstrong> du runtime Nuxt pour produire du texte via IA. Elle accepte en entrée : un \u003Ccode>prompt\u003C\u002Fcode>, un \u003Ccode>systemPrompt\u003C\u002Fcode> optionnel, un \u003Ccode>provider\u003C\u002Fcode> optionnel, un \u003Ccode>model\u003C\u002Fcode> optionnel, un nombre maximum de tokens (défaut 4096) et une température (défaut 0,7). La réponse est normalisée et expose : le contenu généré, le fournisseur qui a \u003Cem>effectivement\u003C\u002Fem> répondu après éventuel basculement, le modèle utilisé, le décompte de tokens (entrée\u002Fsortie) et la durée en millisecondes.\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Fournisseur par défaut = Mistral\u003C\u002Fstrong> — choix de souveraineté (données FR, API européenne), cohérent avec le défaut embeddings du routeur Python.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Modèles par défaut\u003C\u002Fstrong> : Mistral → \u003Ccode>mistral-large-latest\u003C\u002Fcode> ; Anthropic → \u003Ccode>claude-sonnet-4-6\u003C\u002Fcode> ; OpenAI → \u003Ccode>gpt-4o\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Basculement automatique (ordre souverain)\u003C\u002Fh3>\n\n\u003Cp>L'ordre de basculement est : \u003Cstrong>Mistral → Anthropic → OpenAI\u003C\u002Fstrong>. À l'appel, le système construit l'ordre effectif en plaçant le fournisseur demandé en tête, puis itère selon la logique suivante :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>générerContenu(requête)\n   fournisseur effectif = requête.provider ?? 'mistral'\n   ordre = [fournisseur, ...ordre_souverain \\ fournisseur]\n        │\n        ▼   pour chaque fournisseur de l'ordre :\n   clé API absente ? ──oui──► passer (fournisseur suivant, silencieux)\n        │ non\n        ▼\n   appelerFournisseur()  ──succès──► retour { provider, durationMs, … }\n        │ exception\n        ▼\n   dernier de l'ordre ? ──oui──► erreur « Tous les fournisseurs IA sont indisponibles »\n        │ non\n        ▼\n   journaliser le basculement → fournisseur suivant\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Deux conditions déclenchent le passage au fournisseur suivant :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Clé API absente\u003C\u002Fstrong> : le système saute silencieusement le fournisseur concerné.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Exception à l'appel\u003C\u002Fstrong> : le système bascule sur le suivant et journalise l'événement.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Lors d'un basculement, le modèle utilisé est réalligné sur le modèle par défaut du fournisseur de secours — et \u003Cstrong>non\u003C\u002Fstrong> sur le modèle demandé pour le fournisseur initial. Le fournisseur retourné dans la réponse reflète toujours qui a réellement répondu, ce qui est utile à journaliser côté appelant.\u003C\u002Fp>\n\n\u003Cp>Les trois fournisseurs sont appelés via des fonctions distinctes : Mistral et OpenAI par requête HTTP directe (timeout 120 s, format messages identique avec \u003Ccode>system\u003C\u002Fcode> optionnel + \u003Ccode>user\u003C\u002Fcode>) ; Anthropic via une fonction utilitaire dédiée.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point à confirmer :\u003C\u002Fstrong> la fonction utilitaire Anthropic est référencée dans le code mais son implémentation n'a pas été localisée dans le périmètre du module. Soit elle est résolue par auto-import Nitro depuis un utilitaire non indexé, soit c'est une dette latente qui ferait échouer la branche Anthropic au runtime. Le basculement Mistral → Anthropic n'est donc \u003Cstrong>pas garanti opérationnel\u003C\u002Fstrong> sans vérification préalable.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Clés API (anti-fuite)\u003C\u002Fh3>\n\n\u003Cp>Les clés API sont résolues exclusivement par nom de variable d'environnement ; aucune valeur n'apparaît dans le code ni dans cette documentation. Les variables concernées sont : la clé API Mistral, la clé API Anthropic et la clé API OpenAI — toutes stockées dans les fichiers d'environnement exclus du contrôle de version.\u003C\u002Fp>\n\n\u003Cp>Une clé absente n'est \u003Cstrong>pas\u003C\u002Fstrong> une erreur bloquante : elle entraîne simplement le saut silencieux du fournisseur correspondant dans la boucle de basculement.\u003C\u002Fp>\n\n\u003Ch3>Choix du fournisseur par client — résolution de la configuration tenant\u003C\u002Fh3>\n\n\u003Cp>La fonction de résolution par client lit la configuration IA stockée dans la table de configuration des clients (colonne JSON) et retourne le couple \u003Ccode>{ provider, model }\u003C\u002Fcode>. La logique d'extraction est :\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Fournisseur IA\u003C\u002Fstrong> configuré → défaut \u003Ccode>mistral\u003C\u002Fcode> si absent ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Modèle IA\u003C\u002Fstrong> configuré → défaut sur le modèle par défaut du fournisseur si absent.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Toute erreur (client inconnu, JSON invalide, helper en échec) retombe silencieusement sur Mistral avec son modèle par défaut. Le filtre \u003Ccode>active=1\u003C\u002Fcode> garantit qu'un client inactif n'est jamais retourné, même si sa configuration renseigne un fournisseur.\u003C\u002Fp>\n\n\u003Cp>État observé : la majorité des clients ont le champ fournisseur IA absent de leur configuration (→ Mistral souverain par défaut) ; seuls quelques-uns le renseignent explicitement.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point à confirmer :\u003C\u002Fstrong> la fonction de résolution par client n'a \u003Cstrong>aucun appelant identifié\u003C\u002Fstrong> dans le périmètre du module. Les deux consommateurs réels de la fonction de génération passent le fournisseur en dur dans la requête et n'interrogent jamais la configuration client. Le routage par-tenant est donc \u003Cstrong>câblé mais pas encore branché\u003C\u002Fstrong> sur le flux de génération.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Appelants réels de la fonction de génération\u003C\u002Fh3>\n\n\u003Cp>Deux points d'entrée Nuxt consomment aujourd'hui le gateway IA :\u003C\u002Fp>\n\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Point d'entrée\u003C\u002Fth>\n\u003Cth>Usage\u003C\u002Fth>\n\u003Cth>Fournisseur demandé\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Génération du brouillon de réponse client (pipeline Atlas)\u003C\u002Ftd>\n\u003Ctd>Rédige la réponse client d'une run Atlas terminée (vouvoiement, 3–6 phrases, signature d'équipe)\u003C\u002Ftd>\n\u003Ctd>Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Espace de dialogue multi-agents (client)\u003C\u002Ftd>\n\u003Ctd>Perspectives multi-agents sur des notes de support, puis réponse du lead\u003C\u002Ftd>\n\u003Ctd>Notes : Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode> ; réponse lead : Anthropic\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Lecture importante : le brouillon de réponse Atlas n'utilise \u003Cstrong>pas\u003C\u002Fstrong> le routeur Python — bien qu'il soit la pièce finale du pipeline Atlas Inbox, il est servi par un endpoint Nuxt, donc par le gateway TypeScript. C'est l'illustration concrète de la frontière :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Classification et déclenchement = routeur Python\u003C\u002Fstrong> (automates) ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Rédaction du brouillon depuis le cockpit hub = routeur TypeScript\u003C\u002Fstrong> (Nuxt).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>L'espace de dialogue multi-agents est le seul appelant qui mélange volontairement deux fournisseurs dans une même requête HTTP (Mistral pour les notes d'analyse, Anthropic pour la voix finale du lead).\u003C\u002Fp>\n\n\u003Ch2 id=\"orbites-agents\">Orbites : Direction, Cadrage, Exécution, Validation\u003C\u002Fh2>\n\n\u003Ch3>Le modèle de données\u003C\u002Fh3>\n\n\u003Cp>Les agents actifs sont répartis selon deux axes qui cohabitent et qu'il faut distinguer :\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>La famille fonctionnelle\u003C\u002Fstrong> — source de vérité métier, qui correspond aux quatre rôles structurants : \u003Cem>direction\u003C\u002Fem>, \u003Cem>cadrage\u003C\u002Fem>, \u003Cem>exécution\u003C\u002Fem>, \u003Cem>validation\u003C\u002Fem>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>L'anneau numérique\u003C\u002Fstrong> (1\u002F2\u002F3) — un attribut stocké en base qui ne mappe pas 1:1 sur la famille fonctionnelle (par exemple, la famille \u003Cem>cadrage\u003C\u002Fem> apparaît en anneau 1 \u003Cem>et\u003C\u002Fem> en anneau 2).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Répartition observée sur les agents actifs :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode> anneau | famille    | nombre\n--------+------------+--------\n   1    | direction  |   4     ← Atlas, Hill, Colbert, Winnicott\n   1    | cadrage    |   3     ← Montesquieu, Gauss, Clausewitz\n   2    | cadrage    |   2     ← Marco Polo, Socrate\n   2    | validation |   8     ← Otlet, Mitnick, Itten…\n   3    | exécution  |  12\n   3    | validation |   1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Le rendu visuel du réacteur\u003C\u002Fh3>\n\n\u003Cp>La page de visualisation du réacteur \u003Cstrong>ne lit pas la colonne d'anneau en base\u003C\u002Fstrong>. Elle recalcule le ring d'affichage à partir de la famille fonctionnelle via une table de correspondance locale :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>const ringMap = { direction: 1, cadrage: 2, execution: 2, validation: 3 }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Ring 1 — Direction\u003C\u002Fstrong> (rotation 60 s)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ring 2 — Cadrage + Exécution\u003C\u002Fstrong> (90 s, sens inverse)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ring 3 — Validation\u003C\u002Fstrong> (120 s)\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point à confirmer :\u003C\u002Fstrong> la divergence entre la colonne d'anneau en base et la table de correspondance de l'interface est-elle intentionnelle ou une dette ? Le rendu visuel étant piloté exclusivement par la famille fonctionnelle, il est considéré comme fiable ; la colonne d'anneau semble sous-utilisée côté front.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Routage par domaine de prompt\u003C\u002Fh3>\n\n\u003Cp>En parallèle des anneaux, les agents sont sélectionnables par domaine via un attribut CSV. Un endpoint dédié permet d'interroger les agents compatibles avec un domaine donné (\u003Ccode>contenu\u003C\u002Fcode>, \u003Ccode>faq\u003C\u002Fcode>, \u003Ccode>cover\u003C\u002Fcode>, \u003Ccode>podcast\u003C\u002Fcode>, \u003Ccode>linkedin\u003C\u002Fcode>, \u003Ccode>reels\u003C\u002Fcode>), avec proxy cross-tenant vers le Vaisseau Mère si l'appel provient d'un VPS client distant.\u003C\u002Fp>\n\n\u003Ch2 id=\"calibration-classifieur\">Calibration du classifieur Atlas\u003C\u002Fh2>\n\n\u003Cp>Un script de reporting mensuel agrège les données de classification et les journaux d'audit pour produire un rapport de santé du classifieur. Il calcule les métriques suivantes :\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Volumétrie par intention\u003C\u002Fstrong> : nombre de classifications, confiance moyenne et écart-type.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Taux de succès proxy\u003C\u002Fstrong> : par intention, ratio chantiers non-annulés \u002F total des brouillons générés. Un brouillon annulé signifie que le responsable a refusé la suggestion.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Taux d'incertitude\u003C\u002Fstrong> : proportion de classifications marquées incertaines sur le total.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Échecs\u003C\u002Fstrong> : décompte des classifications échouées et des blocages pour pièces jointes non sûres.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Seuil de passage en Phase 3 (multi-tenant)\u003C\u002Fstrong> : le critère de promotion est \u003Cstrong>un taux de succès supérieur à 90 % sur au moins 30 chantiers internes\u003C\u002Fstrong>. Tant que ce seuil n'est pas franchi, Atlas reste cantonné à l'usage interne.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Note importante :\u003C\u002Fstrong> la « calibration » décrite ici est de l'\u003Cstrong>observabilité et du pilotage de seuil\u003C\u002Fstrong>, pas un réentraînement de modèle ni un ajustement automatique de poids. La calibration automatique du prompt système Atlas (apprentissage par exemples, amélioration continue) est une fonctionnalité en feuille de route, non encore livrée.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"orchestration-post-spawn\">Orchestration post-spawn : déploiement, QA et notification\u003C\u002Fh2>\n\n\u003Cp>Une fois le spawn de l'agent terminé, le système enchaîne automatiquement trois phases : déploiement en pré-production, vérification qualité, puis notification par e-mail.\u003C\u002Fp>\n\n\u003Ch3>Lecture du contrat de spawn\u003C\u002Fh3>\n\u003Cp>L'orchestrateur post-spawn commence par lire le contrat JSON produit par l'agent. Ce contrat peut contenir une correction d'intention décidée par l'agent lui-même : il est autorisé à \u003Cem>déclasser\u003C\u002Fem> l'intention initiale vers une intention sans production de code (question, négociation, run). Cette correction est possible parce que l'agent a lu le fil de conversation complet, alors que le classificateur d'entrée n'avait accès qu'à un extrait tronqué.\u003C\u002Fp>\n\n\u003Ch3>Branche sans code\u003C\u002Fh3>\n\u003Cp>Lorsque l'intention finale est de type question, négociation ou run, le système court-circuite entièrement les phases de déploiement et de QA. Il envoie directement un récapitulatif et marque le dossier comme traité.\u003C\u002Fp>\n\n\u003Ch3>Branche code (chantier)\u003C\u002Fh3>\n\u003Cp>Lorsque l'intention est un chantier, la séquence complète s'enclenche :\u003C\u002Fp>\n\u003Col>\n  \u003Cli>Déploiement en pré-production via le script de déploiement standard, avec un délai d'expiration de 15 minutes.\u003C\u002Fli>\n  \u003Cli>Passage de la suite de tests QA automatisés (Playwright) sur chaque route concernée.\u003C\u002Fli>\n  \u003Cli>Selon le résultat de la QA, l'orchestrateur prend l'une des deux décisions décrites ci-dessous.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Boucle d'itération QA\u003C\u002Fh3>\n\u003Cp>Si la QA échoue, l'orchestrateur tente un nouveau cycle de spawn enrichi du contexte d'erreur, jusqu'à un maximum de trois itérations. Au-delà, il déclenche une escalade humaine. Si la QA passe, il envoie le récapitulatif et clôt le dossier.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>spawn ok ──► deploy préprod ──► QA route(s)\n                                    │\n              ┌── QA OK ────────────┴── QA FAIL ──┐\n              ▼                                    ▼\n        notification Atlas                  itération &lt; 3 ?\n        dossier 'actioned'          ┌── oui ──┴── non ──┐\n                                    ▼                    ▼\n                          re-spawn (contexte    escalade humaine\n                          erreur QA)            (max iter atteint)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Notification e-mail\u003C\u002Fh3>\n\u003Cp>La notification finale transite par la façade d'envoi d'e-mail du système. Elle est toujours adressée à l'équipe interne Synedre — jamais directement au demandeur externe. Cette règle est une doctrine fondamentale : \u003Cstrong>l'IA ne contacte jamais le client final de sa propre initiative\u003C\u002Fstrong>. Les identifiants de connexion à la boîte d'envoi sont lus depuis les variables d'environnement du serveur et ne transitent jamais en clair dans le code.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"scheduling\">Boucle de déclenchement du pipeline\u003C\u002Fh2>\n\n\u003Cp>Le pipeline Atlas est piloté par des tâches planifiées système classiques. Chaque tâche passe par un composant de supervision qui gère les verrous et les journaux d'exécution.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note :\u003C\u002Fstrong> Le moteur de scheduling applicatif intégré au serveur est hors service depuis mai 2026. Le pipeline repose entièrement sur les tâches planifiées système.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>Trois tâches forment la boucle de bout en bout :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Fréquence\u003C\u002Fth>\n      \u003Cth>Composant\u003C\u002Fth>\n      \u003Cth>Rôle dans la boucle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Toutes les minutes\u003C\u002Ftd>\n      \u003Ctd>Collecteur de la boîte Atlas\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Ingestion et chaînage.\u003C\u002Fstrong> Interroge la boîte de réception dédiée, insère les nouveaux messages, puis déclenche en cascade l'analyse antivirus et la classification des intentions. La classification est donc déclenchée ici, dans le même cycle que l'ingestion.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Toutes les 5 minutes (avec décalage de 2 minutes)\u003C\u002Ftd>\n      \u003Ctd>Orchestrateur de spawn\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Spawn et orchestration.\u003C\u002Fstrong> Récupère les messages dont l'intention est classifiée et éligible, puis lance le spawn et l'orchestration post-spawn (déploiement, QA, notification).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Toutes les 15 minutes\u003C\u002Ftd>\n      \u003Ctd>Gardien de verrous\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Filet de sécurité.\u003C\u002Fstrong> Émet une alerte si un verrou zombie persiste au-delà de 10 minutes, évitant tout blocage silencieux du pipeline.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>L'ordre réel d'exécution est donc : \u003Cstrong>ingestion (minute 0) → analyse + classification (même tick) → spawn (minute 2 du cycle de 5 minutes)\u003C\u002Fstrong>. Le décalage de deux minutes entre le collecteur et l'orchestrateur de spawn garantit que la classification est terminée avant que le spawn ne cherche des messages éligibles. Un verrou consultatif au niveau base de données couvre la collision résiduelle entre l'exécution planifiée et un déclenchement manuel.\u003C\u002Fp>\n\n\u003Cp>Le collecteur de la boîte Atlas est distinct du collecteur de la boîte générale de la plateforme, qui alimente une table séparée et ne doit pas être confondu avec lui.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"composants-du-pipeline\">Composants du pipeline : rôles et responsabilités\u003C\u002Fh2>\n\n\u003Ch3>Composants applicatifs principaux\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Composant\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Collecteur de boîte Atlas\u003C\u002Ftd>\n      \u003Ctd>Interroge la boîte de réception dédiée, insère les messages bruts et les entrées de suivi, puis déclenche en cascade l'analyse antivirus et la classification. S'exécute toutes les minutes.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Moteur de classification\u003C\u002Ftd>\n      \u003Ctd>Classifie l'intention du message via un LLM et matérialise le dossier correspondant (run, question, négociation, conseil, chantier). Depuis juin 2026 : enrichissement OCR des pièces jointes et double garde-fou contre les faux-positifs et les demandes de prospects non encore clients.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Orchestrateur de spawn\u003C\u002Ftd>\n      \u003Ctd>Prend en charge le spawn de l'agent codeur, le déploiement en pré-production, la boucle QA et la notification finale. Depuis juin 2026 : délai d'expiration du spawn porté à 40 minutes, avec instruction de point de contrôle anticipé dans le prompt système.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Outil de calibration du classificateur\u003C\u002Ftd>\n      \u003Ctd>Produit le reporting mensuel de qualité de la classification et pilote le seuil de passage à la phase suivante.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Enveloppe de spawn en streaming\u003C\u002Ftd>\n      \u003Ctd>Composant Node.js obligatoire pour lancer l'agent IA en mode flux JSON interactif via un terminal virtuel.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Chargeur de persona agent\u003C\u002Ftd>\n      \u003Ctd>Assemble la personnalité de l'agent sur trois niveaux, enrichit le contexte via la recherche vectorielle dans la base de cicatrices, et injecte la lettre de mission.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Client d'invocation agent (héritage)\u003C\u002Ftd>\n      \u003Ctd>Invocation agent par sous-processus, maintenu pour compatibilité. Les helpers de cicatrices et de doctrines de ce composant reposent encore sur l'ancienne base de données relationnelle.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Routeur IA multi-fournisseur (côté serveur web)\u003C\u002Ftd>\n      \u003Ctd>Expose une interface unifiée de génération de contenu avec bascule automatique entre fournisseurs IA (ordre de priorité configurable). Résout le fournisseur préféré par client selon la configuration de topologie.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Domaine agents (interface web)\u003C\u002Ftd>\n      \u003Ctd>Registre des agents, réacteur d'événements, façades base de données et API du tableau de bord central.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Tables et structures de données\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Registre des agents\u003C\u002Fstrong> : table principale des agents déclarés, exposée également via une vue de synthèse.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Messages bruts ingérés\u003C\u002Fstrong> : stockage des e-mails entrants avant traitement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Suivi Atlas\u003C\u002Fstrong> : table de suivi du cycle de vie de chaque message traité par Atlas.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Brouillons de réponse\u003C\u002Fstrong> : corps de réponse préparés, lus par le classificateur lors de la matérialisation d'une intention de type question.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Journal d'audit Atlas\u003C\u002Fstrong> : trace de toutes les actions du pipeline.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Événements de spawn\u003C\u002Fstrong> : vue actualisable sur la table de base des événements de spawn (cf. section sur la vue actualisable).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Dossiers de travail\u003C\u002Fstrong> : tables dédiées aux runs, questions, négociations, conseils et chantiers.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Base RAG\u003C\u002Fstrong> : stockage des embeddings vectoriels pour la recherche de cicatrices.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Topologie clients\u003C\u002Fstrong> : configuration des VPS clients et de leurs préférences IA.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Module externe : OCR\u003C\u002Fh3>\n\u003Cp>Depuis juin 2026, le classificateur fait appel à un module OCR local (Tesseract) pour extraire le texte des pièces jointes images avant classification. Ce module est indépendant des fournisseurs IA externes.\u003C\u002Fp>","\u003Ch2 id=\"coeur-agentique-vue-ensemble\">The agentic core: orchestrator and agents\u003C\u002Fh2>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Purpose of this section\u003C\u002Fstrong> — It describes the engine that transforms an incoming email (or a project task) into an executed action: LLM-based intent classification, launching a headless code session via a pseudo-terminal, injection of the designated agent's persona, and post-execution orchestration (pre-production deployment, quality control, summary email).\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"vue-ensemble\">1. Overview\u003C\u002Fh2>\n\n\u003Cp>The agentic core rests on two main building blocks:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>The orchestrator\u003C\u002Fstrong> (directing agent, public codename: \u003Cstrong>Atlas\u003C\u002Fstrong>). It has no dedicated runtime: \"being Atlas\" consists of starting an LLM-assisted code session with a dedicated system prompt. It is driven by a set of Python modules and a Node coordinator script.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Specialised agents\u003C\u002Fstrong> (around thirty in total, organised into functional families): personas — identity, cognitive framework, business scope — injected into an LLM's context at the moment a task is executed.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>The main flow, called the \u003Cstrong>Atlas Inbox pipeline\u003C\u002Fstrong>, proceeds as follows:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Forwarded email → Atlas inbox\n        │\n        ▼  (ingestion → messages table + tracking row status='received')\n ┌──────────────────────────┐\n │ Intent classifier        │  intent ∈ {run, chantier, question,\n │  (LLM via facade)        │           noise, negociation, conseil}\n └──────────────────────────┘  → status='classified'\n        │                        + materialises run \u002F question \u002F\n        │                          negociation \u002F conseil depending on intent\n        ▼\n ┌──────────────────────────┐\n │ Spawn module             │  whitelist {run, chantier, question, negociation}\n │  → Node coordinator      │  launches headless code session via pseudo-TTY\n │    (JSON stream)         │  + Atlas system prompt (CODE + COMMIT only)\n └──────────────────────────┘\n        │  The session writes a temporary result file then terminates\n        ▼\n ┌──────────────────────────┐\n │ Post-spawn orchestration │  pre-prod deployment → Playwright QA →\n │                          │  Atlas summary email → UPDATE 'actioned'\n └──────────────────────────┘  (re-spawn up to 3 iterations if QA fails)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Ingestion step\u003C\u002Fstrong> — The transition \"email received → tracking row\" is handled by an IMAP polling module. This module queries the Atlas inbox via the parent platform's messaging facade, performs an unread message search, then for each message: inserts a row into the messages table (idempotent operation enforced by a uniqueness constraint on the message identifier) and creates a tracking row with status \u003Ccode>received\u003C\u002Fcode>, before marking the message as read. It \u003Cstrong>automatically chains\u003C\u002Fstrong> the antivirus scan followed by classification — it is this chaining, and not a disabled external scheduler, that loops the pipeline end to end.\u003C\u002Fp>\n\n\u003Ch2 id=\"modele-agents\">2. Agent model\u003C\u002Fh2>\n\n\u003Ch3>2.1 Storage\u003C\u002Fh3>\n\n\u003Cp>Agents are stored in a central table within the main schema. A compatibility view, bearing a name inherited from an earlier migration (\u003Cem>Strangler Fig pattern\u003C\u002Fem>), exposes the same columns. Python code reads via this view; the web interface reads directly from the table via the ORM. Both surfaces are strictly equivalent.\u003C\u002Fp>\n\n\u003Ch3>2.2 Key columns\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Column\u003C\u002Fth>\n      \u003Cth>Type\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>codename\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Lookup key, unique kebab-case (e.g. \u003Ccode>orchestrator\u003C\u002Fcode>, \u003Ccode>backend\u003C\u002Fcode>, \u003Ccode>securite\u003C\u002Fcode>, \u003Ccode>seo-technique\u003C\u002Fcode>).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>nickname\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Public human alias (e.g. Atlas, Gauss, Mitnick, Otlet).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>role\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>Job title (e.g. \"SEO Technical Officer\").\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>group_name\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(64)\u003C\u002Ftd>\n      \u003Ctd>Functional family: \u003Ccode>direction\u003C\u002Fcode>, \u003Ccode>cadrage\u003C\u002Fcode>, \u003Ccode>execution\u003C\u002Fcode>, \u003Ccode>validation\u003C\u002Fcode>.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>orbite\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>Orbit ring (1, 2 or 3). ⚠️ Does not always match the visually displayed ring (see section 7).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>heritage\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>Geographic\u002Fhistorical anchor of the persona (e.g. \"Ancient Greece\" for Atlas, \"United States, 1963\" for Mitnick).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>cognitive_frame\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>\"Way of thinking\" — block injected with top priority into the briefing (the 4 cognitive dimensions).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>personality\u003C\u002Fcode>, \u003Ccode>quote\u003C\u002Fcode>, \u003Ccode>inspiration\u003C\u002Fcode>, \u003Ccode>proof\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Rich identity (\u003Ccode>full\u003C\u002Fcode> tier only).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>job_mission\u003C\u002Fcode>, \u003Ccode>job_perimeter\u003C\u002Fcode>, \u003Ccode>job_key_checks\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Business scope (\u003Ccode>metier\u003C\u002Fcode> tier and above).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>content_md\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Long markdown profile, consumed by the legacy agent call module.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>active\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>1 = agent is recruitable.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>prompt_domains\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>varchar(255)\u003C\u002Ftd>\n      \u003Ctd>CSV of prompt domains (content, faq, cover…) for routing to generation agents.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>auto_spawn\u003C\u002Fcode>, \u003Ccode>spawn_count\u003C\u002Fcode>, \u003Ccode>error_count\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>integer\u003C\u002Ftd>\n      \u003Ctd>Runtime counters.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Internationalisation columns (EN) are present for narrative fields: \u003Ccode>role_en\u003C\u002Fcode>, \u003Ccode>heritage_en\u003C\u002Fcode>, \u003Ccode>cognitive_frame_en\u003C\u002Fcode>, etc.\u003C\u002Fp>\n\n\u003Ch3>2.3 Satellite tables\u003C\u002Fh3>\n\n\u003Cp>Several satellite tables orbit the main agents table: activity, events, heartbeats, inter-agent relationships, skills, tools, experience and experience history. Some of these tables have a legacy compatibility view (activity, heartbeat, relationships, experience and history); others do not (events, skills, tools). The agents domain manifest lists the full surface of exposed tables and API routes.\u003C\u002Fp>\n\n\u003Ch3>2.4 Persona loading\u003C\u002Fh3>\n\n\u003Cp>Before an LLM executes a task, it \"becomes\" the designated agent through a three-layer context assembly mechanism:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Identity + cognitive framework + business scope\u003C\u002Fstrong> sourced from the agents table.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Relevant scars\u003C\u002Fstrong> via vector semantic search (RAG pgvector) on the embeddings table, prioritising scars belonging to the agent in question before supplementing with global scars. Explicit objective: prevent an agent from seeing another agent's lessons as a priority.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Project mission brief\u003C\u002Fstrong> if the project can be resolved from the task.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>User preferences\u003C\u002Fstrong> (operator profile, \u003Ccode>core\u003C\u002Fcode> slice).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Three \u003Cstrong>rendering tiers\u003C\u002Fstrong> are calibrated by token budget, in order to limit attention dilution and maximise prompt cache hit rate (TTL ~5 min):\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tier\u003C\u002Fth>\n      \u003Cth>Approx. budget\u003C\u002Fth>\n      \u003Cth>Included content\u003C\u002Fth>\n      \u003Cth>Typical use\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Core\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~150 tokens\u003C\u002Ftd>\n      \u003Ctd>Cognitive framework only + minimal identity\u003C\u002Ftd>\n      \u003Ctd>Code-free fast path, Task-type sub-agents\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Métier\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~350 tokens\u003C\u002Ftd>\n      \u003Ctd>Core + role + business scope\u003C\u002Ftd>\n      \u003Ctd>Default for full Atlas spawn and task worker\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Full\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>~600 tokens\u003C\u002Ftd>\n      \u003Ctd>Métier + heritage + quote + personality + inspiration + proof\u003C\u002Ftd>\n      \u003Ctd>Review, drill, mission brief\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>A backward-compatibility alias exposes the \u003Cem>métier\u003C\u002Fem> tier under the generic name \u003Ccode>format_briefing\u003C\u002Fcode> for legacy calls.\u003C\u002Fp>\n\n\u003Ch3>2.5 Note on the legacy agent call module\u003C\u002Fh3>\n\n\u003Cp>An older agent call module allows an agent to be invoked as a subprocess and the exchange to be logged in the reactor. \u003Cstrong>⚠️ This module carries significant technical debt\u003C\u002Fstrong>: its scar and doctrine loading helpers still connect to a legacy MySQL database — a remnant predating the migration to PostgreSQL. Exceptions are silently swallowed, meaning the call does not crash but may return profiles with no scars or doctrines.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point to confirm\u003C\u002Fstrong> — Is this legacy path still invoked in production, or has it been superseded by the Atlas spawn pipeline associated with the current persona module? The module's internal comment indicates an ongoing migration to the PostgreSQL agents table, but both scar\u002Fdoctrine helpers still point to the old MySQL source.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"atlas-orchestrateur-spawn\">Atlas the orchestrator — the spawn engine\u003C\u002Fh2>\n\n\u003Ch3>Restricted role of the spawned agent\u003C\u002Fh3>\n\n\u003Cp>The system prompt passed to the spawned agent strictly scopes its operational perimeter: \u003Cstrong>investigation, code writing, and commit\u003C\u002Fstrong>. The agent does not trigger deployments, does not send emails, and does not update the inbound message registry — all of these operations are orchestrated by the Python engine after the agent finishes execution.\u003C\u002Fp>\n\n\u003Cp>Perimeter guardrails are encoded in the system prompt: modifications are permitted only within the hub, back-office, and the relevant tenant zones; never within a shared public component.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>\"Early checkpoint\" instruction\u003C\u002Fstrong> (introduced mid-2026): the system prompt now requires the agent to write an intermediate result file \u003Cem>as early as possible\u003C\u002Fem> — from the first partial diagnostic — and to overwrite it after each major milestone, rather than waiting until the work is fully complete. The rationale is straightforward: if the agent is interrupted by the timeout, the last persisted state serves as salvage to notify the team. A partial file is better than nothing. This instruction was introduced following an incident in which the result file had not yet been written at the moment the process was killed.\u003C\u002Fp>\n\n\u003Ch3>Spawn candidate selection\u003C\u002Fh3>\n\n\u003Cp>The selection engine queries the inbound message registry and retains only entries that meet all three of the following conditions:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>status \u003Ccode>classified\u003C\u002Fcode>;\u003C\u002Fli>\n  \u003Cli>classified intent among: \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>;\u003C\u002Fli>\n  \u003Cli>no recent spawn audit (started, completed, or failed within the last hour) — idempotence guarantee against double-spawn.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Applied safety constants:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Allowed intents\u003C\u002Fstrong>: \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode> — the \u003Ccode>noise\u003C\u002Fcode> and \u003Ccode>conseil\u003C\u002Fcode> intents are excluded from spawn.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Maximum spawns per cycle\u003C\u002Fstrong>: 3, to cap costs in the event of runaway behavior.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Timeout per spawn\u003C\u002Fstrong>: 40 minutes. This timeout was raised (from 25 minutes) following an incident in which an investigation on a complex production environment was cut off just before the deliverable was written — the spawn was marked as failed even though the fix was already in place. This change was deployed together with the \"early checkpoint\" instruction.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Kill-switch\u003C\u002Fstrong>: an environment variable allows the spawn engine to be disabled entirely without touching the code.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Default model\u003C\u002Fstrong>: Sonnet.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Caution note\u003C\u002Fstrong>: the header comment of the main module is stale — it still advertises a two-intent allowlist, a 10-minute timeout, and a different cron offset. The \u003Cstrong>effective constants in the code\u003C\u002Fstrong> are authoritative: four allowed intents, 40-minute timeout. This page uses the actual values; do not rely on the file's header comment.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Anti-race: advisory database lock\u003C\u002Fh3>\n\n\u003Cp>To cover the collision between the 5-minute cron and a simultaneous manual invocation, the engine acquires a session-level advisory lock on the identifier of the message being processed before any spawn. This lock is held via a dedicated interactive connection and released automatically upon its closure or process death. If the lock is already held by another instance, the message is skipped for that cycle and a \u003Ccode>spawn_skipped_lock_held\u003C\u002Fcode> audit record is written.\u003C\u002Fp>\n\n\u003Ch3>Prompt construction\u003C\u002Fh3>\n\n\u003Cp>The prompt passed to the agent is built from three sources:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>the forwarded email (sender, subject, body truncated to 6,000 characters);\u003C\u002Fli>\n  \u003Cli>the tenant identity, resolved by domain heuristic (explicit lookup table first, then fallback to the registered contact);\u003C\u002Fli>\n  \u003Cli>the output of the intent classifier (intent, confidence score, model reasoning).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>In the event of a QA failure, a re-spawn prompt is built by appending the previous commits and the detected errors (HTTP errors at the application layer, browser errors at the interface layer).\u003C\u002Fp>\n\n\u003Ch3>Agent contractual deliverable\u003C\u002Fh3>\n\n\u003Cp>Before terminating its execution, the agent must unconditionally write a structured result file containing: the status (\u003Ccode>ok\u003C\u002Fcode> or otherwise), the relevant tenant, the routes to submit to QA, the list of commits performed, a markdown summary, and a draft reply to the user. This file constitutes the contract read by the Python orchestrator to decide on post-spawn actions.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"invocation-node-pty\">Programmatic invocation via pseudo-TTY\u003C\u002Fh2>\n\n\u003Cp>Fundamental harness rule: a Claude agent launched programmatically must go through \u003Cstrong>a real pseudo-TTY\u003C\u002Fstrong> via a dedicated Node.js wrapper — never via a direct Python subprocess call.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Why\u003C\u002Fstrong>: launching \u003Ccode>claude\u003C\u002Fcode> in non-interactive mode from a Python subprocess causes a \u003Cstrong>silent hang\u003C\u002Fstrong> — buffer deadlock and\u002For non-TTY mode detection that refuses the prompt passed as an argument. A pseudo-TTY of type \u003Ccode>xterm-color\u003C\u002Fcode> works around both pitfalls.\u003C\u002Fp>\n\n\u003Ch3>Call chain\u003C\u002Fh3>\n\n\u003Cp>The Python engine writes the prompt and the system prompt to temporary files, then delegates execution to the Node.js wrapper by passing the paths to those files, the spawn identifier, the target model, and the timeout. The call looks schematically like:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>node un composant interne \\\n  --id &lt;ID&gt; \\\n  --prompt-file &lt;prompt-file&gt; \\\n  --system-prompt-file &lt;system-file&gt; \\\n  --model sonnet \\\n  --stream-log &lt;log-file&gt; \\\n  --add-dir &lt;project-directory&gt; \\\n  --timeout-sec 2400\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>The Node wrapper\u003C\u002Fh3>\n\n\u003Cp>The Node.js wrapper (\u003Ccode>node-pty\u003C\u002Fcode> dependency) handles the following:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Locating the Claude binary on the host machine.\u003C\u002Fli>\n  \u003Cli>Building the arguments passed to Claude: non-interactive mode, allowed working directory, no session persistence, verbose stream-JSON output format with partial messages and hook events, model and system prompt appended.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Critical argument ordering\u003C\u002Fstrong>: the \u003Ccode>--add-dir\u003C\u002Fcode> option must be placed before all other flags — otherwise its varargs behavior \"consumes\" the prompt as a directory. This constraint cost 30 minutes of debugging during a past incident.\u003C\u002Fli>\n  \u003Cli>Spawning the pseudo-TTY with generous dimensions (200 columns × 50 rows).\u003C\u002Fli>\n  \u003Cli>Injecting the environment variables required by the agent's Stop hooks (database coordinates, worker context) — the database password is inherited from the parent environment, never hardcoded.\u003C\u002Fli>\n  \u003Cli>Emitting each stream-JSON event as JSONL on stdout (with tee to a log file), detecting the final result event.\u003C\u002Fli>\n  \u003Cli>Exit codes: \u003Ccode>0\u003C\u002Fcode> on success, \u003Ccode>1\u003C\u002Fcode> on error, \u003Ccode>2\u003C\u002Fcode> on invalid arguments. A final wrapper event of type \u003Ccode>node_pty_exit\u003C\u002Fcode> is emitted for the parent Python process.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Live event persistence\u003C\u002Fh3>\n\n\u003Cp>On the Python side, each received JSONL line is parsed and persisted in real time to the spawn event log. Each record carries: a sequence number, an event type (sub-typed according to whether it is a stream event, a system event, or a tool result), the name of the tool involved if any, and the full JSON payload.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Architecture note\u003C\u002Fstrong>: the event log table is in fact a \u003Cstrong>simple updatable view\u003C\u002Fstrong> layered on top of an underlying physical table — the same Strangler-Fig pattern used for the agent registry. Inserts through the view work normally on the exposed columns.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>This real-time persistence allows a hang to be diagnosed (the last event visible in the database reveals where the agent is blocked) and enables cost, duration, and token aggregation from the final result event. Everything is also audited in the central audit log.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"classification-intents\">Intent classification\u003C\u002Fh2>\n\n\u003Ch3>Intent enumeration\u003C\u002Fh3>\n\n\u003Cp>The classifier recognizes six valid intents. The following table describes their semantics and downstream effect:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Intent\u003C\u002Fth>\n      \u003Cth>Meaning\u003C\u002Fth>\n      \u003Cth>Downstream effect\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>run\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Atomic task executable in a single command\u003C\u002Ftd>\n      \u003Ctd>Creation or update of a run in the run registry\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>chantier\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Structured multi-step mission (skeleton + agents)\u003C\u002Ftd>\n      \u003Ctd>Creation of a chantier draft\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>question\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Request for opinion or analysis, reply as draft\u003C\u002Ftd>\n      \u003Ctd>Creation or update of a question in the registry\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>noise\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Newsletter, spam, nothing actionable\u003C\u002Ftd>\n      \u003Ctd>No action\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>negociation\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Inbound commercial request (prospect, quote)\u003C\u002Ftd>\n      \u003Ctd>Insertion of a negotiation into the commercial registry\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>conseil\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Request for opinion or expertise on an existing external item\u003C\u002Ftd>\n      \u003Ctd>Insertion of a conseil record into the dedicated registry\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Each materialization is mutually exclusive and idempotent: unique indexes and conditional guards prevent duplicate creation even under repeated calls. The created identifier is traced back to the originating message record.\u003C\u002Fp>\n\n\u003Ch3>LLM engine\u003C\u002Fh3>\n\n\u003Cp>Classification relies on the \u003Cstrong>AI provider facade\u003C\u002Fstrong> — a centralized abstraction layer — never on a direct SDK call. Model routing is resolved dynamically by configuration; the default provider and model are Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode>. Output is requested as strict JSON, with a 30-second timeout.\u003C\u002Fp>\n\n\u003Ch3>Guardrails\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Anti-prompt-injection\u003C\u002Fstrong>: the email body is sandwiched between two explicit delimiters and preceded by a warning instructing the model that this is email data, not instructions. The body is truncated to 8,000 characters.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Strict enumeration\u003C\u002Fstrong>: any intent outside the six recognized values is rejected, regardless of the model's response. Any confidence score outside the [0, 1] range is also rejected.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Confidence auto-floor\u003C\u002Fstrong>: a confidence score below 0.5 forces the intent to \u003Ccode>noise\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Uncertainty threshold\u003C\u002Fstrong>: a confidence score below 0.7 triggers a \u003Ccode>classification_uncertain\u003C\u002Fcode> audit record.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Pre-LLM short-circuit\u003C\u002Fstrong>: if the founder re-forwards an Atlas summary (subject starting with \u003Ccode>Fwd: [Atlas]…\u003C\u002Fcode>), the intent is forced to \u003Ccode>noise\u003C\u002Fcode> without an LLM call, to avoid a costly no-op spawn.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Antivirus scan first\u003C\u002Fstrong>: classification is blocked if any attachment has a verdict other than \u003Ccode>clean\u003C\u002Fcode> — \"scan before opening\" doctrine.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>False-noise guardrail with founder attachment\u003C\u002Fstrong>: if an email is classified as \u003Ccode>noise\u003C\u002Fcode> but originates from the founder \u003Cem>and\u003C\u002Fem> carries attachments, the intent is reclassified as \u003Ccode>question\u003C\u002Fcode>. Rationale: actionable content may be present in a screenshot that the text model cannot see. A \u003Ccode>noise_override_founder_attachment\u003C\u002Fcode> audit record is written.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Existing client guardrail\u003C\u002Fstrong>: if an email is classified as \u003Ccode>negociation\u003C\u002Fcode> or \u003Ccode>conseil\u003C\u002Fcode> but the sender address matches an active tenant, the intent is downgraded to \u003Ccode>run\u003C\u002Fcode> scoped to the relevant tenant. Client emails are not new commercial negotiations. An \u003Ccode>existing_client_guard\u003C\u002Fcode> audit record is written.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>At the end of classification, the inbound message registry is updated with the intent, confidence score, model used, classifier reasoning, and the \u003Ccode>classified\u003C\u002Fcode> status. A \u003Ccode>classified\u003C\u002Fcode> audit record is written.\u003C\u002Fp>\n\n\u003Ch3>OCR enrichment of attachments\u003C\u002Fh3>\n\n\u003Cp>Before the LLM call, if attachments are present and the antivirus verdict is \u003Ccode>clean\u003C\u002Fcode>, an optical character recognition (OCR) text extraction module is invoked on the temporary attachment directory. The engine used is Tesseract, running locally — no cloud call is made.\u003C\u002Fp>\n\n\u003Cp>The extracted text is injected into the sandwich prompt context, between its own delimiters (\u003Ccode>IMAGE_OCR_TEXT_START\u003C\u002Fcode> \u002F \u003Ccode>END\u003C\u002Fcode>), after the email body. The OCR engine configuration is resolved by the dynamic routing system.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Important — personal data\u003C\u002Fstrong>: the extracted OCR text is \u003Cem>never\u003C\u002Fem> persisted to the database (personal data risk — in-memory use only within the classification cycle). Only an \u003Ccode>ocr_extracted\u003C\u002Fcode> audit record is written, carrying the number of characters extracted and the approximate number of images processed.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Rationale\u003C\u002Fstrong>: the text model cannot see screenshots attached to emails. Without OCR, tickets containing screenshots of issues were incorrectly classified as \u003Ccode>noise\u003C\u002Fcode>, blocking all automated processing. Local Tesseract OCR extracts visible text at no additional cost and without leaking data to a third-party service.\u003C\u002Fp>\n\u003Ch2 id=\"routeurs-ia-paralleles\">The two parallel AI routers\u003C\u002Fh2>\n\n\u003Cp>The system runs \u003Cstrong>two distinct multi-provider AI routing engines\u003C\u002Fstrong> that share neither code nor configuration. This is not an accidental duplicate: they serve two different execution environments. Knowing which one to modify depends on the development context.\u003C\u002Fp>\n\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Axis\u003C\u002Fth>\n\u003Cth>Python router (automations)\u003C\u002Fth>\n\u003Cth>TypeScript router (Nuxt hub)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Runtime\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Scheduled automations (crons, nightly loop, reminder, classifier, learning…)\u003C\u002Ftd>\n\u003Ctd>Nitro\u002FNuxt hub runtime (server API entry points)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Exposed surface\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>embed(…)\u003C\u002Fcode> + \u003Ccode>complete(…)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Content generation + provider resolution per client\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Providers\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Embeddings: Mistral \u002F Voyage \u002F OpenAI; completion: Claude \u002F Mistral \u002F OpenAI\u003C\u002Ftd>\n\u003Ctd>Mistral \u002F Anthropic \u002F OpenAI\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Routing source\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>AI routing configuration YAML file\u003C\u002Ftd>\n\u003Ctd>Per request (explicit provider) or per client configuration\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Default\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Embeddings → Mistral; completion → Claude\u003C\u002Ftd>\n\u003Ctd>Mistral (FR\u002FEU data sovereignty)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Claude provider call\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Delegated to the agent invocation mechanism (subprocess)\u003C\u002Ftd>\n\u003Ctd>Direct HTTP request via a dedicated utility function\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Doctrine\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Mandatory pass-through via the validated routing facade\u003C\u002Ftd>\n\u003Ctd>Internal Nuxt module service, no hook-guarded facade\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n\u003Cp>The full detail of the Python router (embeddings, completion, YAML file, retries, guardrails) is documented in the Memory &amp; Learning chapter. This section describes the \u003Cstrong>TypeScript\u003C\u002Fstrong> counterpart, symmetrically.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>The generation function — single entry point of the Nuxt runtime\u003C\u002Fh3>\n\n\u003Cp>The content generation function is the \u003Cstrong>single entry point\u003C\u002Fstrong> of the Nuxt runtime for producing text via AI. It accepts as input: a \u003Ccode>prompt\u003C\u002Fcode>, an optional \u003Ccode>systemPrompt\u003C\u002Fcode>, an optional \u003Ccode>provider\u003C\u002Fcode>, an optional \u003Ccode>model\u003C\u002Fcode>, a maximum token count (default 4096), and a temperature (default 0.7). The response is normalized and exposes: the generated content, the provider that \u003Cem>actually\u003C\u002Fem> responded after any failover, the model used, the token count (input\u002Foutput), and the duration in milliseconds.\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Default provider = Mistral\u003C\u002Fstrong> — a sovereignty choice (FR data, European API), consistent with the Python router's default for embeddings.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Default models\u003C\u002Fstrong>: Mistral → \u003Ccode>mistral-large-latest\u003C\u002Fcode>; Anthropic → \u003Ccode>claude-sonnet-4-6\u003C\u002Fcode>; OpenAI → \u003Ccode>gpt-4o\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Automatic failover (sovereign order)\u003C\u002Fh3>\n\n\u003Cp>The failover order is: \u003Cstrong>Mistral → Anthropic → OpenAI\u003C\u002Fstrong>. On invocation, the system builds the effective order by placing the requested provider first, then iterates according to the following logic:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>generateContent(request)\n   effective provider = request.provider ?? 'mistral'\n   order = [provider, ...sovereign_order \\ provider]\n        │\n        ▼   for each provider in order:\n   API key missing? ──yes──► skip (next provider, silently)\n        │ no\n        ▼\n   callProvider()  ──success──► return { provider, durationMs, … }\n        │ exception\n        ▼\n   last in order? ──yes──► error \"All AI providers are unavailable\"\n        │ no\n        ▼\n   log failover → next provider\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Two conditions trigger a move to the next provider:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Missing API key\u003C\u002Fstrong>: the system silently skips the affected provider.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Call exception\u003C\u002Fstrong>: the system fails over to the next provider and logs the event.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>During a failover, the model used is realigned to the fallback provider's default model — and \u003Cstrong>not\u003C\u002Fstrong> to the model requested for the initial provider. The provider returned in the response always reflects who actually responded, which is useful to log on the caller side.\u003C\u002Fp>\n\n\u003Cp>The three providers are called via distinct functions: Mistral and OpenAI via direct HTTP request (120 s timeout, identical message format with optional \u003Ccode>system\u003C\u002Fcode> + \u003Ccode>user\u003C\u002Fcode>); Anthropic via a dedicated utility function.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point to confirm:\u003C\u002Fstrong> the Anthropic utility function is referenced in the code but its implementation was not located within the module scope. Either it is resolved by Nitro auto-import from an unindexed utility, or it is latent technical debt that would cause the Anthropic branch to fail at runtime. The Mistral → Anthropic failover is therefore \u003Cstrong>not guaranteed to be operational\u003C\u002Fstrong> without prior verification.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>API keys (anti-leak)\u003C\u002Fh3>\n\n\u003Cp>API keys are resolved exclusively by environment variable name; no value appears in the code or in this documentation. The variables concerned are: the Mistral API key, the Anthropic API key, and the OpenAI API key — all stored in environment files excluded from version control.\u003C\u002Fp>\n\n\u003Cp>A missing key is \u003Cstrong>not\u003C\u002Fstrong> a blocking error: it simply causes the corresponding provider to be silently skipped in the failover loop.\u003C\u002Fp>\n\n\u003Ch3>Per-client provider selection — tenant configuration resolution\u003C\u002Fh3>\n\n\u003Cp>The per-client resolution function reads the AI configuration stored in the client configuration table (JSON column) and returns the pair \u003Ccode>{ provider, model }\u003C\u002Fcode>. The extraction logic is:\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>AI provider\u003C\u002Fstrong> configured → defaults to \u003Ccode>mistral\u003C\u002Fcode> if absent;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>AI model\u003C\u002Fstrong> configured → defaults to the provider's default model if absent.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Any error (unknown client, invalid JSON, helper failure) falls back silently to Mistral with its default model. The \u003Ccode>active=1\u003C\u002Fcode> filter ensures that an inactive client is never returned, even if its configuration specifies a provider.\u003C\u002Fp>\n\n\u003Cp>Observed state: the majority of clients have the AI provider field absent from their configuration (→ sovereign Mistral by default); only a few set it explicitly.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point to confirm:\u003C\u002Fstrong> the per-client resolution function has \u003Cstrong>no identified caller\u003C\u002Fstrong> within the module scope. The two actual consumers of the generation function pass the provider hard-coded in the request and never query the client configuration. Per-tenant routing is therefore \u003Cstrong>wired but not yet connected\u003C\u002Fstrong> to the generation flow.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Actual callers of the generation function\u003C\u002Fh3>\n\n\u003Cp>Two Nuxt entry points currently consume the AI gateway:\u003C\u002Fp>\n\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Entry point\u003C\u002Fth>\n\u003Cth>Usage\u003C\u002Fth>\n\u003Cth>Requested provider\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Client reply draft generation (Atlas pipeline)\u003C\u002Ftd>\n\u003Ctd>Drafts the client reply for a completed Atlas run (formal register, 3–6 sentences, team signature)\u003C\u002Ftd>\n\u003Ctd>Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Multi-agent dialogue workspace (client)\u003C\u002Ftd>\n\u003Ctd>Multi-agent perspectives on support notes, then lead response\u003C\u002Ftd>\n\u003Ctd>Notes: Mistral \u002F \u003Ccode>mistral-small-latest\u003C\u002Fcode>; lead response: Anthropic\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Important reading: the Atlas reply draft does \u003Cstrong>not\u003C\u002Fstrong> use the Python router — although it is the final piece of the Atlas Inbox pipeline, it is served by a Nuxt endpoint, and therefore by the TypeScript gateway. This is the concrete illustration of the boundary:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Classification and triggering = Python router\u003C\u002Fstrong> (automations);\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Draft authoring from the hub cockpit = TypeScript router\u003C\u002Fstrong> (Nuxt).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The multi-agent dialogue workspace is the only caller that deliberately mixes two providers within a single HTTP request (Mistral for the analysis notes, Anthropic for the lead's final voice).\u003C\u002Fp>\n\n\u003Ch2 id=\"orbites-agents\">Orbits: Direction, Scoping, Execution, Validation\u003C\u002Fh2>\n\n\u003Ch3>The data model\u003C\u002Fh3>\n\n\u003Cp>Active agents are distributed along two axes that coexist and must be distinguished:\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>The functional family\u003C\u002Fstrong> — the business source of truth, corresponding to the four structural roles: \u003Cem>direction\u003C\u002Fem>, \u003Cem>scoping\u003C\u002Fem>, \u003Cem>execution\u003C\u002Fem>, \u003Cem>validation\u003C\u002Fem>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The numeric ring\u003C\u002Fstrong> (1\u002F2\u002F3) — an attribute stored in the database that does not map 1:1 to the functional family (for example, the \u003Cem>scoping\u003C\u002Fem> family appears in both ring 1 \u003Cem>and\u003C\u002Fem> ring 2).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Observed distribution across active agents:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode> ring   | family     | count\n--------+------------+--------\n   1    | direction  |   4     ← Atlas, Hill, Colbert, Winnicott\n   1    | scoping    |   3     ← Montesquieu, Gauss, Clausewitz\n   2    | scoping    |   2     ← Marco Polo, Socrate\n   2    | validation |   8     ← Otlet, Mitnick, Itten…\n   3    | execution  |  12\n   3    | validation |   1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>The visual reactor render\u003C\u002Fh3>\n\n\u003Cp>The reactor visualization page \u003Cstrong>does not read the ring column from the database\u003C\u002Fstrong>. It recomputes the display ring from the functional family via a local lookup table:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>const ringMap = { direction: 1, cadrage: 2, execution: 2, validation: 3 }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Ring 1 — Direction\u003C\u002Fstrong> (60 s rotation)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ring 2 — Scoping + Execution\u003C\u002Fstrong> (90 s, counter-clockwise)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ring 3 — Validation\u003C\u002Fstrong> (120 s)\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Point to confirm:\u003C\u002Fstrong> is the divergence between the ring column in the database and the interface lookup table intentional or technical debt? Since the visual render is driven exclusively by the functional family, it is considered reliable; the ring column appears underused on the front-end side.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Routing by prompt domain\u003C\u002Fh3>\n\n\u003Cp>In parallel with the rings, agents are selectable by domain via a CSV attribute. A dedicated endpoint allows querying agents compatible with a given domain (\u003Ccode>contenu\u003C\u002Fcode>, \u003Ccode>faq\u003C\u002Fcode>, \u003Ccode>cover\u003C\u002Fcode>, \u003Ccode>podcast\u003C\u002Fcode>, \u003Ccode>linkedin\u003C\u002Fcode>, \u003Ccode>reels\u003C\u002Fcode>), with cross-tenant proxy to the Mother Ship if the call originates from a remote client VPS.\u003C\u002Fp>\n\n\u003Ch2 id=\"calibration-classifieur\">Atlas classifier calibration\u003C\u002Fh2>\n\n\u003Cp>A monthly reporting script aggregates classification data and audit logs to produce a classifier health report. It computes the following metrics:\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>Volume by intent\u003C\u002Fstrong>: number of classifications, average confidence, and standard deviation.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Proxy success rate\u003C\u002Fstrong>: per intent, ratio of non-cancelled jobs to total drafts generated. A cancelled draft means the responsible party rejected the suggestion.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Uncertainty rate\u003C\u002Fstrong>: proportion of classifications flagged as uncertain out of the total.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Failures\u003C\u002Fstrong>: count of failed classifications and blocks due to unsafe attachments.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Phase 3 promotion threshold (multi-tenant)\u003C\u002Fstrong>: the promotion criterion is \u003Cstrong>a success rate above 90% over at least 30 internal jobs\u003C\u002Fstrong>. Until this threshold is reached, Atlas remains confined to internal use.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Important note:\u003C\u002Fstrong> the \"calibration\" described here is \u003Cstrong>observability and threshold management\u003C\u002Fstrong>, not model retraining or automatic weight adjustment. Automatic calibration of the Atlas system prompt (example-based learning, continuous improvement) is a roadmap feature, not yet delivered.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"orchestration-post-spawn\">Post-spawn orchestration: deployment, QA and notification\u003C\u002Fh2>\n\n\u003Cp>Once the agent spawn completes, the system automatically chains three phases: pre-production deployment, quality assurance, then e-mail notification.\u003C\u002Fp>\n\n\u003Ch3>Reading the spawn contract\u003C\u002Fh3>\n\u003Cp>The post-spawn orchestrator starts by reading the JSON contract produced by the agent. This contract may contain an intent correction decided by the agent itself: it is allowed to \u003Cem>downgrade\u003C\u002Fem> the initial intent to a non-code-producing intent (question, negotiation, run). This correction is possible because the agent has read the full conversation thread, whereas the input classifier only had access to a truncated excerpt.\u003C\u002Fp>\n\n\u003Ch3>No-code branch\u003C\u002Fh3>\n\u003Cp>When the final intent is of type question, negotiation or run, the system entirely bypasses the deployment and QA phases. It directly sends a summary and marks the record as handled.\u003C\u002Fp>\n\n\u003Ch3>Code branch (chantier)\u003C\u002Fh3>\n\u003Cp>When the intent is a chantier, the full sequence is triggered:\u003C\u002Fp>\n\u003Col>\n  \u003Cli>Pre-production deployment via the standard deployment script, with a 15-minute timeout.\u003C\u002Fli>\n  \u003Cli>Execution of the automated QA test suite (Playwright) against each affected route.\u003C\u002Fli>\n  \u003Cli>Depending on the QA result, the orchestrator takes one of the two decisions described below.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>QA iteration loop\u003C\u002Fh3>\n\u003Cp>If QA fails, the orchestrator attempts a new spawn cycle enriched with the error context, up to a maximum of three iterations. Beyond that, it triggers a human escalation. If QA passes, it sends the summary and closes the record.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>spawn ok ──► deploy preprod ──► QA route(s)\n                                    │\n              ┌── QA OK ────────────┴── QA FAIL ──┐\n              ▼                                    ▼\n        Atlas notification               iteration &lt; 3 ?\n        record 'actioned'       ┌── yes ──┴── no ──┐\n                                ▼                   ▼\n                      re-spawn (QA error    human escalation\n                      context)             (max iter reached)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>E-mail notification\u003C\u002Fh3>\n\u003Cp>The final notification is routed through the system's e-mail sending facade. It is always addressed to the internal Synedre team — never directly to the external requester. This rule is a fundamental doctrine: \u003Cstrong>the AI never contacts the end client on its own initiative\u003C\u002Fstrong>. The credentials for the sending mailbox are read from server environment variables and never appear in plaintext in the code.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"scheduling\">Pipeline trigger loop\u003C\u002Fh2>\n\n\u003Cp>The Atlas pipeline is driven by standard system scheduled tasks. Each task goes through a supervision component that manages locks and execution logs.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note:\u003C\u002Fstrong> The application-level scheduling engine built into the server has been out of service since May 2026. The pipeline relies entirely on system scheduled tasks.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>Three tasks form the end-to-end loop:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Frequency\u003C\u002Fth>\n      \u003Cth>Component\u003C\u002Fth>\n      \u003Cth>Role in the loop\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Every minute\u003C\u002Ftd>\n      \u003Ctd>Atlas mailbox collector\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Ingestion and chaining.\u003C\u002Fstrong> Polls the dedicated inbox, inserts new messages, then cascades into antivirus analysis and intent classification. Classification is therefore triggered here, within the same cycle as ingestion.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Every 5 minutes (with a 2-minute offset)\u003C\u002Ftd>\n      \u003Ctd>Spawn orchestrator\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Spawn and orchestration.\u003C\u002Fstrong> Retrieves messages whose intent is classified and eligible, then launches the spawn and post-spawn orchestration (deployment, QA, notification).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Every 15 minutes\u003C\u002Ftd>\n      \u003Ctd>Lock guardian\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Safety net.\u003C\u002Fstrong> Emits an alert if a zombie lock persists beyond 10 minutes, preventing any silent pipeline blockage.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>The actual execution order is therefore: \u003Cstrong>ingestion (minute 0) → analysis + classification (same tick) → spawn (minute 2 of the 5-minute cycle)\u003C\u002Fstrong>. The two-minute offset between the collector and the spawn orchestrator ensures that classification is complete before the spawn looks for eligible messages. An advisory database-level lock covers the residual collision between a scheduled run and a manual trigger.\u003C\u002Fp>\n\n\u003Cp>The Atlas mailbox collector is distinct from the platform's general mailbox collector, which feeds a separate table and must not be confused with it.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"composants-du-pipeline\">Pipeline components: roles and responsibilities\u003C\u002Fh2>\n\n\u003Ch3>Main application components\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Component\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Atlas mailbox collector\u003C\u002Ftd>\n      \u003Ctd>Polls the dedicated inbox, inserts raw messages and tracking entries, then cascades into antivirus analysis and classification. Runs every minute.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Classification engine\u003C\u002Ftd>\n      \u003Ctd>Classifies the message intent via an LLM and materialises the corresponding record (run, question, negotiation, advice, chantier). Since June 2026: OCR enrichment of attachments and a dual safeguard against false positives and requests from prospects not yet clients.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Spawn orchestrator\u003C\u002Ftd>\n      \u003Ctd>Handles the coder agent spawn, pre-production deployment, the QA loop and the final notification. Since June 2026: spawn timeout extended to 40 minutes, with an early checkpoint instruction in the system prompt.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Classifier calibration tool\u003C\u002Ftd>\n      \u003Ctd>Produces the monthly classification quality report and drives the threshold for advancing to the next phase.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Streaming spawn wrapper\u003C\u002Ftd>\n      \u003Ctd>Mandatory Node.js component for launching the AI agent in interactive JSON streaming mode via a virtual terminal.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Agent persona loader\u003C\u002Ftd>\n      \u003Ctd>Assembles the agent personality across three levels, enriches context via vector search in the scar database, and injects the mission letter.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Agent invocation client (legacy)\u003C\u002Ftd>\n      \u003Ctd>Subprocess-based agent invocation, maintained for compatibility. The scar and doctrine helpers in this component still rely on the legacy relational database.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Multi-provider AI router (server-side web)\u003C\u002Ftd>\n      \u003Ctd>Exposes a unified content generation interface with automatic failover between AI providers (configurable priority order). Resolves the preferred provider per client according to the topology configuration.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Agents domain (web interface)\u003C\u002Ftd>\n      \u003Ctd>Agent registry, event reactor, database facades and central dashboard API.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Tables and data structures\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Agent registry\u003C\u002Fstrong>: main table of declared agents, also exposed via a summary view.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Raw ingested messages\u003C\u002Fstrong>: storage of incoming e-mails prior to processing.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Atlas tracking\u003C\u002Fstrong>: lifecycle tracking table for each message processed by Atlas.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Response drafts\u003C\u002Fstrong>: prepared response bodies, read by the classifier when materialising a question-type intent.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Atlas audit log\u003C\u002Fstrong>: trace of all pipeline actions.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Spawn events\u003C\u002Fstrong>: updatable view over the base spawn events table (see section on the updatable view).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Work records\u003C\u002Fstrong>: dedicated tables for runs, questions, negotiations, advice and chantiers.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>RAG database\u003C\u002Fstrong>: storage of vector embeddings for scar retrieval.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Client topology\u003C\u002Fstrong>: configuration of client VPS instances and their AI preferences.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>External module: OCR\u003C\u002Fh3>\n\u003Cp>Since June 2026, the classifier calls a local OCR module (Tesseract) to extract text from image attachments before classification. This module is independent of external AI providers.\u003C\u002Fp>",{"slug":14,"chapterNum":15,"title":16,"titleEn":17},"data-layer","02","La couche données","The Data Layer",{"slug":19,"chapterNum":20,"title":21,"titleEn":22},"chantiers","04","Chantiers, travaux & tâches — modèle de données et API","Worksites, Jobs & Tasks — Data Model and API",{"chapters":24},[25,32,35,36,39,46,53,60,67,74,81],{"slug":26,"chapterNum":27,"title":28,"titleEn":29,"summary":30,"summaryEn":31},"overview","01","Vue d'ensemble du harness agentique","Agentic harness overview","Le harness transforme une demande (courriel, console, cron) en action déployée en enchaînant classification, spawn LLM via pseudo-TTY, validation qualité à deux niveaux et boucle d'apprentissage, le tout sans état métier hors base de données.","The harness transforms a request (email, console, cron) into a deployed action by chaining intent classification, pseudo-TTY LLM spawn, two-level quality validation, and a learning loop — with no business state stored outside the database.",{"slug":14,"chapterNum":15,"title":16,"titleEn":17,"summary":33,"summaryEn":34},"Décrit l'architecture données de Synedre OS : une base PostgreSQL unique (un composant interne\u002Fun composant interne), les trois chemins d'accès (Nuxt\u002FNitro, Python agentique, Drizzle ORM DDL), les familles de tables par préfixe et les conventions de nommage associées.","Describes the Synedre OS data architecture: a single PostgreSQL database (un composant interne\u002Fun composant interne), the three access paths (Nuxt\u002FNitro, agentic Python, Drizzle ORM DDL), the table families by prefix, and the associated naming conventions.",{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10},{"slug":19,"chapterNum":20,"title":21,"titleEn":22,"summary":37,"summaryEn":38},"Décrit la hiérarchie chantier\u002Ftravail\u002Ftâche de Synedre OS, le modèle de données DB, les statuts\u002Fscopes canoniques et l'API Python associée.","Describes the worksite\u002Fjob\u002Ftask hierarchy in Synedre OS, the DB data model, canonical statuses\u002Fscopes, and the associated Python API.",{"slug":40,"chapterNum":41,"title":42,"titleEn":43,"summary":44,"summaryEn":45},"automates","05","Automates, crons & runs","Automations, crons & runs","Décrit la couche d'exécution non-conversationnelle de Synedre OS : les 209 façades Python, leur enrobage cron, le registre un composant interne et la frontière entre doctrine run et unité agent.","Describes the non-conversational execution layer of Synedre OS: the 209 Python facades, their cron wrapper, the un composant interne registry, and the boundary between run doctrine and agent unit.",{"slug":47,"chapterNum":48,"title":49,"titleEn":50,"summary":51,"summaryEn":52},"hub","06","Le Hub (\u002Fhub\u002F*)","The Hub (\u002Fhub\u002F*)","Présentation de l'interface d'administration privée Synedre OS (mothership-app), son architecture en couches Nuxt, la carte de ses modules et pages, ainsi que le système d'authentification et de session.","Presentation of the Synedre OS private administration interface (mothership-app), its Nuxt layered architecture, the map of its modules and pages, as well as the authentication and session management system.",{"slug":54,"chapterNum":55,"title":56,"titleEn":57,"summary":58,"summaryEn":59},"email","07","Inbox hub & Atlas Inbox — deux pipelines email","Inbox hub & Atlas Inbox — two email pipelines","Décrit les deux pipelines IMAP→DB du harness (contact@ trié pour le hub, atlas@ déclencheur d'actions agentiques), la façade d'envoi sortant et les doctrines bloquantes associées (scan AV, zéro contact client direct).","Describes the two IMAP→DB pipelines of the harness (contact@ sorted for the hub, atlas@ triggering agentic actions), the outbound sending facade, and the associated blocking doctrines (AV scan, zero direct client contact).",{"slug":61,"chapterNum":62,"title":63,"titleEn":64,"summary":65,"summaryEn":66},"memory","08","Mémoire & apprentissage — architecture à trois niveaux","Memory & Learning — Three-Level Architecture","Ce chapitre décrit les trois niveaux de mémoire du harness Synedre OS (réflexe, Zettelkasten, vectoriel pgvector), les automates d'indexation associés et la boucle de capitalisation des erreurs en règles réutilisables.","This chapter describes the three memory levels of the Synedre OS harness (reflex, Zettelkasten, vector pgvector), the associated indexing automata, and the loop for capitalizing errors into reusable rules.",{"slug":68,"chapterNum":69,"title":70,"titleEn":71,"summary":72,"summaryEn":73},"deploy","09","Déploiement & infrastructure","Deployment & infrastructure","Décrit le pipeline de mise en ligne Synedre OS : asymétrie entre .\u002Fdeploy (IA, preprod) et .\u002Fship (Alex, production), le dispatcher YAML, le pattern build-host sans build VPS, et les règles associées (secrets, commit-avant-deploy, drift, smoke).","Describes the Synedre OS release pipeline: asymmetry between .\u002Fdeploy (AI, preprod) and .\u002Fship (Alex, production), the YAML dispatcher, the build-host pattern without build VPS, and the associated rules (secrets, commit-before-deploy, drift, smoke).",{"slug":75,"chapterNum":76,"title":77,"titleEn":78,"summary":79,"summaryEn":80},"facades","10","catalogue des façades et points d'entrée du harness","harness facade and entry-point catalogue","comment le harness synedre organise ses centaines d'automates en familles fonctionnelles, avec les garde-fous en temps réel qui encadrent chaque action.","how the synedre harness organizes its hundreds of automations into functional families, with the real-time guard-rails that frame every action.",{"slug":82,"chapterNum":83,"title":84,"titleEn":85,"summary":86,"summaryEn":87},"skills-hooks","11","Skills, agents & hooks — harness agentique Synedre OS","Skills, agents & hooks — Synedre OS agentic harness","Décrit le câblage complet du harness Claude Code de synedre-os : skills disponibles, sous-agents délégables et hooks d'événements qui imposent la doctrine commit-en-flux, garde-fous prod\u002Femail et injection mémoire.","Describes the complete wiring of the synedre-os Claude Code harness: available skills, delegatable sub-agents, and event hooks that enforce the commit-in-flow doctrine, prod\u002Femail guardrails, and memory injection."]