diff --git a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/handoff.md index 541fbc6..fd61702 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: 53773ad0-5bb6-4e24-8456-d7ab17be3db0 +upTo: 1cbd7c12-6608-45b4-aa9c-41dff0fedd51 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:** 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`. -- **Prompt:** Main reprend le cycle après cadrage Architect. État connu: repo `/home/anthony/Documents/Projects/IdeA`, branche actuelle `develop`, `develop...origin/develop [ahead 9]`, dirty: `.ideai/conversations/.../handoff.md`, `.ideai/conversations/.../log.jsonl`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, nouveau `.ideai/memory/checkpoint-blocked-until-appimage-030-restart.md`. Architect recommande de ne pas merger la vieille branche `feature/agent-skill-awareness`, mais de créer une branche neuve depuis `develop`, par ex. `feature/agent-skill-awareness-v2`. Merci de décider et effectuer uniquement les actions Git/locales nécessaires: gestion de branche, éventuellement commit/ignore/stash des fichiers runtime si tu le juges nécessaire selon ton rôle. Pas de push. Retourne: branche active, actions effectuées, état `git status --short --branch`, et consignes pour DevBackend/DevFrontend/QA. - **Response:** Décision Git appliquée (local uniquement, aucun push). **Branche active : `feature/agent-skill-awareness-v2`** (créée depuis `develop`, working tree propre). **Actions effectuées :** 1. Les 7 fichiers dirty étaient tous de l'état runtime `.ideai/` (conversations handoff+log, layouts.json, MEMORY.md, nouveau checkpoint-blocked-until-appimage-030-restart.md). Conformément à mon rôle (séparer code de feature et état runtime), je les ai committés sur `develop` : - `c... ` chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint blocage AppImage 0.3.0) 2. Création de la branche neuve `feature/agent-skill-awareness-v2` depuis `develop` (tip = ce commit). J'ai suivi la reco Architect : la vieille `feature/agent-skill-awareness` (5be8987) n'est PAS mergée et reste en place comme archive — repart d'une base develop propre, pas de dette d'historique divergent (develop avait 7 commits d'avance / la feature 5 d'écart). **`git status --short --branch` :** ``` ## feature/agent-skill-awareness-v2 ``` (working tree propre) **Contexte branches :** - `develop` : +9 puis +1 (le commit runtime) vs origin → désormais ahead 10, non poussé. - `feature/agent-skill-awareness` : intacte (5be8987), abandonnée, conservée comme archive. - `feature/agent-skill-awareness-v2` : NOUVELLE base de travail. **Consignes pour DevBackend / DevFrontend / QA :** - Travaillez tous sur **`feature/agent-skill-awareness-v2`**. Ne touchez pas à `develop` ni à la vieille `feature/agent-skill-awareness`. - Ne committez pas vous-mêmes : signalez-moi (via Main) quand l'implémentation est prête, je fais les commits atomiques (code feature séparé de l'état runtime `.ideai/`). - Rappel checkpoint mémoire : un blocage de validation e2e attendait un restart sur AppImage 0.3.0 — vérifier que l'AppImage qui tourne est bien rebuildée avant de conclure une validation (le binaire qui tourne = AppImage, pas les sources). - Quand QA est vert avec sortie réelle, revenez vers moi : je rebase si besoin puis merge `--no-ff` v2 → develop. - **Prompt:** Reprise chantier `feature/agent-skill-awareness-v2`. État observé: branche actuelle `feature/agent-skill-awareness-v2`, dirty avec changements code du hotfix livraison délégation/logs submit et fichiers runtime `.ideai/conversations/*`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, plus nouvelle mémoire `checkpoint-delivery-submit-logging-fix.md`. Merci de décider la stratégie locale: rester sur cette branche ou ajuster, quoi committer plus tard, quoi laisser hors commit/ignorer, et conditions avant commit/merge. Ne fais pas de commit pour l'instant. - **Response:** Décision Git (aucun commit effectué). ## État constaté - Branche courante : `feature/agent-skill-awareness-v2`, pointée exactement sur `e832af5` = tip de `develop`. `develop` est ancêtre de HEAD → **base saine, aucune divergence** (rebase inutile pour l'instant). - Working tree dirty, deux natures bien distinctes : - **Code (hotfix livraison délégation + logs submit)** — 8 fichiers, +352/-23 : - `crates/app-tauri/src/commands.rs`, `crates/application/src/orchestrator/service.rs`, `crates/infrastructure/src/input/mod.rs` - `frontend/src/adapters/input.ts`, `frontend/src/adapters/terminal.ts`, `frontend/src/domain/index.ts` - `frontend/src/features/terminals/useWritePortal.ts` + `useWritePortal.test.tsx` - **État runtime `.ideai/`** — conversations (handoff/log.jsonl ×3), `layouts.json`, `memory/MEMORY.md`, + nouveau `memory/checkpoint-delivery-submit-logging-fix.md` (non suivi). ## Stratégie de branche **On reste sur `feature/agent-skill-awareness-v2`.** Elle part proprement de `develop`, pas de divergence, c'est le bon support pour reprendre le chantier. Pas de nouvelle branche. (Réserve mineure : le nom de branche parle de « skill-awareness » alors que le diff actuel est un hotfix livraison/logs ; je l'assume car Main reprend ce chantier sous ce nom. Si tu préfères isoler le hotfix sous `fix/delivery-submit-logging`, dis-le, je crée et bascule — sinon on garde.) ## Plan de commit (PLUS TARD, sur ton GO) Deux commits atomiques séparés, par intention : 1. `fix(input): livraison délégation + journalisation submit` → les 8 fichiers code (crates + frontend + test). 2. `chore(wip): état runtime .ideai` → conversations, `layouts.json`, `MEMORY.md` + le nouveau checkpoint mémoire (cohérent avec l'historique `chore(wip)` existant). ## Hors commit / à laisser Rien à exclure ni à ignorer en plus pour l'instant : l'historique du dépôt versionne déjà l'état runtime `.ideai/` via des `chore(wip)`, donc je le committe à part plutôt que de le mélanger au code. (Remarque de fond : ces fichiers runtime sont du bruit de diff ; si tu veux un jour les `.gitignore`, c'est une décision produit séparée à trancher hors de ce ticket.) ## Conditions avant commit / merge - **Avant le commit code** : QA verte — tests Rust (`cargo test` sur les crates touchés) **et** front (`useWritePortal.test.tsx` notamment). Tant que c'est rouge, je ne committe pas le hotfix. - **Avant merge `feature/agent-skill-awareness-v2` → `develop`** : tests verts + rebase sur `develop` uniquement si elle a avancé entre-temps (actuellement non), puis merge `--no-ff`. - **Aucune action sortante** (push/PR) sans ton GO explicite. J'attends ton signal pour committer. @@ -23,4 +21,6 @@ objective: Nouvelle feature cadrée par Architect, prête à démarrer le dével - **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. - **Response:** Repo prêt pour le Lot B « workstate delegation/queue snapshot ». ## Actions 1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte : - `7453181` — chore(wip): état runtime .ideai (flux conversation live). 2. Nouvelle branche feature créée depuis `develop` propre : - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime). ## État final - **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`). - Base : `develop`, historique linéaire, working tree **propre**. - `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.** DevBackend/DevFrontend peuvent implémenter le Lot B. 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:** Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte: - Branche active attendue: `feature/workstate-delegation-queue`. - QA vert: - `cargo fmt --all -- --check` OK - `cargo test -p infrastructure mailbox --lib` OK (13 passed) - `cargo test -p application --test workstate` OK (12 passed) - `cargo test -p app-tauri --test dto_agents` OK (20 passed) - `cargo check -p app-tauri` OK - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests) - `cd frontend && npx tsc --noEmit` OK - `cargo test -p domain mailbox --lib` OK (6 passed) - Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`. - Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code. Décide: 1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire), 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final. \ No newline at end of file +- **Prompt:** Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte: - Branche active attendue: `feature/workstate-delegation-queue`. - QA vert: - `cargo fmt --all -- --check` OK - `cargo test -p infrastructure mailbox --lib` OK (13 passed) - `cargo test -p application --test workstate` OK (12 passed) - `cargo test -p app-tauri --test dto_agents` OK (20 passed) - `cargo check -p app-tauri` OK - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests) - `cd frontend && npx tsc --noEmit` OK - `cargo test -p domain mailbox --lib` OK (6 passed) - Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`. - Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code. Décide: 1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire), 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final. +- **Response:** Lot B `workstate delegation/queue snapshot` intégré. QA re-vérifié vert de mon côté avant tout commit (fmt OK ; domain 6, infra mailbox 13, application workstate 12, app-tauri dto_agents 20 ; vitest 2 fichiers/17 ; tsc clean). **3 commits atomiques** sur `feature/workstate-delegation-queue` (séparation code feature / runtime) : - `cc7d99a` feat(workstate): snapshot des délégations en file par agent (Lot B backend) — domain `QueuedTicketSnapshot` + trait ségrégué `AgentQueueSnapshot` (ISP), infra `InMemoryMailbox` vue lecture, application read-model, DTO camelCase app-tauri + état. - `c600604` feat(workstate): UI des délégations en file par agent (Lot B frontend) — domaine/mock alignés, `ProjectWorkStatePanel` enrichi (requester, aperçu, position FIFO), hook. - `5cb99fd` chore(wip): état runtime .ideai (flux conversation live, layouts) — runtime isolé, jamais mélangé au code feature. **Merge local** : feature TERMINÉE et VERTE → `git merge --no-ff feature/workstate-delegation-queue` dans **develop** → commit de merge `64c2c14`. `develop` n'avait pas divergé (il pointait sur la base de la feature) ; `--no-ff` choisi pour garder la trace d'intégration du lot, cohérent avec l'intégration du Lot A (`3bfb932`). **Pas de push, aucune branche supprimée.** `feature/workstate-delegation-queue` est conservée (sur `cc7d99a→5cb99fd`). **État final** : - Branche courante : `develop` (HEAD = `64c2c14`). - Working tree : propre (`git status` vide). - `feature/workstate-delegation-queue` préservée et entièrement contenue dans `develop`. - Rien en attente côté git. Prêt pour une future release `develop → main` sur ta validation explicite. +- **Prompt:** Prépare le dépôt pour le Lot C `workstate conversation summaries` après Lot B mergé dans `develop` @ `64c2c14`. État attendu côté Main: `develop` ahead origin, avec dirty runtime/memory lié au checkpoint récent (`.ideai/conversations/...`, `.ideai/memory/MEMORY.md`, `.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md`). Merci de: - gérer ce dirty sans perte et sans le mélanger au futur code applicatif (commit runtime/memory si tu juges cohérent avec l'historique), - créer/basculer sur une branche feature propre depuis `develop` pour Lot C, - ne push pas, ne supprime pas de branche, - retourner branche courante, commits éventuels, et `git status` final. Nom de branche suggéré: `feature/workstate-conversation-summaries`. \ 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 27b5763..99f0e8c 100644 --- a/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl +++ b/.ideai/conversations/6bc594e8-a37c-0dbd-1de6-6e3b73002cb4/log.jsonl @@ -49,3 +49,5 @@ {"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."} {"id":"9654549c-fd9d-4661-8dc2-e6d2a78eab7a","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971855209,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Repo prêt pour le Lot B « workstate delegation/queue snapshot ».\n\n## Actions\n1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte :\n - `7453181` — chore(wip): état runtime .ideai (flux conversation live).\n2. Nouvelle branche feature créée depuis `develop` propre :\n - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime).\n\n## État final\n- **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`).\n- Base : `develop`, historique linéaire, working tree **propre**.\n- `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.**\n\nDevBackend/DevFrontend peuvent implémenter le Lot B. 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":"53773ad0-5bb6-4e24-8456-d7ab17be3db0","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974288762,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push.\n\nContexte:\n- Branche active attendue: `feature/workstate-delegation-queue`.\n- QA vert:\n - `cargo fmt --all -- --check` OK\n - `cargo test -p infrastructure mailbox --lib` OK (13 passed)\n - `cargo test -p application --test workstate` OK (12 passed)\n - `cargo test -p app-tauri --test dto_agents` OK (20 passed)\n - `cargo check -p app-tauri` OK\n - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests)\n - `cd frontend && npx tsc --noEmit` OK\n - `cargo test -p domain mailbox --lib` OK (6 passed)\n- Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`.\n- Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`.\n- Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code.\n\nDécide:\n1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire),\n2. merge local éventuel vers `develop`,\n3. état final.\n\nNe push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final."} +{"id":"58b15907-ee31-488c-afaa-40246fce5228","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974372502,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot B `workstate delegation/queue snapshot` intégré. QA re-vérifié vert de mon côté avant tout commit (fmt OK ; domain 6, infra mailbox 13, application workstate 12, app-tauri dto_agents 20 ; vitest 2 fichiers/17 ; tsc clean).\n\n**3 commits atomiques** sur `feature/workstate-delegation-queue` (séparation code feature / runtime) :\n- `cc7d99a` feat(workstate): snapshot des délégations en file par agent (Lot B backend) — domain `QueuedTicketSnapshot` + trait ségrégué `AgentQueueSnapshot` (ISP), infra `InMemoryMailbox` vue lecture, application read-model, DTO camelCase app-tauri + état.\n- `c600604` feat(workstate): UI des délégations en file par agent (Lot B frontend) — domaine/mock alignés, `ProjectWorkStatePanel` enrichi (requester, aperçu, position FIFO), hook.\n- `5cb99fd` chore(wip): état runtime .ideai (flux conversation live, layouts) — runtime isolé, jamais mélangé au code feature.\n\n**Merge local** : feature TERMINÉE et VERTE → `git merge --no-ff feature/workstate-delegation-queue` dans **develop** → commit de merge `64c2c14`. `develop` n'avait pas divergé (il pointait sur la base de la feature) ; `--no-ff` choisi pour garder la trace d'intégration du lot, cohérent avec l'intégration du Lot A (`3bfb932`).\n\n**Pas de push, aucune branche supprimée.** `feature/workstate-delegation-queue` est conservée (sur `cc7d99a→5cb99fd`).\n\n**État final** :\n- Branche courante : `develop` (HEAD = `64c2c14`).\n- Working tree : propre (`git status` vide).\n- `feature/workstate-delegation-queue` préservée et entièrement contenue dans `develop`.\n- Rien en attente côté git. Prêt pour une future release `develop → main` sur ta validation explicite."} +{"id":"1cbd7c12-6608-45b4-aa9c-41dff0fedd51","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781975270116,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Prépare le dépôt pour le Lot C `workstate conversation summaries` après Lot B mergé dans `develop` @ `64c2c14`.\n\nÉtat attendu côté Main: `develop` ahead origin, avec dirty runtime/memory lié au checkpoint récent (`.ideai/conversations/...`, `.ideai/memory/MEMORY.md`, `.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md`).\n\nMerci de:\n- gérer ce dirty sans perte et sans le mélanger au futur code applicatif (commit runtime/memory si tu juges cohérent avec l'historique),\n- créer/basculer sur une branche feature propre depuis `develop` pour Lot C,\n- ne push pas, ne supprime pas de branche,\n- retourner branche courante, commits éventuels, et `git status` final.\n\nNom de branche suggéré: `feature/workstate-conversation-summaries`."} diff --git a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md index 5f7a4eb..577cb49 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: 001f112c-4ece-4f49-a7cc-140ee76eabec +upTo: 998a4f67-a352-4e89-a5ee-71260d89f112 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:** 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. - **Response:** VERDICT : **B**. Le niveau 3 n'est PAS complet en l'état. Une slice supplémentaire (LS8) est requise AVANT le merge. Le découpage et le contrat exact suivent. ## Pourquoi B (et pas A) Le design verrouillé est sans ambiguïté : niveau 3 = « IdeA **DEMANDE** à l'utilisateur » et « les TROIS niveaux d'emblée ». « Demander » est un acte qui **arme** une reprise, pas une note d'information. La preuve est dans le code lui-même : 1. `domain::session_limit::RateLimitSource::Human` existe déjà, documenté « niveau 3 : confirmée par l'utilisateur », et n'est **produit nulle part** aujourd'hui. C'est du code mort qui attend exactement cette entrée. L'archi a toujours prévu B. 2. La doc de `ResumePlan::HumanFallback` dit : « L'application **demandera l'heure** à l'utilisateur plutôt que d'agir à l'aveugle ». La boucle d'action fait partie du contrat domaine, pas une extension. Surfacer + relancement manuel (chemin `ListResumableAgents`) ne satisfait PAS « jamais d'inaction silencieuse » au sens de la feature : c'est un **autre mécanisme**, qui ne donne ni la reprise auto programmée ni la fenêtre **annulable** que les niveaux 1/2 garantissent. Sur niveau 3, l'utilisateur est aujourd'hui informé mais le mécanisme central de la feature lui est inaccessible. Incohérence de contrat ⇒ non mergeable tel quel. DevFrontend a eu raison de poser le TODO plutôt que d'inventer la commande. ## Découpage minimal — LS8 « filet humain : armement par heure saisie » **Domaine : RIEN à ajouter.** `plan_resume`, `SessionLimit`, `RateLimitSource::Human`, `ResumePlan::Scheduled` couvrent déjà tout. Une heure saisie par l'utilisateur est fonctionnellement une `SessionLimit` de source `Human` avec `resets_at_ms = Some(epoch)`. Le clamp anti-passé (`max(now)`) protège déjà une saisie déjà échue ⇒ reprise immédiate. C'est le payoff de l'hexagonal : zéro nouveau port, zéro nouvel adapter. **Application — `SessionLimitService` : une méthode publique.** N'élargis PAS `on_rate_limited` (sémantique « signal de détection auto »). Ajoute une entrée dédiée, en réutilisant strictement la branche `Scheduled` existante : ```rust /// (d) Filet humain (§21.1 niveau 3). L'utilisateur a saisi l'heure de reset /// pour un agent en limite SUSPECTÉE (AgentRateLimitSuspected, sans heure fiable). /// Construit une SessionLimit source `Human`, calcule le plan et arme la reprise /// EXACTEMENT comme la branche auto : mêmes événements, même dédoublonnage, /// même annulabilité via cancel_resume. pub fn confirm_human_resume( &self, agent_id: AgentId, node_id: NodeId, conversation_id: Option, resets_at_ms: i64, // i64 nu, pas Option : la saisie EST l'heure ) ``` Corps = copie de la branche `ResumePlan::Scheduled` de `on_rate_limited` (publish `AgentRateLimited{Some}` → `disarm` → `scheduler.arm(ResumeAgent)` → mémoriser le `ScheduleId` → publish `AgentResumeScheduled`), avec `SessionLimit::new(Some(resets_at_ms), now, RateLimitSource::Human)`. **Factorise** la branche en un `fn arm_scheduled(&self, agent_id, fire_at_ms, node_id, conversation_id)` privé appelé par les deux chemins, pour ne pas dupliquer le dédoublonnage. `execute_resume` et `cancel_resume` restent **inchangés** : l'armement humain est annulable et s'exécute par les mêmes voies (c'est l'invariant à préserver — un seul mécanisme de reprise). **app-tauri — une commande.** Miroir exact de `cancel_resume` : ```rust #[tauri::command] pub async fn set_resume_at( agent_id: String, resets_at_ms: i64, state: State<'_, AppState>, ) -> Result<(), ErrorDto> ``` Corps : `parse_agent_id` → résoudre `node_id` et `conversation_id` **côté backend** depuis la registry (le front n'a que l'`agent_id` ; `agentRateLimitSuspected` ne porte que ça) : - `node_id` via `TerminalSessions::node_for_agent(&id)` (ou la registry unifiée). Si `None` ⇒ `ErrorDto` INVALID/NOT_FOUND (l'agent n'a plus de cellule vivante — la saisie n'a pas de cible). - `conversation_id` via la session structurée `session_for_agent(&id).map(|s| s.conversation_id())` (best-effort `None` toléré, comme tout le chemin de reprise dégradé). Puis `state.session_limit_service.confirm_human_resume(id, node_id, conv_id, resets_at_ms)`. Enregistre la commande dans `lib.rs` à côté de `cancel_resume`. **Front — un formulaire minimal.** Sur le badge « limité · heure inconnue — reprise à préciser » : un petit input heure (ou datetime) → calcule l'epoch ms → `invoke("set_resume_at", { agentId, resetsAtMs })`. Dès réception de `AgentResumeScheduled` (déjà consommé !), le badge bascule **automatiquement** sur l'état nominal « limité jusqu'à HH:MM » + compte à rebours + bouton « Annuler la reprise » déjà câblés. Aucun nouvel événement, aucune nouvelle vue d'état côté front : la boucle se referme sur l'UI existante. ## Contrat des événements — RÉUTILISE l'existant, n'en crée AUCUN C'est le point clé de cohérence : `confirm_human_resume` émet la **même** paire que la branche auto — `AgentRateLimited { agent_id, resets_at_ms: Some(t) }` puis **`AgentResumeScheduled { agent_id, fire_at_ms }`**. Le front les consomme déjà. La transition « suspecté → programmé » se fait donc sans code de présentation neuf : `AgentResumeScheduled` est le pivot qui retire l'état « heure inconnue » et réutilise le rendu nominal. Annulation ⇒ `AgentResumeCancelled` (inchangé). Reprise ⇒ `AgentResumed` (inchangé). `AgentRateLimitSuspected` reste l'unique signal « il faut demander », rien d'autre. ## Garde-fous QA pour le binôme - Saisie dans le passé : pas un cas d'erreur — `plan_resume` clampe à `now` ⇒ reprise quasi-immédiate. Test à ajouter. - Re-saisie / second suspect : `confirm_human_resume` doit passer par le **même `disarm`** que l'auto ⇒ un seul armement par agent (§21.10-4). Test de dédoublonnage croisé (humain après auto, et inverse). - Agent disparu entre suspicion et saisie : `node_for_agent == None` ⇒ erreur propre, pas d'armement orphelin. - Course cancel-pile-au-tir : inchangée, déjà couverte par `cancel_resume`. Périmètre total : ~1 méthode service (factorisée) + 1 commande + 1 input front + tests. Aucune frontière nouvelle, aucun port, aucun adapter. À cadrer et livrer en LS8 avant que Git n'envisage le merge `feature/* → develop`. - **Prompt:** Diagnostic architecture demandé par Main. Contexte: IdeA intègre Codex CLI. Le profil Codex actuel utilise McpConfigStrategy::TomlConfigHome { target: ".codex/config.toml", home_env: "CODEX_HOME" }, donc au lancement IdeA écrit {runDir}/.codex/config.toml et pousse CODEX_HOME vers ce dossier pour isoler MCP/permissions par agent. Problème produit: chaque agent Codex redemande une connexion ChatGPT/OpenAI, et si l’utilisateur crée beaucoup d’agents il atteint une limite de requests/login. De plus un agent jamais lancé peut échouer lors d’une délégation car il n’est pas authentifié. Questions: 1) quelle solution respecte l’architecture hexagonale/SOLID pour partager l’auth Codex au niveau ordinateur/utilisateur tout en gardant la config MCP/permissions isolée par agent ? 2) faut-il introduire un port Auth/RuntimeHealth/Readiness pour préflight au moment de création/configuration d’agent ? 3) quels changements concrets recommandes-tu dans le domaine/application/infrastructure ? Merci de répondre en français, concis mais actionnable. @@ -23,4 +21,6 @@ objective: CADRAGE ARCHITECTURE (pas de code) — Support de la délégation int - **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. - **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 +- **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. +- **Prompt:** Après Lot A (ProjectWorkState live/busy) et Lot B (tickets FIFO par agent) mergés dans `develop` @ `64c2c14`, l'utilisateur demande d'enchaîner sur tout ce qu'il reste. Cadre le prochain lot prioritaire du chantier UX conversations/délégations. Objectif pressenti: Lot C — conversation summaries depuis log/handoff en lecture best-effort, pour que le panneau Work donne une vue lisible des threads sans charger tout le journal. Merci de définir précisément: - objectif produit et inclus/exclus, - source de vérité backend (ConversationLog/HandoffStore/ConversationRegistry ou autre), - frontières hexagonales: domaine/application/infra/Tauri/frontend, - DTOs exacts et évolution de `ProjectWorkState` ou nouvelle commande si préférable, - stratégie preview/handoff (bornes, erreurs best-effort, pas de mutation), - impacts frontend et UI minimale, - tests attendus et commandes. Contraintes: - Pas de nouvelle persistance durable si les logs/handoff existent déjà. - Ne pas exposer tout `log.jsonl`; previews/summaries seulement. - Ne pas bloquer l’état live si un log est corrompu/absent; best-effort. - Respecter la suite du chantier UX et éviter de mélanger avec actions Lot D ou mémoire automatique Lot E. - Ne code pas. +- **Response:** ## Lot C — Conversation summaries dans le panneau Work ### Objectif produit Objectif : enrichir le panneau `Work` avec une vue lisible des fils de conversation associés aux tickets/live work, sans exposer ni charger tout `log.jsonl`. Inclus : - afficher, pour chaque `conversationId` visible dans le work-state, un résumé compact issu du `handoff.md` quand il existe ; - compléter par un aperçu borné des derniers tours du `ConversationLog` quand le handoff est absent ou vide ; - signaler sobrement l’état de preview : `ready`, `missing`, `partial`, `unavailable` ; - rester strictement read-only et best-effort : aucune écriture, aucun recalcul de handoff, aucun blocage de `live/busy/tickets` si un fichier est absent/corrompu. Exclus : - pas d’action utilisateur sur les conversations ; - pas de lecture complète affichée du journal ; - pas de mutation/réparation des logs/handoff ; - pas de génération LLM de résumé ; - pas de mémoire automatique Lot E ; - pas de changement de FIFO, reprise, cancel, requeue ou actions Lot D. ### Source de vérité backend Source durable primaire : `HandoffStore` (`.ideai/conversations//handoff.md`). Source durable secondaire : `ConversationLog` (`.ideai/conversations//log.jsonl`) uniquement via une lecture bornée (`last(n)`), pour construire une preview courte quand le handoff est absent ou inutilisable. `ConversationRegistry` ne doit pas être la source de vérité du Lot C : il est live/in-memory, utile pour résoudre ou suivre des paires pendant l’orchestration, mais le panneau Work doit refléter les conversations déjà matérialisées par les tickets et leur persistance durable. Source des conversations à inspecter : les `conversation_id` déjà présents dans les `QueuedTicketSnapshot` projetés par `GetProjectWorkState`. En option additive, si le snapshot live expose déjà un `conversation_id` stable ailleurs, on peut l’ajouter plus tard, mais Lot C doit se limiter aux conversations visibles par les tickets pour éviter d’ouvrir un chantier layout/session. ### Frontières hexagonales Domaine : - réutiliser les ports existants `ConversationLog` et `HandoffStore` ; - ajouter seulement des value objects purs de read-model si nécessaire : `ConversationWorkSummary`, `ConversationPreviewStatus`, éventuellement `ConversationTurnPreview` ; - aucun accès FS, aucun Tauri, aucun type UI. Application : - étendre `GetProjectWorkState` avec un collaborateur de lecture de previews, ou extraire un petit service applicatif `ConversationPreviewReader` utilisé par `GetProjectWorkState` ; - dépendances injectées : `Arc` et `Arc`, ou `Option>` si on veut rendre l’enrichissement désactivable dans les tests legacy ; - le use case continue à renvoyer le work-state même si toutes les previews échouent. Infrastructure : - aucune nouvelle persistance ; - réutiliser `FsHandoffStore` et `FsConversationLog` déjà câblés sous `/.ideai/conversations/` ; - si `FsHandoffStore::load` renvoie une erreur de parsing, l’application la convertit en preview `unavailable/partial`, pas en `AppError` global ; - `FsConversationLog::last` est déjà tolérant aux lignes corrompues, donc c’est le bon fallback. Tauri : - garder la commande existante `get_project_work_state` ; - faire évoluer `ProjectWorkStateDto` additivement ; - ne pas créer `get_conversation_log` ni endpoint brut. Frontend : - types TS miroir dans le domaine UI ; - adapter Tauri inchangé côté méthode (`getProjectWorkState`) ; - mock gateway normalise `conversations: []` pour compat tests ; - composant `ProjectWorkStatePanel` mappe `conversationId -> summary` et affiche une ligne compacte par ticket/thread. ### DTO recommandé Préférence : étendre `ProjectWorkState` avec une liste top-level de résumés, référencée par `conversationId`. Ça évite de dupliquer le résumé sur plusieurs tickets du même fil. Rust application : ```rust pub struct ProjectWorkState { pub agents: Vec, pub conversations: Vec, } pub struct ConversationWorkSummary { pub conversation_id: ConversationId, pub status: ConversationPreviewStatus, pub objective_preview: Option, pub summary_preview: Option, pub summary_len: usize, pub up_to: Option, pub recent_turns: Vec, } pub enum ConversationPreviewStatus { Ready, Missing, Partial, Unavailable, } pub struct ConversationTurnWorkPreview { pub role: TurnRole, pub source: TicketWorkSource, pub at_ms: u64, pub text_preview: String, pub text_len: usize, } ``` Wire DTO camelCase : ```ts export interface ProjectWorkState { agents: AgentWorkState[]; conversations: ConversationWorkSummary[]; } export type ConversationPreviewStatus = | "ready" | "missing" | "partial" | "unavailable"; export interface ConversationWorkSummary { conversationId: string; status: ConversationPreviewStatus; objectivePreview?: string; summaryPreview?: string; summaryLen: number; upTo?: string; recentTurns: ConversationTurnWorkPreview[]; } export interface ConversationTurnWorkPreview { role: "prompt" | "response" | "toolActivity"; source: TicketWorkSource; atMs: number; textPreview: string; textLen: number; } ``` `AgentTicketState` ne change pas : il porte déjà `conversationId`, `taskPreview`, `taskLen`, `status`, `requesterLabel`. Le frontend joint via `conversationId`. Compatibilité : les mocks et tests peuvent normaliser `conversations` à `[]` quand absent, comme ils le font déjà pour `tickets`. ### Stratégie preview/handoff Bornes proposées : - `HANDOFF_PREVIEW_MAX_CHARS = 480` ; - `OBJECTIVE_PREVIEW_MAX_CHARS = 160` ; - `RECENT_TURNS_MAX = 3` ; - `TURN_PREVIEW_MAX_CHARS = 220`. Algorithme par `conversationId` unique : 1. tenter `handoffs.load(conversation)` ; 2. si `Ok(Some(handoff))` : - `status = ready` ; - `summaryPreview = normalize_truncate(handoff.summary_md, 480)` ; - `summaryLen = handoff.summary_md.chars().count()` ; - `objectivePreview = handoff.objective.map(truncate 160)` ; - `upTo = Some(handoff.up_to)` ; 3. si `Ok(None)` : `status = missing`, puis tenter `log.last(conversation, 3)` ; 4. si handoff illisible mais `log.last` marche : `status = partial`, `summaryPreview = None`, `recentTurns = ...` ; 5. si handoff et log échouent : `status = unavailable`, champs preview vides ; 6. dans tous les cas, ne jamais propager l’erreur au `ProjectWorkState` global. Normalisation : même règle que `task_preview` Lot B : trim + collapse whitespace + troncature à la borne, jamais mid-codepoint. On peut factoriser un helper applicatif privé dans `workstate`. Important : ne pas appeler `ConversationLog::read(None)` dans Lot C. Le fallback doit être `last(RECENT_TURNS_MAX)` uniquement. ### UI minimale Dans `ProjectWorkStatePanel` : - garder la structure actuelle agent > tickets ; - sous chaque ticket, afficher une ligne thread compacte si un résumé existe : - objectif si présent : `Goal: ` ; - sinon résumé : `` ; - sinon dernier tour : `Last: ` ; - badge discret selon statut : `Summary`, `No summary`, `Partial`, `Unavailable` ; - ne pas ajouter de modal ni bouton “ouvrir log” ; - ne pas afficher les fichiers, chemins, ni JSON brut ; - si `conversationId` n’a pas de summary dans la map, ne pas casser la ligne ticket : afficher seulement l’état Lot B existant. Le panneau doit rester scannable : pas de cartes imbriquées, pas de gros bloc Markdown rendu. Texte simple, clamp/line wrap raisonnable. ### Tests attendus Application (`crates/application/tests/workstate.rs`) : - conserve agents/live/busy/tickets même si le preview reader échoue ; - déduplique les `conversationId` de plusieurs tickets ; - handoff présent produit `ready`, `objectivePreview`, `summaryPreview`, `summaryLen`, `upTo` ; - handoff absent + log présent produit `missing` avec `recentTurns` borné ; - handoff corrompu + log lisible produit `partial` ; - handoff/log indisponibles produit `unavailable` sans `AppError` global ; - previews tronquées et whitespace normalisé. Infrastructure (`crates/infrastructure/tests/conversation_log.rs`) : - normalement peu ou pas de nouveau test requis si `FsConversationLog::last` et `FsHandoffStore::load` sont déjà couverts ; - ajouter seulement un test si un helper d’adapter dédié apparaît, mais éviter d’en créer un. Tauri DTO (`crates/app-tauri/tests/dto_agents.rs`) : - sérialisation camelCase de `conversations` ; - conversion des statuts `ready/missing/partial/unavailable` ; - compat additive avec agents/tickets existants. Frontend (`frontend/src/features/workstate/workstate.test.tsx`) : - mock sans `conversations` normalisé à `[]` ; - ticket avec `conversationId` affiche objectif/résumé ; - status `partial`/`unavailable` ne masque pas le ticket ; - refresh existant sur `agentBusyChanged` / `delegationReady` continue. Commandes : ```bash cargo test -p application workstate cargo test -p app-tauri dto_agents cargo test -p infrastructure conversation_log npm --prefix frontend test -- workstate ``` Selon l’organisation actuelle du workspace, finir par : ```bash cargo test npm --prefix frontend test ``` ### Décision de lot Lot C doit être un enrichissement read-only de `ProjectWorkState`, pas une nouvelle surface d’action. Le backend reste source d’autorité pour les bornes et la robustesse ; le frontend ne fait que joindre `tickets[].conversationId` vers `state.conversations[]` et rendre une preview compacte. Ça respecte les contraintes : aucune nouvelle persistance, aucun log brut exposé, best-effort par conversation, et pas de mélange avec les futures actions de gestion ou la mémoire automatique. \ 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 03291b5..e50659b 100644 --- a/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl +++ b/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/log.jsonl @@ -37,3 +37,5 @@ {"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."} +{"id":"8f8bddcd-1d3b-4739-a00f-6457469fa1a2","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781975163138,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Après Lot A (ProjectWorkState live/busy) et Lot B (tickets FIFO par agent) mergés dans `develop` @ `64c2c14`, l'utilisateur demande d'enchaîner sur tout ce qu'il reste. Cadre le prochain lot prioritaire du chantier UX conversations/délégations.\n\nObjectif pressenti: Lot C — conversation summaries depuis log/handoff en lecture best-effort, pour que le panneau Work donne une vue lisible des threads sans charger tout le journal.\n\nMerci de définir précisément:\n- objectif produit et inclus/exclus,\n- source de vérité backend (ConversationLog/HandoffStore/ConversationRegistry ou autre),\n- frontières hexagonales: domaine/application/infra/Tauri/frontend,\n- DTOs exacts et évolution de `ProjectWorkState` ou nouvelle commande si préférable,\n- stratégie preview/handoff (bornes, erreurs best-effort, pas de mutation),\n- impacts frontend et UI minimale,\n- tests attendus et commandes.\n\nContraintes:\n- Pas de nouvelle persistance durable si les logs/handoff existent déjà.\n- Ne pas exposer tout `log.jsonl`; previews/summaries seulement.\n- Ne pas bloquer l’état live si un log est corrompu/absent; best-effort.\n- Respecter la suite du chantier UX et éviter de mélanger avec actions Lot D ou mémoire automatique Lot E.\n- Ne code pas."} +{"id":"998a4f67-a352-4e89-a5ee-71260d89f112","conversation":"7a2f446c-2efa-0c56-08dd-07cdc5cac19b","atMs":1781975262249,"source":{"kind":"agent","agentId":"dce19c75-9669-4e45-b8de-9950025157da"},"role":"response","text":"## Lot C — Conversation summaries dans le panneau Work\n\n### Objectif produit\n\nObjectif : enrichir le panneau `Work` avec une vue lisible des fils de conversation associés aux tickets/live work, sans exposer ni charger tout `log.jsonl`.\n\nInclus :\n- afficher, pour chaque `conversationId` visible dans le work-state, un résumé compact issu du `handoff.md` quand il existe ;\n- compléter par un aperçu borné des derniers tours du `ConversationLog` quand le handoff est absent ou vide ;\n- signaler sobrement l’état de preview : `ready`, `missing`, `partial`, `unavailable` ;\n- rester strictement read-only et best-effort : aucune écriture, aucun recalcul de handoff, aucun blocage de `live/busy/tickets` si un fichier est absent/corrompu.\n\nExclus :\n- pas d’action utilisateur sur les conversations ;\n- pas de lecture complète affichée du journal ;\n- pas de mutation/réparation des logs/handoff ;\n- pas de génération LLM de résumé ;\n- pas de mémoire automatique Lot E ;\n- pas de changement de FIFO, reprise, cancel, requeue ou actions Lot D.\n\n### Source de vérité backend\n\nSource durable primaire : `HandoffStore` (`.ideai/conversations//handoff.md`).\n\nSource durable secondaire : `ConversationLog` (`.ideai/conversations//log.jsonl`) uniquement via une lecture bornée (`last(n)`), pour construire une preview courte quand le handoff est absent ou inutilisable.\n\n`ConversationRegistry` ne doit pas être la source de vérité du Lot C : il est live/in-memory, utile pour résoudre ou suivre des paires pendant l’orchestration, mais le panneau Work doit refléter les conversations déjà matérialisées par les tickets et leur persistance durable.\n\nSource des conversations à inspecter : les `conversation_id` déjà présents dans les `QueuedTicketSnapshot` projetés par `GetProjectWorkState`. En option additive, si le snapshot live expose déjà un `conversation_id` stable ailleurs, on peut l’ajouter plus tard, mais Lot C doit se limiter aux conversations visibles par les tickets pour éviter d’ouvrir un chantier layout/session.\n\n### Frontières hexagonales\n\nDomaine :\n- réutiliser les ports existants `ConversationLog` et `HandoffStore` ;\n- ajouter seulement des value objects purs de read-model si nécessaire : `ConversationWorkSummary`, `ConversationPreviewStatus`, éventuellement `ConversationTurnPreview` ;\n- aucun accès FS, aucun Tauri, aucun type UI.\n\nApplication :\n- étendre `GetProjectWorkState` avec un collaborateur de lecture de previews, ou extraire un petit service applicatif `ConversationPreviewReader` utilisé par `GetProjectWorkState` ;\n- dépendances injectées : `Arc` et `Arc`, ou `Option>` si on veut rendre l’enrichissement désactivable dans les tests legacy ;\n- le use case continue à renvoyer le work-state même si toutes les previews échouent.\n\nInfrastructure :\n- aucune nouvelle persistance ;\n- réutiliser `FsHandoffStore` et `FsConversationLog` déjà câblés sous `/.ideai/conversations/` ;\n- si `FsHandoffStore::load` renvoie une erreur de parsing, l’application la convertit en preview `unavailable/partial`, pas en `AppError` global ;\n- `FsConversationLog::last` est déjà tolérant aux lignes corrompues, donc c’est le bon fallback.\n\nTauri :\n- garder la commande existante `get_project_work_state` ;\n- faire évoluer `ProjectWorkStateDto` additivement ;\n- ne pas créer `get_conversation_log` ni endpoint brut.\n\nFrontend :\n- types TS miroir dans le domaine UI ;\n- adapter Tauri inchangé côté méthode (`getProjectWorkState`) ;\n- mock gateway normalise `conversations: []` pour compat tests ;\n- composant `ProjectWorkStatePanel` mappe `conversationId -> summary` et affiche une ligne compacte par ticket/thread.\n\n### DTO recommandé\n\nPréférence : étendre `ProjectWorkState` avec une liste top-level de résumés, référencée par `conversationId`. Ça évite de dupliquer le résumé sur plusieurs tickets du même fil.\n\nRust application :\n\n```rust\npub struct ProjectWorkState {\n pub agents: Vec,\n pub conversations: Vec,\n}\n\npub struct ConversationWorkSummary {\n pub conversation_id: ConversationId,\n pub status: ConversationPreviewStatus,\n pub objective_preview: Option,\n pub summary_preview: Option,\n pub summary_len: usize,\n pub up_to: Option,\n pub recent_turns: Vec,\n}\n\npub enum ConversationPreviewStatus {\n Ready,\n Missing,\n Partial,\n Unavailable,\n}\n\npub struct ConversationTurnWorkPreview {\n pub role: TurnRole,\n pub source: TicketWorkSource,\n pub at_ms: u64,\n pub text_preview: String,\n pub text_len: usize,\n}\n```\n\nWire DTO camelCase :\n\n```ts\nexport interface ProjectWorkState {\n agents: AgentWorkState[];\n conversations: ConversationWorkSummary[];\n}\n\nexport type ConversationPreviewStatus =\n | \"ready\"\n | \"missing\"\n | \"partial\"\n | \"unavailable\";\n\nexport interface ConversationWorkSummary {\n conversationId: string;\n status: ConversationPreviewStatus;\n objectivePreview?: string;\n summaryPreview?: string;\n summaryLen: number;\n upTo?: string;\n recentTurns: ConversationTurnWorkPreview[];\n}\n\nexport interface ConversationTurnWorkPreview {\n role: \"prompt\" | \"response\" | \"toolActivity\";\n source: TicketWorkSource;\n atMs: number;\n textPreview: string;\n textLen: number;\n}\n```\n\n`AgentTicketState` ne change pas : il porte déjà `conversationId`, `taskPreview`, `taskLen`, `status`, `requesterLabel`. Le frontend joint via `conversationId`.\n\nCompatibilité : les mocks et tests peuvent normaliser `conversations` à `[]` quand absent, comme ils le font déjà pour `tickets`.\n\n### Stratégie preview/handoff\n\nBornes proposées :\n- `HANDOFF_PREVIEW_MAX_CHARS = 480` ;\n- `OBJECTIVE_PREVIEW_MAX_CHARS = 160` ;\n- `RECENT_TURNS_MAX = 3` ;\n- `TURN_PREVIEW_MAX_CHARS = 220`.\n\nAlgorithme par `conversationId` unique :\n1. tenter `handoffs.load(conversation)` ;\n2. si `Ok(Some(handoff))` :\n - `status = ready` ;\n - `summaryPreview = normalize_truncate(handoff.summary_md, 480)` ;\n - `summaryLen = handoff.summary_md.chars().count()` ;\n - `objectivePreview = handoff.objective.map(truncate 160)` ;\n - `upTo = Some(handoff.up_to)` ;\n3. si `Ok(None)` : `status = missing`, puis tenter `log.last(conversation, 3)` ;\n4. si handoff illisible mais `log.last` marche : `status = partial`, `summaryPreview = None`, `recentTurns = ...` ;\n5. si handoff et log échouent : `status = unavailable`, champs preview vides ;\n6. dans tous les cas, ne jamais propager l’erreur au `ProjectWorkState` global.\n\nNormalisation : même règle que `task_preview` Lot B : trim + collapse whitespace + troncature à la borne, jamais mid-codepoint. On peut factoriser un helper applicatif privé dans `workstate`.\n\nImportant : ne pas appeler `ConversationLog::read(None)` dans Lot C. Le fallback doit être `last(RECENT_TURNS_MAX)` uniquement.\n\n### UI minimale\n\nDans `ProjectWorkStatePanel` :\n- garder la structure actuelle agent > tickets ;\n- sous chaque ticket, afficher une ligne thread compacte si un résumé existe :\n - objectif si présent : `Goal: ` ;\n - sinon résumé : `` ;\n - sinon dernier tour : `Last: ` ;\n - badge discret selon statut : `Summary`, `No summary`, `Partial`, `Unavailable` ;\n- ne pas ajouter de modal ni bouton “ouvrir log” ;\n- ne pas afficher les fichiers, chemins, ni JSON brut ;\n- si `conversationId` n’a pas de summary dans la map, ne pas casser la ligne ticket : afficher seulement l’état Lot B existant.\n\nLe panneau doit rester scannable : pas de cartes imbriquées, pas de gros bloc Markdown rendu. Texte simple, clamp/line wrap raisonnable.\n\n### Tests attendus\n\nApplication (`crates/application/tests/workstate.rs`) :\n- conserve agents/live/busy/tickets même si le preview reader échoue ;\n- déduplique les `conversationId` de plusieurs tickets ;\n- handoff présent produit `ready`, `objectivePreview`, `summaryPreview`, `summaryLen`, `upTo` ;\n- handoff absent + log présent produit `missing` avec `recentTurns` borné ;\n- handoff corrompu + log lisible produit `partial` ;\n- handoff/log indisponibles produit `unavailable` sans `AppError` global ;\n- previews tronquées et whitespace normalisé.\n\nInfrastructure (`crates/infrastructure/tests/conversation_log.rs`) :\n- normalement peu ou pas de nouveau test requis si `FsConversationLog::last` et `FsHandoffStore::load` sont déjà couverts ;\n- ajouter seulement un test si un helper d’adapter dédié apparaît, mais éviter d’en créer un.\n\nTauri DTO (`crates/app-tauri/tests/dto_agents.rs`) :\n- sérialisation camelCase de `conversations` ;\n- conversion des statuts `ready/missing/partial/unavailable` ;\n- compat additive avec agents/tickets existants.\n\nFrontend (`frontend/src/features/workstate/workstate.test.tsx`) :\n- mock sans `conversations` normalisé à `[]` ;\n- ticket avec `conversationId` affiche objectif/résumé ;\n- status `partial`/`unavailable` ne masque pas le ticket ;\n- refresh existant sur `agentBusyChanged` / `delegationReady` continue.\n\nCommandes :\n\n```bash\ncargo test -p application workstate\ncargo test -p app-tauri dto_agents\ncargo test -p infrastructure conversation_log\nnpm --prefix frontend test -- workstate\n```\n\nSelon l’organisation actuelle du workspace, finir par :\n\n```bash\ncargo test\nnpm --prefix frontend test\n```\n\n### Décision de lot\n\nLot C doit être un enrichissement read-only de `ProjectWorkState`, pas une nouvelle surface d’action. Le backend reste source d’autorité pour les bornes et la robustesse ; le frontend ne fait que joindre `tickets[].conversationId` vers `state.conversations[]` et rendre une preview compacte. Ça respecte les contraintes : aucune nouvelle persistance, aucun log brut exposé, best-effort par conversation, et pas de mélange avec les futures actions de gestion ou la mémoire automatique."} diff --git a/.ideai/memory/MEMORY.md b/.ideai/memory/MEMORY.md index 31cf3e6..14f6f65 100644 --- a/.ideai/memory/MEMORY.md +++ b/.ideai/memory/MEMORY.md @@ -14,3 +14,4 @@ - [checkpoint-orchestrator-designation-appimage-build](checkpoint-orchestrator-designation-appimage-build.md) — memory note checkpoint-orchestrator-designation-appimage-build - [checkpoint-blocked-until-appimage-030-restart](checkpoint-blocked-until-appimage-030-restart.md) — memory note checkpoint-blocked-until-appimage-030-restart - [checkpoint-delivery-submit-logging-fix](checkpoint-delivery-submit-logging-fix.md) — memory note checkpoint-delivery-submit-logging-fix +- [checkpoint-workstate-delegation-queue-lot-b](checkpoint-workstate-delegation-queue-lot-b.md) — memory note checkpoint-workstate-delegation-queue-lot-b diff --git a/.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md b/.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md new file mode 100644 index 0000000..a7dc469 --- /dev/null +++ b/.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md @@ -0,0 +1,62 @@ +--- +name: checkpoint-workstate-delegation-queue-lot-b +description: memory note checkpoint-workstate-delegation-queue-lot-b +metadata: + type: project +--- +# Checkpoint — Workstate delegation/queue snapshot Lot B + +Date: 2026-06-20 + +## État final + +Lot B `workstate delegation/queue snapshot` terminé, validé QA et mergé localement dans `develop`. + +Branche finale: +- `develop` @ `64c2c14` — merge local `feature/workstate-delegation-queue`. +- `feature/workstate-delegation-queue` conservée. +- Aucun push, aucune branche supprimée. +- Working tree propre après décision Git. + +## Commits créés + +- `cc7d99a` — `feat(workstate): snapshot des délégations en file par agent (Lot B backend)` + - `QueuedTicketSnapshot` + port read-only `AgentQueueSnapshot`. + - Impl `InMemoryMailbox` snapshot FIFO sans cloner les senders. + - `GetProjectWorkState.agents[].tickets` avec statut dérivé `inProgress/queued`, source `human/agent`, preview bornée. + - DTO Tauri camelCase et wiring `AppState` avec le même mailbox partagé en deux ports. + +- `c600604` — `feat(workstate): UI des délégations en file par agent (Lot B frontend)` + - Types TS `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`. + - Mock gateway normalise `tickets: []`. + - Panneau Work affiche les tickets FIFO, source Human/Agent, preview, ticket court. + - Refresh ajouté sur `delegationReady`. + +- `5cb99fd` — `chore(wip): état runtime .ideai (flux conversation live, layouts)` + - Runtime isolé des commits feature. + +- `64c2c14` — merge local dans `develop`. + +## QA verte + +Commandes QA réelles validées: +- `cargo fmt --all -- --check` OK. +- `cargo test -p infrastructure mailbox --lib` OK, 13 passed. +- `cargo test -p application --test workstate` OK, 12 passed. +- `cargo test -p app-tauri --test dto_agents` OK, 20 passed. +- `cargo check -p app-tauri` OK. +- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 2 files / 17 tests. +- `cd frontend && npx tsc --noEmit` OK. +- QA ajoutée: `cargo test -p domain mailbox --lib` OK, 6 passed. + +Warnings observés uniquement côté Vite sur options `esbuild` dépréciées/ignorées au profit de `oxc`, non bloquants. + +## Décisions produit/techniques + +- Les tickets `human` et `agent` sont inclus dans le snapshot; l'UI distingue explicitement `Human` vs agent/requester au lieu de tout appeler délégation agent. +- Le statut est dérivé à la lecture: `inProgress` si le ticket courant correspond au `busy_state`, sinon `queued`. +- Pas de nouvelle persistance, pas de lecture `log.jsonl`/handoff, pas d'événement `agentQueueChanged`, pas d'action UX d'annulation/résolution. + +## Suite probable + +Le prochain lot logique du chantier UX conversations/délégations est Lot C: résumés de conversations depuis log/handoff en lecture best-effort, ou Lot D actions UX selon priorité produit. Suivre le cycle Main: Architect -> Git -> DevBackend/DevFrontend -> QA -> Git. \ No newline at end of file