From 7453181e6cb0ee39d924adc904ad9f65a6079489 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 20 Jun 2026 18:10:48 +0200 Subject: [PATCH] =?UTF-8?q?chore(wip):=20=C3=A9tat=20runtime=20.ideai=20(f?= =?UTF-8?q?lux=20conversation=20live)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md | 8 ++++---- .../6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl | 2 ++ .../7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md | 8 ++++---- .../7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md index 61b79de..fb1bb2c 100644 --- a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md +++ b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md @@ -1,11 +1,9 @@ --- -upTo: 93ed87f6-8dc6-4ff8-85d9-81aa30d30bed +upTo: 95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est --- **Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est -- **Response:** Chantier `orchestrator-designation` clôturé et intégré dans `develop`. Tout est local, aucune action sortante. ## Commits créés (4 atomiques, sur feature/orchestrator-designation puis rebasés sur develop) - `287681c` **feat(orchestrator)** : modèle de désignation (AgentManifest { version, entries, orchestrator } + may_write_directly(&OrchestratorDesignation)) câblé domain→application→infrastructure→app-tauri, + nouveau `crates/application/src/diag.rs` (sink de diagnostic best-effort du rendez-vous inter-agents). → tout `crates/`. - `e462136` **feat(terminals)** : durcissement du portail d'écriture de délégation (writes UTF-8 chunkés 512 o / 8 ms, réconciliation `frontAttachedAgentRef`) + tests vitest. → `frontend/`. - `09f5362` **docs** : resynchronisation de `CLAUDE.md` (rôle, méthode, cycle, vision). - `5ef001e` **chore(wip)** : état runtime `.ideai/` (agents, layouts, permissions, conversations, MEMORY.md) + les 3 checkpoints mémoire que tu as écrits + note conversation-rotation-safety-design. Découpage : code de feature / frontend / doc / runtime strictement séparés, conformément à ma règle d'atomicité. ## Rebase + merge - **Rebase** de la branche sur `develop` (qui portait le bump 0.3.0) : trivial, aucun conflit (aucun fichier en commun avec le bump). Historique linéaire. - **Merge `--no-ff`** dans `develop` : `55d887f merge(orchestrator): intègre le chantier orchestrator-designation dans develop`. 78 fichiers, +2410/−882. - **Branche `feature/orchestrator-designation` supprimée** (entièrement mergée). ## État final - Branche courante : **`develop`** @ `55d887f` (en avance de 8 sur `origin/develop`, non poussé). - `main` inchangé @ `29232dd` (release 0.3.0, non poussé). - Worktree : **propre (0 fichier dirty)**. - Rien laissé dirty. ## Jugement sur le « rouge » résiduel J'ai mergé **malgré les 8 tests app-tauri rouges** parce que QA les a qualifiés comme **contrainte d'environnement, pas défaut de code** : ils bindent un vrai socket Unix et échouent en `EPERM`, reproduit même avec une sonde Node minimale sous `/run/user/1000` et `/tmp`. Tout le reste est vert (application complète, orchestrator_service 45, infra input 35, vitest 384, tsc). La règle d'or « pas de merge d'une feature rouge » vise les défauts de feature ; ici le code est validé, la sandbox bloque juste le binding socket. **Recommandation** : rejouer `cargo test -p app-tauri --lib` (les 8 e2e socket : mcp_bridge loopback + state::mcp_e2e_loopback_tests) hors sandbox (host réel / AppImage) avant toute release, pour lever ce résidu. ## Prochaine recommandation `develop` est de nouveau une base propre. Pour le chantier suivant, dis-moi le sujet : je créerai une `feature/* dédiée depuis develop`. Avant d'en ouvrir de nouvelles, je rappelle le ménage de branches en attente de ta décision : - `feature/agent-session-limits` : intégrée → je peux la supprimer. - `feature/agent-skills` (1 commit), `feature/agent-skill-awareness` (5 commits), `fix/cold-start-delivery-race` (3 commits) : non intégrées — à passer par le cycle (Architect→Dev→QA) pour merge dans develop, ou à abandonner. Indique-moi le sort de chacune ; je n'efface rien sans ton feu vert. -- **Prompt:** Etat après rebuild AppImage du chantier `orchestrator-designation`. Contexte: - Tu as intégré `orchestrator-designation` dans `develop` (`55d887f`) et supprimé la branche feature. - Main a reconstruit l'AppImage localement. - Artefact produit: `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`. - La commande Tauri a échoué sur `linuxdeploy`, puis Main a contourné avec `appimagetool --runtime-file` extrait de `/home/anthony/Documents/IdeA_0.2.0_amd64.AppImage`. Checkpoint mémoire écrit: `checkpoint-orchestrator-designation-appimage-build`. Etat git observé après build: ```text ## develop...origin/develop [ahead 8] M .ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md M .ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl M .ideai/layouts.json ``` Ta mission Git: 1. Inspecte ces modifications runtime. 2. Décide quoi faire pour revenir à une base propre avant le prochain chantier: commit runtime, laisser dirty, ou autre action non destructive appropriée. 3. Ne push pas. Ne supprime rien sans raison solide. Ne touche pas aux artefacts build si hors git. 4. Rends compte: décision, action, branche finale, status final. Réponds via `idea_reply`. - **Response:** Base remise au propre après le rebuild AppImage. Aucune action sortante, aucune suppression. ## Inspection Les modifications observées (3 affichées + 2 autres détectées) sont **100 % de l'état runtime/mémoire `.ideai/`**, zéro ligne de code source : - `.ideai/conversations/6bc594e8…/handoff.md` + `log.jsonl` : dérive de conversation. - `.ideai/layouts.json` : état de layout UI. - `.ideai/memory/MEMORY.md` : +1 ligne d'index. - `.ideai/memory/checkpoint-orchestrator-designation-appimage-build.md` (non suivi) : ton checkpoint de build. Vérifié : l'artefact `target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage` est **git-ignored (hors git)** → je n'y touche pas, comme demandé. ## Décision & action Pur runtime → **un seul commit `chore(wip)` sur `develop`**, directement (ce n'est pas une feature, pas de cycle Architect→Dev→QA requis), pour repartir d'une base propre sans rien laisser dirty ni perdre ton checkpoint : - `9c71a5b` **chore(wip): état runtime .ideai post-build AppImage 0.3.0** (conversation 6bc594e8 handoff+log, layouts, checkpoint de build + son index MEMORY.md). ## État final - Branche courante : **`develop`** @ `9c71a5b` (en avance de 9 sur `origin/develop`, non poussé). - `main` inchangé @ `29232dd`. - Worktree : **propre (0 dirty)**. `develop` est de nouveau une base saine pour le prochain chantier. Quand tu me l'annonces, je crée une `feature/* dédiée depuis develop`. Rappel : décision toujours en attente sur le ménage des branches `feature/agent-session-limits` (intégrée, supprimable), `feature/agent-skills`, `feature/agent-skill-awareness`, `fix/cold-start-delivery-race`. - **Prompt:** Préparation du prochain chantier après fermeture de `orchestrator-designation`. Contexte: - Branche courante propre: `develop` @ `9c71a5b`, en avance de 9 sur `origin/develop`, aucun push. - `orchestrator-designation` est intégré et sa branche supprimée. - L'utilisateur a donné autonomie pour terminer les chantiers restants. Branches anciennes à statuer selon tes rappels: - `feature/agent-session-limits`: tu l'as qualifiée comme intégrée/supprimable. - `feature/agent-skills`: 1 commit non intégré. - `feature/agent-skill-awareness`: 5 commits non intégrés. - `fix/cold-start-delivery-race`: 3 commits non intégrés, probable sous-ensemble de skill-awareness. Ta mission Git: 1. Inspecte ces branches et leur relation à `develop`. 2. Prends la décision git la plus saine en autonomie: supprimer les branches déjà intégrées, conserver celles qui représentent un chantier réel, identifier les doublons/sous-ensembles. 3. Ne push pas. Pas d'action sortante. 4. Si une branche non intégrée doit être reprise comme prochain chantier, dis à Main laquelle et pourquoi. Si elle doit passer par Architect/Dev/QA avant merge, indique-le. 5. Rends compte des actions effectuées et de l'état final. Réponds via `idea_reply`. - **Response:** Ménage des branches fait + cadrage du prochain chantier. Aucune action sortante, aucune suppression risquée. ## Action exécutée - **Supprimé `feature/agent-session-limits`** (`git branch -d`, refus auto si non mergée) : 0 commit propre, entièrement intégrée dans develop. Suppression sûre. ## Analyse des 3 branches restantes (relations git vérifiées) - **`feature/agent-skill-awareness`** — base `8452333`, 5 commits propres, NON intégrée. Le chantier le plus riche et récent : `ab34363` (manifeste de skills + outil MCP `idea_skill_read`), `e93a2c1` (fix cold-start delivery), `1a10d67` (test compteur outils MCP 11→12), `566bff4` (brief « capacités IdeA »), `5be8987` (runtime). **À conserver.** - **`feature/agent-skills`** — base `9736c42`, 1 commit `ef101db` (skills domain/usecases/FS store, L12), NON intégrée, **divergente** de skill-awareness (pas ancêtre). ⚠️ Elle touche **les mêmes fichiers** que skill-awareness (`domain/src/skill.rs`, `application/src/skill/*`, `infrastructure/src/store/skill.rs`) → ce sont très probablement **deux itérations concurrentes de la même couche skill**, pas des compléments. **Conservée**, mais l'arbitrage « laquelle fait foi » est une décision d'**architecture, pas git**. - **`fix/cold-start-delivery-race`** — base `6969dc7` (ligne main : elle traîne les commits de release 0.1.0/0.2.0). Son **seul contenu réel** est `9590eac` (fix cold-start), dont le **jumeau logique `e93a2c1` est déjà dans skill-awareness** (message identique). → **doublon superseded**. `git cherry` confirme qu'aucun de ces patchs n'est encore dans develop. ## Décisions / recommandations 1. **Prochain chantier = `feature/agent-skill-awareness`.** Pourquoi : c'est le travail le plus avancé et non intégré, et il englobe le fix cold-start de la branche `fix/cold-start-delivery-race`. 2. **Il DOIT passer par le cycle avant tout merge**, dans cet ordre : - **Architect d'abord** : trancher le chevauchement skill-awareness ⟷ `feature/agent-skills` (couche skill implémentée sur les mêmes fichiers). Décider qui est la source de vérité (probablement skill-awareness subsume agent-skills) avant de figer quoi que ce soit. - **Dev/QA** : revalider vert, en particulier `1a10d67` (compteur d'outils MCP, qui a déjà bougé 11→12 puis 12→13 dans state.rs côté develop) et le fix cold-start. 3. ⚠️ **Le rebase de skill-awareness sur develop VA conflicter** : la branche modifie `crates/app-tauri/src/state.rs` et `crates/infrastructure/src/input/mod.rs`, tous deux lourdement réécrits par orchestrator-designation déjà mergé dans develop. Ce n'est **pas un rebase aveugle** : je le ferai avec l'appui de Dev pour résoudre les conflits, puis QA revalide avant mon merge `--no-ff`. 4. **`fix/cold-start-delivery-race` : je recommande sa suppression** (force `-D`, car non mergée), son contenu unique étant préservé dans skill-awareness et le reste n'étant que des commits de release déjà sur main. Je ne l'ai PAS forcée en autonomie (force-delete = action plus difficile à annuler). **Donne-moi le feu vert et je la supprime**, idéalement une fois skill-awareness intégré dans develop. ## État final - Branche courante : **`develop`** @ `9c71a5b` (+9 vs origin, non poussé). - Branches : `develop`, `main` (@ `29232dd`), `feature/agent-skill-awareness`, `feature/agent-skills`, `fix/cold-start-delivery-race`. - Worktree : **propre**. Dis-moi : (a) je lance le prochain chantier sur skill-awareness — confie d'abord à Architect l'arbitrage du chevauchement avec agent-skills ; (b) feu vert ou non pour supprimer `fix/cold-start-delivery-race`. @@ -23,4 +21,6 @@ objective: Nouvelle feature cadrée par Architect, prête à démarrer le dével - **Response:** Gestion repo locale faite pour démarrer le chantier « UX conversations/délégations + read-model live-state minimal ». ## Décisions & exécution 1. **Dirty runtime géré sans perte** : 4 fichiers `.ideai/conversations/*` (handoff + log.jsonl, flux live append-only) étaient modifiés sur `develop`. Je les ai **committés sur develop** (leur place naturelle, develop accumule déjà l'état runtime) plutôt que de les stash/jeter → zéro perte. - `338051e` — chore(wip): état runtime .ideai (flux conversation live) 2. **Nouvelle branche de feature créée depuis un `develop` propre** : - `git checkout -b feature/conversation-ux-live-state` (depuis `develop` @ `338051e`). - Nom dérivé du sujet Architect (kebab-case). ## État final - **Branche courante : `feature/conversation-ux-live-state`** (tip `338051e`). - Base : `develop` (`338051e` = `63eb49a` + le chore runtime), historique linéaire, prête pour l'implémentation. - Working tree : propre au moment de la bascule (le flux live peut le re-salir, c'est attendu ; je re-figerai au moment de committer le code de la feature en séparant code / runtime). - `develop` désormais ahead origin/develop de **16** (local only). Aucune branche obsolète supprimée. **Aucun push.** DevBackend/DevFrontend peuvent implémenter sur `feature/conversation-ux-live-state`. Reviens vers moi à l'implémentation terminée (tests verts) pour les commits atomiques et la décision de merge. - **Prompt:** Reprise après interruption utilisateur. On démarre maintenant l'implémentation du chantier `UX conversations/délégations + read-model live-state minimal`. Vérifie l'état repo actuel, décide et applique la stratégie locale: partir de `develop`, créer/basculer sur une branche feature appropriée, gérer proprement les éventuels fichiers runtime `.ideai/*` dirty sans perte, ne pas push, ne pas supprimer les branches obsolètes. Retourne branche courante + état final. Si l'appel précédent a partiellement fait quelque chose, tiens-en compte. - **Response:** État repo vérifié et stratégie locale appliquée — en tenant compte du travail partiel du ticket précédent. ## Constat (travail antérieur pris en compte) - La branche **`feature/conversation-ux-live-state`** existait déjà (créée au ticket précédent depuis `develop`), j'étais dessus. Pas besoin de la recréer. - Elle était à jour sur `develop` (tip commun `338051e`, develop ancêtre). - Seul dirty : 1 conversation runtime live (`.ideai/conversations/6bc594e8…` handoff + log.jsonl), append-only. ## Actions 1. Runtime live figé **sur `develop`** (sa place : baseline runtime partagée), sans perte : - `6cfa0b0` — chore(wip): état runtime .ideai (flux conversation live). 2. **`feature/conversation-ux-live-state` réalignée** en fast-forward sur `develop` → la feature reste **propre, sans commit runtime parasite**, prête à recevoir le code. ## État final - **Branche courante : `feature/conversation-ux-live-state`** (tip `6cfa0b0`, = `develop`). - Working tree **propre**. - `develop` ahead origin/develop de **17** (local only). Aucune branche obsolète supprimée. **Aucun push.** La branche est prête : DevBackend/DevFrontend peuvent implémenter le chantier « UX conversations/délégations + read-model live-state minimal ». Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge. -- **Prompt:** QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet. Changements code attendus: Backend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests. Frontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests. Runtime dirty `.ideai/*` aussi présent suite aux conversations live. Stratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status. \ No newline at end of file +- **Prompt:** QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet. Changements code attendus: Backend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests. Frontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests. Runtime dirty `.ideai/*` aussi présent suite aux conversations live. Stratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status. +- **Response:** Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée. ## Commits (sur la feature) **1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)** - Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs` **2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)** - Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx) **3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)** - Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6) ## Exclus / hors commit Rien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code. ## Décision merge : MERGÉ Lot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct). - Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54. ## État final - **Branche courante : `develop`** (tip `3bfb932`). - `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme). - Working tree **propre**. - Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`). - Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages. Dis-moi quand le Lot B est cadré : je prépare la branche. +- **Prompt:** Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état. \ No newline at end of file diff --git a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl index 6fc8355..cb83648 100644 --- a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl +++ b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl @@ -45,3 +45,5 @@ {"id":"47deb70a-ada1-42a2-bdae-e23b635cf4e5","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781946599405,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Reprise après interruption utilisateur. On démarre maintenant l'implémentation du chantier `UX conversations/délégations + read-model live-state minimal`. Vérifie l'état repo actuel, décide et applique la stratégie locale: partir de `develop`, créer/basculer sur une branche feature appropriée, gérer proprement les éventuels fichiers runtime `.ideai/*` dirty sans perte, ne pas push, ne pas supprimer les branches obsolètes. Retourne branche courante + état final. Si l'appel précédent a partiellement fait quelque chose, tiens-en compte."} {"id":"e77d94b0-05cd-42f0-9d3f-bb6418e521e7","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781946633041,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"État repo vérifié et stratégie locale appliquée — en tenant compte du travail partiel du ticket précédent.\n\n## Constat (travail antérieur pris en compte)\n- La branche **`feature/conversation-ux-live-state`** existait déjà (créée au ticket précédent depuis `develop`), j'étais dessus. Pas besoin de la recréer.\n- Elle était à jour sur `develop` (tip commun `338051e`, develop ancêtre).\n- Seul dirty : 1 conversation runtime live (`.ideai/conversations/6bc594e8…` handoff + log.jsonl), append-only.\n\n## Actions\n1. Runtime live figé **sur `develop`** (sa place : baseline runtime partagée), sans perte :\n - `6cfa0b0` — chore(wip): état runtime .ideai (flux conversation live).\n2. **`feature/conversation-ux-live-state` réalignée** en fast-forward sur `develop` → la feature reste **propre, sans commit runtime parasite**, prête à recevoir le code.\n\n## État final\n- **Branche courante : `feature/conversation-ux-live-state`** (tip `6cfa0b0`, = `develop`).\n- Working tree **propre**.\n- `develop` ahead origin/develop de **17** (local only). Aucune branche obsolète supprimée. **Aucun push.**\n\nLa branche est prête : DevBackend/DevFrontend peuvent implémenter le chantier « UX conversations/délégations + read-model live-state minimal ». Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge."} {"id":"93ed87f6-8dc6-4ff8-85d9-81aa30d30bed","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971560322,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet.\n\nChangements code attendus:\nBackend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests.\nFrontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests.\nRuntime dirty `.ideai/*` aussi présent suite aux conversations live.\n\nStratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status."} +{"id":"d93a5849-41e6-4249-9dbf-9691c760c64b","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971634198,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée.\n\n## Commits (sur la feature)\n**1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)**\n- Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs`\n\n**2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)**\n- Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx)\n\n**3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)**\n- Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6)\n\n## Exclus / hors commit\nRien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code.\n\n## Décision merge : MERGÉ\nLot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct).\n- Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54.\n\n## État final\n- **Branche courante : `develop`** (tip `3bfb932`).\n- `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme).\n- Working tree **propre**.\n- Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`).\n- Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages.\n\nDis-moi quand le Lot B est cadré : je prépare la branche."} +{"id":"95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971838757,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état."} diff --git a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md index a40bf61..5f7a4eb 100644 --- a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md +++ b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md @@ -1,11 +1,9 @@ --- -upTo: fc80726d-b0ec-4747-bea9-a5287ed9526e +upTo: 001f112c-4ece-4f49-a7cc-140ee76eabec objective: CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour --- **Objectif :** CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour -- **Prompt:** Cadrage architecture — nouvelle feature « Gestion des limites de session des agents » (détection + reprise auto). Étape 1 du cycle §3, AVANT tout code. Produis la cartographie : frontières domaine/application/infra, ports/contrats à créer ou étendre, et l'arborescence des fichiers touchés. Mets aussi à jour ARCHITECTURE.md. CONTEXTE PRODUIT (verrouillé avec l'utilisateur le 2026-06-16) : Besoin : IdeA doit savoir quand un agent est en limite de session ET jusqu'à quelle heure, puis lui demander de reprendre où il en était une fois la limite levée. Priorités : (1) SOLIDE — pas de bidouille, marche dans ~100% des cas même pour un novice ; (2) si possible sans dépendance au modèle de l'agent. CONSTAT DUR à intégrer : l'heure exacte de reset n'existe nulle part de façon universelle (ni OS, ni code de sortie, ni API inter-modèles). Elle est fabriquée par le fournisseur et seulement exposée dans le flux de sa CLI. Donc « 100% fiable + zéro dépendance modèle + heure exacte » sont incompatibles simultanément. SOLUTION RETENUE — détecteur HIÉRARCHIQUE calqué sur la hiérarchie de readiness existante (domain/readiness.rs) : - Niveau 1 (solide, structuré) : l'adapter structuré extrait limite + reset du flux machine. Pour Claude, `rate_limit_event.rate_limit_info` est DÉJÀ parsé dans infrastructure/session/claude.rs (~ligne 90) mais jeté (réduit à Heartbeat) — il suffit de lire le timestamp de reset (resetsAt) au lieu de le dropper. - Niveau 2 (déclaratif, configurable) : champ de profil `rate_limit_pattern` (regex + groupe de capture pour l'heure) pour agents PTY/TUI sans adapter structuré, dans la lignée des profils déclaratifs §9 (domain/profile.rs). - Niveau 3 (filet humain) : si rien ne matche mais agent `Stalled` (variante DÉJÀ prévue dans domain/readiness.rs), IdeA DEMANDE à l'utilisateur. Garantit le « 100% même pour un novice » : jamais d'inaction silencieuse. MODEL-AGNOSTIC tenu AU DOMAINE : le domaine ne connaît que quelque chose comme `RateLimited { until: Option }`. Tout le savoir spécifique modèle reste confiné aux adapters/profils. REPRISE (model-agnostique, briques existantes) : pivot sur le `conversation_id` du moteur + `--resume` natif, déjà câblés (session/claude.rs build_spawn_line + application/agent/resume.rs ListResumableAgents). Le `--resume` porte tout l'historique → pas de reconstruction manuelle. Le SessionInspector (infrastructure/inspector/claude.rs) fournit le « dernier sujet » pour l'UI. Un planificateur arme un réveil sur le port Clock (infrastructure/clock/) à resets_at puis relance via SessionPlan::Resume + prompt de reprise court. DÉCISIONS PRODUIT VERROUILLÉES : - Reprise : AUTOMATIQUE à l'heure de reset, ANNULABLE (fenêtre + notification UI). - Couverture : les TROIS niveaux d'emblée (y compris le repli regex niveau 2). - État : EN MÉMOIRE uniquement — PAS de persistance de SessionLimit. Conséquence assumée : le réveil auto ne joue que tant qu'IdeA reste ouvert ; si l'IDE est fermé/rouvert après le reset, le chemin existant ListResumableAgents (agent_was_running/conversation_id) prend le relais. DÉCOUPAGE PRESSENTI (à valider/affiner par toi) : 1) domaine (variante ReplyEvent::RateLimited, ReadinessSignal::RateLimited, type SessionLimit, état/statut agent RateLimited) ; 2) adapter Claude (extraire resetsAt) ; 3) profil (champ rate_limit_pattern) ; 4) application (service de planification de reprise sur le port Clock, annulable) ; 5) UI front (badge « limité jusqu'à HH:MM » + filet humain de confirmation). Livrable attendu : la cartographie complète (ports à créer/étendre avec leurs signatures, où vit chaque pièce, fichiers à créer/modifier, lots ordonnés pour le cycle dev/test), + ARCHITECTURE.md mis à jour. Signale tout point où ma solution entre en tension avec l'archi hexagonale existante. -- **Response:** # Cadrage architecture validé — « Gestion des limites de session des agents » Cartographie produite et **ARCHITECTURE.md mis à jour (nouvelle §21)**. J'ai vérifié ta solution contre le code réel (readiness.rs, session/claude.rs, profile.rs, ports.rs, input.rs, resume.rs, clock, events.rs). Elle est **architecturalement saine** ; je valide les 5 niveaux, avec **5 tensions hexagonales corrigées** (à respecter au dev). ## Frontières (où vit quoi) - **Domaine (pur)** : le fait neutre « limité, reset à T (peut-être) ». Rien de spécifique modèle. - **Infra (adapters)** : forme du `rate_limit_event` Claude, regex d'une TUI, parsing d'heure locale, minuterie. - **Application** : orchestration détecter→planifier→reprendre. - **Présentation** : badge + countdown + Annuler + dialogue filet humain. ## ⚠️ 5 tensions avec l'hexagonal (corrigées dans §21.2 — à lire avant de coder) 1. **T1 — `Instant` rejeté.** Le domaine parle `i64` époche-ms (via `Clock::now_millis`), `Instant` est non sérialisable/monotone/absent. ⇒ **`RateLimited { resets_at_ms: Option }`**. *Déviation assumée de ta proposition.* 2. **T2 — pas de regex dans le domaine.** `prompt_ready_pattern` a été choisi littéral exprès pour éviter la dép `regex`. ⇒ le domaine **stocke la donnée** (`RateLimitPattern{pattern,…}`), le **moteur regex + parsing d'heure vivent en infra** (`regex` ajouté au seul `Cargo.toml` d'`infrastructure`). 3. **T3 — `Clock` ne réveille pas.** Il donne l'heure, pas un timer. ⇒ **1 seul nouveau port `Scheduler`** (arm/cancel), tout le reste réutilise l'existant. 4. **T4 — `RateLimited` non terminal.** Le contrat `ReplyStream` dit « seul `Final` est terminal » et `claude.rs` rompt sur `Final`. ⇒ `RateLimited` s'intercale comme `Heartbeat`. **Point d'intégration** : un tour clos *sans* `Final` *parce que limité* ne doit **pas** devenir `AgentSessionError::Io` — `drain_bounded` doit le traiter en fin gracieuse. 5. **T5 — niveau 3 dépend du lot 2.** `ReadinessSignal::Stalled` est réservé/non produit aujourd'hui. ⇒ **le filet humain (LS6) est gated sur la livraison du lot 2 (stagnation)**. Niveaux 1+2 couvrent déjà structuré + PTY entre-temps. ## Ports à créer / étendre - **NOUVEAU** — `Scheduler` (domaine, `ports.rs`) : `arm(deadline_ms, ScheduledTask) -> ScheduleId` + `cancel(id) -> bool`, minuterie one-shot annulable in-memory ; adapter `TokioScheduler` (infra). `ScheduledTask::ResumeAgent{agent_id,node_id,conversation_id}` = donnée pure (pas de closure traversant la frontière), drain côté application — même motif que le dispatch orchestrateur §14.3. - **ÉTENDUS (aucun autre port neuf)** : `ReplyEvent` +`RateLimited` ; `ReadinessSignal` +`RateLimited` (+`classify`) ; `AgentProfile` +`rate_limit_pattern` ; `EventBus`/`DomainEvent` +5 variantes. Reprise = **réutilise** `AgentSessionFactory::start`/`LaunchAgent` + `SessionPlan::Resume` + `conversation_id` (déjà câblés) — zéro reconstruction. ## Fichiers touchés - **domaine** : `ports.rs`, `readiness.rs`, `profile.rs`, `events.rs`, `lib.rs`, **`session_limit.rs` (NEW : VO `SessionLimit` + fn pure `plan_resume`)**. - **infra** : `session/claude.rs` (lire `resetsAt`→époche-ms au lieu de jeter en Heartbeat, ligne ~90), `session/codex.rs` (si signal Codex), **`ratelimit/` (NEW : `RateLimitParser` regex niveau 2)**, **`scheduler/` (NEW : `TokioScheduler`)**, `clock/` inchangé. - **application** : **`agent/session_limit.rs` (NEW : `SessionLimitService`)**, `agent/structured.rs` (réagir au signal + réconcilier T4), `agent/mod.rs`. - **app-tauri** : composition root (wire scheduler+service+drain), commandes `cancel_agent_resume`/`resume_agent_now`/`confirm_agent_rate_limit`, relais events camelCase. - **frontend** : `features/{agents,terminals}` (badge « limité jusqu'à HH:MM » + countdown + Annuler + dialogue filet humain), gateway TS + adapter + mocks. ## Lots ordonnés (cycle dev/test §3) **LS1** domaine (variantes + VO + `plan_resume` + profil, tests purs) → **LS2** adapter Claude niv.1 ∥ **LS3** port `Scheduler`+`TokioScheduler` → **LS4** `SessionLimitService` + réconciliation T4 → **LS5** parser regex niv.2 → **LS6** filet humain niv.3 *(gate : lot 2 stagnation livré)* → **LS7** app-tauri → **LS8** frontend. **LS1+LS2+LS4 = niveau 1 Claude de bout en bout** : valeur atteinte tôt. ## Confirmé sain - **In-mémoire seul** : aucun store, aucun schéma `.ideai/` modifié. IDE rouvert après reset ⇒ le chemin existant `ListResumableAgents` reprend (popup, pas d'auto). Cohérent. - **Auto + annulable** = `Scheduler::arm`/`cancel` + events `AgentResumeScheduled`/`AgentResumeCancelled`. ## Spikes (§21.10) Format réel de `resetsAt` (époch/ISO/relatif) ; heure locale→époche + passage de minuit (niv.2) ; signal de limite côté Codex ; dédoublonnage si niv.1 **et** niv.2 matchent le même épisode (1 `SessionLimit` vivante/agent). **Prochaine étape du cycle** : passer la main à **Git** pour décider de la branche (`feature/session-limits` ?) avant que le binôme dev/test attaque LS1. - **Prompt:** Feature session-limits — cadrage du lot LS7 (câblage au composition root). Project root: /home/anthony/Documents/Projects/IdeA. État : LS1→LS6 committés. Tout le mécanisme existe mais n'est PAS branché dans app-tauri : `application::agent::session_limit::SessionLimitService` (ports injectés : Clock, Scheduler, EventBus, AgentResumer) n'est référencé nulle part dans le composition root → aucun `DomainEvent::AgentRateLimited/ResumeScheduled/Resumed/RateLimitSuspected` n'est jamais émis, aucune reprise armée. LS7 doit câbler au composition root (app-tauri), conformément à ARCHITECTURE §21. J'ai besoin d'une carte de câblage précise (PAS de code), répondant à ces points, en nommant les fichiers/structs/fonctions exacts du repo où chaque tap se branche : 1. **Instanciation du service** : où, dans app-tauri (state.rs ? di/composition root ?), instancier `SessionLimitService::new(clock, scheduler, events, resumer)`. Quel `Scheduler` concret (TokioScheduler déjà en infra), quel EventBus (TokioBroadcastEventBus partagé), quel Clock. Cycle de vie/partage (Arc) cohérent avec l'existant. 2. **Port AgentResumer → LaunchAgent** : comment implémenter `AgentResumer::resume(agent_id, node_id, conversation_id, resume_prompt)` par-dessus le mécanisme de lancement existant (`LaunchAgent` + `AgentSessionFactory` + `SessionPlan::Resume`). Où vit ce code (un adapter app-tauri ?), et quelles dépendances il capture. Référence : les passerelles voisines `HandoffProvider`/`ProviderSessionProvider` mentionnées dans session_limit.rs. 3. **Tap détection niveau 1 (structuré)** : où le flux `ReplyStream`/`ReplyEvent` des sessions structurées est consommé aujourd'hui (le « pump » qui draine les tours — cf. chat.rs `chunk_from_event`), et comment y intercepter `ReplyEvent::RateLimited{resets_at_ms}` pour appeler `service.on_rate_limited(agent_id, node_id, conversation_id, resets_at_ms)`. Comment récupérer node_id + conversation_id à ce point. 4. **Tap détection niveau 2 (PTY)** : où la sortie PTY des agents non-structurés est drainée, et où instancier un `infrastructure::RateLimitParser` (règle de sélection `ratelimit::applies(profile)`) pour le nourrir des fragments et router un `SessionLimit` détecté vers le service. Respect de l'anti-double-détection §21.10-4. 5. **Drain du Scheduler** : comment `TokioScheduler` remet les `ScheduledTask::ResumeAgent` échus (canal de remise) et où câbler le récepteur qui appelle `service.execute_resume(task)` sur le runtime Tokio. 6. **Commande Tauri d'annulation** : exposer `cancel_resume(agent_id)` comme `#[tauri::command]` (la fenêtre annulable côté UI l'appellera en LS8) — où l'enregistrer dans le `generate_handler!`. Donne aussi : ordre d'implémentation recommandé, points de fragilité/race à surveiller pour QA, et confirme que rien ne viole l'hexagonal (domaine pur, regex/parse confinés infra). Rends via idea_reply. - **Response:** # LS7 — Carte de câblage « session-limits » au composition root Tous les chemins sont relatifs au project root. Aucun code ici : seulement les points d'ancrage exacts (fichier · struct/fn · ligne). Faits vérifiés en amont : - Service : `crates/application/src/agent/session_limit.rs` — `SessionLimitService::new(clock, scheduler, events, resumer)` ; API publique : `on_rate_limited(agent_id, node_id, conversation_id, resets_at_ms)`, `execute_resume(task).await -> Result<(),AppError>`, `cancel_resume(agent_id) -> bool`. Port à implémenter : `AgentResumer::resume(agent_id, node_id, conversation_id, resume_prompt)` (async). - Scheduler : `crates/infrastructure/src/scheduler/mod.rs` — `TokioScheduler::new(tx: UnboundedSender, clock: Arc)`. Réexporté `infrastructure::TokioScheduler`. Il **pousse** la `ScheduledTask` échue dans `tx` ; le drain détient `rx`. - Parser N2 : `crates/infrastructure/src/ratelimit/mod.rs` — `RateLimitParser::new(&RateLimitPattern) -> Option`, `detect(text, now_ms) -> Option`, et la règle de sélection `ratelimit::applies(profile) -> bool` (= `structured_adapter.is_none() && rate_limit_pattern.is_some()`). Réexportés `infrastructure::RateLimitParser` / `infrastructure::ratelimit::applies`. - LS6 a déjà câblé les 6 variantes `DomainEvent::Agent{RateLimited,ResumeScheduled,ResumeCancelled,Resumed,RateLimitSuspected}` → DTO → relay (`crates/app-tauri/src/events.rs:208-253, 426-452`). Rien à faire côté event wire. --- ## 1. Instanciation du service (composition root) **Fichier : `crates/app-tauri/src/state.rs`, dans `AppState::build` (à partir de la ligne 355).** Réutiliser les adapters déjà construits en tête de `build` : - `clock` (l.358, `SystemClock`, implémente `Clock::now_millis`) → caster en `Arc` comme partout (cf. `Arc::clone(&clock) as Arc`). - `event_bus` (l.357) → `events_port` (l.369, `Arc`), le **même** bus partagé que tout le reste (donc l'`AgentRateLimited` émis passera par `spawn_relay`). Séquence d'instanciation (à placer après la construction de `launch_agent` l.668-700 et de `project_store` l.750, car le resumer en dépend ; idéalement juste avant le bloc orchestrateur l.917) : 1. `let (resume_tx, resume_rx) = tokio::sync::mpsc::unbounded_channel::();` 2. `let scheduler = Arc::new(TokioScheduler::new(resume_tx, Arc::clone(&clock) as Arc)) as Arc;` 3. `let resumer = Arc::new(AppAgentResumer::new(Arc::clone(&launch_agent), Arc::clone(&store_port), )) as Arc;` (cf. §2). 4. `let session_limit_service = Arc::new(SessionLimitService::new(Arc::clone(&clock) as Arc, scheduler, Arc::clone(&events_port), resumer));` **Cycle de vie / partage :** ajouter un champ `pub session_limit_service: Arc` à la struct `AppState` (déclaration vers l.326, à côté de `orchestrator_service`) et le renvoyer dans le littéral final (l.967-1055). Le `Arc` est partagé par : (a) la commande `cancel_resume` (§6), (b) la tâche de drain (§5), (c) les taps de détection N1/N2 (§3/§4) qui appellent `on_rate_limited`. Le `resume_rx` n'entre **pas** dans `AppState` : il est *moved* dans la tâche de drain spawné à l'intérieur de `build` (§5), exactement comme `sweep_stalled` (l.899-911). Imports à ajouter en tête de `state.rs` : `application::{SessionLimitService, AgentResumer}`, `domain::ports::{Scheduler, ScheduledTask}`, `infrastructure::TokioScheduler`. --- ## 2. Port `AgentResumer` → `LaunchAgent` (adapter app-tauri) **Nouvel adapter dans `crates/app-tauri/src/state.rs`**, à côté des passerelles stateless existantes `AppHandoffProvider` (l.100-109) / `AppProviderSessionProvider` (l.119-128) / `AppRecordTurnProvider` (l.81-90) — même patron `impl application::Trait for AppXxx`. `impl application::AgentResumer for AppAgentResumer { async fn resume(agent_id, node_id, conversation_id, resume_prompt) -> Result<(), AppError> }` recompose un `LaunchAgentInput` et appelle `self.launch_agent.execute(...)` (le **même** `Arc` que la commande `launch_agent`, l.1140). C'est `LaunchAgent` qui, via `with_handoff_provider` / `with_provider_session_provider` (l.687-695) et le routage §17.4, applique déjà `SessionPlan::Resume` quand un `conversation_id` est présent (chemin de reprise P7/P8b/§15). Le `resume_prompt` (constante `application::RESUME_PROMPT`) est le premier tour — pour le chemin **PTY natif** (composition B-2, voir l.645-653), il devra être écrit dans le PTY après spawn ; pour le chemin structuré (dormant), passé en premier `send`. **À cadrer dev :** où injecter ce premier tour. La voie la plus cohérente avec l'existant est de réutiliser le médiateur d'entrée (`MediatedInbox`/portail d'écriture PTY, l.883-892) plutôt qu'un write direct. **⚠️ Point dur de conception (à trancher, c'est LE risque du lot) :** `AgentResumer::resume` et `ScheduledTask::ResumeAgent` ne portent **pas** de `project_id`, alors que `LaunchAgentInput` exige un `Project` complet + `rows`/`cols` + `mcp_runtime` (cf. commande l.1126-1152). Le service est volontairement model/projet-agnostique. Il faut donc que l'adapter **résolve le `Project` à partir de l'`agent_id`**. Recommandation : `AppAgentResumer` détient un `Arc>>` (`ResumeContext = { project: Project, rows: u16, cols: u16 }`) **alimenté par le chemin de lancement** (commande `launch_agent`, l.1140, où `project`/`rows`/`cols` sont en main) et lu au moment du resume. Le `mcp_runtime` est **recalculé** dans `resume` à partir de `project.id` via `crate::mcp_endpoint::{idea_exe_path, mcp_endpoint}` (recette identique à l.1126-1133). Évite un scan coûteux de `project_store.list_projects` + manifestes. `store_port` reste injecté en repli (résolution si le contexte est absent après un restart — cohérent avec « état en mémoire only » : après restart c'est `ListResumableAgents` qui reprend, pas le scheduler). Dépendances capturées par l'adapter : `Arc`, `Arc` (repli), le registre `ResumeContext` partagé. --- ## 3. Tap détection niveau 1 (structuré) **Fichier : `crates/app-tauri/src/commands.rs`, fn `agent_send`, boucle de pump l.1283-1294.** C'est le seul drain actif d'un `ReplyStream` structuré (`for event in stream { ... }`). Aujourd'hui `chunk_from_event` (`crates/app-tauri/src/chat.rs:186-193`) **mappe `ReplyEvent::RateLimited` → None** (jeté). Le tap : **avant** d'appeler `chunk_from_event`, faire un `if let ReplyEvent::RateLimited { resets_at_ms } = &event { service.on_rate_limited(agent_id, node_id, conversation_id, *resets_at_ms); }`, puis continuer le drain normalement (l'event reste non-terminal, le tour continue jusqu'au `Final`). **Récupération de `node_id` + `agent_id`** : le pump ne connaît que `sid: SessionId`. Le registre `state.structured_sessions` (`crates/application/src/terminal/registry.rs:297`) mappe `SessionId → (agent_id, node_id)` — mais il manque un accès **par session_id**. Ajouter une petite méthode `meta_for_session(&SessionId) -> Option<(AgentId, NodeId)>` sur `StructuredSessions` (jumeau trivial de `live_agents` l.388, lookup direct dans `entries`). `conversation_id` : `StructuredEntry` ne le porte pas ; passer `None` (le resume dégrade proprement sans id, contrat `ScheduledTask.conversation_id: Option`) **ou**, plus précis, le lire via `providers.json` (passerelle `AppProviderSessionProvider`) — optionnel, `None` est acceptable pour LS7. **État actuel : ce tap est DORMANT.** En composition B-2 la fabrique structurée est décâblée (`launch_agent` retombe toujours sur PTY, l.645-653 ; orchestrateur sans `.with_structured`, l.947-958). Aucun agent n'a de session structurée vivante ⇒ `agent_send` n'est jamais drainé. Le câbler quand même = robustesse forward (réactivation structurée). La détection réelle aujourd'hui passe **exclusivement par le niveau 2**. Passage du service au pump : `agent_send` a `state: State` ⇒ `Arc::clone(&state.session_limit_service)` avant le `thread::spawn` (l.1282) et le *move* dans le thread. --- ## 4. Tap détection niveau 2 (PTY) **Fichier : `crates/app-tauri/src/commands.rs`, fn `launch_agent`, branche PTY l.1165-1190** (le `if output.structured.is_none()` + le `thread::spawn` du pump d'octets l.1176-1183). C'est le drain de la sortie des agents non-structurés — donc le chemin **actif** en B-2. Câblage : 1. **Sélection (anti-double-détection §21.10-4)** : avant d'armer un parser, résoudre le `AgentProfile` de l'agent et appeler `infrastructure::ratelimit::applies(&profile)`. `applies` impose déjà `structured_adapter.is_none()` (or on est dans la branche `output.structured.is_none()` ⇒ cohérent) **et** `rate_limit_pattern.is_some()`. **Besoin de wiring** : la commande `launch_agent` ne charge pas le profil. Deux options — (a) exposer le `AgentProfile` (ou au minimum le `RateLimitPattern`) résolu sur `LaunchAgentOutput` (`LaunchAgent::execute` le résout déjà en interne — le plus propre, zéro I/O en plus) ; (b) le relire via le profile store. Recommandation : (a). 2. **Instanciation** : `RateLimitParser::new(&pattern)` (retourne `Option` ⇒ regex invalide = pas de détecteur, jamais de panique). À construire **une fois par lancement**, déplacé dans le thread de pump. 3. **Alimentation** : dans la boucle `for chunk in stream` (l.1177), après `send_output`, décoder le fragment (`String::from_utf8_lossy`) et appeler `parser.detect(&text, clock.now_millis())`. Sur `Some(SessionLimit)` ⇒ `service.on_rate_limited(agent_id, node_id, conversation_id, limit.resets_at_ms)`. Ici `agent_id`, `node_id` (l.1115) et `conversation_id` (`request.conversation_id`, l.1150) sont **déjà en main** dans la commande ⇒ les cloner avant le `thread::spawn`. Idem `Arc::clone(&state.session_limit_service)` et un `Arc` (ajouter un champ `clock` à `AppState`, ou réutiliser `SystemMillisClock`). **Anti-double-détection** : garantie par construction — `applies` ne renvoie `true` que pour les agents **sans** adapter structuré ; un agent structuré (N1) n'arme jamais de parser N2. Un seul tap actif par agent. **Fragilité fragmentation** : `detect` reçoit des fragments PTY ; un motif peut être coupé entre deux chunks. Pour LS7, accepter la détection best-effort par fragment (les bannières de limite des CLI arrivent en général d'un bloc). Si QA observe des ratés, prévoir un petit buffer glissant borné (dernières ~4 Kio) côté thread — **note pour QA, pas bloquant**. --- ## 5. Drain du Scheduler `TokioScheduler` (`crates/infrastructure/src/scheduler/mod.rs:62-85`) pousse la `ScheduledTask` échue dans son `tx` (canal `mpsc` non borné). Le récepteur `resume_rx` (créé en §1) est **drainé dans une tâche détachée spawné à l'intérieur de `AppState::build`**, sur le **patron exact** du sweeper `sweep_stalled` (`crates/app-tauri/src/state.rs:899-911`) — `tauri::async_runtime::spawn` (et **pas** `tokio::spawn` : `build` tourne dans le hook `setup` sans runtime ambiant, cf. commentaire l.901-903). Boucle : `while let Some(task) = resume_rx.recv().await { if let Err(e) = service.execute_resume(task).await { /* log best-effort */ } }`. Le `Arc` et `resume_rx` sont *moved* dans la closure. `execute_resume` désarme l'entrée puis appelle `AgentResumer::resume` puis publie `AgentResumed` (déjà relayé par LS6). La tâche vit autant que l'app (le canal se ferme au drop du `TokioScheduler`/AppState). --- ## 6. Commande Tauri `cancel_resume` **Déclaration** : `crates/app-tauri/src/commands.rs` — nouvelle `#[tauri::command] pub async fn cancel_resume(agent_id: String, state: State<'_, AppState>) -> Result` : `let id = parse_agent_id(&agent_id)?; Ok(state.session_limit_service.cancel_resume(id))`. (`parse_agent_id` existe déjà, cf. l.1111/1215.) Retourne le `bool` (true = reprise effectivement annulée ; false = rien d'armé ou « cancel pile au tir » — voir doc service l.188-213). **Enregistrement** : `crates/app-tauri/src/lib.rs`, dans `tauri::generate_handler![ … ]` (l.122) — ajouter `commands::cancel_resume,` à côté des commandes agent (p.ex. après `commands::change_agent_profile`, l.166). LS8 (UI fenêtre annulable) l'appellera via `invoke("cancel_resume", { agentId })`. --- ## Ordre d'implémentation recommandé 1. **Service + scheduler + drain** (§1 + §5) avec un `AgentResumer` *stub* (renvoie `Ok(())`) ⇒ valide que `on_rate_limited` arme, que le canal remet la tâche, que `execute_resume`/`AgentResumed` partent. Testable sans toucher aux pumps. 2. **`cancel_resume`** (§6) ⇒ boucle détecter→annuler bout-en-bout vérifiable manuellement (events au front via LS6). 3. **Tap N2 PTY** (§4) — chemin actif ⇒ première vraie détection. Implique d'exposer le `RateLimitPattern`/`AgentProfile` sur `LaunchAgentOutput`. 4. **`AppAgentResumer` réel** (§2) + registre `ResumeContext` alimenté par `launch_agent` ⇒ reprise réelle. 5. **Tap N1 structuré** (§3) + `meta_for_session` ⇒ forward-compat (dormant), à câbler en dernier. ## Points de fragilité / race pour QA - **Résolution `agent_id → Project` au resume** (§2) : le gros risque. Vérifier qu'un resume après fermeture/réouverture de projet ne plante pas (repli `None`/erreur propre, jamais de panique). Tester resume quand le `ResumeContext` est absent. - **Cancel « pile au tir »** (service l.188-213, scheduler `cancel` l.87-100) : sous runtime multi-thread, `cancel` peut renvoyer `false` car la tâche vient de tirer ⇒ **pas** d'`AgentResumeCancelled`, la reprise suit son cours. UI LS8 doit tolérer un `false` (la reprise arrive quand même). Test de course explicite recommandé. - **Rafraîchissement (dédoublonnage §21.10-4)** : deux signaux de limite rapprochés pour le même agent ⇒ `disarm` puis ré-arm ; vérifier qu'on n'empile pas deux `ScheduledTask` et qu'un seul `AgentResumed` sort. - **Fragmentation PTY** (§4) : motif coupé entre chunks (best-effort en LS7, buffer glissant en repli si raté). - **Injection du `resume_prompt` en PTY** (§2) : timing d'écriture après spawn (la CLI doit être prête) — réutiliser le portail d'écriture médié plutôt qu'un write brut. - **Premier tour différé / cold-start** : interaction avec `release_agent_cold_start` (l.1132-1137) si le resume relance un agent dont le pont MCP n'est pas encore connecté. ## Conformité hexagonale — confirmée - **Domaine pur** : `SessionLimit`/`plan_resume`/`RateLimitPattern`/`ScheduledTask`/`Scheduler`/`Clock`/`EventBus` ne portent aucune I/O ni regex. ✅ - **Regex + parsing d'heure confinés infra** : `RateLimitParser` et `timeparse` vivent dans `crates/infrastructure/src/ratelimit|timeparse` ; ils produisent une **valeur domaine** (`SessionLimit`). Aucune `regex` ne franchit la frontière. ✅ - **Service applicatif pur-ports** : `SessionLimitService` ne dépend que de traits (`Clock`/`Scheduler`/`EventBus`/`AgentResumer`). ✅ - **Composition root seul à connaître le concret** : `TokioScheduler`, `RateLimitParser`, `AppAgentResumer`, le canal `mpsc` et les taps sont **tous** dans `app-tauri` (state.rs/commands.rs/lib.rs), jamais ailleurs. `AgentResumer` est un port applicatif implémenté à la racine, exactement comme `HandoffProvider`/`ProviderSessionProvider`. ✅ Aucun nouveau port domaine, aucun nouvel adapter infra : LS7 est purement du **câblage** (composition de l'existant), conforme à ARCHITECTURE §21 et au principe « zéro nouveau port/adapter » du lot. - **Prompt:** Arbitrage de scope sur le filet humain NIVEAU 3 de la feature limites de session (ARCHITECTURE §21.1 / §21 « niveau 3 »). Contexte : backend (commit 9df5923) + front (en cours, vert) de la gestion des limites de session sont posés. Niveaux 1 (structuré) et 2 (regex PTY) : pleinement fonctionnels (détecter→planifier reprise auto annulable→reprendre). Le front consomme les 5 DOMAIN_EVENT, affiche le badge « limité jusqu'à HH:MM », le compte à rebours + bouton « Annuler la reprise », et appelle la commande Tauri `cancel_resume`. GAP constaté sur le niveau 3 : l'événement `agentRateLimitSuspected` (limite suspectée SANS heure de reset fiable) est bien émis et affiché côté front (note « limité · heure inconnue — reprise à préciser »), MAIS il n'existe AUCUNE commande backend pour que l'utilisateur SAISISSE l'heure de reprise et déclenche une reprise auto à cet instant. DevFrontend a (correctement) refusé de fabriquer une commande inexistante et a posé un TODO. Donc aujourd'hui, sur niveau 3, l'utilisateur est INFORMÉ mais ne peut pas AGIR via le mécanisme de reprise auto — il lui reste seulement le relancement manuel de l'agent par les contrôles existants. Le design verrouillé (mémoire session-limit-handling-design) dit : niveau 3 = « filet humain : IdeA DEMANDE à l'utilisateur. Garantit le 100% même pour un novice : jamais d'inaction silencieuse » ; et « Couverture : les TROIS niveaux d'emblée ». QUESTION (tranche, c'est ton domaine — contrat/frontière) : A) Le niveau 3 est-il considéré COMPLET en l'état (surfacer la suspicion + s'appuyer sur le relancement manuel existant satisfait « jamais d'inaction silencieuse ») → alors la feature est terminée, on merge. OU B) Le niveau 3 EXIGE une boucle d'action : une commande applicative type `set_resume_at(agentId, epochMs)` qui arme le même `SessionLimitService` (planifier une reprise à l'instant saisi, annulable comme les autres) + un formulaire de saisie d'heure côté front. → alors c'est une slice supplémentaire LSx (domaine? non / application: méthode service + port? / app-tauri: commande / front: form) que je dois cadrer avant le merge. Si B), précise le découpage minimal et le contrat exact de la nouvelle entrée (signature commande, où elle branche dans SessionLimitService, événements émis — réutilise-t-on AgentResumeScheduled ?). Réponds de façon décisionnelle. @@ -23,4 +21,6 @@ objective: CADRAGE ARCHITECTURE (pas de code) — Support de la délégation int - **Prompt:** Inventaire architecture des chantiers applicatifs restant en cours après intégration locale de `agent-skill-awareness-v2`. Références mémoire: `remaining-work-idea-agent-control-ide` liste comme suites possibles: registre session/singleton agent, live-state partagé projet, handoff/canonical conversation log cross-session/cross-profile, UX conversations/délégations, MCP IdeA-only en flux réel, mise à jour mémoire/contexte automatique, restrictions profils supportés. Merci de classer ce qui est déjà livré vs encore actif, choisir le prochain chantier à reprendre en premier, et fournir un cadrage architectural initial. Ne code pas. - **Response:** ## Inventaire post `agent-skill-awareness-v2` Contexte observé : la branche locale est `develop...origin/develop [ahead 15]`, avec seulement des fichiers runtime `.ideai/conversations/*` dirty. Je considère donc `agent-skill-awareness-v2` intégré localement et je reclasse depuis le code actuel, pas depuis la mémoire seule. ### Déjà livré / à considérer fondation acquise 1. **Registre session / singleton agent** - Livré côté application : `TerminalSessions`, `StructuredSessions`, agrégateur `LiveSessions`, `session_for_agent`, `node_for_agent`, `rebind_agent_node`. - Livré côté Tauri/UI : `list_live_agents`, `attach_live_agent`, guards frontend de lancement singleton. - Statut : **fondation livrée**, à durcir uniquement par tests de flux réel. 2. **Messagerie inter-agents / FIFO / réponse synchrone** - Livré : `AgentMailbox`, `InputMediator`, `AgentBusyChanged`, tickets, `idea_reply`, résolution par ticket, timeout/cancel. - `OrchestratorService::ask_agent` et `reply` existent, avec conversation par paire. - Statut : **livré applicativement**, UX encore perfectible. 3. **MCP IdeA-only en flux backend** - Livré : bridge `idea mcp-server`, endpoint app-tauri, serveur MCP, outils `idea_*`, runtime MCP injecté au launch, badge source `mcp/file` côté UI. - Statut : **livré côté infrastructure/app**, reste validation produit en flux réel et polish observabilité. 4. **Handoff / canonical conversation log cross-session / cross-profile** - Plus avancé que la mémoire ne le dit : `domain/src/conversation_log.rs` définit `ConversationLog`, `HandoffStore`, `HandoffSummarizer`, `ProviderSessionStore`. - Infra livrée : `FsConversationLog`, `FsHandoffStore`, `FsProviderSessionStore`, `HeuristicHandoffSummarizer`. - App livrée : `RecordTurn`, injection handoff au `LaunchAgent`, persistance provider session, séparation pair id IdeA vs engine session id. - Statut : **architecture et première implémentation livrées** ; reste qualité de résumé, couverture UX, audit de complétude de tous les chemins de record. 5. **Restrictions profils supportés** - Livré partiellement : profils structurés Claude/Codex, `structured_adapter`, `materializes_idea_bridge`, garde `guard_mcp_bridge_supported`, profils sélectionnables. - Statut : **règle technique présente**, reste formulation produit/UI des capacités et fallbacks. 6. **Agent skill awareness** - Après intégration locale : **livré comme couche de contexte**, sans nouveau store ni DTO. `agent-skills` reste L12, `awareness` reste composition de convention file. ### Encore actif / pas complètement produit 1. **UX conversations / délégations** - Backend riche, frontend encore dispersé entre agents/layout/terminal/chat/events. - Manque une lecture utilisateur claire : qui a demandé quoi, à qui, état FIFO, réponse reçue, conversation par paire, reprise. - Statut : **actif, prochain meilleur chantier**. 2. **Live-state partagé projet** - Il existe un live-state runtime : sessions, busy, mailbox, events. - Il n’existe pas encore un modèle applicatif unifié, consultable et éventuellement persistable, du “travail en cours” projet. - Statut : **partiellement livré en runtime, pas encore comme read-model produit**. 3. **Mise à jour mémoire/contexte automatique pendant la vie d’un agent** - Injection au launch : livrée. - Outils contexte/mémoire et FileGuard : présents. - Promotion automatique, signal/bruit, mise à jour continue : pas livrés. - Statut : **actif mais à repousser après UX**, car il faut d’abord rendre les fils et décisions visibles. 4. **Handoff/canonical log qualité produit** - Socle livré, mais pas encore “expérience de conversation retrouvée” garantie dans tous les cas. - Résumeur heuristique seulement ; UX de consultation/reprise à clarifier. - Statut : **fondation livrée, produit actif**. ## Prochain chantier recommandé Je recommande de reprendre en premier : **UX conversations & délégations, avec un read-model live-state minimal**. Raison : les briques backend existent maintenant. Continuer par mémoire automatique ou nouveaux mécanismes MCP risque d’empiler de l’invisible. Le prochain verrou produit est de rendre l’orchestration compréhensible et opérable : l’utilisateur doit voir ses “employés IA” travailler, attendre, déléguer, répondre et reprendre. Ce chantier peut aussi servir d’audit d’intégration : s’il manque un événement ou une donnée backend, on l’ajoute proprement comme read-model, au lieu de découvrir les trous plus tard. ## Cadrage architectural initial ### Objectif Fournir une vue produit unifiée des conversations et délégations d’un projet : - agents vivants et cellule hôte, - état `idle/busy/limited/starting` si disponible, - délégations en cours et en attente par agent, - dernière requête/réponse utile, - conversation `User↔Agent` ou `Agent↔Agent` associée, - capacité à ouvrir/rattacher la cellule concernée. Le but n’est pas encore de créer un “journal complet” frontend. Le journal canonique existe côté `.ideai/conversations`; le premier lot doit exposer un **état opérationnel scannable**. ### Frontière backend Préférer un use case de lecture applicatif plutôt que de faire recomposer le frontend depuis dix events. Nouveau read-model applicatif proposé : ```rust ProjectWorkState { live_agents: Vec, conversations: Vec, delegations: Vec, } ``` Port/domain à éviter au départ : pas de nouveau store durable tant qu’on lit les registres existants. Le read-model peut composer : - `LiveSessions` / `TerminalSessions` / `StructuredSessions`, - `InputMediator::busy_state`, - `ConversationRegistry`, - `AgentMailbox` si une méthode d’inspection propre est ajoutée, - `ConversationLog` / `HandoffStore` en lecture best-effort pour les résumés. Si inspection mailbox nécessaire, ne pas exposer l’impl `InMemoryMailbox` : ajouter un petit port ségrégué, par exemple `AgentQueueSnapshot`, ou étendre prudemment `AgentMailbox` avec une méthode read-only si l’impact reste faible. ### Frontière Tauri / DTO Ajouter une commande de lecture, pas une mutation : - `get_project_work_state(projectId) -> ProjectWorkStateDto` DTOs camelCase, stables et tolérants aux champs absents : - `LiveAgentStateDto { agentId, name, nodeId?, sessionId?, kind: "pty"|"structured", busy, limited? }` - `DelegationStateDto { ticketId, requesterAgentId?, targetAgentId, taskPreview, status: "queued"|"delivered"|"awaitingReply"|"replied"|"timedOut", source: "mcp"|"file"|"user"?, createdAtMs? }` - `ConversationThreadSummaryDto { conversationId, left, right, lastTurnPreview?, handoffPreview?, updatedAtMs? }` Ne pas exposer tout `log.jsonl` dans ce lot. Préviews seulement. ### Frontière frontend Ajouter une feature dédiée, sans mélanger avec `TerminalView` : - `frontend/src/features/workstate/` ou `features/conversations/` - hook `useProjectWorkState(projectId)` consommant un nouveau `WorkStateGateway` - panneau dans `ProjectsView` ou extension du sidebar Agents : “Work” / “Conversations” Le frontend doit continuer à consommer des ports TS, pas `invoke()` directement : - `domain/index.ts` : types miroirs DTO. - `ports/index.ts` : `WorkStateGateway`. - `adapters/workState.ts` : Tauri invoke. - mock gateway + tests Vitest. ### Ordre d’implémentation recommandé 1. **Lot A — read-model minimal sans nouvelle persistance** - Backend command `get_project_work_state` qui agrège live agents + busy state + conversations connues. - Frontend panneau simple affichant agents live/busy et dernières délégations connues. 2. **Lot B — queue/delegation snapshot propre** - Ajouter l’inspection read-only de mailbox/FIFO si le Lot A ne peut pas montrer les tickets en attente. - Tester FIFO visible : deux demandes vers le même agent apparaissent dans l’ordre. 3. **Lot C — conversation summaries depuis log/handoff** - Lire `ConversationLog::last` et `HandoffStore::load` best-effort. - Afficher preview et reprise sans charger tout le journal. 4. **Lot D — actions UX** - “Ouvrir/rattacher l’agent”, “voir la conversation”, “arrêter”, “copier résumé”, selon capacités déjà existantes. 5. **Lot E — mémoire/contexte automatique** - Seulement après visibilité des fils : proposer/persister les apprentissages utiles depuis les conversations vers mémoire/contexte via FileGuard. ### Contraintes à respecter - Ne pas faire du frontend la source de vérité du live-state. - Ne pas polluer `.ideai/memory/` avec le journal conversationnel. - Ne pas dupliquer les registres session existants. - Garder PTY brut et structured sessions substituables dans le read-model. - Toute écriture mémoire/contexte doit rester derrière les use cases/FileGuard existants. ## Synthèse Le cœur “agent control IDE” est maintenant largement livré côté backend : singleton, mailbox/FIFO, MCP IdeA-only, log/handoff/provider sessions. Le chantier prioritaire n’est plus d’inventer ces mécanismes, mais de les rendre visibles et pilotables. Je reprendrais donc par **UX conversations/délégations + read-model live-state projet minimal**, puis seulement ensuite la mise à jour automatique mémoire/contexte. - **Prompt:** Nous passons à l'implémentation du chantier `UX conversations/délégations + read-model live-state minimal`. Merci de transformer ton cadrage en périmètre de Lot A implémentable maintenant. Donne les contrats exacts backend/Tauri/frontend, les fichiers probables, ce qui est inclus/exclu, et les tests attendus. Objectif: premier lot utile, minimal, sans nouvelle persistance durable si possible. Ne code pas. -- **Response:** ## Lot A — `ProjectWorkState` minimal, implémentable maintenant ### Objectif du lot Livrer une première vue utile et read-only du travail agent en cours dans un projet, sans nouvelle persistance durable : - lister les agents du projet avec leur état live, - indiquer cellule/session hôte quand l’agent tourne, - indiquer `idle` / `busy` à partir du médiateur d’entrée, - exposer le ticket courant si busy, - permettre au frontend d’afficher un panneau “Work” simple et scannable. Ce lot ne cherche pas encore à afficher l’historique, les previews de log, ni la file FIFO complète. Il pose le read-model minimal qui rend l’état agent visible. ## Inclus - Nouvelle commande Tauri read-only `get_project_work_state(projectId)`. - Agrégation backend depuis les stores/registres existants : manifest agents, `LiveSessions`, `InputMediator::busy_state`. - DTO frontend miroir + gateway TS. - Panneau frontend minimal dans la sidebar, probablement onglet `Work`. - Tests backend DTO/commande + tests frontend hook/panel. ## Exclus - Aucune nouvelle persistance `.ideai/`. - Pas de lecture `log.jsonl` / `handoff.md` dans le Lot A. - Pas d’inspection complète de la mailbox/FIFO. - Pas de mutation : pas de stop/reply/attach depuis ce panneau dans le premier lot. - Pas de migration de `list_live_agents` existant. - Pas de refonte des panneaux Agents/Terminal/Chat. ## Contrat backend application Le plus minimal peut rester dans `app-tauri` en composition de DTO, mais je recommande un petit use case application pour garder Tauri adapter mince. ### Nouveau module probable - `crates/application/src/workstate/mod.rs` - export dans `crates/application/src/lib.rs` ### Types application proposés ```rust pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, } pub struct GetProjectWorkStateInput { pub project: Project, } pub struct ProjectWorkState { pub agents: Vec, } pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option, pub busy: AgentBusyState, } pub struct LiveWorkSession { pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } pub enum LiveSessionKind { Pty, Structured, } ``` ### Important sur `kind` `LiveSessions::live_agents()` agrège déjà PTY + structured mais ne porte pas le kind. Pour Lot A, deux options : 1. **Option minimale** : omettre `kind` du DTO. Suffisant pour afficher “live”. 2. **Option préférable** : ajouter une méthode read-only à `LiveSessions`, sans casser l’existant : ```rust pub fn live_agent_entries(&self) -> Vec pub struct LiveAgentEntry { pub agent_id: AgentId, pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } ``` Garder `live_agents()` existant pour compatibilité avec `list_live_agents`. ### Algorithme use case 1. `manifest = contexts.load_manifest(&project).await?` 2. Convertir chaque entry en `Agent` via `entry.to_agent()`. 3. Construire une map `agent_id -> live entry` depuis `LiveSessions`. 4. Pour chaque agent : - `busy = input.busy_state(agent.id)` - `live = live_map.get(agent.id)` - produire `AgentWorkState` 5. Trier par `name` ou conserver l’ordre manifeste. Recommandation : conserver ordre manifeste pour stabilité avec `list_agents`. ## Contrat Tauri ### Nouveau DTO dans `crates/app-tauri/src/dto.rs` ```rust #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProjectWorkStateDto { pub agents: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentWorkStateDto { pub agent_id: String, pub name: String, pub profile_id: String, pub live: Option, pub busy: BusyStateDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LiveWorkSessionDto { pub node_id: String, pub session_id: String, pub kind: LiveSessionKindDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub enum LiveSessionKindDto { Pty, Structured, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", tag = "state")] pub enum BusyStateDto { Idle, Busy { ticket: String, since_ms: u64 }, } ``` Si l’équipe veut réduire encore le lot : supprimer `kind` et `LiveSessionKindDto`. ### Nouvelle commande dans `crates/app-tauri/src/commands.rs` ```rust #[tauri::command] pub async fn get_project_work_state( project_id: String, state: State<'_, AppState>, ) -> Result ``` Comportement : - `resolve_project(&project_id, &state).await?` - appeler `state.get_project_work_state.execute(...)` - mapper en DTO Erreurs : - `INVALID` si `projectId` invalide, - `NOT_FOUND` si projet inconnu, - `STORE` si manifeste illisible. ### Wiring `AppState` Fichiers probables : - `crates/app-tauri/src/state.rs` - ajouter `pub get_project_work_state: Arc`. - construire avec `IdeaiContextStore`, `LiveSessions::new(terminal_sessions, structured_sessions)`, `input_mediator`. - `crates/app-tauri/src/lib.rs` - enregistrer `commands::get_project_work_state` dans `invoke_handler`. ## Contrat frontend ### Types dans `frontend/src/domain/index.ts` ```ts export interface ProjectWorkState { agents: AgentWorkState[]; } export interface AgentWorkState { agentId: string; name: string; profileId: string; live?: LiveWorkSession | null; busy: WorkBusyState; } export interface LiveWorkSession { nodeId: string; sessionId: string; kind: "pty" | "structured"; } export type WorkBusyState = | { state: "idle" } | { state: "busy"; ticket: string; sinceMs: number }; ``` Si backend omet `kind`, retirer `kind` ici aussi. ### Port dans `frontend/src/ports/index.ts` ```ts export interface WorkStateGateway { getProjectWorkState(projectId: string): Promise; } export interface Gateways { // existants... workState: WorkStateGateway; } ``` ### Adapter Tauri Nouveau fichier : `frontend/src/adapters/workState.ts` ```ts export class TauriWorkStateGateway implements WorkStateGateway { getProjectWorkState(projectId: string): Promise { return invoke("get_project_work_state", { projectId }); } } ``` Puis wiring : - `frontend/src/adapters/index.ts` : instancier `workState: new TauriWorkStateGateway()`. - `frontend/src/adapters/mock/index.ts` : ajouter `MockWorkStateGateway`. ### Feature frontend Nouveau dossier recommandé : - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/index.ts` - `frontend/src/features/workstate/workstate.test.tsx` Hook : ```ts export interface ProjectWorkStateViewModel { state: ProjectWorkState | null; busy: boolean; error: string | null; refresh: () => Promise; } ``` Refresh initial + refresh sur events existants : - `agentLaunched` - `agentExited` - `agentBusyChanged` - `orchestratorRequestProcessed` - éventuellement `agentProfileChanged` ### UI minimale Dans `ProjectsView.tsx` : - ajouter `SidebarTab = ... | "work"` - ajouter `{ id: "work", label: "Work" }` - afficher `ProjectWorkStatePanel projectId={active.id}` si actif. Contenu minimal du panneau : - ligne par agent : nom, live/offline, busy/idle. - si live : afficher `sessionId` court ou `nodeId` court. - si busy : afficher ticket court + âge approximatif facultatif. - empty state : “No agents yet.” - erreur et bouton refresh. Pas d’actions dans Lot A. Le panneau est observabilité seulement. ## Fichiers probables Backend : - `crates/application/src/workstate/mod.rs` nouveau - `crates/application/src/lib.rs` - `crates/application/src/terminal/registry.rs` si ajout `live_agent_entries()` avec kind - `crates/app-tauri/src/dto.rs` - `crates/app-tauri/src/commands.rs` - `crates/app-tauri/src/state.rs` - `crates/app-tauri/src/lib.rs` Frontend : - `frontend/src/domain/index.ts` - `frontend/src/ports/index.ts` - `frontend/src/adapters/workState.ts` nouveau - `frontend/src/adapters/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/useProjectWorkState.ts` nouveau - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` nouveau - `frontend/src/features/workstate/index.ts` nouveau - `frontend/src/features/workstate/workstate.test.tsx` nouveau - `frontend/src/features/projects/ProjectsView.tsx` ## Tests attendus ### Rust application Nouveau test application, selon conventions existantes : - `cargo test -p application workstate` Cas : 1. Projet avec deux agents, aucun live, aucun busy → deux rows `live=None`, `busy=Idle`. 2. Agent live PTY → row porte `live.session_id`, `live.node_id`, `kind=Pty` si kind inclus. 3. Agent busy → row porte `Busy { ticket, since_ms }`. 4. Agent absent du manifeste mais présent dans registry → ignoré dans le read-model projet. ### Rust app-tauri Selon structure actuelle, ajouter dans tests de wiring/commands : - commande enregistrée dans `invoke_handler`, - DTO sérialise en camelCase : `agentId`, `profileId`, `sinceMs`, `sessionId`, `nodeId`, `kind`. Commandes de vérification probables : - `cargo test -p app-tauri --test orchestrator_wiring` - ou test dédié si les commands sont déjà testées ailleurs. ### Frontend Vitest Nouveau `workstate.test.tsx` : 1. Le panneau affiche `No agents yet.` quand liste vide. 2. Il affiche agent idle/offline. 3. Il affiche agent live + busy avec ticket court. 4. Un event `agentBusyChanged` déclenche refresh. 5. `ProjectsView` expose l’onglet `Work` et rend le panneau pour le projet actif. Commande ciblée : ```bash npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` ### Typecheck ```bash npx tsc --noEmit cargo check -p app-tauri ``` ## Notes de rigueur - Le Lot A doit rester read-only. Les actions “ouvrir/rattacher”, “stop”, “voir conversation” sont Lot D. - Ne pas lire `ConversationLog` dans Lot A : dès qu’on affiche des previews de conversation, on bascule vers Lot C. - Ne pas ajouter une persistance `live_state.json` : le live-state est dérivé des registres runtime existants. - Ne pas dupliquer `list_live_agents`; le nouveau read-model peut l’utiliser conceptuellement, mais il doit ajouter le busy state et le nom agent, donc il mérite sa commande dédiée. - Si `LiveSessions` est difficile à injecter tel quel, injecter les deux registres et agréger dans le use case, mais garder l’agrégation dans `application`, pas dans Tauri ni frontend. \ No newline at end of file +- **Response:** ## Lot A — `ProjectWorkState` minimal, implémentable maintenant ### Objectif du lot Livrer une première vue utile et read-only du travail agent en cours dans un projet, sans nouvelle persistance durable : - lister les agents du projet avec leur état live, - indiquer cellule/session hôte quand l’agent tourne, - indiquer `idle` / `busy` à partir du médiateur d’entrée, - exposer le ticket courant si busy, - permettre au frontend d’afficher un panneau “Work” simple et scannable. Ce lot ne cherche pas encore à afficher l’historique, les previews de log, ni la file FIFO complète. Il pose le read-model minimal qui rend l’état agent visible. ## Inclus - Nouvelle commande Tauri read-only `get_project_work_state(projectId)`. - Agrégation backend depuis les stores/registres existants : manifest agents, `LiveSessions`, `InputMediator::busy_state`. - DTO frontend miroir + gateway TS. - Panneau frontend minimal dans la sidebar, probablement onglet `Work`. - Tests backend DTO/commande + tests frontend hook/panel. ## Exclus - Aucune nouvelle persistance `.ideai/`. - Pas de lecture `log.jsonl` / `handoff.md` dans le Lot A. - Pas d’inspection complète de la mailbox/FIFO. - Pas de mutation : pas de stop/reply/attach depuis ce panneau dans le premier lot. - Pas de migration de `list_live_agents` existant. - Pas de refonte des panneaux Agents/Terminal/Chat. ## Contrat backend application Le plus minimal peut rester dans `app-tauri` en composition de DTO, mais je recommande un petit use case application pour garder Tauri adapter mince. ### Nouveau module probable - `crates/application/src/workstate/mod.rs` - export dans `crates/application/src/lib.rs` ### Types application proposés ```rust pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, } pub struct GetProjectWorkStateInput { pub project: Project, } pub struct ProjectWorkState { pub agents: Vec, } pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option, pub busy: AgentBusyState, } pub struct LiveWorkSession { pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } pub enum LiveSessionKind { Pty, Structured, } ``` ### Important sur `kind` `LiveSessions::live_agents()` agrège déjà PTY + structured mais ne porte pas le kind. Pour Lot A, deux options : 1. **Option minimale** : omettre `kind` du DTO. Suffisant pour afficher “live”. 2. **Option préférable** : ajouter une méthode read-only à `LiveSessions`, sans casser l’existant : ```rust pub fn live_agent_entries(&self) -> Vec pub struct LiveAgentEntry { pub agent_id: AgentId, pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } ``` Garder `live_agents()` existant pour compatibilité avec `list_live_agents`. ### Algorithme use case 1. `manifest = contexts.load_manifest(&project).await?` 2. Convertir chaque entry en `Agent` via `entry.to_agent()`. 3. Construire une map `agent_id -> live entry` depuis `LiveSessions`. 4. Pour chaque agent : - `busy = input.busy_state(agent.id)` - `live = live_map.get(agent.id)` - produire `AgentWorkState` 5. Trier par `name` ou conserver l’ordre manifeste. Recommandation : conserver ordre manifeste pour stabilité avec `list_agents`. ## Contrat Tauri ### Nouveau DTO dans `crates/app-tauri/src/dto.rs` ```rust #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProjectWorkStateDto { pub agents: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentWorkStateDto { pub agent_id: String, pub name: String, pub profile_id: String, pub live: Option, pub busy: BusyStateDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LiveWorkSessionDto { pub node_id: String, pub session_id: String, pub kind: LiveSessionKindDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub enum LiveSessionKindDto { Pty, Structured, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", tag = "state")] pub enum BusyStateDto { Idle, Busy { ticket: String, since_ms: u64 }, } ``` Si l’équipe veut réduire encore le lot : supprimer `kind` et `LiveSessionKindDto`. ### Nouvelle commande dans `crates/app-tauri/src/commands.rs` ```rust #[tauri::command] pub async fn get_project_work_state( project_id: String, state: State<'_, AppState>, ) -> Result ``` Comportement : - `resolve_project(&project_id, &state).await?` - appeler `state.get_project_work_state.execute(...)` - mapper en DTO Erreurs : - `INVALID` si `projectId` invalide, - `NOT_FOUND` si projet inconnu, - `STORE` si manifeste illisible. ### Wiring `AppState` Fichiers probables : - `crates/app-tauri/src/state.rs` - ajouter `pub get_project_work_state: Arc`. - construire avec `IdeaiContextStore`, `LiveSessions::new(terminal_sessions, structured_sessions)`, `input_mediator`. - `crates/app-tauri/src/lib.rs` - enregistrer `commands::get_project_work_state` dans `invoke_handler`. ## Contrat frontend ### Types dans `frontend/src/domain/index.ts` ```ts export interface ProjectWorkState { agents: AgentWorkState[]; } export interface AgentWorkState { agentId: string; name: string; profileId: string; live?: LiveWorkSession | null; busy: WorkBusyState; } export interface LiveWorkSession { nodeId: string; sessionId: string; kind: "pty" | "structured"; } export type WorkBusyState = | { state: "idle" } | { state: "busy"; ticket: string; sinceMs: number }; ``` Si backend omet `kind`, retirer `kind` ici aussi. ### Port dans `frontend/src/ports/index.ts` ```ts export interface WorkStateGateway { getProjectWorkState(projectId: string): Promise; } export interface Gateways { // existants... workState: WorkStateGateway; } ``` ### Adapter Tauri Nouveau fichier : `frontend/src/adapters/workState.ts` ```ts export class TauriWorkStateGateway implements WorkStateGateway { getProjectWorkState(projectId: string): Promise { return invoke("get_project_work_state", { projectId }); } } ``` Puis wiring : - `frontend/src/adapters/index.ts` : instancier `workState: new TauriWorkStateGateway()`. - `frontend/src/adapters/mock/index.ts` : ajouter `MockWorkStateGateway`. ### Feature frontend Nouveau dossier recommandé : - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/index.ts` - `frontend/src/features/workstate/workstate.test.tsx` Hook : ```ts export interface ProjectWorkStateViewModel { state: ProjectWorkState | null; busy: boolean; error: string | null; refresh: () => Promise; } ``` Refresh initial + refresh sur events existants : - `agentLaunched` - `agentExited` - `agentBusyChanged` - `orchestratorRequestProcessed` - éventuellement `agentProfileChanged` ### UI minimale Dans `ProjectsView.tsx` : - ajouter `SidebarTab = ... | "work"` - ajouter `{ id: "work", label: "Work" }` - afficher `ProjectWorkStatePanel projectId={active.id}` si actif. Contenu minimal du panneau : - ligne par agent : nom, live/offline, busy/idle. - si live : afficher `sessionId` court ou `nodeId` court. - si busy : afficher ticket court + âge approximatif facultatif. - empty state : “No agents yet.” - erreur et bouton refresh. Pas d’actions dans Lot A. Le panneau est observabilité seulement. ## Fichiers probables Backend : - `crates/application/src/workstate/mod.rs` nouveau - `crates/application/src/lib.rs` - `crates/application/src/terminal/registry.rs` si ajout `live_agent_entries()` avec kind - `crates/app-tauri/src/dto.rs` - `crates/app-tauri/src/commands.rs` - `crates/app-tauri/src/state.rs` - `crates/app-tauri/src/lib.rs` Frontend : - `frontend/src/domain/index.ts` - `frontend/src/ports/index.ts` - `frontend/src/adapters/workState.ts` nouveau - `frontend/src/adapters/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/useProjectWorkState.ts` nouveau - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` nouveau - `frontend/src/features/workstate/index.ts` nouveau - `frontend/src/features/workstate/workstate.test.tsx` nouveau - `frontend/src/features/projects/ProjectsView.tsx` ## Tests attendus ### Rust application Nouveau test application, selon conventions existantes : - `cargo test -p application workstate` Cas : 1. Projet avec deux agents, aucun live, aucun busy → deux rows `live=None`, `busy=Idle`. 2. Agent live PTY → row porte `live.session_id`, `live.node_id`, `kind=Pty` si kind inclus. 3. Agent busy → row porte `Busy { ticket, since_ms }`. 4. Agent absent du manifeste mais présent dans registry → ignoré dans le read-model projet. ### Rust app-tauri Selon structure actuelle, ajouter dans tests de wiring/commands : - commande enregistrée dans `invoke_handler`, - DTO sérialise en camelCase : `agentId`, `profileId`, `sinceMs`, `sessionId`, `nodeId`, `kind`. Commandes de vérification probables : - `cargo test -p app-tauri --test orchestrator_wiring` - ou test dédié si les commands sont déjà testées ailleurs. ### Frontend Vitest Nouveau `workstate.test.tsx` : 1. Le panneau affiche `No agents yet.` quand liste vide. 2. Il affiche agent idle/offline. 3. Il affiche agent live + busy avec ticket court. 4. Un event `agentBusyChanged` déclenche refresh. 5. `ProjectsView` expose l’onglet `Work` et rend le panneau pour le projet actif. Commande ciblée : ```bash npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` ### Typecheck ```bash npx tsc --noEmit cargo check -p app-tauri ``` ## Notes de rigueur - Le Lot A doit rester read-only. Les actions “ouvrir/rattacher”, “stop”, “voir conversation” sont Lot D. - Ne pas lire `ConversationLog` dans Lot A : dès qu’on affiche des previews de conversation, on bascule vers Lot C. - Ne pas ajouter une persistance `live_state.json` : le live-state est dérivé des registres runtime existants. - Ne pas dupliquer `list_live_agents`; le nouveau read-model peut l’utiliser conceptuellement, mais il doit ajouter le busy state et le nom agent, donc il mérite sa commande dédiée. - Si `LiveSessions` est difficile à injecter tel quel, injecter les deux registres et agréger dans le use case, mais garder l’agrégation dans `application`, pas dans Tauri ni frontend. +- **Prompt:** Lot A `ProjectWorkState` minimal est intégré localement dans `develop` (`3bfb932`). Cadrage demandé pour le Lot B du chantier `UX conversations/délégations + read-model live-state`: queue/delegation snapshot propre. Objectif: exposer dans le read-model les délégations/tickets en attente/en cours de façon read-only, sans nouvelle persistance durable et sans refonte FIFO. Merci de définir précisément: source de vérité backend, éventuelle extension read-only de `AgentMailbox`/`InputMediator`, DTOs, impacts frontend, inclus/exclus, tests attendus. Ne code pas. +- **Response:** ## Lot B — Queue / Delegation Snapshot Read-Only Objectif : enrichir `ProjectWorkState` avec les tickets actuellement présents dans la FIFO par agent, en lecture seule, sans nouvelle persistance durable et sans changer la sémantique `enqueue/resolve/cancel`. ### Source de vérité backend La source de vérité des délégations/tickets est **la mailbox existante**, pas le frontend et pas un nouveau store : - `domain::mailbox::Ticket` porte déjà `id`, `source`, `conversation`, `requester`, `task`. - `infrastructure::InMemoryMailbox` possède déjà les `VecDeque` par `AgentId`. - `InputMediator::busy_state(agent)` reste la source de vérité du ticket **en cours**. Donc : - **queue/order** = snapshot de `InMemoryMailbox` ; - **status inProgress/queued** = dérivé en croisant la queue avec `InputMediator::busy_state` ; - **live session** = déjà Lot A via `LiveSessions` ; - **manifest boundary** = toujours `AgentContextStore.load_manifest(project)`. Ne pas reconstruire une deuxième FIFO dans `ProjectWorkState`. ## Extension read-only recommandée Ne pas étendre `InputMediator` : il est l’autorité busy/livraison, pas l’autorité de contenu de queue. Éviter aussi de grossir le port mutateur `AgentMailbox` si possible. Ajouter un port ségrégué read-only dans `domain/src/mailbox.rs` : ```rust #[derive(Debug, Clone, PartialEq, Eq)] pub struct QueuedTicketSnapshot { pub id: TicketId, pub source: InputSource, pub conversation: ConversationId, pub requester: String, pub task: String, pub position: u32, } pub trait AgentQueueSnapshot: Send + Sync { fn queue_for(&self, agent: AgentId) -> Vec; } ``` Implémenter `AgentQueueSnapshot` pour `InMemoryMailbox` en clonant uniquement les données de `Ticket`, jamais les `oneshot::Sender`. Pourquoi ce choix : - ISP : le read-model ne dépend pas de `enqueue/resolve/cancel`. - Aucun changement de comportement FIFO. - Les tests peuvent fournir un fake `AgentQueueSnapshot` sans implémenter tout `AgentMailbox`. Alternative acceptable si l’équipe préfère moins de types : ajouter `fn snapshot_for(&self, agent: AgentId) -> Vec` directement à `AgentMailbox`, avec impl par défaut vide. Mais c’est moins propre : les ports mutateur et lecture restent mélangés. ## Modèle application Fichier principal : `crates/application/src/workstate/mod.rs`. Étendre `GetProjectWorkState` : ```rust pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, queue: Arc, } ``` Étendre les structs : ```rust pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option, pub busy: AgentBusyState, pub tickets: Vec, } pub struct AgentTicketState { pub ticket_id: TicketId, pub conversation_id: ConversationId, pub position: u32, pub status: TicketWorkStatus, pub source: TicketWorkSource, pub requester_label: String, pub task_preview: String, pub task_len: usize, } pub enum TicketWorkStatus { InProgress, Queued, } pub enum TicketWorkSource { Human, Agent { agent_id: AgentId }, } ``` Dérivation status : ```rust let busy_ticket = self.input.busy_state(agent.id).ticket(); status = if busy_ticket == Some(ticket.id) { TicketWorkStatus::InProgress } else { TicketWorkStatus::Queued }; ``` `task_preview` : générer côté application pour éviter d’envoyer tout le prompt au panneau. Recommandation : trim + remplacer whitespace runs par espace + couper à 160 caractères. Garder `task_len` pour indiquer qu’il y a plus. Important : conserver l’ordre FIFO via `position`, ne pas retrier les tickets. ## Tauri / DTO Étendre `crates/app-tauri/src/dto.rs`. ```rust #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentWorkStateDto { pub agent_id: String, pub name: String, pub profile_id: String, pub live: Option, pub busy: AgentBusyState, pub tickets: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentTicketStateDto { pub ticket_id: String, pub conversation_id: String, pub position: u32, pub status: TicketWorkStatusDto, pub source: TicketWorkSourceDto, pub requester_label: String, pub task_preview: String, pub task_len: usize, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub enum TicketWorkStatusDto { InProgress, Queued, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", tag = "kind")] pub enum TicketWorkSourceDto { Human, Agent { agent_id: String }, } ``` Wire JSON attendu : ```json { "agents": [ { "agentId": "...", "name": "Architect", "profileId": "...", "live": { "nodeId": "...", "sessionId": "...", "kind": "pty" }, "busy": { "state": "busy", "ticket": "...", "sinceMs": 1234 }, "tickets": [ { "ticketId": "...", "conversationId": "...", "position": 0, "status": "inProgress", "source": { "kind": "agent", "agentId": "..." }, "requesterLabel": "Main", "taskPreview": "Analyser...", "taskLen": 842 } ] } ] } ``` La commande Tauri reste la même : ```rust get_project_work_state(project_id) -> ProjectWorkStateDto ``` Aucune nouvelle commande nécessaire. ## Wiring AppState Dans `crates/app-tauri/src/state.rs` : - garder l’`Arc` déjà créé au composition root ; - le passer à `GetProjectWorkState::new(...)` comme `Arc` ; - continuer à passer le même `Arc` au `MediatedInbox` et à l’orchestrateur comme `AgentMailbox`. But : un seul objet `InMemoryMailbox`, deux vues de port : ```rust let inmemory_mailbox = Arc::new(InMemoryMailbox::new()); let mailbox = Arc::clone(&inmemory_mailbox) as Arc; let queue_snapshot = Arc::clone(&inmemory_mailbox) as Arc; ``` Puis : ```rust GetProjectWorkState::new(contexts, live_sessions, input_mediator, queue_snapshot) ``` ## Frontend contracts Étendre `frontend/src/domain/index.ts` : ```ts export type TicketWorkStatus = "inProgress" | "queued"; export type TicketWorkSource = | { kind: "human" } | { kind: "agent"; agentId: string }; export interface AgentTicketState { ticketId: string; conversationId: string; position: number; status: TicketWorkStatus; source: TicketWorkSource; requesterLabel: string; taskPreview: string; taskLen: number; } export interface AgentWorkState { agentId: string; name: string; profileId: string; live?: LiveWorkSession; busy: WorkBusyState; tickets: AgentTicketState[]; } ``` `WorkStateGateway` ne change pas : ```ts getProjectWorkState(projectId: string): Promise; ``` Adapter Tauri inchangé sauf types. Mock : `MockWorkStateGateway` accepte désormais `tickets` dans ses seeds. Pour compat tests, soit mettre `tickets: []` dans les fixtures, soit normaliser dans le mock au retour. ## UI Lot B Fichier principal : `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`. Afficher sous chaque agent : - badge `Live/Offline`, `Busy/Idle` existant ; - si `tickets.length > 0`, une mini-liste compacte : - `#1 In progress` ou `#2 Queued`, - requester : `Human` ou label requester / source agent, - preview du task, - ticket court en monospace. UX minimale : - Agent sans ticket : rien ou texte discret “No queued tickets”. - Ticket `inProgress` : badge warning, cohérent avec `Busy`. - Ticket `queued` : badge neutral. - Pas d’action de résolution/annulation dans Lot B. Refresh : - Garder refresh manuel. - Garder refresh sur événements Lot A : `agentBusyChanged`, `agentLaunched`, `agentExited`, `orchestratorRequestProcessed`. - Ajouter refresh sur `delegationReady`, car c’est le signal existant le plus proche d’un ticket qui démarre physiquement côté UI. Limite connue du Lot B : un ticket ajouté derrière un agent déjà busy peut ne pas émettre de nouvel `agentBusyChanged` ni `delegationReady` immédiatement. Il sera visible au refresh manuel ou au prochain événement. Une vraie notification `agentQueueChanged` est un Lot B+ ou Lot C, pas indispensable pour le snapshot read-only. ## Inclus - Port read-only `AgentQueueSnapshot`. - Impl snapshot de `InMemoryMailbox`, sans mutation. - Extension de `ProjectWorkState` avec `agents[].tickets`. - Statuts `inProgress/queued` dérivés, pas stockés. - Task preview bornée, pas full prompt. - UI compacte des tickets par agent. - Tests de snapshot FIFO et read-model. ## Exclus - Pas de `agentQueueChanged` dans ce lot, sauf si l’équipe juge indispensable pour UX live immédiate. - Pas d’annulation de ticket depuis UI. - Pas de résolution manuelle depuis UI. - Pas de persistance queue dans `.ideai/`. - Pas de lecture `ConversationLog` / `handoff.md`. - Pas d’historique des tickets terminés. - Pas de statut `delivered` : `delegation_delivered` est aujourd’hui un log/ack best-effort, pas une donnée persistée dans la mailbox. Ne pas l’exposer comme état tant qu’il n’a pas de source de vérité. - Pas de refonte des chemins `ask_agent`, `submit_human_input`, `idea_reply`. ## Point de vigilance technique Le snapshot va rendre visibles les tickets réellement restés dans `InMemoryMailbox`. C’est précisément utile, mais cela peut révéler des tickets qui ne sont pas drainés sur certains chemins de succès. À vérifier par tests avant de conclure le lot : - succès `idea_reply` PTY : ticket retiré ; - timeout/cancel : ticket retiré par `BusyTurnGuard` ; - succès structured `Final` : le ticket ne doit pas rester dans la queue ; - submit humain fire-and-forget : décider s’il doit apparaître comme ticket ou être exclu du snapshot. Pour le MVP UX “délégations”, je recommande d’afficher uniquement les tickets dont `source == Agent`. Si l’équipe veut afficher aussi les entrées humaines, le libellé UI doit parler de “tickets d’entrée”, pas seulement de délégations, et il faut clarifier leur drainage. ## Tests attendus ### Domain / infrastructure mailbox Fichiers : - `crates/domain/src/mailbox.rs` - `crates/infrastructure/src/mailbox/mod.rs` Tests : 1. `snapshot_preserves_fifo_order_and_positions` : deux tickets A→B, positions `0`, `1`. 2. `snapshot_carries_ticket_metadata` : source, conversation, requester, task présents. 3. `snapshot_is_read_only` : appeler snapshot ne change pas `pending` ni `head_ticket`. 4. `snapshot_updates_after_resolve_ticket` : le ticket résolu disparaît. 5. `snapshot_updates_after_cancel_head` : le head annulé disparaît, le suivant devient position `0`. ### Application workstate Fichier : `crates/application/tests/workstate.rs`. Tests : 1. Agent sans queue → `tickets == []`. 2. Deux tickets pour un agent manifeste → `tickets` dans l’ordre FIFO. 3. `busy.ticket == first` → premier ticket `InProgress`, second `Queued`. 4. `busy == Idle` → tous tickets `Queued`. 5. Queue pour agent absent du manifeste → ignorée. 6. `task_preview` tronque et `task_len` conserve la longueur originale. 7. Source agent → DTO application contient `TicketWorkSource::Agent { agent_id }`; source human → `Human` si inclus. ### App-tauri DTO / command Fichiers : - `crates/app-tauri/src/dto.rs` - tests existants type `dto_agents.rs` ou nouveau test DTO. Tests : 1. Sérialisation camelCase : `ticketId`, `conversationId`, `requesterLabel`, `taskPreview`, `taskLen`. 2. Enum status sérialisée en `inProgress` / `queued`. 3. Source agent sérialisée en `{ "kind": "agent", "agentId": "..." }`. 4. `get_project_work_state` renvoie `tickets` imbriqués avec les agents. ### Frontend Fichier : `frontend/src/features/workstate/workstate.test.tsx`. Tests : 1. Agent avec ticket `inProgress` affiche badge `In progress`, preview et ticket court. 2. Agent avec deux tickets affiche l’ordre `#1`, `#2`. 3. Agent sans tickets n’affiche pas de liste parasite. 4. Event `delegationReady` déclenche un refresh. 5. Mock gateway accepte et renvoie `tickets`. Commandes de vérification : ```bash cargo test -p infrastructure mailbox --lib cargo test -p application --test workstate cargo test -p app-tauri --test dto_agents npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx npx tsc --noEmit cargo check -p app-tauri ``` ## Résumé d’implémentation Le Lot B est une extension de projection : `InMemoryMailbox` expose une vue snapshot, `GetProjectWorkState` l’agrège par agent manifeste, Tauri sérialise `tickets`, le panneau Work les affiche. Aucun nouveau stockage, aucune nouvelle queue, aucune mutation UX. \ No newline at end of file diff --git a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl index 3adb33e..03291b5 100644 --- a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl +++ b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl @@ -35,3 +35,5 @@ {"id":"9dc7772c-d574-470e-a792-56a71d4a85cc","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781946451575,"source":{"kind":"agent","agentId":"dce19c75-9669-4e45-b8de-9950025157da"},"role":"response","text":"## Inventaire post `agent-skill-awareness-v2`\n\nContexte observé : la branche locale est `develop...origin/develop [ahead 15]`, avec seulement des fichiers runtime `.ideai/conversations/*` dirty. Je considère donc `agent-skill-awareness-v2` intégré localement et je reclasse depuis le code actuel, pas depuis la mémoire seule.\n\n### Déjà livré / à considérer fondation acquise\n\n1. **Registre session / singleton agent**\n - Livré côté application : `TerminalSessions`, `StructuredSessions`, agrégateur `LiveSessions`, `session_for_agent`, `node_for_agent`, `rebind_agent_node`.\n - Livré côté Tauri/UI : `list_live_agents`, `attach_live_agent`, guards frontend de lancement singleton.\n - Statut : **fondation livrée**, à durcir uniquement par tests de flux réel.\n\n2. **Messagerie inter-agents / FIFO / réponse synchrone**\n - Livré : `AgentMailbox`, `InputMediator`, `AgentBusyChanged`, tickets, `idea_reply`, résolution par ticket, timeout/cancel.\n - `OrchestratorService::ask_agent` et `reply` existent, avec conversation par paire.\n - Statut : **livré applicativement**, UX encore perfectible.\n\n3. **MCP IdeA-only en flux backend**\n - Livré : bridge `idea mcp-server`, endpoint app-tauri, serveur MCP, outils `idea_*`, runtime MCP injecté au launch, badge source `mcp/file` côté UI.\n - Statut : **livré côté infrastructure/app**, reste validation produit en flux réel et polish observabilité.\n\n4. **Handoff / canonical conversation log cross-session / cross-profile**\n - Plus avancé que la mémoire ne le dit : `domain/src/conversation_log.rs` définit `ConversationLog`, `HandoffStore`, `HandoffSummarizer`, `ProviderSessionStore`.\n - Infra livrée : `FsConversationLog`, `FsHandoffStore`, `FsProviderSessionStore`, `HeuristicHandoffSummarizer`.\n - App livrée : `RecordTurn`, injection handoff au `LaunchAgent`, persistance provider session, séparation pair id IdeA vs engine session id.\n - Statut : **architecture et première implémentation livrées** ; reste qualité de résumé, couverture UX, audit de complétude de tous les chemins de record.\n\n5. **Restrictions profils supportés**\n - Livré partiellement : profils structurés Claude/Codex, `structured_adapter`, `materializes_idea_bridge`, garde `guard_mcp_bridge_supported`, profils sélectionnables.\n - Statut : **règle technique présente**, reste formulation produit/UI des capacités et fallbacks.\n\n6. **Agent skill awareness**\n - Après intégration locale : **livré comme couche de contexte**, sans nouveau store ni DTO. `agent-skills` reste L12, `awareness` reste composition de convention file.\n\n### Encore actif / pas complètement produit\n\n1. **UX conversations / délégations**\n - Backend riche, frontend encore dispersé entre agents/layout/terminal/chat/events.\n - Manque une lecture utilisateur claire : qui a demandé quoi, à qui, état FIFO, réponse reçue, conversation par paire, reprise.\n - Statut : **actif, prochain meilleur chantier**.\n\n2. **Live-state partagé projet**\n - Il existe un live-state runtime : sessions, busy, mailbox, events.\n - Il n’existe pas encore un modèle applicatif unifié, consultable et éventuellement persistable, du “travail en cours” projet.\n - Statut : **partiellement livré en runtime, pas encore comme read-model produit**.\n\n3. **Mise à jour mémoire/contexte automatique pendant la vie d’un agent**\n - Injection au launch : livrée.\n - Outils contexte/mémoire et FileGuard : présents.\n - Promotion automatique, signal/bruit, mise à jour continue : pas livrés.\n - Statut : **actif mais à repousser après UX**, car il faut d’abord rendre les fils et décisions visibles.\n\n4. **Handoff/canonical log qualité produit**\n - Socle livré, mais pas encore “expérience de conversation retrouvée” garantie dans tous les cas.\n - Résumeur heuristique seulement ; UX de consultation/reprise à clarifier.\n - Statut : **fondation livrée, produit actif**.\n\n## Prochain chantier recommandé\n\nJe recommande de reprendre en premier : **UX conversations & délégations, avec un read-model live-state minimal**.\n\nRaison : les briques backend existent maintenant. Continuer par mémoire automatique ou nouveaux mécanismes MCP risque d’empiler de l’invisible. Le prochain verrou produit est de rendre l’orchestration compréhensible et opérable : l’utilisateur doit voir ses “employés IA” travailler, attendre, déléguer, répondre et reprendre.\n\nCe chantier peut aussi servir d’audit d’intégration : s’il manque un événement ou une donnée backend, on l’ajoute proprement comme read-model, au lieu de découvrir les trous plus tard.\n\n## Cadrage architectural initial\n\n### Objectif\n\nFournir une vue produit unifiée des conversations et délégations d’un projet :\n\n- agents vivants et cellule hôte,\n- état `idle/busy/limited/starting` si disponible,\n- délégations en cours et en attente par agent,\n- dernière requête/réponse utile,\n- conversation `User↔Agent` ou `Agent↔Agent` associée,\n- capacité à ouvrir/rattacher la cellule concernée.\n\nLe but n’est pas encore de créer un “journal complet” frontend. Le journal canonique existe côté `.ideai/conversations`; le premier lot doit exposer un **état opérationnel scannable**.\n\n### Frontière backend\n\nPréférer un use case de lecture applicatif plutôt que de faire recomposer le frontend depuis dix events.\n\nNouveau read-model applicatif proposé :\n\n```rust\nProjectWorkState {\n live_agents: Vec,\n conversations: Vec,\n delegations: Vec,\n}\n```\n\nPort/domain à éviter au départ : pas de nouveau store durable tant qu’on lit les registres existants. Le read-model peut composer :\n\n- `LiveSessions` / `TerminalSessions` / `StructuredSessions`,\n- `InputMediator::busy_state`,\n- `ConversationRegistry`,\n- `AgentMailbox` si une méthode d’inspection propre est ajoutée,\n- `ConversationLog` / `HandoffStore` en lecture best-effort pour les résumés.\n\nSi inspection mailbox nécessaire, ne pas exposer l’impl `InMemoryMailbox` : ajouter un petit port ségrégué, par exemple `AgentQueueSnapshot`, ou étendre prudemment `AgentMailbox` avec une méthode read-only si l’impact reste faible.\n\n### Frontière Tauri / DTO\n\nAjouter une commande de lecture, pas une mutation :\n\n- `get_project_work_state(projectId) -> ProjectWorkStateDto`\n\nDTOs camelCase, stables et tolérants aux champs absents :\n\n- `LiveAgentStateDto { agentId, name, nodeId?, sessionId?, kind: \"pty\"|\"structured\", busy, limited? }`\n- `DelegationStateDto { ticketId, requesterAgentId?, targetAgentId, taskPreview, status: \"queued\"|\"delivered\"|\"awaitingReply\"|\"replied\"|\"timedOut\", source: \"mcp\"|\"file\"|\"user\"?, createdAtMs? }`\n- `ConversationThreadSummaryDto { conversationId, left, right, lastTurnPreview?, handoffPreview?, updatedAtMs? }`\n\nNe pas exposer tout `log.jsonl` dans ce lot. Préviews seulement.\n\n### Frontière frontend\n\nAjouter une feature dédiée, sans mélanger avec `TerminalView` :\n\n- `frontend/src/features/workstate/` ou `features/conversations/`\n- hook `useProjectWorkState(projectId)` consommant un nouveau `WorkStateGateway`\n- panneau dans `ProjectsView` ou extension du sidebar Agents : “Work” / “Conversations”\n\nLe frontend doit continuer à consommer des ports TS, pas `invoke()` directement :\n\n- `domain/index.ts` : types miroirs DTO.\n- `ports/index.ts` : `WorkStateGateway`.\n- `adapters/workState.ts` : Tauri invoke.\n- mock gateway + tests Vitest.\n\n### Ordre d’implémentation recommandé\n\n1. **Lot A — read-model minimal sans nouvelle persistance**\n - Backend command `get_project_work_state` qui agrège live agents + busy state + conversations connues.\n - Frontend panneau simple affichant agents live/busy et dernières délégations connues.\n\n2. **Lot B — queue/delegation snapshot propre**\n - Ajouter l’inspection read-only de mailbox/FIFO si le Lot A ne peut pas montrer les tickets en attente.\n - Tester FIFO visible : deux demandes vers le même agent apparaissent dans l’ordre.\n\n3. **Lot C — conversation summaries depuis log/handoff**\n - Lire `ConversationLog::last` et `HandoffStore::load` best-effort.\n - Afficher preview et reprise sans charger tout le journal.\n\n4. **Lot D — actions UX**\n - “Ouvrir/rattacher l’agent”, “voir la conversation”, “arrêter”, “copier résumé”, selon capacités déjà existantes.\n\n5. **Lot E — mémoire/contexte automatique**\n - Seulement après visibilité des fils : proposer/persister les apprentissages utiles depuis les conversations vers mémoire/contexte via FileGuard.\n\n### Contraintes à respecter\n\n- Ne pas faire du frontend la source de vérité du live-state.\n- Ne pas polluer `.ideai/memory/` avec le journal conversationnel.\n- Ne pas dupliquer les registres session existants.\n- Garder PTY brut et structured sessions substituables dans le read-model.\n- Toute écriture mémoire/contexte doit rester derrière les use cases/FileGuard existants.\n\n## Synthèse\n\nLe cœur “agent control IDE” est maintenant largement livré côté backend : singleton, mailbox/FIFO, MCP IdeA-only, log/handoff/provider sessions. Le chantier prioritaire n’est plus d’inventer ces mécanismes, mais de les rendre visibles et pilotables. Je reprendrais donc par **UX conversations/délégations + read-model live-state projet minimal**, puis seulement ensuite la mise à jour automatique mémoire/contexte."} {"id":"349b4712-a955-4643-90d6-ada618c17770","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781946633052,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Nous passons à l'implémentation du chantier `UX conversations/délégations + read-model live-state minimal`. Merci de transformer ton cadrage en périmètre de Lot A implémentable maintenant. Donne les contrats exacts backend/Tauri/frontend, les fichiers probables, ce qui est inclus/exclu, et les tests attendus. Objectif: premier lot utile, minimal, sans nouvelle persistance durable si possible. Ne code pas."} {"id":"fc80726d-b0ec-4747-bea9-a5287ed9526e","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781946729129,"source":{"kind":"agent","agentId":"dce19c75-9669-4e45-b8de-9950025157da"},"role":"response","text":"## Lot A — `ProjectWorkState` minimal, implémentable maintenant\n\n### Objectif du lot\n\nLivrer une première vue utile et read-only du travail agent en cours dans un projet, sans nouvelle persistance durable :\n\n- lister les agents du projet avec leur état live,\n- indiquer cellule/session hôte quand l’agent tourne,\n- indiquer `idle` / `busy` à partir du médiateur d’entrée,\n- exposer le ticket courant si busy,\n- permettre au frontend d’afficher un panneau “Work” simple et scannable.\n\nCe lot ne cherche pas encore à afficher l’historique, les previews de log, ni la file FIFO complète. Il pose le read-model minimal qui rend l’état agent visible.\n\n## Inclus\n\n- Nouvelle commande Tauri read-only `get_project_work_state(projectId)`.\n- Agrégation backend depuis les stores/registres existants : manifest agents, `LiveSessions`, `InputMediator::busy_state`.\n- DTO frontend miroir + gateway TS.\n- Panneau frontend minimal dans la sidebar, probablement onglet `Work`.\n- Tests backend DTO/commande + tests frontend hook/panel.\n\n## Exclus\n\n- Aucune nouvelle persistance `.ideai/`.\n- Pas de lecture `log.jsonl` / `handoff.md` dans le Lot A.\n- Pas d’inspection complète de la mailbox/FIFO.\n- Pas de mutation : pas de stop/reply/attach depuis ce panneau dans le premier lot.\n- Pas de migration de `list_live_agents` existant.\n- Pas de refonte des panneaux Agents/Terminal/Chat.\n\n## Contrat backend application\n\nLe plus minimal peut rester dans `app-tauri` en composition de DTO, mais je recommande un petit use case application pour garder Tauri adapter mince.\n\n### Nouveau module probable\n\n- `crates/application/src/workstate/mod.rs`\n- export dans `crates/application/src/lib.rs`\n\n### Types application proposés\n\n```rust\npub struct GetProjectWorkState {\n contexts: Arc,\n live: Arc,\n input: Arc,\n}\n\npub struct GetProjectWorkStateInput {\n pub project: Project,\n}\n\npub struct ProjectWorkState {\n pub agents: Vec,\n}\n\npub struct AgentWorkState {\n pub agent_id: AgentId,\n pub name: String,\n pub profile_id: ProfileId,\n pub live: Option,\n pub busy: AgentBusyState,\n}\n\npub struct LiveWorkSession {\n pub node_id: NodeId,\n pub session_id: SessionId,\n pub kind: LiveSessionKind,\n}\n\npub enum LiveSessionKind {\n Pty,\n Structured,\n}\n```\n\n### Important sur `kind`\n\n`LiveSessions::live_agents()` agrège déjà PTY + structured mais ne porte pas le kind. Pour Lot A, deux options :\n\n1. **Option minimale** : omettre `kind` du DTO. Suffisant pour afficher “live”.\n2. **Option préférable** : ajouter une méthode read-only à `LiveSessions`, sans casser l’existant :\n\n```rust\npub fn live_agent_entries(&self) -> Vec\n\npub struct LiveAgentEntry {\n pub agent_id: AgentId,\n pub node_id: NodeId,\n pub session_id: SessionId,\n pub kind: LiveSessionKind,\n}\n```\n\nGarder `live_agents()` existant pour compatibilité avec `list_live_agents`.\n\n### Algorithme use case\n\n1. `manifest = contexts.load_manifest(&project).await?`\n2. Convertir chaque entry en `Agent` via `entry.to_agent()`.\n3. Construire une map `agent_id -> live entry` depuis `LiveSessions`.\n4. Pour chaque agent :\n - `busy = input.busy_state(agent.id)`\n - `live = live_map.get(agent.id)`\n - produire `AgentWorkState`\n5. Trier par `name` ou conserver l’ordre manifeste. Recommandation : conserver ordre manifeste pour stabilité avec `list_agents`.\n\n## Contrat Tauri\n\n### Nouveau DTO dans `crates/app-tauri/src/dto.rs`\n\n```rust\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProjectWorkStateDto {\n pub agents: Vec,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AgentWorkStateDto {\n pub agent_id: String,\n pub name: String,\n pub profile_id: String,\n pub live: Option,\n pub busy: BusyStateDto,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct LiveWorkSessionDto {\n pub node_id: String,\n pub session_id: String,\n pub kind: LiveSessionKindDto,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum LiveSessionKindDto {\n Pty,\n Structured,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\", tag = \"state\")]\npub enum BusyStateDto {\n Idle,\n Busy { ticket: String, since_ms: u64 },\n}\n```\n\nSi l’équipe veut réduire encore le lot : supprimer `kind` et `LiveSessionKindDto`.\n\n### Nouvelle commande dans `crates/app-tauri/src/commands.rs`\n\n```rust\n#[tauri::command]\npub async fn get_project_work_state(\n project_id: String,\n state: State<'_, AppState>,\n) -> Result\n```\n\nComportement :\n\n- `resolve_project(&project_id, &state).await?`\n- appeler `state.get_project_work_state.execute(...)`\n- mapper en DTO\n\nErreurs :\n\n- `INVALID` si `projectId` invalide,\n- `NOT_FOUND` si projet inconnu,\n- `STORE` si manifeste illisible.\n\n### Wiring `AppState`\n\nFichiers probables :\n\n- `crates/app-tauri/src/state.rs`\n - ajouter `pub get_project_work_state: Arc`.\n - construire avec `IdeaiContextStore`, `LiveSessions::new(terminal_sessions, structured_sessions)`, `input_mediator`.\n- `crates/app-tauri/src/lib.rs`\n - enregistrer `commands::get_project_work_state` dans `invoke_handler`.\n\n## Contrat frontend\n\n### Types dans `frontend/src/domain/index.ts`\n\n```ts\nexport interface ProjectWorkState {\n agents: AgentWorkState[];\n}\n\nexport interface AgentWorkState {\n agentId: string;\n name: string;\n profileId: string;\n live?: LiveWorkSession | null;\n busy: WorkBusyState;\n}\n\nexport interface LiveWorkSession {\n nodeId: string;\n sessionId: string;\n kind: \"pty\" | \"structured\";\n}\n\nexport type WorkBusyState =\n | { state: \"idle\" }\n | { state: \"busy\"; ticket: string; sinceMs: number };\n```\n\nSi backend omet `kind`, retirer `kind` ici aussi.\n\n### Port dans `frontend/src/ports/index.ts`\n\n```ts\nexport interface WorkStateGateway {\n getProjectWorkState(projectId: string): Promise;\n}\n\nexport interface Gateways {\n // existants...\n workState: WorkStateGateway;\n}\n```\n\n### Adapter Tauri\n\nNouveau fichier : `frontend/src/adapters/workState.ts`\n\n```ts\nexport class TauriWorkStateGateway implements WorkStateGateway {\n getProjectWorkState(projectId: string): Promise {\n return invoke(\"get_project_work_state\", { projectId });\n }\n}\n```\n\nPuis wiring :\n\n- `frontend/src/adapters/index.ts` : instancier `workState: new TauriWorkStateGateway()`.\n- `frontend/src/adapters/mock/index.ts` : ajouter `MockWorkStateGateway`.\n\n### Feature frontend\n\nNouveau dossier recommandé :\n\n- `frontend/src/features/workstate/useProjectWorkState.ts`\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`\n- `frontend/src/features/workstate/index.ts`\n- `frontend/src/features/workstate/workstate.test.tsx`\n\nHook :\n\n```ts\nexport interface ProjectWorkStateViewModel {\n state: ProjectWorkState | null;\n busy: boolean;\n error: string | null;\n refresh: () => Promise;\n}\n```\n\nRefresh initial + refresh sur events existants :\n\n- `agentLaunched`\n- `agentExited`\n- `agentBusyChanged`\n- `orchestratorRequestProcessed`\n- éventuellement `agentProfileChanged`\n\n### UI minimale\n\nDans `ProjectsView.tsx` :\n\n- ajouter `SidebarTab = ... | \"work\"`\n- ajouter `{ id: \"work\", label: \"Work\" }`\n- afficher `ProjectWorkStatePanel projectId={active.id}` si actif.\n\nContenu minimal du panneau :\n\n- ligne par agent : nom, live/offline, busy/idle.\n- si live : afficher `sessionId` court ou `nodeId` court.\n- si busy : afficher ticket court + âge approximatif facultatif.\n- empty state : “No agents yet.”\n- erreur et bouton refresh.\n\nPas d’actions dans Lot A. Le panneau est observabilité seulement.\n\n## Fichiers probables\n\nBackend :\n\n- `crates/application/src/workstate/mod.rs` nouveau\n- `crates/application/src/lib.rs`\n- `crates/application/src/terminal/registry.rs` si ajout `live_agent_entries()` avec kind\n- `crates/app-tauri/src/dto.rs`\n- `crates/app-tauri/src/commands.rs`\n- `crates/app-tauri/src/state.rs`\n- `crates/app-tauri/src/lib.rs`\n\nFrontend :\n\n- `frontend/src/domain/index.ts`\n- `frontend/src/ports/index.ts`\n- `frontend/src/adapters/workState.ts` nouveau\n- `frontend/src/adapters/index.ts`\n- `frontend/src/adapters/mock/index.ts`\n- `frontend/src/features/workstate/useProjectWorkState.ts` nouveau\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` nouveau\n- `frontend/src/features/workstate/index.ts` nouveau\n- `frontend/src/features/workstate/workstate.test.tsx` nouveau\n- `frontend/src/features/projects/ProjectsView.tsx`\n\n## Tests attendus\n\n### Rust application\n\nNouveau test application, selon conventions existantes :\n\n- `cargo test -p application workstate`\n\nCas :\n\n1. Projet avec deux agents, aucun live, aucun busy → deux rows `live=None`, `busy=Idle`.\n2. Agent live PTY → row porte `live.session_id`, `live.node_id`, `kind=Pty` si kind inclus.\n3. Agent busy → row porte `Busy { ticket, since_ms }`.\n4. Agent absent du manifeste mais présent dans registry → ignoré dans le read-model projet.\n\n### Rust app-tauri\n\nSelon structure actuelle, ajouter dans tests de wiring/commands :\n\n- commande enregistrée dans `invoke_handler`,\n- DTO sérialise en camelCase : `agentId`, `profileId`, `sinceMs`, `sessionId`, `nodeId`, `kind`.\n\nCommandes de vérification probables :\n\n- `cargo test -p app-tauri --test orchestrator_wiring`\n- ou test dédié si les commands sont déjà testées ailleurs.\n\n### Frontend Vitest\n\nNouveau `workstate.test.tsx` :\n\n1. Le panneau affiche `No agents yet.` quand liste vide.\n2. Il affiche agent idle/offline.\n3. Il affiche agent live + busy avec ticket court.\n4. Un event `agentBusyChanged` déclenche refresh.\n5. `ProjectsView` expose l’onglet `Work` et rend le panneau pour le projet actif.\n\nCommande ciblée :\n\n```bash\nnpx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx\n```\n\n### Typecheck\n\n```bash\nnpx tsc --noEmit\ncargo check -p app-tauri\n```\n\n## Notes de rigueur\n\n- Le Lot A doit rester read-only. Les actions “ouvrir/rattacher”, “stop”, “voir conversation” sont Lot D.\n- Ne pas lire `ConversationLog` dans Lot A : dès qu’on affiche des previews de conversation, on bascule vers Lot C.\n- Ne pas ajouter une persistance `live_state.json` : le live-state est dérivé des registres runtime existants.\n- Ne pas dupliquer `list_live_agents`; le nouveau read-model peut l’utiliser conceptuellement, mais il doit ajouter le busy state et le nom agent, donc il mérite sa commande dédiée.\n- Si `LiveSessions` est difficile à injecter tel quel, injecter les deux registres et agréger dans le use case, mais garder l’agrégation dans `application`, pas dans Tauri ni frontend."} +{"id":"4791b166-e24b-492a-8bbb-529dbc379fcd","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781971649587,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot A `ProjectWorkState` minimal est intégré localement dans `develop` (`3bfb932`). Cadrage demandé pour le Lot B du chantier `UX conversations/délégations + read-model live-state`: queue/delegation snapshot propre. Objectif: exposer dans le read-model les délégations/tickets en attente/en cours de façon read-only, sans nouvelle persistance durable et sans refonte FIFO. Merci de définir précisément: source de vérité backend, éventuelle extension read-only de `AgentMailbox`/`InputMediator`, DTOs, impacts frontend, inclus/exclus, tests attendus. Ne code pas."} +{"id":"001f112c-4ece-4f49-a7cc-140ee76eabec","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781971838745,"source":{"kind":"agent","agentId":"dce19c75-9669-4e45-b8de-9950025157da"},"role":"response","text":"## Lot B — Queue / Delegation Snapshot Read-Only\n\nObjectif : enrichir `ProjectWorkState` avec les tickets actuellement présents dans la FIFO par agent, en lecture seule, sans nouvelle persistance durable et sans changer la sémantique `enqueue/resolve/cancel`.\n\n### Source de vérité backend\n\nLa source de vérité des délégations/tickets est **la mailbox existante**, pas le frontend et pas un nouveau store :\n\n- `domain::mailbox::Ticket` porte déjà `id`, `source`, `conversation`, `requester`, `task`.\n- `infrastructure::InMemoryMailbox` possède déjà les `VecDeque` par `AgentId`.\n- `InputMediator::busy_state(agent)` reste la source de vérité du ticket **en cours**.\n\nDonc :\n\n- **queue/order** = snapshot de `InMemoryMailbox` ;\n- **status inProgress/queued** = dérivé en croisant la queue avec `InputMediator::busy_state` ;\n- **live session** = déjà Lot A via `LiveSessions` ;\n- **manifest boundary** = toujours `AgentContextStore.load_manifest(project)`.\n\nNe pas reconstruire une deuxième FIFO dans `ProjectWorkState`.\n\n## Extension read-only recommandée\n\nNe pas étendre `InputMediator` : il est l’autorité busy/livraison, pas l’autorité de contenu de queue.\n\nÉviter aussi de grossir le port mutateur `AgentMailbox` si possible. Ajouter un port ségrégué read-only dans `domain/src/mailbox.rs` :\n\n```rust\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct QueuedTicketSnapshot {\n pub id: TicketId,\n pub source: InputSource,\n pub conversation: ConversationId,\n pub requester: String,\n pub task: String,\n pub position: u32,\n}\n\npub trait AgentQueueSnapshot: Send + Sync {\n fn queue_for(&self, agent: AgentId) -> Vec;\n}\n```\n\nImplémenter `AgentQueueSnapshot` pour `InMemoryMailbox` en clonant uniquement les données de `Ticket`, jamais les `oneshot::Sender`.\n\nPourquoi ce choix :\n\n- ISP : le read-model ne dépend pas de `enqueue/resolve/cancel`.\n- Aucun changement de comportement FIFO.\n- Les tests peuvent fournir un fake `AgentQueueSnapshot` sans implémenter tout `AgentMailbox`.\n\nAlternative acceptable si l’équipe préfère moins de types : ajouter `fn snapshot_for(&self, agent: AgentId) -> Vec` directement à `AgentMailbox`, avec impl par défaut vide. Mais c’est moins propre : les ports mutateur et lecture restent mélangés.\n\n## Modèle application\n\nFichier principal : `crates/application/src/workstate/mod.rs`.\n\nÉtendre `GetProjectWorkState` :\n\n```rust\npub struct GetProjectWorkState {\n contexts: Arc,\n live: Arc,\n input: Arc,\n queue: Arc,\n}\n```\n\nÉtendre les structs :\n\n```rust\npub struct AgentWorkState {\n pub agent_id: AgentId,\n pub name: String,\n pub profile_id: ProfileId,\n pub live: Option,\n pub busy: AgentBusyState,\n pub tickets: Vec,\n}\n\npub struct AgentTicketState {\n pub ticket_id: TicketId,\n pub conversation_id: ConversationId,\n pub position: u32,\n pub status: TicketWorkStatus,\n pub source: TicketWorkSource,\n pub requester_label: String,\n pub task_preview: String,\n pub task_len: usize,\n}\n\npub enum TicketWorkStatus {\n InProgress,\n Queued,\n}\n\npub enum TicketWorkSource {\n Human,\n Agent { agent_id: AgentId },\n}\n```\n\nDérivation status :\n\n```rust\nlet busy_ticket = self.input.busy_state(agent.id).ticket();\nstatus = if busy_ticket == Some(ticket.id) {\n TicketWorkStatus::InProgress\n} else {\n TicketWorkStatus::Queued\n};\n```\n\n`task_preview` : générer côté application pour éviter d’envoyer tout le prompt au panneau. Recommandation : trim + remplacer whitespace runs par espace + couper à 160 caractères. Garder `task_len` pour indiquer qu’il y a plus.\n\nImportant : conserver l’ordre FIFO via `position`, ne pas retrier les tickets.\n\n## Tauri / DTO\n\nÉtendre `crates/app-tauri/src/dto.rs`.\n\n```rust\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AgentWorkStateDto {\n pub agent_id: String,\n pub name: String,\n pub profile_id: String,\n pub live: Option,\n pub busy: AgentBusyState,\n pub tickets: Vec,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AgentTicketStateDto {\n pub ticket_id: String,\n pub conversation_id: String,\n pub position: u32,\n pub status: TicketWorkStatusDto,\n pub source: TicketWorkSourceDto,\n pub requester_label: String,\n pub task_preview: String,\n pub task_len: usize,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum TicketWorkStatusDto {\n InProgress,\n Queued,\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"camelCase\", tag = \"kind\")]\npub enum TicketWorkSourceDto {\n Human,\n Agent { agent_id: String },\n}\n```\n\nWire JSON attendu :\n\n```json\n{\n \"agents\": [\n {\n \"agentId\": \"...\",\n \"name\": \"Architect\",\n \"profileId\": \"...\",\n \"live\": { \"nodeId\": \"...\", \"sessionId\": \"...\", \"kind\": \"pty\" },\n \"busy\": { \"state\": \"busy\", \"ticket\": \"...\", \"sinceMs\": 1234 },\n \"tickets\": [\n {\n \"ticketId\": \"...\",\n \"conversationId\": \"...\",\n \"position\": 0,\n \"status\": \"inProgress\",\n \"source\": { \"kind\": \"agent\", \"agentId\": \"...\" },\n \"requesterLabel\": \"Main\",\n \"taskPreview\": \"Analyser...\",\n \"taskLen\": 842\n }\n ]\n }\n ]\n}\n```\n\nLa commande Tauri reste la même :\n\n```rust\nget_project_work_state(project_id) -> ProjectWorkStateDto\n```\n\nAucune nouvelle commande nécessaire.\n\n## Wiring AppState\n\nDans `crates/app-tauri/src/state.rs` :\n\n- garder l’`Arc` déjà créé au composition root ;\n- le passer à `GetProjectWorkState::new(...)` comme `Arc` ;\n- continuer à passer le même `Arc` au `MediatedInbox` et à l’orchestrateur comme `AgentMailbox`.\n\nBut : un seul objet `InMemoryMailbox`, deux vues de port :\n\n```rust\nlet inmemory_mailbox = Arc::new(InMemoryMailbox::new());\nlet mailbox = Arc::clone(&inmemory_mailbox) as Arc;\nlet queue_snapshot = Arc::clone(&inmemory_mailbox) as Arc;\n```\n\nPuis :\n\n```rust\nGetProjectWorkState::new(contexts, live_sessions, input_mediator, queue_snapshot)\n```\n\n## Frontend contracts\n\nÉtendre `frontend/src/domain/index.ts` :\n\n```ts\nexport type TicketWorkStatus = \"inProgress\" | \"queued\";\n\nexport type TicketWorkSource =\n | { kind: \"human\" }\n | { kind: \"agent\"; agentId: string };\n\nexport interface AgentTicketState {\n ticketId: string;\n conversationId: string;\n position: number;\n status: TicketWorkStatus;\n source: TicketWorkSource;\n requesterLabel: string;\n taskPreview: string;\n taskLen: number;\n}\n\nexport interface AgentWorkState {\n agentId: string;\n name: string;\n profileId: string;\n live?: LiveWorkSession;\n busy: WorkBusyState;\n tickets: AgentTicketState[];\n}\n```\n\n`WorkStateGateway` ne change pas :\n\n```ts\ngetProjectWorkState(projectId: string): Promise;\n```\n\nAdapter Tauri inchangé sauf types.\n\nMock : `MockWorkStateGateway` accepte désormais `tickets` dans ses seeds. Pour compat tests, soit mettre `tickets: []` dans les fixtures, soit normaliser dans le mock au retour.\n\n## UI Lot B\n\nFichier principal : `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`.\n\nAfficher sous chaque agent :\n\n- badge `Live/Offline`, `Busy/Idle` existant ;\n- si `tickets.length > 0`, une mini-liste compacte :\n - `#1 In progress` ou `#2 Queued`,\n - requester : `Human` ou label requester / source agent,\n - preview du task,\n - ticket court en monospace.\n\nUX minimale :\n\n- Agent sans ticket : rien ou texte discret “No queued tickets”.\n- Ticket `inProgress` : badge warning, cohérent avec `Busy`.\n- Ticket `queued` : badge neutral.\n- Pas d’action de résolution/annulation dans Lot B.\n\nRefresh :\n\n- Garder refresh manuel.\n- Garder refresh sur événements Lot A : `agentBusyChanged`, `agentLaunched`, `agentExited`, `orchestratorRequestProcessed`.\n- Ajouter refresh sur `delegationReady`, car c’est le signal existant le plus proche d’un ticket qui démarre physiquement côté UI.\n\nLimite connue du Lot B : un ticket ajouté derrière un agent déjà busy peut ne pas émettre de nouvel `agentBusyChanged` ni `delegationReady` immédiatement. Il sera visible au refresh manuel ou au prochain événement. Une vraie notification `agentQueueChanged` est un Lot B+ ou Lot C, pas indispensable pour le snapshot read-only.\n\n## Inclus\n\n- Port read-only `AgentQueueSnapshot`.\n- Impl snapshot de `InMemoryMailbox`, sans mutation.\n- Extension de `ProjectWorkState` avec `agents[].tickets`.\n- Statuts `inProgress/queued` dérivés, pas stockés.\n- Task preview bornée, pas full prompt.\n- UI compacte des tickets par agent.\n- Tests de snapshot FIFO et read-model.\n\n## Exclus\n\n- Pas de `agentQueueChanged` dans ce lot, sauf si l’équipe juge indispensable pour UX live immédiate.\n- Pas d’annulation de ticket depuis UI.\n- Pas de résolution manuelle depuis UI.\n- Pas de persistance queue dans `.ideai/`.\n- Pas de lecture `ConversationLog` / `handoff.md`.\n- Pas d’historique des tickets terminés.\n- Pas de statut `delivered` : `delegation_delivered` est aujourd’hui un log/ack best-effort, pas une donnée persistée dans la mailbox. Ne pas l’exposer comme état tant qu’il n’a pas de source de vérité.\n- Pas de refonte des chemins `ask_agent`, `submit_human_input`, `idea_reply`.\n\n## Point de vigilance technique\n\nLe snapshot va rendre visibles les tickets réellement restés dans `InMemoryMailbox`. C’est précisément utile, mais cela peut révéler des tickets qui ne sont pas drainés sur certains chemins de succès.\n\nÀ vérifier par tests avant de conclure le lot :\n\n- succès `idea_reply` PTY : ticket retiré ;\n- timeout/cancel : ticket retiré par `BusyTurnGuard` ;\n- succès structured `Final` : le ticket ne doit pas rester dans la queue ;\n- submit humain fire-and-forget : décider s’il doit apparaître comme ticket ou être exclu du snapshot.\n\nPour le MVP UX “délégations”, je recommande d’afficher uniquement les tickets dont `source == Agent`. Si l’équipe veut afficher aussi les entrées humaines, le libellé UI doit parler de “tickets d’entrée”, pas seulement de délégations, et il faut clarifier leur drainage.\n\n## Tests attendus\n\n### Domain / infrastructure mailbox\n\nFichiers :\n\n- `crates/domain/src/mailbox.rs`\n- `crates/infrastructure/src/mailbox/mod.rs`\n\nTests :\n\n1. `snapshot_preserves_fifo_order_and_positions` : deux tickets A→B, positions `0`, `1`.\n2. `snapshot_carries_ticket_metadata` : source, conversation, requester, task présents.\n3. `snapshot_is_read_only` : appeler snapshot ne change pas `pending` ni `head_ticket`.\n4. `snapshot_updates_after_resolve_ticket` : le ticket résolu disparaît.\n5. `snapshot_updates_after_cancel_head` : le head annulé disparaît, le suivant devient position `0`.\n\n### Application workstate\n\nFichier : `crates/application/tests/workstate.rs`.\n\nTests :\n\n1. Agent sans queue → `tickets == []`.\n2. Deux tickets pour un agent manifeste → `tickets` dans l’ordre FIFO.\n3. `busy.ticket == first` → premier ticket `InProgress`, second `Queued`.\n4. `busy == Idle` → tous tickets `Queued`.\n5. Queue pour agent absent du manifeste → ignorée.\n6. `task_preview` tronque et `task_len` conserve la longueur originale.\n7. Source agent → DTO application contient `TicketWorkSource::Agent { agent_id }`; source human → `Human` si inclus.\n\n### App-tauri DTO / command\n\nFichiers :\n\n- `crates/app-tauri/src/dto.rs`\n- tests existants type `dto_agents.rs` ou nouveau test DTO.\n\nTests :\n\n1. Sérialisation camelCase : `ticketId`, `conversationId`, `requesterLabel`, `taskPreview`, `taskLen`.\n2. Enum status sérialisée en `inProgress` / `queued`.\n3. Source agent sérialisée en `{ \"kind\": \"agent\", \"agentId\": \"...\" }`.\n4. `get_project_work_state` renvoie `tickets` imbriqués avec les agents.\n\n### Frontend\n\nFichier : `frontend/src/features/workstate/workstate.test.tsx`.\n\nTests :\n\n1. Agent avec ticket `inProgress` affiche badge `In progress`, preview et ticket court.\n2. Agent avec deux tickets affiche l’ordre `#1`, `#2`.\n3. Agent sans tickets n’affiche pas de liste parasite.\n4. Event `delegationReady` déclenche un refresh.\n5. Mock gateway accepte et renvoie `tickets`.\n\nCommandes de vérification :\n\n```bash\ncargo test -p infrastructure mailbox --lib\ncargo test -p application --test workstate\ncargo test -p app-tauri --test dto_agents\nnpx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx\nnpx tsc --noEmit\ncargo check -p app-tauri\n```\n\n## Résumé d’implémentation\n\nLe Lot B est une extension de projection : `InMemoryMailbox` expose une vue snapshot, `GetProjectWorkState` l’agrège par agent manifeste, Tauri sérialise `tickets`, le panneau Work les affiche. Aucun nouveau stockage, aucune nouvelle queue, aucune mutation UX."}