From 162e3ae6410caca4340cdf3d895a926b1cb5e2fc Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 23 Jul 2026 08:57:15 +0200 Subject: [PATCH] =?UTF-8?q?chore(ideai):=20=C3=A9tat=20runtime=20=E2=80=94?= =?UTF-8?q?=20tickets,=20m=C3=A9moire,=20t=C3=A2ches=20de=20fond?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket #92 rouvert, mises à jour de carnets/tâches de fond. Co-Authored-By: Claude Opus 4.8 --- .../97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json | 710 +++++++++++++++++- .ideai/tickets/43/carnet.md | 6 +- .ideai/tickets/43/issue.md | 8 +- .ideai/tickets/64/carnet.md | 6 +- .ideai/tickets/64/issue.md | 8 +- .ideai/tickets/7/carnet.md | 6 +- .ideai/tickets/7/issue.md | 8 +- .ideai/tickets/92/carnet.md | 25 + .ideai/tickets/92/issue.md | 16 + .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 22 +- 11 files changed, 772 insertions(+), 45 deletions(-) create mode 100644 .ideai/tickets/92/carnet.md create mode 100644 .ideai/tickets/92/issue.md diff --git a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json index 81d54e7..7450570 100644 --- a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json +++ b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json @@ -11466,7 +11466,7 @@ "stdout_tail": "Cadrage terminé et carnet du ticket `#43` mis à jour.\n\nJ’ai coordonné avec UX sur les surfaces visibles, puis tranché les points ouverts : dossier global, manifeste `idea-plugin.json`, chargement ESM via `idea-plugin://...//...`, cycle install/enable/disable/uninstall, contrats menus/layouts/MCP, et compat archive `.ideaplug`/future `.vsix`.\n\nLe carnet contient aussi le découpage prêt à assigner :\n\n- `B1` à `B4` pour DevBackend\n- `F1` à `F4` pour DevFrontend\n- `Q1` pour QA intégration clean removal\n\nPoint clé explicité : en full-trust ESM, le hot-unload mémoire parfait dans la même session n’est pas garanti par le WebView ; la garantie stricte “zéro trace” est donc définie après uninstall + redémarrage, avec masquage UI et arrêt MCP immédiats en best-effort pendant la session courante.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "4a28e304-7548-4703-8a61-9e0971d23a0e", @@ -11492,7 +11492,7 @@ "stdout_tail": "Voici les décisions UX pour #43, ciblées surface utilisateur.\n\n**1. Menus de premier niveau plugin**\n\nLes menus natifs restent les repères stables. Ordre recommandé :\n\n`Panneaux` → menus plugin de premier niveau → `Paramètres`\n\n`Paramètres` doit rester dernier, car c’est le point d’administration global. Les menus plugin sont triés de façon déterministe : ordre manifeste si fourni, sinon nom affiché du plugin. Si trop de menus plugin sont installés, ils basculent dans un menu natif `Plugins`, mais pas en v1 sauf besoin réel.\n\n**Contraint DTO** : exposer `topLevelMenuOrder?: number`, `pluginId`, `pluginDisplayName`, `pluginIcon?`, `menuId`, `label`.\n\n**2. Items plugin dans les menus existants**\n\nDans un menu natif existant, les items plugin apparaissent après les items natifs, dans une section séparée par un séparateur.\n\nReprésentation :\n- Libellé principal = action utilisateur, sans préfixe technique.\n- Provenance plugin affichée à droite en texte discret : `NomPlugin`.\n- Si plusieurs items du même plugin sont consécutifs, afficher un petit en-tête de groupe `NomPlugin`.\n- État disabled identique aux items natifs, avec tooltip ou sous-texte court indiquant la raison : `Plugin désactivé`, `Profil requis`, `Indisponible dans ce panneau`.\n- Les items provenant d’un plugin disabled ne doivent pas apparaître, sauf dans une vue de diagnostic/admin, pas dans les menus courants.\n\n**Contraint DTO** : chaque item doit porter `enabled`, `disabledReason?`, `pluginId`, `pluginDisplayName`, `groupLabel?`, `order`, `targetMenuId`.\n\n**3. Parcours utilisateur v1 plugins globaux**\n\nSurface : `Paramètres` → `Plugins`.\n\nVue principale :\n- liste des plugins installés globalement ;\n- nom, éditeur si déclaré, version, source locale, état `Activé` / `Désactivé`, indicateur `Redémarrage requis` ;\n- actions : `Activer/Désactiver`, `Désinstaller`, `Afficher le dossier/source` si disponible.\n\nInstallation :\n1. Bouton `Installer`.\n2. Choix `Archive locale` ou `Dossier local`.\n3. Sélection fichier/dossier.\n4. Écran de revue avant installation : nom, version, éditeur, contribution points détectés, mention claire `Plugin full-trust`.\n5. Action finale `Installer`.\n6. Après succès : plugin visible dans la liste, avec état `Activé après redémarrage` si les contributions UI ne sont chargées qu’au boot.\n\nDésactivation :\n- toggle global dans la liste ;\n- si l’effet nécessite restart, afficher `Sera désactivé au prochain redémarrage`.\n- ne pas mélanger enable/disable avec uninstall : uninstall est une action destructive séparée avec confirmation.\n\nUninstall :\n- confirmation sobre : nom plugin + impact `menus, layouts et tools fournis ne seront plus disponibles après redémarrage`.\n- après confirmation : état `Suppression au redémarrage` ou retrait immédiat de la liste si techniquement déjà désinstallé.\n\n**Contraint DTO** : distinguer `installed`, `enabled`, `pendingEnableState`, `pendingUninstall`, `restartRequired`, `sourceKind: archive|directory`, `trustLevel: full`.\n\n**4. Layout custom React**\n\nDans le sélecteur de panneau/layout, les layouts plugin apparaissent dans une section séparée :\n\n`Layouts natifs` puis `Layouts plugins`\n\nChaque layout plugin affiche :\n- nom du layout ;\n- provenance discrète `NomPlugin` ;\n- icône si fournie ;\n- disabled si le plugin est installé mais désactivé ou si le layout n’est pas compatible avec le contexte courant.\n\nFallback si plugin absent/disabled :\n- si une cellule utilise un layout plugin indisponible, afficher un panneau de remplacement non destructif ;\n- titre : `Layout indisponible`;\n- détails courts : `Fourni par NomPlugin`;\n- actions selon état : `Ouvrir Plugins`, `Choisir un autre layout`;\n- ne pas convertir automatiquement le layout, pour éviter la perte d’intention utilisateur.\n\nDans les menus et sélecteurs, un layout plugin disabled ne doit pas être proposé comme nouveau choix. Il ne reste visible que comme fallback pour les cellules existantes qui le référencent.\n\n**Contraint DTO** : les layouts persistés doivent garder `layoutType`, `providerPluginId?`, `providerPluginDisplayName?`, `availability: available|plugin-disabled|plugin-missing|incompatible`, `fallbackMessage?`.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "4cd94154-e24f-48c2-a696-25f27c9509ce", @@ -11518,7 +11518,7 @@ "stdout_tail": "Branche créée : **`feature/ticket43-plugin-system`**, basée sur `develop` (à jour, propre côté code). DevBackend et DevFrontend peuvent y implémenter en parallèle les lots B1-B4 et F1-F4.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "d45d048d-ec1f-4b89-aee8-e6a3f513122c", @@ -11544,7 +11544,7 @@ "stdout_tail": null, "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "d526cc49-8fef-4efe-9074-9dc0b18ced54", @@ -11570,7 +11570,7 @@ "stdout_tail": "All changes are left uncommitted per instructions, ready for Git to commit/merge.\n\n## Rapport de livraison — Ticket #43, lots F1-F4\n\n**Branche** : `feature/ticket43-plugin-system` (non commité, laissé pour Git). Backend (B1-B4) intact, aucun fichier Rust touché.\n\n### Implémenté\n\n**F1 — Port + loader runtime**\n- `domain/index.ts` : tous les types plugin (manifest DTOs, lifecycle, `ResolvedPluginMenuItem`, `CustomPluginLayoutCell`, etc.), extension `LeafCell.pluginLayout`.\n- `ports/index.ts` : `PluginGateway`, `ReviewPluginPackageInput`, + `SystemGateway.pickArchiveFile`.\n- Adapters : `TauriPluginGateway`, `MockPluginGateway` (in-memory, review/install/enable/uninstall), `WebPluginGateway` (desktop-only stub cohérent avec le reste du transport web).\n- `frontend/src/plugins/runtime/` : `PluginCommandRegistry`/`PluginLayoutRegistry`/`PluginMenuRegistry` (avec refus des ids non déclarés), `loadPlugins()` (import ESM réel via `data:` URL en test, best-effort `dispose()`), évaluateur `when` (mini-langage complet).\n- `PluginRuntimeProvider` monté dans `App.tsx`, bootstrap au démarrage.\n\n**F2 — Paramètres > Plugins** : liste, install archive/dossier avec revue pré-install (mention full-trust), enable/disable/uninstall avec confirmation, états `pending`/`invalid`/`restartRequired`.\n\n**F3 — Menus** : menus top-level plugin entre Panneaux et Paramètres, items ajoutés dans les menus natifs (provenance visible via suffixe), `when` évalué, dispatch de commandes via la registry.\n\n**F4 — Layouts custom** : `PluginLayoutCellView` (rendu du composant enregistré ou fallback), `PluginLayoutFallback` (non destructif, actions Ouvrir Plugins / Choisir un autre layout), section \"Layouts plugins\" dans le dropdown de `LayoutTabs`, branché dans `LayoutGrid` (branche `leaf.pluginLayout`, inerte pour tout layout existant).\n\n### Tests — résultat réel\n`npm run typecheck` : clean. `npm test` : **939/939 passent** (103 fichiers), y compris 33 tests nouveaux dédiés au plugin system. Un test préexistant (`ProjectsView.docking.test.tsx`) a timeout une fois sous charge complète du run parallèle mais passe systématiquement en isolation — flaky préexistant, pas une régression introduite.\n\n### Écarts / points ouverts à remonter à Architect/DevBackend\n\n1. **F4 non branché en écriture réelle** : `CustomPluginLayoutCell` sur `LeafCell` et la section \"Layouts plugins\" sont posés côté frontend selon le contrat du carnet, mais aucune commande backend (`create_layout` avec un kind plugin, opération de persistance de `state`) n'existe encore — B1-B4 ne couvre pas cette extension de DTO layout que le carnet §10 anticipait (\"F4 — DevFrontend + DevBackend si ajustement DTO\"). La sélection d'un layout plugin affiche un message explicite au lieu de simuler un succès.\n2. **`when` en contexte menu** : `gitRepository`, `agentSelected`, `terminalFocused`, `layoutCellFocused` défaultent à `false` au niveau de la barre de menu — aucun signal global de focus cellule/agent n'existe à cet endroit dans le code actuel ; seul `projectOpen` est réellement câblé.\n3. `PluginLayoutCellView`'s `onOpenPlugins`/`onChooseAnotherLayout` sont inertes dans `LayoutGrid` (pas de callback de navigation vers Paramètres threadé jusque-là) — sans risque puisqu'aucune cellule n'a `pluginLayout` défini aujourd'hui.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "6ec58427-17c9-4bfc-91a9-3a3c946ab207", @@ -11596,7 +11596,7 @@ "stdout_tail": null, "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "e0124f87-9e81-4252-b717-01a6201c56eb", @@ -11622,7 +11622,7 @@ "stdout_tail": "J’ai vérifié l’état réel du ticket #43 et corrigé le manque B1 trouvé : ajout de `CustomPluginLayoutCell` dans [layout.rs](/home/anthony/Documents/Projects/IdeA/crates/domain/src/layout.rs), export domaine, roundtrip serde, et adaptation des helpers de tests exhaustifs.\n\n**Lots**\n- B1 : complet côté backend après correction layout custom plugin.\n- B2 : présent et câblé : store FS, registry, install/review/list/enable/uninstall.\n- B3 : présent et câblé : commandes Tauri, DTO, protocole `idea-plugin://`, registration au démarrage.\n- B4 : câblé au boot via `BackendCore`/`AppState` et `ReconcilePluginMcpServers`; point ouvert ci-dessous.\n\n**Tests/build exécutés**\n- `cargo build` : OK.\n- `cargo test -p domain plugin` : OK, 3 pass.\n- `cargo test -p domain layout` : OK, 2 pass dont `custom_plugin_layout_roundtrips_with_opaque_state`.\n- `cargo test -p application plugin` : OK, 3 pass.\n- `cargo test -p infrastructure plugin` : OK, 3 pass.\n- `cargo test -p backend plugin` : OK, 0 selected, crate compile OK.\n- `cargo test -p app-tauri plugin` : OK, 1 pass.\n- `cargo test -p app-tauri --test dto_plugins` : OK, 2 pass.\n\nÀ noter : le shell imprime à chaque commande un warning Python `ModuleNotFoundError: No module named 'encodings'`, mais les commandes Cargo ci-dessus terminent bien en code 0 sauf une première invocation incorrecte `cargo test -p domain plugin layout` rejetée par Cargo comme syntaxe invalide.\n\n**Écarts restants**\n- B4 reste à auditer/renforcer : l’adapter actuel supervise des process stdio plugin, mais je n’ai pas de preuve testée qu’il réutilise pleinement le pipeline MCP existant de discovery/exposition des tools. Tests manquants : `stop on disable`, `no spawn pending uninstall`, et intégration MCP réelle.\n- Pas de commit effectué.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "0e211db2-fe78-47a8-9c7f-9094cedacd3d", @@ -11648,7 +11648,7 @@ "stdout_tail": "Validation QA backend B1-B4 effectuée sur `feature/ticket43-plugin-system`, sans modification frontend.\n\nJ’ai ajouté/ajusté uniquement des tests backend :\n- [crates/domain/tests/layout.rs](/home/anthony/Documents/Projects/IdeA/crates/domain/tests/layout.rs:47) : helpers de tests rendus exhaustifs avec `LayoutNode::CustomPluginLayout(_)`.\n- [crates/application/src/plugin/mod.rs](/home/anthony/Documents/Projects/IdeA/crates/application/src/plugin/mod.rs:1509) : tests in-memory des use cases plugin pour runtime catalog, disable/uninstall, events, registry/package cleanup et MCP reconcile/stop.\n\nRésultats verts :\n```text\ncargo test -p application plugin --offline --no-fail-fast\n# 7 passed; 0 failed\n\ncargo test -p domain -p application -p infrastructure -p backend -p app-tauri plugin --offline --no-fail-fast\n# OK\n\ncargo test -p app-tauri --test dto_plugins --offline --no-fail-fast\n# 2 passed; 0 failed\n\ncargo fmt --check\n# OK\n```\n\nWorkspace complet relancé :\n```text\ncargo test --workspace --offline --no-fail-fast -- --test-threads=1\n```\n\nRésultat : KO attendu hors plugin dans ce sandbox :\n- `-p infrastructure --lib` : 10 échecs `session::openai_compat::*` sur `bind ... Operation not permitted`.\n- `-p web-server --lib` : bind `127.0.0.1:0 Operation not permitted` et tests websocket/terminal recevant `error` au lieu de `terminal.attached`, cohérents avec restrictions loopback/PTY.\n\nVerdict QA : B1-B4 backend plugin validés. Mémo durable écrit : `ticket43-backend-b1-b4-qa-validation`.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "1126e71d-9cc0-44f2-8f7d-b5dde149cf92", @@ -11674,7 +11674,7 @@ "stdout_tail": "B4 écart fermé côté backend.\n\nJ’ai ajouté :\n- un test applicatif `reconcile_mcp_does_not_spawn_pending_uninstall_plugin_servers` : `PendingUninstall` produit un reconcile vide, donc aucun spawn.\n- la vérification existante `disable_stops_plugin_and_removes_it_from_runtime_catalog` couvre maintenant explicitement `stop_plugin` sur disable.\n- une seam interne `ExternalMcpServerBridge` dans [plugin/mod.rs](/home/anthony/Documents/Projects/IdeA/crates/infrastructure/src/plugin/mod.rs), utilisée par `ExternalMcpPluginSupervisor`.\n- deux tests infra :\n - `supervisor_delegates_stdio_servers_to_external_mcp_bridge`\n - `supervisor_stop_plugin_stops_only_matching_plugin_servers`\n\nRésultats réels :\n- `cargo test -p application plugin` : OK, 8 passed.\n- `cargo test -p infrastructure plugin` : OK, 5 passed.\n- `cargo test -p backend plugin` : OK, crate compile OK, 0 selected.\n- `cargo test -p app-tauri plugin` : OK, 1 passed.\n- `cargo build` : OK.\n\nNote : le shell continue d’imprimer le warning Python `ModuleNotFoundError: No module named 'encodings'` avant les commandes, mais Cargo termine bien en code 0 pour toutes les commandes ci-dessus.\n\nPas de commit effectué.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "c27689cd-f685-4961-b6fc-0ada5c73084d", @@ -11700,7 +11700,7 @@ "stdout_tail": "**Verdict Global**\nORANGE. Les suites automatisées backend plugin et frontend sont vertes, mais Q1 n’est pas validable de bout en bout en l’état et F4 a au moins un écart bloquant de contrat layout backend/frontend.\n\n**Tests Exécutés**\nBackend ciblé :\n```text\ncargo test -p domain -p application -p infrastructure -p backend -p app-tauri plugin --offline --no-fail-fast\n# OK\n# application plugin: 8 passed\n# domain plugin: 3 passed\n# infrastructure plugin: 5 passed\n# custom_plugin_layout_roundtrips_with_opaque_state: ok\n```\n\nFrontend :\n```text\nnpm run typecheck\n# exit 0\n\nnpm test -- --run\n# Test Files 103 passed (103)\n# Tests 939 passed (939)\n```\n\nWorkspace Cargo global :\n```text\ncargo test --workspace --offline --no-fail-fast -- --test-threads=1\n# FAILED\n# 2 targets failed: -p infrastructure --lib, -p web-server --lib\n```\n\nÉchecs réels hors plugin :\n```text\ncargo test -p infrastructure --lib session::openai_compat --offline --no-fail-fast -- --test-threads=1\n# 9 passed; 9 failed\n# failures: bind: Os { code: 1, kind: PermissionDenied, message: \"Operation not permitted\" }\n```\n\n`web-server --lib` échoue aussi hors plugin : bind `127.0.0.1:0 Operation not permitted`, puis plusieurs tests websocket/terminal reçoivent `error` au lieu de `terminal.attached`, et certains finissent en `Too many open files`. C’est cohérent avec les limites sandbox loopback/PTY.\n\n**Q1**\nCouvert par tests automatisés :\n- Install archive/dossier : backend FS archive/directory verts, UI install archive mock vert.\n- Runtime contributions : loader ESM, registry, commandes, menus et layouts mock verts.\n- Disable/uninstall : backend stop MCP + retrait runtime catalog + registry/package cleanup verts ; UI disable/uninstall mock verts.\n- Réinstall/cache stale : hash contenu différent quand bundle modifié, URL runtime inclut `contentHash`.\n\nNon validé en conditions réelles :\n- Redémarrage applicatif complet Tauri/WebView avec `idea-plugin://...`.\n- Plugin archive réel menu + layout + MCP dans une app lancée.\n- Absence réelle de process MCP après restart.\n- Uninstall + restart avec vérification visuelle “aucune contribution”.\n\n**Écarts Qualifiés**\nBloque merge #43 si F4/Q1 doivent être livrés :\n- Contrat layout custom désaligné : Rust sérialise `LayoutNode::CustomPluginLayout` en `type: \"customPluginLayout\"` ([layout.rs](/home/anthony/Documents/Projects/IdeA/crates/domain/src/layout.rs:185)), mais TS n’accepte que `leaf|split|grid` et attend `leaf.node.pluginLayout` ([index.ts](/home/anthony/Documents/Projects/IdeA/frontend/src/domain/index.ts:848)). Le fallback layout ne peut donc pas être atteint depuis un layout backend réel.\n- Persistance state custom layout non branchée : `onStateChange` est no-op dans [LayoutGrid.tsx](/home/anthony/Documents/Projects/IdeA/frontend/src/features/layout/LayoutGrid.tsx:158).\n- Actions fallback inertes : `onOpenPlugins` et `onChooseAnotherLayout` sont no-op dans [LayoutGrid.tsx](/home/anthony/Documents/Projects/IdeA/frontend/src/features/layout/LayoutGrid.tsx:166).\n\nDette/ticket de suivi acceptable :\n- `when` supporte bien toutes les variables, mais `ProjectsView` ne câble que `projectOpen`; `gitRepository`, `agentSelected`, `terminalFocused`, `layoutCellFocused` restent `false` ([ProjectsView.tsx](/home/anthony/Documents/Projects/IdeA/frontend/src/features/projects/ProjectsView.tsx:155)).\n- Backend MCP ne substitue que `${pluginRoot}` dans `active_mcp_specs`; `${appDataDir}` annoncé dans le carnet n’est pas réellement remplacé. À corriger avant d’autoriser des plugins qui s’appuient dessus.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "3104d01f-7dcd-47ca-b068-400d3cf7c515", @@ -11726,7 +11726,7 @@ "stdout_tail": "Carnet `#43` mis à jour en version 5 avec l’arbitrage définitif.\n\nDécision : `CustomPluginLayout` est une vraie variante top-level de `LayoutNode`, au format :\n\n```ts\n{ type: \"customPluginLayout\"; node: { id, pluginId, layoutType, state } }\n```\n\nDonc convergence F4 :\n- Backend : à conserver. Son `LayoutNode::CustomPluginLayout` sérialisé `type: \"customPluginLayout\"` est canonique.\n- Frontend : à ajuster. Il doit ajouter cette variante à l’union TS, retirer `LeafCell.pluginLayout` du contrat, et brancher le renderer/fallback sur `type === \"customPluginLayout\"`.\n\nJ’ai aussi tranché les deux dettes QA :\n- `${appDataDir}` MCP : pas bloquant F4, mais bloquant pour clôture B4/#43 si le contrat continue de l’annoncer. Décision par défaut : DevBackend doit l’implémenter.\n- `when` focus vars : `gitRepository` est bloquant pour F3/#43 ; `agentSelected`, `terminalFocused`, `layoutCellFocused` sont dette acceptable v1 si documentées comme best-effort non câblé.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "28f68ecc-bb52-4a9f-96e5-be2bf83f4555", @@ -11752,7 +11752,7 @@ "stdout_tail": "You've hit your session limit · resets 2:10am (Europe/Paris)", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "0d510f33-ee97-409d-b840-6680578363b9", @@ -11778,7 +11778,7 @@ "stdout_tail": "Implémenté.\n\nChangements principaux :\n- Ajout de `PluginPackageStore::app_data_dir_label()` dans [ports.rs](/home/anthony/Documents/Projects/IdeA/crates/domain/src/ports.rs:409).\n- `FsPluginPackageStore` renvoie maintenant le parent de son store `{appDataDir}/plugins`.\n- `active_mcp_specs` substitue désormais `${appDataDir}` avec `${pluginRoot}` dans `command`, `args`, `env`, `cwd`.\n- Test ajouté : `reconcile_mcp_substitutes_app_data_dir_in_plugin_server_specs`.\n\nRésultats réels :\n- `cargo test -p domain plugin` : OK, 3 passed.\n- `cargo test -p application plugin` : OK, 9 passed.\n- `cargo test -p infrastructure plugin` : OK, 5 passed.\n- `cargo test -p backend plugin` : OK, compile OK, 0 selected.\n- `cargo build` : OK.\n\nNote : les commandes shell impriment toujours le warning Python `ModuleNotFoundError: No module named 'encodings'`, mais Cargo termine bien en code 0.\n\nPas de commit effectué.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "f7a67819-cb46-4f3d-b15d-456e5df3e7e7", @@ -11804,7 +11804,7 @@ "stdout_tail": "## Rapport de convergence — Ticket #43, carnet v2\n\n**Résultat réel** : `npm run typecheck` → clean. `npm test -- --run` → **947/947 tests passent** (104 fichiers), non commité.\n\n### Changements\n\n1. **`LayoutNode` union** (`domain/index.ts`) : ajout de la variante top-level `{ type: \"customPluginLayout\"; node: CustomPluginLayoutCell }`. `CustomPluginLayoutCell` réécrit au schéma canonique exact (`id`, `pluginId`, `layoutType`, `state`, camelCase) — plus de `kind`/`nodeId`/`providerPluginId`/`providerPluginDisplayName` inventés.\n2. **`LeafCell.pluginLayout` retiré** — plus aucune trace du contrat interdit.\n3. **Renderer/fallback branchés sur `type === \"customPluginLayout\"`** : nouveau `case` dédié dans `NodeView` (`LayoutGrid.tsx`), plus `case \"customPluginLayout\"` ajouté à `mapNode` (`layout.ts`, corrige aussi une erreur TS préexistante d'exhaustivité). `layoutAvailability.ts`/`PluginLayoutFallback.tsx`/`PluginLayoutCellView.tsx` adaptés pour lire `cell.pluginId`/`cell.id` (plus de displayName sur le fil — résolu côté UI via le registry/admin list).\n4. **`onStateChange` et `onChooseAnotherLayout` réellement câblés** : ajout de `setPluginLayoutState`/`replacePluginLayoutWithTerminal` à `LayoutViewModel` (`useLayout.ts`), appuyés sur deux nouvelles fonctions pures `setCustomPluginLayoutState`/`replaceCustomPluginLayoutWithTerminal` (`layout.ts`). **Portée explicite** : ces mutations sont locales/in-session — aucune persistance backend, car le carnet confirme qu'aucun `LayoutOperation` n'existe côté Rust pour ça et qu'aucune refonte backend n'est attendue pour F4. Documenté en commentaire à chaque point d'usage.\n5. **`onOpenPlugins` réellement câblé** : nouveau prop `onOpenPluginsSettings` threadé `ProjectsView → LayoutGrid → NodeView/SplitView/GridView → PluginLayoutCellView`, appelant `setSettingsSection(\"plugins\")`.\n6. **`gitRepository` câblé sur un état réel** : `ProjectsView` interroge désormais `GitGateway.branches(active.id)` à chaque changement de projet actif ; succès → `true`, échec → `false`. Testé bout en bout (item de menu `when: \"projectOpen && gitRepository\"` désactivé/activé selon un vrai résultat de gateway git).\n\n### Tests ajoutés\n- `layout.test.ts` : parsing JSON exact produit par Rust (racine + imbriqué dans un split), `leaves()` ignore le nœud plugin, roundtrip `setCustomPluginLayoutState`/`replaceCustomPluginLayoutWithTerminal`.\n- `layoutAvailability.test.ts`, `PluginLayoutCellView.test.tsx` : migrés au nouveau schéma.\n- `pluginGitRepository.test.tsx` (nouveau) : intégration `ProjectsView` + plugin réel prouvant le `when` bloquant/débloquant selon le vrai check git.\n\n`agentSelected`/`terminalFocused`/`layoutCellFocused` restent `false` (dette v1 acceptée, non touchés conformément à l'arbitrage).", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "9d625c12-19c9-44fc-ba59-c27fc6e4b5f2", @@ -11830,7 +11830,7 @@ "stdout_tail": "**Verdict final : VERT pour merge du ticket #43.**\n\nCommandes exécutées sur `feature/ticket43-plugin-system` :\n\n```text\ncargo test -p domain -p application -p infrastructure -p backend -p app-tauri plugin --offline --no-fail-fast\n# OK\n# application plugin: 9 passed\n# domain plugin: 3 passed\n# infrastructure plugin: 5 passed\n# custom_plugin_layout_roundtrips_with_opaque_state: ok\n```\n\n```text\ncd frontend && npm run typecheck\n# exit 0\n```\n\n```text\ncd frontend && npm test -- --run\n# Test Files 104 passed (104)\n# Tests 947 passed (947)\n```\n\nContrôles ciblés confirmés :\n- `customPluginLayout` est maintenant une variante top-level TS alignée backend.\n- `LayoutGrid` route bien vers `PluginLayoutCellView`.\n- `onStateChange`, `onOpenPlugins`, `onChooseAnotherLayout` sont câblés in-session.\n- `gitRepository` est câblé via `GitGateway.branches(active.id)`.\n- `${appDataDir}` est substitué dans les specs MCP plugin, avec test dédié vert.\n\nDette v1 assumée, non bloquante :\n- `agentSelected`, `terminalFocused`, `layoutCellFocused` restent figés à `false`.\n- Persistance backend du `state` de layout plugin non implémentée ; périmètre F4 confirmé in-session.\n\nAucun nouveau blocage détecté. Mémo QA final écrit : `ticket43-plugin-system-final-qa-verdict`.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "37617e16-890d-438d-bc43-d9d4443560cd", @@ -11843,10 +11843,686 @@ "ticket_id": "50b3ce2b-b145-4446-b0e5-731aa5c16fbb", "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" }, - "state": "running", + "state": "completed", "wakePolicy": "recordOnly", "createdAtMs": 1784698592227, - "updatedAtMs": 1784698592227, + "updatedAtMs": 1784698657566, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784698657566, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "**Résumé Git — ticket #43**\n\n- 3 commits atomiques sur `feature/ticket43-plugin-system` :\n - `bb35641` `feat(backend): système de plugins — domaine, application, infrastructure (#43)`\n - `ac726d0` `feat(frontend): système de plugins — runtime, menus, layouts custom (#43)`\n - `98fb054` `chore(ideai): état runtime — tickets, mémoire, tâches de fond` (housekeeping non lié à #43, séparé du code)\n- `develop` n'avait pas divergé depuis la création de la branche → pas de rebase nécessaire.\n- Merge `--no-ff` de `feature/ticket43-plugin-system` dans `develop` (`bece7c9`), tests QA vert des deux côtés.\n- Branche de feature supprimée (mergée proprement, plus utile).\n\nRien poussé vers un remote — travail strictement local, `develop` est en avance de 37 commits sur `origin/develop`.", + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "1686eea0-9763-4184-ac2c-e3c98ff2ebbc", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "a7a0e944-3348-4fc1-a28b-0ebdd849a018", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737148382, + "updatedAtMs": 1784737151858, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784737151858, + "exit_code": null, + "error": "NoReply: agent UX returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "2d7dc097-81b1-4269-aa0f-70499cc38240", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "189414f7-c72e-4e0d-b90f-c9469e423b4a", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737159938, + "updatedAtMs": 1784737162133, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784737162133, + "exit_code": null, + "error": "NoReply: agent UX returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "86035d0e-51d7-4f9b-9893-624629813962", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "f1e2429e-22c9-4161-a70d-8347cfbf1c6f", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737181226, + "updatedAtMs": 1784737183366, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784737183366, + "exit_code": null, + "error": "NoReply: agent UX returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "d372744c-d85a-4841-98dd-c1c6b7a36d73", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "73874c22-65d0-4759-b54e-902c9fb76dd9", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737199554, + "updatedAtMs": 1784737201451, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784737201451, + "exit_code": null, + "error": "NoReply: agent UX returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "e918f3f5-9b27-4a48-bfee-0994af53f07a", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "dce19c75-9669-4e45-b8de-9950025157da", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "dce19c75-9669-4e45-b8de-9950025157da", + "ticket_id": "eae56bc9-890f-4e85-89a0-4204f51a753f", + "conversation_id": "7a2f446c-2efa-0c56-08dd-07cdc5cac19b" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737209209, + "updatedAtMs": 1784737212293, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784737212293, + "exit_code": null, + "error": "NoReply: agent Architect returned to its prompt without calling idea_reply (no answer was rendered); retry the request and ensure the agent replies via idea_reply", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "be0d9ec3-7a72-49c2-804b-caee90bb4edf", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "dce19c75-9669-4e45-b8de-9950025157da", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "dce19c75-9669-4e45-b8de-9950025157da", + "ticket_id": "ac35f4cf-0347-48c6-b94d-f9212613396b", + "conversation_id": "7a2f446c-2efa-0c56-08dd-07cdc5cac19b" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737351362, + "updatedAtMs": 1784737355018, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784737355018, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "pong", + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "66191d92-488f-4a2e-9b68-c5aceda7057f", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "4145a841-732c-4e5e-a5bb-b0182f88a213", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737365417, + "updatedAtMs": 1784737465956, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784737465956, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Waiting for the research agent to finish before drafting the design spec.", + "stderr_tail": null + }, + "completionDelivered": true + }, + { + "id": "c36e225b-6750-481f-a931-d0b0e46856e9", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "6d3f5c34-91c3-45fa-aedc-f4c75f7267d1", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784737486329, + "updatedAtMs": 1784737519484, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784737519484, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Vous avez raison, voici la conception complète, livrée directement dans ce tour.\n\n## 1. Emplacement dans le flow existant\n\nPas de nouvel écran ni d'assistant séparé. On modifie le fieldset OpenCode déjà présent dans l'écran de configuration de profil AI (celui qui affiche aujourd'hui Base URL / Model / clé API optionnelle / choix de serveur local). Ce fieldset se dédouble en deux modes affichés l'un à la place de l'autre — l'utilisateur choisit d'abord la *source du modèle*, pas un nouveau type de profil : le profil reste \"OpenCode\" dans les deux cas.\n\n```\n┌─ Profil OpenCode ─────────────────────────────────────────┐\n│ Name [________________] │\n│ Command [________________] │\n│ Arguments [________________] │\n│ │\n│ ── Source du modèle ──────────────────────────────────── │\n│ [ Local (llama.cpp) ] [ Provider cloud ] ← segmented │\n│ │\n│ ... champs propres au mode choisi (voir §3/§4) ... │\n│ │\n│ ☐ Reasoning ☐ Attachments │\n│ (dropdown serveur local — visible seulement en mode Local) │\n└──────────────────────────────────────────────────────────────┘\n```\n\nLes champs \"Name\"/\"Command\"/\"Arguments\" en haut et \"Reasoning\"/\"Attachments\" en bas restent communs, inchangés, aux deux modes.\n\n## 2. Sélecteur de mode\n\nUn contrôle segmenté à deux options, visuellement identique aux sélecteurs de source déjà présents ailleurs dans l'app (bouton actif en fond plein/texte inversé, `role=\"group\"`, `aria-pressed` par bouton) — même geste que pour choisir une source de modèle local, donc rien de nouveau à apprendre pour l'utilisateur.\n\n- **\"Local (llama.cpp)\"** — sélectionné par défaut pour tout profil existant ou nouveau ; comportement actuel inchangé au pixel près.\n- **\"Provider cloud\"** — révèle la section décrite en §3.\n\nBasculer entre les deux modes ne doit jamais faire perdre les valeurs déjà saisies dans l'autre mode : si l'utilisateur va Local → Cloud → Local, ses champs Base URL/Model doivent être toujours là. Ce n'est pas un reset de profil, juste un changement de vue sur le même formulaire.\n\n## 3. Écran de sélection de provider (mode \"Provider cloud\")\n\nÀ la place de Base URL / Model / dropdown serveur local, on affiche :\n\n```\n── Source du modèle : Provider cloud ────────────────────\n\nProvider [ ▼ Anthropic ]\n (Anthropic, OpenAI, Google, Groq, …)\n\nClé API [ •••••••••••••••••••••• ] [ 👁 ]\n Obtenue depuis le tableau de bord d'Anthropic.\n\n [ Enregistrer et continuer ]\n```\n\n- **Provider** : un menu déroulant (`select`), pas une recherche texte — la liste des providers gérés par `/connect`/`opencode auth login` est courte et vient d'une source externe à IdeA (voir réserve technique en §7). Aucun provider n'est présélectionné par défaut.\n- Le sous-texte d'aide sous \"Clé API\" s'actualise avec le nom du provider une fois choisi (*\"Obtenue depuis le tableau de bord d'Anthropic\"*) ; générique tant qu'aucun provider n'est sélectionné (*\"Obtenue depuis le tableau de bord du provider choisi\"*).\n\n## 4. Champ clé API — libellés et erreurs\n\n- Libellé : **\"Clé API\"**. Contrairement au mode Local où ce champ est optionnel, il est **obligatoire** en mode Provider cloud.\n- Champ masqué (`type=\"password\"`) avec bouton afficher/masquer (icône œil) — aucun composant de ce type n'existe encore dans l'app aujourd'hui (le champ clé API actuel du mode Local est en clair, non masqué) ; il faut donc le concevoir comme un composant réutilisable, pas une variante locale.\n- Messages d'erreur (affichés sous le champ, même style que les autres erreurs de champ du formulaire) :\n - Champ vide à la soumission : **\"La clé API est requise pour ce provider.\"**\n - Clé rejetée par le provider (retour après tentative de connexion réelle) : **\"Clé API refusée par {Provider}. Vérifiez la clé et réessayez.\"**\n - La clé n'est jamais réaffichée en clair dans un message d'erreur.\n- Le bouton \"Enregistrer et continuer\" reste désactivé tant que provider + clé API valides ne sont pas tous deux renseignés.\n\n## 5. État de chargement de la liste des providers\n\nDès que l'utilisateur bascule sur \"Provider cloud\" (ou à l'ouverture de l'écran si ce mode est déjà actif en édition) :\n\n```\nProvider [ ⟳ Chargement des providers… ] (select désactivé)\n```\n\nLe champ Clé API reste visible mais désactivé tant qu'aucun provider n'est chargé et choisi — on évite que l'utilisateur tape une clé avant de savoir pour quel provider.\n\n## 6. Échec de la récupération de la liste des providers\n\n```\nProvider ⚠ Impossible de récupérer la liste des providers.\n [ Réessayer ]\n```\n\n- Bandeau d'erreur inline au même emplacement que le select.\n- Bouton \"Réessayer\" qui relance uniquement la récupération de la liste, pas tout le formulaire.\n- Le mode \"Provider cloud\" reste sélectionnable et affiché même en échec — l'utilisateur ne doit jamais être éjecté hors de ce mode par une panne réseau ; \"Enregistrer et continuer\" reste simplement désactivé tant que la liste n'a pas fini par charger.\n\n## 7. Accessibilité\n\n- Segmented control : `role=\"group\"`, `aria-pressed` par bouton, focus visible.\n- Select Provider : `