# Plan d'action — la suite (post-livraison DGX clinique) - `Date`: 2026-06-23 - `Auteur`: Claude (mandat Dom) - `Statut`: vivant — à mettre à jour au fil des validations - `Portée`: **chapeaute** les plans existants (ne les remplace pas), dédoublonne et priorise les actions **encore ouvertes** après la livraison clinique du jour. > Contexte : le DGX part **définitivement** en clinique aujourd'hui (`192.168.1.178`), puis travail **100 % à distance** (VPN Stormshield + scp). Le jour J lui-même est couvert par `MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23.md` — non répété ici. --- ## 0. Deux clarifications de cadrage (lire avant le reste) 1. **Les gaps « pré-clinique » sont clos.** L'audit (`AUDIT_GAPS_APPLI_100PCT_2026-06-10`) et le postmortem (`POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20`) listaient des gaps durs (OVMF G2, IP statique G1, reconnexion Léa G4, reboot 11 services). Le `MEMO_JOUR_J` + `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` confirment qu'ils sont **résolus** (watchdog OVMF LIVE testé, `.178` réservé DHCP, installateur autoportant). → **sortis de ce plan.** 2. **Accès distant = multi-VPN par site (correction Dom 23/06).** Pas « WireGuard caduc » : **WireGuard = notre VPN** (labo/éditeur, reste valide) ; **Stormshield = le VPN de la clinique** (côté client) ; d'autres clients auront **d'autres VPN**. ⇒ l'accès distant devient une **fonctionnalité paramétrée par fiche site** (`INSTALLATION_MULTI_SITE`), avec **SSH cert-only + RDP comme transport commun** au-dessus du VPN propre à chaque site. Le `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (WireGuard) n'est pas suspendu — il devient *un* profil d'accès parmi d'autres. --- ## Tableau de bord — sous-projets (labo d'abord, WIP ≤ 3 actifs) > Chaque feature F# = **un sous-projet** (objectif, branche, prérequis, statut, done). Backlog priorisé ; **on n'ouvre que 2-3 sous-projets actifs à la fois** (réactivité = focus). **Merge prod = supervisé Dom.** Codex orchestre par sous-projet à son retour (28/06). **Prérequis socle (avant parallélisme)** : merger `fix/dashboard-complete-installer` (HEAD `d686c3ac2`) sur `poc-dgx` (`1d6efdb1b`) → base git propre ; ménage code mort (Qwen) cadré. | SP | Feature | Statut | Prérequis bloquant | |----|---------|--------|--------------------| | **SP-0** | Socle git (merge branche + base propre) | ✅ **fait** (23/06 — FF `1d6efdb1b`→`d686c3ac2`, local) | — *(parallélisme débloqué)* | | **SP-1** | **F14/U-B Anchors** (compound) | ✅ **code FAIT, commit `2cabc6cb7` (br. `sp1/anchors-compound`), validé données réelles + persistance** | ré-import **en place** entravé par U-A (import = doublon) → SP-4 | | SP-2 | F2 Rejeu intelligent (R1→R7) | 🟢 **débloqué** (Q-F2-1 ✅ ; point d'entrée tracé = agent_chat 5004 / SemanticMatcher) | gros chantier R1→R7, à cadrer | | SP-3 | F8 Exécution native (durcir + sandbox) | 🔴 bloqué | Q-F8-1..4 + vérif sécurité `/execute/instruction` | | SP-4 | F1/U-A Consolidation fragmentation (T3) | 🟢 **débloqué (Q-F1-1 ✅ signature trajectoire, source=DB Q-F9-1 ✅)** | — (active quand on veut) | | SP-5 | F6/U-C Mutualisation/fédération | 🟠 décidé, à coder | dépend SP-1 (anchors) | | SP-Q | F13 Ménage code mort | 🔵 en cours (Qwen, read-only) | — | | — | F3 F4 F5 F7 F9 F10 F11 F12 | ⚪ backlog priorisé | — | **Cadrage 3 sous-projets (23/06, 3 agents read-only) — interfaces communes & séquencement** : - **3 interfaces partagées à poser UNE fois** : (1) **signature de trajectoire** (SP-4 = propriétaire) ; (2) **index embeddings/FAISS partagé** (`core/embeddings/shared_index.py`, consommé par SP-2-R3 **et** la sélection skill↔tâche) ; (3) **guard `machine_id` centralisé** dans `stream_processor.py` (SP-4 ∩ SP-2-R6, ~L3197/3284). - **Phase 0** (fondation, série, petit) : signature de trajectoire + accès index partagé. - **Phase 1** (parallèle, WIP=2, faible collision) : **SP-4** (propriétaire `stream_processor.py`) ∥ **compétences** (propriétaire `core/competences/`). - **Phase 2** : **SP-2 rejeu** (le plus intriqué : touche `stream_processor` *et* FAISS) — après stabilisation Phase 0 + SP-4. - ⚠️ **Endpoints compétences `verdict`/`promote` EXISTENT déjà** (`api/lea_competences.py`, blueprint `app.py:277`, test `tests/unit/test_lea_competence_verdict_api.py`) → chantier compétences = **auto-déclenchement (hook) + sélection intelligente** seulement. - 1 **branche par sous-projet**, merge supervisé Dom. **QG Qwen (23/06, 18:30) — GO avec 4 ajustements** (intégrés) : 1. **Marqueurs de propriété dans `api_stream.py`** (commentaires par range d'endpoints SP-4 vs compétences) — seul point de contact Phase 1, éviter conflits silencieux. Pas de refactor. 2. **Fallback R2/R3 obligatoire** (SP-2) : chaque nouveau chemin de résolution **retombe sur les coords figées** si la cascade intelligente échoue. Le rejeu *enrichit*, ne remplace pas. **Non-négociable démo.** 3. **`machine_id` guard intouché en Phase 1** : le lifting du silo (`stream_processor` ~L3197/3284) est **entièrement Phase 2 / SP-2-R6**. → lève la collision SP-4 ↔ SP-2. 4. **Dead import `ExternalDecisionClient`** (`api_stream.py:L7285`, module absent, inoffensif via try/except) → à nettoyer dans le **ménage code mort** (catégorie C), pas dans SP-4. --- ## Axe central — Rejeu intelligent des actions apprises 🎯 **C'est le cœur produit** (« Léa apprend, comprend, **rejoue en exploitant ce qu'elle a appris** » — pas du record-and-replay). Vérification **runtime du 2026-06-23** (à reconfirmer `fichier:ligne` avant toute modif — méthode projet) : ### État réel de la chaîne apprentissage → rejeu | Maillon | État | Preuve (à reconfirmer) | Constat | |---|---|---|---| | Import auto Shadow → workflow rejouable | ❌ **débranché** | `finalize` `api_stream` ~2430-2466 : enqueue worker VLM, **pas** de conversion/import auto ; `ShadowLearningHook` jamais appelé | Ce qui est appris n'est **pas** rendu rejouable sans geste manuel | | Rejeu consulte le fonds appris (TargetMemoryStore) | ❌ **débranché** | `build_replay_from_raw_events` (~1841-2200) ne consulte rien ; `TargetResolver.lookup()` (~3263) jamais appelé par `replay-session` | Le rejeu rejoue des **coords/anchors figés**, pas la cible apprise | | Lecture FAISS / GlobalFAISSIndex au rejeu | ❌ **write-only** | `workflow_replay.py` accepte `faiss_manager` en param mais ne l'utilise pas ; aucun `.search()` au rejeu | Index **écrit, jamais lu** au rejeu (cohérent fédération dormante) | | verify post-condition (état UI après action) | ⚠️ **absent** | `safety_checks` + pause supervisée OK si mode supervisé (`api_stream` ~4299-4367) ; **pas** de `verify_screen` post-action | Pas de boucle de feedback succès/échec | | Templating `{{var.field.sub}}` au rejeu | ✅ **marche** | `_resolve_runtime_vars()` `replay_engine` ~2027-2041, appelé ~4293 | Données récupérées (T2A, extract) **réinjectées** en temps réel — **acquis, ne pas refaire** | | Filtre `machine_id` (cross-session) | ✅ actif = **silo** | `stream_processor` ~3197-3200 et ~3285 | Apprentissage **siloté par poste** ; rejeu direct non filtré | **Verdict** : le rejeu est aujourd'hui **« brut » (events → coords), pas « intelligent »**. L'apprentissage tourne (ShadowLearning, TargetMemory, FAISS) mais **en silos jamais wirés au rejeu**. Le templating des données récupérées, lui, fonctionne déjà. ### Chaîne cible (ce vers quoi on va) ``` Capture/Shadow → finalize → [R1] import auto en workflow rejouable → au rejeu, chaque action résolue par : cascade UI (OCR/template/YOLO/VLM) + [R2] fonds appris (TargetMemoryStore) + [R3] FAISS anchors (similarité) → [✅] réinjection des données récupérées (templating) → [R4] verify post-condition (échec = pause supervisée, pas stop = apprentissage) → [R5] le résultat du rejeu réécrit le fonds (boucle) → [R6] mutualisation : lever silo machine_id + brancher fédération ``` ### Chantiers rejeu (ordonnés, du plus structurant au plus fin) | ID | Chantier | Dépend de | |---|---|---| | **R1** | Brancher l'**import auto** d'une session apprise en workflow rejouable post-finalize | décision « provider Léa runtime » | | **R2** | Faire **consulter le fonds appris au rejeu** : câbler `TargetResolver.lookup()` / `TargetMemoryStore` dans le chemin `replay-session` (résoudre par cible apprise, pas coords figées) | R1 | | **R3** | **Lire FAISS au rejeu** : utiliser le `faiss_manager` déjà passé à `workflow_replay.py` comme fallback de résolution par similarité d'anchor | R2 | | **R4** | **verify post-condition** : vérif état UI après action ; échec → pause supervisée (cf. *failure-is-learning*) | — | | **R5** | **Boucle d'apprentissage** : succès/échec/correction humaine du rejeu réécrivent le fonds (TargetMemory + FAISS) | R2, R4 | | **R6** | **Mutualisation** : lever le filtre `machine_id` + brancher la fédération (`GlobalFAISSIndex.search()` jamais appelé) | décisions produit (silo vs fédéré, PII) | | **R7** | Bugs rejeu résiduels : reprise sur crash (R4 audit), OCR span/centre-de-ligne (R5 audit) | — | ⚠️ **Tout cet axe touche le chemin runtime de la démo.** Méthode imposée : **chirurgie itérative supervisée** — un maillon = un test ≤ 2 min = GO Dom, jamais de batch, démo `Urgence_aiva_demo` intacte à chaque étape. Reconfirmer le wiring au runtime **avant** chaque modif (imports lazy). --- ## Feature — Unification Léa ↔ VWB (anchors) 🔗 **Manque corrigé (signalé Dom 23/06).** Chantier dédié : `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17.md`. Cœur = **les anchors**, le pont entre ce que VWB capture au recording et ce que Léa **récupère au rejeu**. Symptôme T3 « Léa ne trouve pas le bloc-notes » = **fragmentation de l'apprentissage**, pas silo. Cycle de vie cible de l'anchor : capture (VWB recording / Shadow) → persistance (`visual_anchors`) → **propagation au workflow** → **récupération au rejeu** (résolution visuelle). | ID | Sous-chantier | État | Risque | |---|---|---|---| | **U-B** | **Anchors — propager `anchor_image_base64` aux substeps *compound*** (`b8b963059` n'a fait que les actions simples ; les compound, majoritaires côté Léa, restent `anchor_id=NULL` → « Ancre requise » sans image) **+ ré-importer** les workflows. `learned_workflow_bridge.py` `_convert_compound_substep` ~L279 | **prêt — fix ciblé bloquant** | faible/additif | | **U-A** | **Consolidation fragmentation** : `workflow_id` = signature stable de trajectoire + create-or-update (fusion + agrégation d'observations) — débloque T3 | design décidé | moyen (touche build/persist démo) | | **U-C** | **Fonds commun** = **F6** (décidé cross + intra) : lever filtre `machine_id` (`stream_processor` ~L3197/L3284) + brancher fédération anonymisée (`GlobalFAISSIndex.search()` jamais appelé) | décision prise, à coder | moyen-élevé | | **U-D** | **Asymétrie grounding** : VWB recording = UI-DETR-1 ; replay Léa = cascade OCR/template/VLM → unifier le chemin de résolution | sujet ouvert post-démo | à trancher | **Lien fort avec F2 (rejeu intelligent)** : la récupération des anchors au rejeu (U-B) est le **substrat de R2/R3** — sans anchors propagés/retrouvés, le rejeu ne peut pas résoudre par la vision et retombe sur des coords figées. **⚠️ Implication de la décision F6 = cross-clinique** : un pack fédéré anonymisé **n'emporte ni coordonnées ni templates** (PII) → seule la **re-résolution visuelle par anchors/FAISS** permet de le rejouer ailleurs. Donc **F6 cross-clinique entraîne quasi-mécaniquement le principe « rejeu intelligent » (Q-F2-1)** : il devient un prérequis, pas une option. *(Décision induite Q-F14-1 au registre.)* **Ordre interne** (du chantier) : confirmer provider Léa (Q-F2-2) → **U-B anchors** (gain visuel immédiat, faible risque) → U-A consolidation → U-C fédération. **Prép SP-1 / U-B (vérif runtime 23/06, read-only)** : - **Gap confirmé** : `learned_workflow_bridge.py:_convert_compound_substep` (L279-321) ne pose **jamais** `_anchor_image_base64` ; la branche action simple (L226-233) le fait. → substeps compound = `anchor_id NULL`. - **Source dispo** : dans le JSON core, l'ancre du compound est à `target.context_hints.anchor_image_base64` (pas `target.anchor_image_base64`). `target` (parent) est **déjà passé** à `_convert_compound_substep`. - **Fix** (additif, ~1 endroit) : dans la boucle compound (L169-187), poser `_anchor_image_base64` (même fallback que simple) sur le **1er substep cliquable** uniquement. - **Impact DB** : **487/582 steps `anchor_id NULL`** (84 %) ; démo `Urgence_aiva_demo` = **8/18** manquants. - ⚠️ **Caveat ré-import** : le ré-import lit la source via `_load_core_workflow(workflow_id, machine_id)` dans `data/training/workflows/{machine_id}/` → **disponible par workflow** ; la source de la démo n'est pas trouvée par nom → **vérifier par core_workflow_id avant de compter sur le ré-import de la démo**. - **Test** : ré-importer un workflow compound → un substep cliquable doit afficher son image via `GET /api/v3/anchor//thumbnail` + `StepNode.tsx`. **Risque** : code = additif (faible) ; **ré-import = étape sensible** (backup `workflows.db` + par workflow + revérifier le replay). - **FAIT 23/06** : fix commité (`2cabc6cb7`, br. `sp1/anchors-compound`), TDD RED→GREEN, **validé sur données réelles** (source réelle : 2/2 clics compound désormais ancrés). Persistance confirmée : `import_learned_workflow` (L332) `pop("_anchor_image_base64")` → `save_anchor_image` → `VisualAnchor` + `step.anchor_id` (même chemin que les actions simples). Backup DB : `instance/workflows.db.bak-sp1-2026-06-23`. - ⚠️ **Découverte** : `import_learned_workflow` **crée un nouveau workflow** (`generate_id`, L301) — **pas de mise à jour en place**. Donc rafraîchir les anchors d'un workflow **existant** (ex. démo) par ré-import = **doublon** → dépend de **U-A** (create-or-update). Le fix U-B est correct **en avant** (tout nouvel import aura les anchors compound) ; le rafraîchissement des workflows déjà en base est porté par U-A (SP-4, décision Q-F1-1). --- ## Feature — Exécution native agentique (computer-use zéro-shot) 🤖 **Manque recadré (signalé Dom 23/06).** Donner un **objectif en langage naturel** (« ouvre un navigateur et va sur YouTube ») et l'exécuter **sans workflow appris**, par planification + grounding visuel. Complément du rejeu appris (F2). **⚠️ Constat runtime (vérif 23/06) : ce n'est PAS absent — les briques existent et sont wirées.** Le manque réel = **durcissement + sandbox + validation humaine**. | ID | Brique | État runtime | Manque | |---|---|---|---| | F8.1 | **Boucle ORA** observe→reason(VLM)→act→verify→retry (`core/execution/observe_reason_act.py:run_instruction`) | ✅ **wired** via endpoint `/execute/instruction` (`api_v3/execute.py:2033`) | durcissement | | F8.2 | **Planner NL→plan** (`agent_v0/server_v1/task_planner.py:understand()` gemma4, mode `_execute_free()`) | ⚠️ présent, **mode « free » peu mûr/peu testé** | maturation + tests | | F8.3 | **Grounding cascade** OCR→**UI-TARS**→VLM (= F3, partagé) | ✅ wired (`input_handler.py:_grounding_ui_tars`) | — | | F8.4 | **Sandbox Worker** (VM/Xvfb/VNC + kill-switch + **validation humaine**) | ❌ **absent** — exécution **directe sur l'host**, sans isolation ni pause | **= le vrai manque** (décision CUA P1) | | F8.5 | **Boucle vers l'apprentissage** : un run natif réussi → capturé comme workflow appris (alimente F1) | ❌ absent | à câbler | | F8.6 | **Replanification dynamique** si l'écran change radicalement (app crash…) | ❌ absent (ORA linéaire) | à ajouter | **🔴 Sécurité — vérifié 23/06 (audit read-only)** : `POST /api/v3/execute/instruction` (VWB backend **5002**, `app.py:321` / `execute.py:2033`) lance la boucle ORA qui pilote **directement l'écran X11 de l'host** (pyautogui/xdotool, **pas de sandbox**, pas de pause humaine, pas de kill-switch) — cible de fait la VM Léa affichée. Auth = middleware Basic global (`DASHBOARD_PASSWORD`) **mais loopback exempté** ; sur le DGX clinique 5002 est **atteignable sur le LAN** (401 sans creds / 200 loopback), pas d'expo WAN. → Un acteur du LAN clinique avec les creds partagés (faibles, `Medecin2026!`) **ou tout process local (loopback)** peut déclencher une instruction agentique arbitraire sur l'host. **Mitigation à décider (prod)** : restreindre `/execute/instruction` à loopback / désactiver le mode « free » tant que F8.4 (sandbox) n'existe pas. Tant que F8.4 n'est pas en place, le mode « free » ne doit **PAS** être ouvert au-delà d'un environnement jetable — « JAMAIS l'hôte » + safety agent. **Articulation avec le mode appris** : routage — pas de workflow appris pour le but → **mode natif** ; sinon **rejeu** (F2). Et le natif réussi **devient** appris (F8.5, la boucle se referme). Décisions → registre F8. --- ## H1 — Stabilisation clinique (jour J → quelques jours, à distance) | # | Action | Source | |---|---|---| | 1 | **Aligner DGX↔local avant débranchement** : git 5 commits behind (`ec1fb81`→`d686c3ac2`) ; **backup `workflows.db` AVANT** reset | Qwen + tableau B′ | | 2 | Ajouter **`RPA_SIGNING_KEY`** dans `.env.local` DGX (HMAC métadonnées FAISS, absent) | KO-1 Qwen | | 3 | **Vérifier accès Stormshield** depuis laptop = point de non-retour avant départ | MEMO §5 | | 4 | **Mot de passe** (pas PIN) compte VM `aivanov` → RDP + presse-papier | handoff | | 5 | Confirmer **profil Stormshield laptop** avec PORQUET (atteint `.178` ?) | handoff | | 6 | **Apprentissage e2e sur 1 poste TIM réel** avant de généraliser aux 5 | MEMO + audit | ## H2 — Consolidation & dette (semaines suivantes, scp via VPN) | # | Chantier | Pourquoi | |---|---|---| | 7 | **Merger les branches** : `fix/dashboard-complete-installer` non mergée sur `poc-dgx` ; clarifier topologie `poc-dgx`↔`main` | base git propre avant tout ménage | | 8 | **Ménage code mort** (mission Qwen en cours) → exécution **par lots + QG** | post-stabilisation | | 9 | **Test de charge multi-agents** (2-3 puis 5 Léa simultanées) | 1 TIM démo ≠ 5 TIM réels | | 10 | Bugs non-rejeu résiduels de l'audit (watchdog `_retry_pending` A2, écran verrouillé non détecté…) — **re-vérifier lesquels sont encore ouverts** | audit gaps | ## H3 — Produit & fond (après stabilisation, sur décisions Dom) | # | Chantier | Bloqué par | |---|---|---| | 11 | **Chantier B anchors VWB** (propager `anchor_image_base64` aux substeps compound) — fix ciblé, risque faible | rien, prêt | | 12 | **CUA Sandbox Worker P1** (décision Dom 18/06 : VM/Xvfb/VNC + kill-switch, jamais l'hôte) | priorisation | | 13 | **Source vérité workflows** (migration JSON→SQLAlchemy, sortir DETTE-015) | post-POC | | 14 | **Shadow → Copilot → Autonomous** au runtime (aujourd'hui design) | R1-R6 + décisions | --- ## Décisions qui reviennent à Dom (registre dédié) → Suivi vivant dans **`DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md`** (Dom remplit à tête reposée). **Déjà tranché (23/06)** : - ✅ **H3 en premier**, décisions avant exécution (séquencement préservé). - ✅ **F6 = mutualisation cross-clinique ET intra-clinique** → fédération anonymisée **dans le périmètre** + lever le silo `machine_id` entre postes. - ✅ **F11 = accès multi-VPN par site** (cf. §0.2). **Encore ouvert** (bloque l'exécution des chantiers liés) : 1. **Principe « rejeu intelligent »** (R2/R3) — le rejeu *doit* consulter le fonds appris ? Change l'archi du replay. 2. **Provider Léa au runtime** (R1 + résolution). 3. **Critère de fusion des workflows** (create-or-update). 4. **Source de vérité workflows** (DB vs JSON) + **métrique produit** (24/79/37) — *reco Claude consignée : DB = vérité, JSON = échange ; métrique = rejouables validés*. ## Séquencement imposé **Stabiliser (H1)** → **base git propre + merge (H2-7)** → **ménage code mort** → **axe rejeu R1→R7** (chirurgie supervisée) → **fond produit (H3)**. On ne dégraisse pas, et on ne recâble pas le rejeu, sur une base mouvante. --- ## Plans sources (référence, ne pas dupliquer) `MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23` · `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` · `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17` · `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (volet WG suspendu) · `PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09` · `AUDIT_GAPS_APPLI_100PCT_2026-06-10` · `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16` · `CHECKLIST_DGX_PRE_CLINIQUE` · `INSTALLATION_MULTI_SITE`.