--- upTo: fc80726d-b0ec-4747-bea9-a5287ed9526e objective: CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour --- **Objectif :** CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour - **Prompt:** Cadrage architecture — nouvelle feature « Gestion des limites de session des agents » (détection + reprise auto). Étape 1 du cycle §3, AVANT tout code. Produis la cartographie : frontières domaine/application/infra, ports/contrats à créer ou étendre, et l'arborescence des fichiers touchés. Mets aussi à jour ARCHITECTURE.md. CONTEXTE PRODUIT (verrouillé avec l'utilisateur le 2026-06-16) : Besoin : IdeA doit savoir quand un agent est en limite de session ET jusqu'à quelle heure, puis lui demander de reprendre où il en était une fois la limite levée. Priorités : (1) SOLIDE — pas de bidouille, marche dans ~100% des cas même pour un novice ; (2) si possible sans dépendance au modèle de l'agent. CONSTAT DUR à intégrer : l'heure exacte de reset n'existe nulle part de façon universelle (ni OS, ni code de sortie, ni API inter-modèles). Elle est fabriquée par le fournisseur et seulement exposée dans le flux de sa CLI. Donc « 100% fiable + zéro dépendance modèle + heure exacte » sont incompatibles simultanément. SOLUTION RETENUE — détecteur HIÉRARCHIQUE calqué sur la hiérarchie de readiness existante (domain/readiness.rs) : - Niveau 1 (solide, structuré) : l'adapter structuré extrait limite + reset du flux machine. Pour Claude, `rate_limit_event.rate_limit_info` est DÉJÀ parsé dans infrastructure/session/claude.rs (~ligne 90) mais jeté (réduit à Heartbeat) — il suffit de lire le timestamp de reset (resetsAt) au lieu de le dropper. - Niveau 2 (déclaratif, configurable) : champ de profil `rate_limit_pattern` (regex + groupe de capture pour l'heure) pour agents PTY/TUI sans adapter structuré, dans la lignée des profils déclaratifs §9 (domain/profile.rs). - Niveau 3 (filet humain) : si rien ne matche mais agent `Stalled` (variante DÉJÀ prévue dans domain/readiness.rs), IdeA DEMANDE à l'utilisateur. Garantit le « 100% même pour un novice » : jamais d'inaction silencieuse. MODEL-AGNOSTIC tenu AU DOMAINE : le domaine ne connaît que quelque chose comme `RateLimited { until: Option }`. Tout le savoir spécifique modèle reste confiné aux adapters/profils. REPRISE (model-agnostique, briques existantes) : pivot sur le `conversation_id` du moteur + `--resume` natif, déjà câblés (session/claude.rs build_spawn_line + application/agent/resume.rs ListResumableAgents). Le `--resume` porte tout l'historique → pas de reconstruction manuelle. Le SessionInspector (infrastructure/inspector/claude.rs) fournit le « dernier sujet » pour l'UI. Un planificateur arme un réveil sur le port Clock (infrastructure/clock/) à resets_at puis relance via SessionPlan::Resume + prompt de reprise court. DÉCISIONS PRODUIT VERROUILLÉES : - Reprise : AUTOMATIQUE à l'heure de reset, ANNULABLE (fenêtre + notification UI). - Couverture : les TROIS niveaux d'emblée (y compris le repli regex niveau 2). - État : EN MÉMOIRE uniquement — PAS de persistance de SessionLimit. Conséquence assumée : le réveil auto ne joue que tant qu'IdeA reste ouvert ; si l'IDE est fermé/rouvert après le reset, le chemin existant ListResumableAgents (agent_was_running/conversation_id) prend le relais. DÉCOUPAGE PRESSENTI (à valider/affiner par toi) : 1) domaine (variante ReplyEvent::RateLimited, ReadinessSignal::RateLimited, type SessionLimit, état/statut agent RateLimited) ; 2) adapter Claude (extraire resetsAt) ; 3) profil (champ rate_limit_pattern) ; 4) application (service de planification de reprise sur le port Clock, annulable) ; 5) UI front (badge « limité jusqu'à HH:MM » + filet humain de confirmation). Livrable attendu : la cartographie complète (ports à créer/étendre avec leurs signatures, où vit chaque pièce, fichiers à créer/modifier, lots ordonnés pour le cycle dev/test), + ARCHITECTURE.md mis à jour. Signale tout point où ma solution entre en tension avec l'archi hexagonale existante. - **Response:** # Cadrage architecture validé — « Gestion des limites de session des agents » Cartographie produite et **ARCHITECTURE.md mis à jour (nouvelle §21)**. J'ai vérifié ta solution contre le code réel (readiness.rs, session/claude.rs, profile.rs, ports.rs, input.rs, resume.rs, clock, events.rs). Elle est **architecturalement saine** ; je valide les 5 niveaux, avec **5 tensions hexagonales corrigées** (à respecter au dev). ## Frontières (où vit quoi) - **Domaine (pur)** : le fait neutre « limité, reset à T (peut-être) ». Rien de spécifique modèle. - **Infra (adapters)** : forme du `rate_limit_event` Claude, regex d'une TUI, parsing d'heure locale, minuterie. - **Application** : orchestration détecter→planifier→reprendre. - **Présentation** : badge + countdown + Annuler + dialogue filet humain. ## ⚠️ 5 tensions avec l'hexagonal (corrigées dans §21.2 — à lire avant de coder) 1. **T1 — `Instant` rejeté.** Le domaine parle `i64` époche-ms (via `Clock::now_millis`), `Instant` est non sérialisable/monotone/absent. ⇒ **`RateLimited { resets_at_ms: Option }`**. *Déviation assumée de ta proposition.* 2. **T2 — pas de regex dans le domaine.** `prompt_ready_pattern` a été choisi littéral exprès pour éviter la dép `regex`. ⇒ le domaine **stocke la donnée** (`RateLimitPattern{pattern,…}`), le **moteur regex + parsing d'heure vivent en infra** (`regex` ajouté au seul `Cargo.toml` d'`infrastructure`). 3. **T3 — `Clock` ne réveille pas.** Il donne l'heure, pas un timer. ⇒ **1 seul nouveau port `Scheduler`** (arm/cancel), tout le reste réutilise l'existant. 4. **T4 — `RateLimited` non terminal.** Le contrat `ReplyStream` dit « seul `Final` est terminal » et `claude.rs` rompt sur `Final`. ⇒ `RateLimited` s'intercale comme `Heartbeat`. **Point d'intégration** : un tour clos *sans* `Final` *parce que limité* ne doit **pas** devenir `AgentSessionError::Io` — `drain_bounded` doit le traiter en fin gracieuse. 5. **T5 — niveau 3 dépend du lot 2.** `ReadinessSignal::Stalled` est réservé/non produit aujourd'hui. ⇒ **le filet humain (LS6) est gated sur la livraison du lot 2 (stagnation)**. Niveaux 1+2 couvrent déjà structuré + PTY entre-temps. ## Ports à créer / étendre - **NOUVEAU** — `Scheduler` (domaine, `ports.rs`) : `arm(deadline_ms, ScheduledTask) -> ScheduleId` + `cancel(id) -> bool`, minuterie one-shot annulable in-memory ; adapter `TokioScheduler` (infra). `ScheduledTask::ResumeAgent{agent_id,node_id,conversation_id}` = donnée pure (pas de closure traversant la frontière), drain côté application — même motif que le dispatch orchestrateur §14.3. - **ÉTENDUS (aucun autre port neuf)** : `ReplyEvent` +`RateLimited` ; `ReadinessSignal` +`RateLimited` (+`classify`) ; `AgentProfile` +`rate_limit_pattern` ; `EventBus`/`DomainEvent` +5 variantes. Reprise = **réutilise** `AgentSessionFactory::start`/`LaunchAgent` + `SessionPlan::Resume` + `conversation_id` (déjà câblés) — zéro reconstruction. ## Fichiers touchés - **domaine** : `ports.rs`, `readiness.rs`, `profile.rs`, `events.rs`, `lib.rs`, **`session_limit.rs` (NEW : VO `SessionLimit` + fn pure `plan_resume`)**. - **infra** : `session/claude.rs` (lire `resetsAt`→époche-ms au lieu de jeter en Heartbeat, ligne ~90), `session/codex.rs` (si signal Codex), **`ratelimit/` (NEW : `RateLimitParser` regex niveau 2)**, **`scheduler/` (NEW : `TokioScheduler`)**, `clock/` inchangé. - **application** : **`agent/session_limit.rs` (NEW : `SessionLimitService`)**, `agent/structured.rs` (réagir au signal + réconcilier T4), `agent/mod.rs`. - **app-tauri** : composition root (wire scheduler+service+drain), commandes `cancel_agent_resume`/`resume_agent_now`/`confirm_agent_rate_limit`, relais events camelCase. - **frontend** : `features/{agents,terminals}` (badge « limité jusqu'à HH:MM » + countdown + Annuler + dialogue filet humain), gateway TS + adapter + mocks. ## Lots ordonnés (cycle dev/test §3) **LS1** domaine (variantes + VO + `plan_resume` + profil, tests purs) → **LS2** adapter Claude niv.1 ∥ **LS3** port `Scheduler`+`TokioScheduler` → **LS4** `SessionLimitService` + réconciliation T4 → **LS5** parser regex niv.2 → **LS6** filet humain niv.3 *(gate : lot 2 stagnation livré)* → **LS7** app-tauri → **LS8** frontend. **LS1+LS2+LS4 = niveau 1 Claude de bout en bout** : valeur atteinte tôt. ## Confirmé sain - **In-mémoire seul** : aucun store, aucun schéma `.ideai/` modifié. IDE rouvert après reset ⇒ le chemin existant `ListResumableAgents` reprend (popup, pas d'auto). Cohérent. - **Auto + annulable** = `Scheduler::arm`/`cancel` + events `AgentResumeScheduled`/`AgentResumeCancelled`. ## Spikes (§21.10) Format réel de `resetsAt` (époch/ISO/relatif) ; heure locale→époche + passage de minuit (niv.2) ; signal de limite côté Codex ; dédoublonnage si niv.1 **et** niv.2 matchent le même épisode (1 `SessionLimit` vivante/agent). **Prochaine étape du cycle** : passer la main à **Git** pour décider de la branche (`feature/session-limits` ?) avant que le binôme dev/test attaque LS1. - **Prompt:** Feature session-limits — cadrage du lot LS7 (câblage au composition root). Project root: /home/anthony/Documents/Projects/IdeA. État : LS1→LS6 committés. Tout le mécanisme existe mais n'est PAS branché dans app-tauri : `application::agent::session_limit::SessionLimitService` (ports injectés : Clock, Scheduler, EventBus, AgentResumer) n'est référencé nulle part dans le composition root → aucun `DomainEvent::AgentRateLimited/ResumeScheduled/Resumed/RateLimitSuspected` n'est jamais émis, aucune reprise armée. LS7 doit câbler au composition root (app-tauri), conformément à ARCHITECTURE §21. J'ai besoin d'une carte de câblage précise (PAS de code), répondant à ces points, en nommant les fichiers/structs/fonctions exacts du repo où chaque tap se branche : 1. **Instanciation du service** : où, dans app-tauri (state.rs ? di/composition root ?), instancier `SessionLimitService::new(clock, scheduler, events, resumer)`. Quel `Scheduler` concret (TokioScheduler déjà en infra), quel EventBus (TokioBroadcastEventBus partagé), quel Clock. Cycle de vie/partage (Arc) cohérent avec l'existant. 2. **Port AgentResumer → LaunchAgent** : comment implémenter `AgentResumer::resume(agent_id, node_id, conversation_id, resume_prompt)` par-dessus le mécanisme de lancement existant (`LaunchAgent` + `AgentSessionFactory` + `SessionPlan::Resume`). Où vit ce code (un adapter app-tauri ?), et quelles dépendances il capture. Référence : les passerelles voisines `HandoffProvider`/`ProviderSessionProvider` mentionnées dans session_limit.rs. 3. **Tap détection niveau 1 (structuré)** : où le flux `ReplyStream`/`ReplyEvent` des sessions structurées est consommé aujourd'hui (le « pump » qui draine les tours — cf. chat.rs `chunk_from_event`), et comment y intercepter `ReplyEvent::RateLimited{resets_at_ms}` pour appeler `service.on_rate_limited(agent_id, node_id, conversation_id, resets_at_ms)`. Comment récupérer node_id + conversation_id à ce point. 4. **Tap détection niveau 2 (PTY)** : où la sortie PTY des agents non-structurés est drainée, et où instancier un `infrastructure::RateLimitParser` (règle de sélection `ratelimit::applies(profile)`) pour le nourrir des fragments et router un `SessionLimit` détecté vers le service. Respect de l'anti-double-détection §21.10-4. 5. **Drain du Scheduler** : comment `TokioScheduler` remet les `ScheduledTask::ResumeAgent` échus (canal de remise) et où câbler le récepteur qui appelle `service.execute_resume(task)` sur le runtime Tokio. 6. **Commande Tauri d'annulation** : exposer `cancel_resume(agent_id)` comme `#[tauri::command]` (la fenêtre annulable côté UI l'appellera en LS8) — où l'enregistrer dans le `generate_handler!`. Donne aussi : ordre d'implémentation recommandé, points de fragilité/race à surveiller pour QA, et confirme que rien ne viole l'hexagonal (domaine pur, regex/parse confinés infra). Rends via idea_reply. - **Response:** # LS7 — Carte de câblage « session-limits » au composition root Tous les chemins sont relatifs au project root. Aucun code ici : seulement les points d'ancrage exacts (fichier · struct/fn · ligne). Faits vérifiés en amont : - Service : `crates/application/src/agent/session_limit.rs` — `SessionLimitService::new(clock, scheduler, events, resumer)` ; API publique : `on_rate_limited(agent_id, node_id, conversation_id, resets_at_ms)`, `execute_resume(task).await -> Result<(),AppError>`, `cancel_resume(agent_id) -> bool`. Port à implémenter : `AgentResumer::resume(agent_id, node_id, conversation_id, resume_prompt)` (async). - Scheduler : `crates/infrastructure/src/scheduler/mod.rs` — `TokioScheduler::new(tx: UnboundedSender, clock: Arc)`. Réexporté `infrastructure::TokioScheduler`. Il **pousse** la `ScheduledTask` échue dans `tx` ; le drain détient `rx`. - Parser N2 : `crates/infrastructure/src/ratelimit/mod.rs` — `RateLimitParser::new(&RateLimitPattern) -> Option`, `detect(text, now_ms) -> Option`, et la règle de sélection `ratelimit::applies(profile) -> bool` (= `structured_adapter.is_none() && rate_limit_pattern.is_some()`). Réexportés `infrastructure::RateLimitParser` / `infrastructure::ratelimit::applies`. - LS6 a déjà câblé les 6 variantes `DomainEvent::Agent{RateLimited,ResumeScheduled,ResumeCancelled,Resumed,RateLimitSuspected}` → DTO → relay (`crates/app-tauri/src/events.rs:208-253, 426-452`). Rien à faire côté event wire. --- ## 1. Instanciation du service (composition root) **Fichier : `crates/app-tauri/src/state.rs`, dans `AppState::build` (à partir de la ligne 355).** Réutiliser les adapters déjà construits en tête de `build` : - `clock` (l.358, `SystemClock`, implémente `Clock::now_millis`) → caster en `Arc` comme partout (cf. `Arc::clone(&clock) as Arc`). - `event_bus` (l.357) → `events_port` (l.369, `Arc`), le **même** bus partagé que tout le reste (donc l'`AgentRateLimited` émis passera par `spawn_relay`). Séquence d'instanciation (à placer après la construction de `launch_agent` l.668-700 et de `project_store` l.750, car le resumer en dépend ; idéalement juste avant le bloc orchestrateur l.917) : 1. `let (resume_tx, resume_rx) = tokio::sync::mpsc::unbounded_channel::();` 2. `let scheduler = Arc::new(TokioScheduler::new(resume_tx, Arc::clone(&clock) as Arc)) as Arc;` 3. `let resumer = Arc::new(AppAgentResumer::new(Arc::clone(&launch_agent), Arc::clone(&store_port), )) as Arc;` (cf. §2). 4. `let session_limit_service = Arc::new(SessionLimitService::new(Arc::clone(&clock) as Arc, scheduler, Arc::clone(&events_port), resumer));` **Cycle de vie / partage :** ajouter un champ `pub session_limit_service: Arc` à la struct `AppState` (déclaration vers l.326, à côté de `orchestrator_service`) et le renvoyer dans le littéral final (l.967-1055). Le `Arc` est partagé par : (a) la commande `cancel_resume` (§6), (b) la tâche de drain (§5), (c) les taps de détection N1/N2 (§3/§4) qui appellent `on_rate_limited`. Le `resume_rx` n'entre **pas** dans `AppState` : il est *moved* dans la tâche de drain spawné à l'intérieur de `build` (§5), exactement comme `sweep_stalled` (l.899-911). Imports à ajouter en tête de `state.rs` : `application::{SessionLimitService, AgentResumer}`, `domain::ports::{Scheduler, ScheduledTask}`, `infrastructure::TokioScheduler`. --- ## 2. Port `AgentResumer` → `LaunchAgent` (adapter app-tauri) **Nouvel adapter dans `crates/app-tauri/src/state.rs`**, à côté des passerelles stateless existantes `AppHandoffProvider` (l.100-109) / `AppProviderSessionProvider` (l.119-128) / `AppRecordTurnProvider` (l.81-90) — même patron `impl application::Trait for AppXxx`. `impl application::AgentResumer for AppAgentResumer { async fn resume(agent_id, node_id, conversation_id, resume_prompt) -> Result<(), AppError> }` recompose un `LaunchAgentInput` et appelle `self.launch_agent.execute(...)` (le **même** `Arc` que la commande `launch_agent`, l.1140). C'est `LaunchAgent` qui, via `with_handoff_provider` / `with_provider_session_provider` (l.687-695) et le routage §17.4, applique déjà `SessionPlan::Resume` quand un `conversation_id` est présent (chemin de reprise P7/P8b/§15). Le `resume_prompt` (constante `application::RESUME_PROMPT`) est le premier tour — pour le chemin **PTY natif** (composition B-2, voir l.645-653), il devra être écrit dans le PTY après spawn ; pour le chemin structuré (dormant), passé en premier `send`. **À cadrer dev :** où injecter ce premier tour. La voie la plus cohérente avec l'existant est de réutiliser le médiateur d'entrée (`MediatedInbox`/portail d'écriture PTY, l.883-892) plutôt qu'un write direct. **⚠️ Point dur de conception (à trancher, c'est LE risque du lot) :** `AgentResumer::resume` et `ScheduledTask::ResumeAgent` ne portent **pas** de `project_id`, alors que `LaunchAgentInput` exige un `Project` complet + `rows`/`cols` + `mcp_runtime` (cf. commande l.1126-1152). Le service est volontairement model/projet-agnostique. Il faut donc que l'adapter **résolve le `Project` à partir de l'`agent_id`**. Recommandation : `AppAgentResumer` détient un `Arc>>` (`ResumeContext = { project: Project, rows: u16, cols: u16 }`) **alimenté par le chemin de lancement** (commande `launch_agent`, l.1140, où `project`/`rows`/`cols` sont en main) et lu au moment du resume. Le `mcp_runtime` est **recalculé** dans `resume` à partir de `project.id` via `crate::mcp_endpoint::{idea_exe_path, mcp_endpoint}` (recette identique à l.1126-1133). Évite un scan coûteux de `project_store.list_projects` + manifestes. `store_port` reste injecté en repli (résolution si le contexte est absent après un restart — cohérent avec « état en mémoire only » : après restart c'est `ListResumableAgents` qui reprend, pas le scheduler). Dépendances capturées par l'adapter : `Arc`, `Arc` (repli), le registre `ResumeContext` partagé. --- ## 3. Tap détection niveau 1 (structuré) **Fichier : `crates/app-tauri/src/commands.rs`, fn `agent_send`, boucle de pump l.1283-1294.** C'est le seul drain actif d'un `ReplyStream` structuré (`for event in stream { ... }`). Aujourd'hui `chunk_from_event` (`crates/app-tauri/src/chat.rs:186-193`) **mappe `ReplyEvent::RateLimited` → None** (jeté). Le tap : **avant** d'appeler `chunk_from_event`, faire un `if let ReplyEvent::RateLimited { resets_at_ms } = &event { service.on_rate_limited(agent_id, node_id, conversation_id, *resets_at_ms); }`, puis continuer le drain normalement (l'event reste non-terminal, le tour continue jusqu'au `Final`). **Récupération de `node_id` + `agent_id`** : le pump ne connaît que `sid: SessionId`. Le registre `state.structured_sessions` (`crates/application/src/terminal/registry.rs:297`) mappe `SessionId → (agent_id, node_id)` — mais il manque un accès **par session_id**. Ajouter une petite méthode `meta_for_session(&SessionId) -> Option<(AgentId, NodeId)>` sur `StructuredSessions` (jumeau trivial de `live_agents` l.388, lookup direct dans `entries`). `conversation_id` : `StructuredEntry` ne le porte pas ; passer `None` (le resume dégrade proprement sans id, contrat `ScheduledTask.conversation_id: Option`) **ou**, plus précis, le lire via `providers.json` (passerelle `AppProviderSessionProvider`) — optionnel, `None` est acceptable pour LS7. **État actuel : ce tap est DORMANT.** En composition B-2 la fabrique structurée est décâblée (`launch_agent` retombe toujours sur PTY, l.645-653 ; orchestrateur sans `.with_structured`, l.947-958). Aucun agent n'a de session structurée vivante ⇒ `agent_send` n'est jamais drainé. Le câbler quand même = robustesse forward (réactivation structurée). La détection réelle aujourd'hui passe **exclusivement par le niveau 2**. Passage du service au pump : `agent_send` a `state: State` ⇒ `Arc::clone(&state.session_limit_service)` avant le `thread::spawn` (l.1282) et le *move* dans le thread. --- ## 4. Tap détection niveau 2 (PTY) **Fichier : `crates/app-tauri/src/commands.rs`, fn `launch_agent`, branche PTY l.1165-1190** (le `if output.structured.is_none()` + le `thread::spawn` du pump d'octets l.1176-1183). C'est le drain de la sortie des agents non-structurés — donc le chemin **actif** en B-2. Câblage : 1. **Sélection (anti-double-détection §21.10-4)** : avant d'armer un parser, résoudre le `AgentProfile` de l'agent et appeler `infrastructure::ratelimit::applies(&profile)`. `applies` impose déjà `structured_adapter.is_none()` (or on est dans la branche `output.structured.is_none()` ⇒ cohérent) **et** `rate_limit_pattern.is_some()`. **Besoin de wiring** : la commande `launch_agent` ne charge pas le profil. Deux options — (a) exposer le `AgentProfile` (ou au minimum le `RateLimitPattern`) résolu sur `LaunchAgentOutput` (`LaunchAgent::execute` le résout déjà en interne — le plus propre, zéro I/O en plus) ; (b) le relire via le profile store. Recommandation : (a). 2. **Instanciation** : `RateLimitParser::new(&pattern)` (retourne `Option` ⇒ regex invalide = pas de détecteur, jamais de panique). À construire **une fois par lancement**, déplacé dans le thread de pump. 3. **Alimentation** : dans la boucle `for chunk in stream` (l.1177), après `send_output`, décoder le fragment (`String::from_utf8_lossy`) et appeler `parser.detect(&text, clock.now_millis())`. Sur `Some(SessionLimit)` ⇒ `service.on_rate_limited(agent_id, node_id, conversation_id, limit.resets_at_ms)`. Ici `agent_id`, `node_id` (l.1115) et `conversation_id` (`request.conversation_id`, l.1150) sont **déjà en main** dans la commande ⇒ les cloner avant le `thread::spawn`. Idem `Arc::clone(&state.session_limit_service)` et un `Arc` (ajouter un champ `clock` à `AppState`, ou réutiliser `SystemMillisClock`). **Anti-double-détection** : garantie par construction — `applies` ne renvoie `true` que pour les agents **sans** adapter structuré ; un agent structuré (N1) n'arme jamais de parser N2. Un seul tap actif par agent. **Fragilité fragmentation** : `detect` reçoit des fragments PTY ; un motif peut être coupé entre deux chunks. Pour LS7, accepter la détection best-effort par fragment (les bannières de limite des CLI arrivent en général d'un bloc). Si QA observe des ratés, prévoir un petit buffer glissant borné (dernières ~4 Kio) côté thread — **note pour QA, pas bloquant**. --- ## 5. Drain du Scheduler `TokioScheduler` (`crates/infrastructure/src/scheduler/mod.rs:62-85`) pousse la `ScheduledTask` échue dans son `tx` (canal `mpsc` non borné). Le récepteur `resume_rx` (créé en §1) est **drainé dans une tâche détachée spawné à l'intérieur de `AppState::build`**, sur le **patron exact** du sweeper `sweep_stalled` (`crates/app-tauri/src/state.rs:899-911`) — `tauri::async_runtime::spawn` (et **pas** `tokio::spawn` : `build` tourne dans le hook `setup` sans runtime ambiant, cf. commentaire l.901-903). Boucle : `while let Some(task) = resume_rx.recv().await { if let Err(e) = service.execute_resume(task).await { /* log best-effort */ } }`. Le `Arc` et `resume_rx` sont *moved* dans la closure. `execute_resume` désarme l'entrée puis appelle `AgentResumer::resume` puis publie `AgentResumed` (déjà relayé par LS6). La tâche vit autant que l'app (le canal se ferme au drop du `TokioScheduler`/AppState). --- ## 6. Commande Tauri `cancel_resume` **Déclaration** : `crates/app-tauri/src/commands.rs` — nouvelle `#[tauri::command] pub async fn cancel_resume(agent_id: String, state: State<'_, AppState>) -> Result` : `let id = parse_agent_id(&agent_id)?; Ok(state.session_limit_service.cancel_resume(id))`. (`parse_agent_id` existe déjà, cf. l.1111/1215.) Retourne le `bool` (true = reprise effectivement annulée ; false = rien d'armé ou « cancel pile au tir » — voir doc service l.188-213). **Enregistrement** : `crates/app-tauri/src/lib.rs`, dans `tauri::generate_handler![ … ]` (l.122) — ajouter `commands::cancel_resume,` à côté des commandes agent (p.ex. après `commands::change_agent_profile`, l.166). LS8 (UI fenêtre annulable) l'appellera via `invoke("cancel_resume", { agentId })`. --- ## Ordre d'implémentation recommandé 1. **Service + scheduler + drain** (§1 + §5) avec un `AgentResumer` *stub* (renvoie `Ok(())`) ⇒ valide que `on_rate_limited` arme, que le canal remet la tâche, que `execute_resume`/`AgentResumed` partent. Testable sans toucher aux pumps. 2. **`cancel_resume`** (§6) ⇒ boucle détecter→annuler bout-en-bout vérifiable manuellement (events au front via LS6). 3. **Tap N2 PTY** (§4) — chemin actif ⇒ première vraie détection. Implique d'exposer le `RateLimitPattern`/`AgentProfile` sur `LaunchAgentOutput`. 4. **`AppAgentResumer` réel** (§2) + registre `ResumeContext` alimenté par `launch_agent` ⇒ reprise réelle. 5. **Tap N1 structuré** (§3) + `meta_for_session` ⇒ forward-compat (dormant), à câbler en dernier. ## Points de fragilité / race pour QA - **Résolution `agent_id → Project` au resume** (§2) : le gros risque. Vérifier qu'un resume après fermeture/réouverture de projet ne plante pas (repli `None`/erreur propre, jamais de panique). Tester resume quand le `ResumeContext` est absent. - **Cancel « pile au tir »** (service l.188-213, scheduler `cancel` l.87-100) : sous runtime multi-thread, `cancel` peut renvoyer `false` car la tâche vient de tirer ⇒ **pas** d'`AgentResumeCancelled`, la reprise suit son cours. UI LS8 doit tolérer un `false` (la reprise arrive quand même). Test de course explicite recommandé. - **Rafraîchissement (dédoublonnage §21.10-4)** : deux signaux de limite rapprochés pour le même agent ⇒ `disarm` puis ré-arm ; vérifier qu'on n'empile pas deux `ScheduledTask` et qu'un seul `AgentResumed` sort. - **Fragmentation PTY** (§4) : motif coupé entre chunks (best-effort en LS7, buffer glissant en repli si raté). - **Injection du `resume_prompt` en PTY** (§2) : timing d'écriture après spawn (la CLI doit être prête) — réutiliser le portail d'écriture médié plutôt qu'un write brut. - **Premier tour différé / cold-start** : interaction avec `release_agent_cold_start` (l.1132-1137) si le resume relance un agent dont le pont MCP n'est pas encore connecté. ## Conformité hexagonale — confirmée - **Domaine pur** : `SessionLimit`/`plan_resume`/`RateLimitPattern`/`ScheduledTask`/`Scheduler`/`Clock`/`EventBus` ne portent aucune I/O ni regex. ✅ - **Regex + parsing d'heure confinés infra** : `RateLimitParser` et `timeparse` vivent dans `crates/infrastructure/src/ratelimit|timeparse` ; ils produisent une **valeur domaine** (`SessionLimit`). Aucune `regex` ne franchit la frontière. ✅ - **Service applicatif pur-ports** : `SessionLimitService` ne dépend que de traits (`Clock`/`Scheduler`/`EventBus`/`AgentResumer`). ✅ - **Composition root seul à connaître le concret** : `TokioScheduler`, `RateLimitParser`, `AppAgentResumer`, le canal `mpsc` et les taps sont **tous** dans `app-tauri` (state.rs/commands.rs/lib.rs), jamais ailleurs. `AgentResumer` est un port applicatif implémenté à la racine, exactement comme `HandoffProvider`/`ProviderSessionProvider`. ✅ Aucun nouveau port domaine, aucun nouvel adapter infra : LS7 est purement du **câblage** (composition de l'existant), conforme à ARCHITECTURE §21 et au principe « zéro nouveau port/adapter » du lot. - **Prompt:** Arbitrage de scope sur le filet humain NIVEAU 3 de la feature limites de session (ARCHITECTURE §21.1 / §21 « niveau 3 »). Contexte : backend (commit 9df5923) + front (en cours, vert) de la gestion des limites de session sont posés. Niveaux 1 (structuré) et 2 (regex PTY) : pleinement fonctionnels (détecter→planifier reprise auto annulable→reprendre). Le front consomme les 5 DOMAIN_EVENT, affiche le badge « limité jusqu'à HH:MM », le compte à rebours + bouton « Annuler la reprise », et appelle la commande Tauri `cancel_resume`. GAP constaté sur le niveau 3 : l'événement `agentRateLimitSuspected` (limite suspectée SANS heure de reset fiable) est bien émis et affiché côté front (note « limité · heure inconnue — reprise à préciser »), MAIS il n'existe AUCUNE commande backend pour que l'utilisateur SAISISSE l'heure de reprise et déclenche une reprise auto à cet instant. DevFrontend a (correctement) refusé de fabriquer une commande inexistante et a posé un TODO. Donc aujourd'hui, sur niveau 3, l'utilisateur est INFORMÉ mais ne peut pas AGIR via le mécanisme de reprise auto — il lui reste seulement le relancement manuel de l'agent par les contrôles existants. Le design verrouillé (mémoire session-limit-handling-design) dit : niveau 3 = « filet humain : IdeA DEMANDE à l'utilisateur. Garantit le 100% même pour un novice : jamais d'inaction silencieuse » ; et « Couverture : les TROIS niveaux d'emblée ». QUESTION (tranche, c'est ton domaine — contrat/frontière) : A) Le niveau 3 est-il considéré COMPLET en l'état (surfacer la suspicion + s'appuyer sur le relancement manuel existant satisfait « jamais d'inaction silencieuse ») → alors la feature est terminée, on merge. OU B) Le niveau 3 EXIGE une boucle d'action : une commande applicative type `set_resume_at(agentId, epochMs)` qui arme le même `SessionLimitService` (planifier une reprise à l'instant saisi, annulable comme les autres) + un formulaire de saisie d'heure côté front. → alors c'est une slice supplémentaire LSx (domaine? non / application: méthode service + port? / app-tauri: commande / front: form) que je dois cadrer avant le merge. Si B), précise le découpage minimal et le contrat exact de la nouvelle entrée (signature commande, où elle branche dans SessionLimitService, événements émis — réutilise-t-on AgentResumeScheduled ?). Réponds de façon décisionnelle. - **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. - **Prompt:** Cadrage architecture obligatoire avant implémentation. Contexte produit : nous voulons ajouter à IdeA une gestion automatique de la croissance des conversations d'agents afin de réduire les coûts tokens. Le système existant a déjà : log canonique `.ideai/conversations/.../log.jsonl`, handoff incrémental `handoff.md`, injection du handoff au lancement/relaunch, conversation_id logique de paire IdeA, provider session store, reprise session provider quand possible. Besoin : mettre en place une compression/rotation automatique des conversations d'agents, mais avec stabilité maximale. Contraintes fortes validées avec l'utilisateur : - Ne jamais couper un agent au milieu du développement d'une feature. - Ne jamais interrompre un tour en cours, outil en cours, terminal occupé, délégation pendante, ou attente de `idea_reply`. - Ne pas casser les contacts inter-agents : les messages doivent cibler l'identité logique AgentId/ConversationId, pas une session physique instable. - Si un autre agent essaie de contacter un agent en rotation, le routage doit être stable : queue, attente ou ancienne session jusqu'à bascule sûre. - La rotation doit être opportuniste, non interruptive : au seuil normal, préparer/planifier au prochain point sûr ; seuil critique = signaler/attendre point sûr, pas tuer au milieu. - Attention au detach/attach de l'agent sur la cellule : la cellule est une vue, pas la session. - Ne jamais faire de rotation automatique si l'utilisateur est en train d'écrire dans la cellule : focus, buffer d'entrée non envoyé, frappe récente, IME/composition, prompt interactif si détectable. - Nouvelle session idéalement démarrée détachée/background, vérifiée prête, puis bascule atomique du routage + attachement cellule. - Checkpoint durable obligatoire avant bascule : log canonique append OK + handoff à jour. Demande : 1. Lire l'architecture et le code existant autour agent lifecycle, terminal sessions, orchestrator routing, conversation log/handoff, provider sessions, layout/cell attach. 2. Proposer un design hexagonal/SOLID précis : domaine, ports éventuels, use cases, adapters, events, UI state. 3. Découper en lots implémentables DevBackend/DevFrontend/QA, avec tests attendus. 4. Identifier les pièces déjà existantes à réutiliser et les risques. 5. Ne pas coder. Rends un rapport utilisable directement par DevBackend/DevFrontend/QA. - **Prompt:** Contexte bug IdeA: une délégation inter-agent longue envoyée à un agent Codex TUI arrive visiblement tronquée/partielle dans le terminal cible. Code actuel: frontend `frontend/src/features/terminals/useWritePortal.ts` écrit `head.text` en un seul `handle.write(encoder.encode(head.text))`, attend `submitDelayMs` puis écrit `\r`; chemin headless `crates/infrastructure/src/input/mod.rs::make_headless_sink` écrit aussi `text.as_bytes()` d'un coup puis submit. `makeTerminalHandle` sérialise déjà les invokes et `PortablePtyAdapter::write` fait `write_all+flush`. Hypothèse: le TUI/paste handling ou IPC/PTY avec gros bloc peut perdre/ignorer une fin avant Enter. Proposition de correction locale: introduire un envoi chunké pour les injections de délégation (front et headless), avec chunks UTF-8 safe, pause courte entre chunks, puis délai anti-paste existant avant submit; ack seulement après tous les chunks + submit. Ajouter tests `useWritePortal` qui vérifient texte long chunké en ordre et ack après submit; tests infra input headless si simple. Est-ce compatible avec l'architecture actuelle (single writer, submit config, hexagonal), et vois-tu un meilleur point de correction ? Réponds court avec risques/contraintes. - **Prompt:** Validation rapide architecture pour correctif IdeA: le runtime structuré Codex lance `codex exec` depuis le run dir isolé et doit permettre l’écriture dans le project root quand les permissions résolues donnent workspace-write. Proposition: faire transiter `PreparedContext.project_root`/équivalent depuis `StructuredSessionFactory::start` vers `CodexExecSession`, puis ajouter `--add-dir ` à la ligne `codex exec` en workspace-write, sans ajouter `--ask-for-approval` car `codex exec` le refuse. Est-ce conforme Ports & Adapters/hexagonal ? Signale toute frontière à ne pas franchir. - **Response:** Oui, c’est conforme à l’architecture Ports & Adapters, avec une réserve de frontière importante. Validation : - Faire porter `project_root` dans un DTO/VO de contexte préparé (`PreparedContext.project_root` ou équivalent) est acceptable si cette donnée représente le périmètre métier d’exécution du projet, pas un détail Tauri/Codex. - `StructuredSessionFactory::start` peut transmettre cette donnée vers l’adapter runtime concret : c’est précisément le rôle de la couche application/composition d’orchestrer les ports avec le contexte résolu. - Ajouter `--add-dir ` dans `CodexExecSession` est au bon endroit si `CodexExecSession` est l’adapter infrastructure/presentation-backend spécifique à Codex CLI. Le flag `--add-dir` est un détail technique de l’adapter Codex, donc il ne doit pas remonter dans le domaine. - Conditionner cet ajout à `workspace-write` est cohérent : l’intention métier est “la session peut écrire dans le workspace résolu”, l’implémentation Codex est “ajouter le root autorisé à la sandbox CLI”. - Ne pas ajouter `--ask-for-approval` est correct si `codex exec` le refuse. Il ne faut pas forcer une option non supportée au niveau adapter. Frontières à ne pas franchir : - Ne pas faire dépendre le domaine/application de `codex exec`, `--add-dir`, `--sandbox`, ni des noms de flags Codex. Ces détails restent dans l’adapter Codex. - Ne pas laisser `CodexExecSession` recalculer seul le project root à partir du run dir isolé si le project root a déjà été résolu par les couches supérieures. L’adapter doit consommer un contexte préparé, pas réinterpréter l’état global. - Ne pas élargir systématiquement les permissions : `--add-dir` doit être ajouté uniquement pour le `projectRoot` validé/résolu du projet et uniquement quand le mode effectif est `workspace-write`. - Ne pas confondre `cwd` et périmètre d’écriture : le cwd peut rester le run dir isolé pour l’exécution/session, tandis que `project_root` devient une racine autorisée explicite. - Ne pas exposer ce comportement côté React/Tauri commands comme une option utilisateur brute. L’UI demande un lancement d’agent/session ; la politique de sandbox reste dans les services/adapters backend. Formulation recommandée côté modèle : plutôt qu’un champ trop Codex-spécifique, utiliser un nom neutre comme `workspace_roots`, `writable_roots`, `project_root`, ou `ExecutionScope { cwd, project_root, sandbox_mode }`. Puis l’adapter Codex traduit `ExecutionScope + workspace-write` en `codex exec --add-dir `. - **Prompt:** Contexte: l'utilisateur veut terminer les chantiers ouverts d'IdeA, en autonomie, via le cycle Main -> Architect -> Git -> Dev -> QA -> Git. Main ne code pas. Decision Git deja obtenue: - Branche courante: `feature/orchestrator-designation`. - Worktree dirty important (~75 fichiers) mais analyse Git: mono-theme = chantier orchestrateur/designation + nouveau sink diagnostic `crates/application/src/diag.rs` pour le rendez-vous inter-agents. - Ne pas stash/switch. Premier chantier logique: finir `orchestrator-designation` sur cette branche, puis QA, puis Git commit/merge develop. Etat memoire projet pertinent: - Chantiers restants: agent=session vivante coherente, persistance conversationnelle, handoff cross-profile, live-state partage, refresh memoire/contexte a chaud, UX delegations/conversations, documentation architecture. - Le chantier actuel semble etre une brique d'orchestration/designation, avec modifications dans domain/application/infrastructure/app-tauri/frontend terminal write portal et permissions/sandbox/session structured. Ta mission Architect: 1. Cadre le chantier courant `orchestrator-designation` a partir de son nom et de l'etat Git ci-dessus: objectif architectural probable, frontieres de couches, contrats/ports/DTO a verifier, invariants attendus. 2. Donne a Main un plan de delegation concret: ce que DevBackend doit verifier/finaliser, ce que DevFrontend doit verifier/finaliser, ce que QA doit tester. 3. Donne des criteres de fin stricts pour considerer ce chantier ferme. 4. Indique s'il faut mettre a jour `architect.md` maintenant ou attendre la fin de la stabilisation. Ne code pas. Reponds avec `idea_reply` si cette tache est deleguee via IdeA. - **Prompt:** Tu es sollicité par Main pour reprendre après le checkpoint AppImage 0.3.0. Contexte utile: le chantier `orchestrator-designation` est intégré localement sur `develop` (develop ahead origin, AppImage 0.3.0 produite). Le prochain chantier recommandé par Git est `feature/agent-skill-awareness`, mais il faut d'abord trancher son chevauchement avec une branche/initiative `feature/agent-skills` mentionnée dans les notes. Merci de cadrer précisément: 1) objectif produit/technique de `agent-skill-awareness`, 2) différence avec `agent-skills` si visible depuis le repo ou les notes, 3) frontières backend/frontend/docs, 4) contrats/ports/DTO à créer ou modifier, 5) risques et ordre d'implémentation. Ne code pas; donne un cadrage actionnable pour Git puis DevBackend/DevFrontend/QA. - **Response:** ## Cadrage `feature/agent-skill-awareness` État repo constaté : `develop` est ahead origin et contient déjà L12 `agent-skills` dans le code courant (`Skill`, `SkillStore`, CRUD, assignation agent↔skill, injection des bodies dans le convention file). La branche `feature/agent-skill-awareness` diverge avant l’intégration `orchestrator-designation`; son diff est très large et mêlé à des changements runtime/anciens. Recommandation Git : **ne pas merger la branche brute**. Rejouer sélectivement les contrats utiles sur une branche neuve depuis `develop`. ## 1) Objectif produit/technique Objectif produit : un agent doit savoir qu’il possède des skills assignés et comment les utiliser, sans que l’utilisateur doive le lui rappeler. Aujourd’hui L12 injecte le contenu complet des skills dans `# Skills`, mais c’est bas dans le contexte, lu comme documentation, et pas présenté comme affordance/action disponible. Objectif technique minimal : transformer l’injection “dump de contenu” en modèle “à la MCP” pour les profils MCP : - section haute altitude `# Skills disponibles`, juste après l’orchestration/capacités IdeA, listant les skills assignés par `name + description` ; - outil MCP read-only `idea_skill_read(name)` pour charger le body complet à la demande ; - champ optionnel `description` sur `Skill`, avec fallback déterministe via `effective_description()` ; - rétrocompat des anciens `skills/index.json` sans `description` ; - conserver un mode non-MCP compatible. Point à trancher avec Architecture : soit garder l’ancien dump complet pour `mcp == None`, soit intégrer cette découverte au futur binaire universel `idea`/skill built-in. Ne pas mélanger avec la refonte C-univ complète. La note de branche ajoute aussi un brief inconditionnel “capacités IdeA” dans `compose_convention_file`. Attention : `ARCHITECTURE.md` récent pousse une décision plus structurante : remplacer la prose libre d’orchestration par un **skill built-in `Orchestration IdeA`** (`SkillScope::Builtin`). Donc, pour ce chantier, je recommande de **ne pas reprendre tel quel le brief inconditionnel** sauf comme correctif transitoire validé par Architecture. Le périmètre propre de `agent-skill-awareness` doit rester : “découverte et lecture des skills assignés”. ## 2) Différence avec `feature/agent-skills` `agent-skills` visible dans le repo = L12, déjà intégré fonctionnellement : - domaine : `Skill`, `SkillScope::{Global, Project}`, `SkillRef`, `Agent.skills`/`ManifestEntry.skills` ; - port : `SkillStore` ; - infra : `FsSkillStore` global + projet ; - application : `CreateSkill`, `UpdateSkill`, `ListSkills`, `DeleteSkill`, `AssignSkillToAgent`, `UnassignSkillFromAgent` ; - backend Tauri + DTO ; - frontend : `features/skills`, `SkillGateway`, assignation dans `AgentsPanel` ; - lancement : `LaunchAgent` résout les skills assignés et injecte leurs bodies dans `compose_convention_file`. `agent-skill-awareness` = couche par-dessus L12 : - rend les skills visibles comme capacités nommées ; - évite de forcer le body complet en contexte MCP ; - expose une lecture explicite du body via `idea_skill_read`; - ajoute `description` comme méta courte, pas une nouvelle famille de skills. Donc ce n’est pas un doublon de `agent-skills`; c’est une amélioration d’ergonomie runtime. Mais son ancienne branche embarque des changements qui chevauchent des zones modifiées depuis, donc reprise manuelle. ## 3) Frontières backend/frontend/docs Backend Domaine : - ajouter `description: Option` à `Skill` avec `#[serde(default)]` ; - ajouter `Skill::with_description` et `Skill::effective_description()` ; - préserver `with_content()` en conservant la description ; - éventuellement ajouter `OrchestratorCommand::ReadSkill { name, requester? }` et action `skill.read` dans `OrchestratorRequest::validate` si on garde le routage MCP via modèle orchestrator. Backend Application : - `CreateSkillInput` reçoit `description: Option` ; - `UpdateSkill` doit idéalement pouvoir modifier `description` aussi, pas seulement `content`, sinon le frontend ne peut pas éditer la méta ; - créer `ReadSkill` use case read-only sur le port existant `SkillStore`, pas de nouveau port ; - résolution par nom : project scope d’abord, global ensuite ; ambiguïté dans un même scope = erreur typée ; absent = not found ; case-insensitive. Backend Lifecycle : - modifier `compose_convention_file` pour les profils MCP : section `# Skills disponibles` en amont, lignes déterministes `**name** — description`, mentionnant `idea_skill_read(name=...)` ; - garder ordre déterministe des skills assignés selon le manifest ; - décider comportement non-MCP. Option conservatrice : garder le dump `# Skills` complet seulement en non-MCP pour zéro régression. Option cible architecture : passer par skill built-in + futur CLI `idea`, mais c’est un autre lot. Infrastructure / MCP : - ajouter `idea_skill_read` au catalogue MCP et au mapping tool → `OrchestratorCommand::ReadSkill` ; - dispatcher dans `OrchestratorService` vers `ReadSkill`, retour inline Markdown ; - câbler dans `state.rs`/composition root ; - mettre à jour les tests de compteur/catalogue MCP, actuellement sensibles aux nombres fixes. App-Tauri DTO/commands : - `SkillDto` bénéficie du champ automatiquement si `Skill` sérialise camelCase ; - `CreateSkillRequestDto` et `UpdateSkillRequestDto` doivent porter `description?: string | null` ; - commandes UI existantes `create_skill`/`update_skill` restent les mêmes noms. Frontend : - `domain Skill` ajoute `description?: string | null` ; - `CreateSkillInput` ajoute `description?: string`; `updateSkill` doit permettre de passer description + content, pas seulement content ; - `SkillEditor` ajoute un champ court “Description” ; en edit, description éditable ; - `SkillsPanel` peut afficher description sous le nom ; - mocks + tests RTL à adapter. Docs : - `ARCHITECTURE.md` : ajouter la décision “awareness MCP” sous §14.2 ou l’aligner avec §16 si le skill built-in devient la voie cible ; - `agents-dev/L12-skills.md` ou note dédiée : préciser que L12 crée/assigne/injecte, et que `agent-skill-awareness` ajoute description + affordance + read tool. ## 4) Contrats / ports / DTO À créer/modifier : - `domain::Skill { description: Option }` avec serde default. - `Skill::effective_description() -> String` : description non vide trimée, sinon première ligne non vide du body sans `#` initial. - `SkillStore` : **pas de nouveau port**. Les impls `list/get/save` transportent simplement le champ. - `FsSkillStore` index : ajouter `description` dans `IndexEntry`, `#[serde(default)]`, écriture camelCase. Le body reste dans `md/.md`. - `CreateSkillInput { name, description, content, scope, project_root }`. - `UpdateSkillInput` à faire évoluer vers `{ scope, skill_id, description, content, project_root }` ou variante patch explicite. Je conseille simple remplacement complet `description + content` pour rester aligné UI. - nouveau use case `ReadSkill { SkillStore }` : input `{ name, project_root }`, output `MarkdownDoc` ou DTO `{ name, scope, contentMd }` si on veut plus de traçabilité. Branche historique renvoyait seulement `MarkdownDoc`; acceptable pour MCP. - `OrchestratorRequest`: action/type `skill.read`, champ `name` requis. - `OrchestratorCommand::ReadSkill { name, requester? }`. Le `requester` est utile si la couche MCP veut garder la symétrie avec context/memory, mais le use case n’en a pas besoin. - MCP tool `idea_skill_read`: input `{ name: string }`, output contenu Markdown inline. - DTO Tauri/TS : `Skill.description?: string | null`, create/update requests. À ne pas créer : - pas de `SkillAwarenessStore` ; - pas de nouveau port dédié ; - pas de mécanisme CLI propriétaire ; - pas de lancement/exécution de skill depuis l’UI. ## 5) Risques et ordre d’implémentation Risques : - la branche historique est obsolète vis-à-vis de `orchestrator-designation`; merge brut très risqué ; - conflit conceptuel avec la décision architecture `SkillScope::Builtin` / “Orchestration IdeA” : ne pas figer de prose libre qui sera supprimée juste après ; - double noms de skills : project shadow global doit être documenté ; doublons intra-scope doivent produire une erreur claire ; - assignation à chaud : un skill assigné pendant une session ne devient visible qu’après relaunch/régénération du convention file, sauf mécanisme futur ; - ancien index sans description : serde default obligatoire ; - tests MCP avec compteurs hardcodés fragiles ; préférer assertion par présence de tool. Ordre recommandé : 1. **Git** : créer une branche neuve depuis `develop`, par exemple `feature/agent-skill-awareness-v2`; ne cherry-pick que si les hunks sont triviaux, sinon réimplémenter depuis la note. Garder `feature/agent-skill-awareness` comme référence de design/test, pas comme base de merge. 2. **DevBackend T1** : domaine + serde + tests purs (`description`, fallback, `with_content` conserve description, legacy JSON). 3. **DevBackend T2** : `FsSkillStore`, use cases create/update/list, DTO Tauri. Tests store legacy + roundtrip. 4. **DevBackend T3** : `ReadSkill` + `OrchestratorCommand::ReadSkill` + validation `skill.read`. Tests résolution project/global/ambiguous/not found. 5. **DevBackend T4** : MCP catalogue/mapping/service/state. Tests tool map + dispatch + catalogue par nom. 6. **DevBackend T5** : `compose_convention_file` awareness. Tests : MCP avec skills affiche `# Skills disponibles`, descriptions, appel `idea_skill_read`, pas de body complet si décision retenue ; non-MCP conserve baseline/dump complet ; zéro skill omet la section ; ordre avant persona ou juste après orchestration selon décision. 7. **DevFrontend T6** : champ description dans domain/ports/adapters/mock/SkillEditor/SkillsPanel/tests. Ne pas toucher à l’exécution de skills. 8. **Docs T7** : architecture + agents-dev, en explicitant la frontière avec futur `SkillScope::Builtin`. 9. **QA T8** : `cargo test --workspace`, `pnpm test`, puis e2e AppImage : agent neuf + skill assigné + profil MCP voit `# Skills disponibles`, appelle `idea_skill_read`, reçoit le body ; profil non-MCP ne régresse pas. Verdict court : `agent-skills` = socle déjà intégré ; `agent-skill-awareness` = affordance + lecture à la demande. Reprendre le chantier, oui, mais en branche neuve depuis `develop`, avec périmètre serré et sans avaler le vieux diff. - **Prompt:** Reprise chantier `feature/agent-skill-awareness-v2` après hotfix livraison délégation/logs submit. Contexte: branche actuelle `feature/agent-skill-awareness-v2`; dirty attendu avec fichiers runtime `.ideai/*` et changements code du hotfix listés dans la mémoire `checkpoint-delivery-submit-logging-fix`. Le checkpoint précédent disait de cadrer `agent-skill-awareness` vs `agent-skills` avant implémentation. Merci de fournir le cadrage architecture: objectif exact, frontières backend/frontend, contrats/DTO/ports à toucher, et ordre d'implémentation recommandé. Ne code pas. - **Response:** ## Cadrage architecture — `feature/agent-skill-awareness-v2` ### Verdict de cadrage Il faut distinguer strictement deux sujets : - **`agent-skills` / L12** = fonctionnalité déjà existante : entité `Skill`, scopes `Global|Project`, CRUD, assignation agent↔skill dans le manifeste, injection des bodies assignés dans le convention file à l’activation. - **`agent-skill-awareness`** = rendre l’agent conscient du système de skills IdeA et de la manière correcte de les utiliser/contribuer, sans réimplémenter L12. Donc le chantier recommandé n’est pas un nouveau store, ni une nouvelle UI CRUD, ni un mécanisme de slash-commands fournisseur. C’est une évolution de **composition de contexte agent** + éventuellement une petite surface de découverte orchestrateur, gardée séparée. ### Objectif exact MVP recommandé : à chaque activation d’agent, le convention file doit expliquer explicitement : 1. Les sections sous `# Skills` sont les workflows assignés à cet agent, utilisables quand pertinents. 2. Un skill assigné est du contexte opérationnel, pas une commande magique ni une sous-tâche fournisseur. 3. Si l’agent identifie un workflow réutilisable, il doit passer par l’orchestration IdeA (`idea_create_skill` côté MCP, ou `skill.create` côté protocole fichier), jamais écrire directement dans `.ideai/skills/`. 4. Les skills non assignés ne doivent pas être injectés intégralement à tous les agents : l’assignation reste la frontière de contexte. Ce MVP ferme le flou `awareness` vs `skills` : on garde L12 comme source métier, on améliore la compréhension de l’agent au moment où son contexte est généré. ### Frontières backend À toucher : - `crates/application/src/agent/lifecycle.rs` - `compose_convention_file(...)` est le point naturel : fonction pure, déjà responsable de `Project root`, orchestration, contexte projet, persona, skills, mémoire, handoff. - Ajouter une sous-section courte, stable et testable, par exemple dans `# Orchestration IdeA` ou juste avant `# Skills` : `## Usage des skills IdeA`. - Garder l’ordre actuel : project root → orchestration/awareness → contexte projet → persona → skills → mémoire → handoff. - Tests application de composition dans le même fichier ou suite existante : - agent sans skills : la consigne awareness peut exister, mais pas de section `# Skills` si la liste est vide, pour préserver le contrat actuel. - agent avec skills : la consigne précède les bodies et les bodies restent en ordre manifeste. - `mcp_enabled=true` : mentionner les outils `idea_create_skill` / outils natifs IdeA. - `mcp_enabled=false` : mentionner le protocole fichier `skill.create`. À ne pas toucher pour le MVP : - Pas de nouveau `SkillStore`. - Pas de nouveau port domaine. - Pas de nouveau DTO Tauri. - Pas de mutation du manifeste. - Pas de changement des invariants `Skill`, `SkillRef`, `Agent.skills`. - Pas de scope `Builtin` tant que le projet reste sur l’état réel actuel : l’architecture historique en parle, mais le code courant a déjà une prose d’orchestration dynamique et le pivot structuré/MCP est livré différemment. Réintroduire `Builtin` serait un autre chantier. ### Frontières frontend MVP : **aucune frontière frontend obligatoire**. L’UI skills existe déjà via : - `frontend/src/domain/index.ts` : `Skill`, `SkillRef`, `SkillScope`. - `frontend/src/ports/index.ts` : `SkillGateway`. - `frontend/src/adapters/skill.ts` : `list/create/update/delete/assign/unassign`. - `frontend/src/features/skills/*` : panneau et view-model L12. Éventuel ajustement UI, à garder non bloquant : afficher dans le panneau skills une phrase sobre du type “Les skills assignés sont injectés au lancement de l’agent”. Pas de nouveau gateway pour ça. ### Contrats/DTO/ports à toucher MVP recommandé : - **Domaine Rust** : aucun nouveau type requis. - **Ports Rust** : aucun nouveau port. - **Application** : seulement la fonction pure de composition du convention file et ses tests. - **Infrastructure** : aucun changement. - **Tauri DTO/commands** : aucun changement. - **Frontend DTO/ports** : aucun changement. Contrats existants à respecter : - `domain::Skill { id, name, content_md, scope }`. - `domain::SkillRef { skill_id, scope }` stocké sur l’agent/manifeste. - `SkillStore::list/get/save/delete` reste la seule abstraction de persistance. - `LaunchAgent::resolve_skills` reste best-effort pour les dangling refs : un skill supprimé ne bloque pas le launch. - `compose_convention_file` reste pure/I-O free. ### Option séparée : découverte typée des skills par agent À ne faire que si le produit veut que les agents découvrent les skills non assignés. Dans ce cas, ne pas injecter tous les bodies. Ajouter plutôt une surface compacte : - `OrchestratorCommand::ListSkills { scope: Option }` ou `ListSkills` avec scope requis. - Alias protocole : `skill.list`. - Outil MCP : `idea_list_skills(scope?)`. - `OrchestratorService` réutilise le `SkillStore` existant et renvoie une liste compacte `{ id, name, scope }`, pas `contentMd` par défaut. Ce serait un lot séparé, parce que cela touche `domain/src/orchestrator.rs`, `application/src/orchestrator/service.rs`, l’adapter MCP / watcher, et éventuellement les DTO de réponse. Ce n’est pas nécessaire pour corriger l’awareness des skills assignés. ### Ordre d’implémentation recommandé 1. **Stabiliser le texte de contrat** - Rédiger une section courte “Usage des skills IdeA”. - Deux variantes : MCP (`idea_create_skill`) et fichier (`skill.create`). - Garder l’interdiction des subagents natifs dans la section orchestration existante. 2. **Modifier `compose_convention_file` uniquement** - Ajouter un helper pur si utile, par exemple `compose_skill_awareness(mcp_enabled: bool) -> &'static str`. - Ne pas déplacer la section `# Skills` ni changer la forme des bodies. 3. **Ajouter les tests de composition** - No-skills : pas de `# Skills`, awareness présente. - With-skills : awareness + bodies dans l’ordre. - MCP vs file : bonne consigne de création/contribution. 4. **Vérifier non-régression launch** - Tests ciblés application `agent/lifecycle`. - Puis `cargo test -p application agent` ou le subset existant pertinent. - Pas besoin de Vitest sauf si un texte UI est ajouté. 5. **Lot optionnel discovery** seulement après validation du MVP - Décider explicitement si un agent doit pouvoir lister les skills non assignés. - Si oui, implémenter `skill.list` / `idea_list_skills` comme nouvelle entrée orchestrateur compacte. ### Point d’attention Le fichier `ARCHITECTURE.md` contient encore des sections historiques sur un vieux plan `Builtin skill + idea CLI + inbox/outbox`, mais l’état réel du code est déjà sur `AskAgent`, `Reply`, sessions structurées, MCP bridge, mailbox/input mediator. Pour ce chantier, il faut suivre l’état réel du code et ne pas rouvrir l’ancien plan. Le bon axe est : **améliorer la conscience contextuelle des agents sur les skills existants**, pas refaire l’orchestration. - **Prompt:** Inventaire architecture des chantiers applicatifs restant en cours après intégration locale de `agent-skill-awareness-v2`. Références mémoire: `remaining-work-idea-agent-control-ide` liste comme suites possibles: registre session/singleton agent, live-state partagé projet, handoff/canonical conversation log cross-session/cross-profile, UX conversations/délégations, MCP IdeA-only en flux réel, mise à jour mémoire/contexte automatique, restrictions profils supportés. Merci de classer ce qui est déjà livré vs encore actif, choisir le prochain chantier à reprendre en premier, et fournir un cadrage architectural initial. Ne code pas. - **Response:** ## Inventaire post `agent-skill-awareness-v2` Contexte observé : la branche locale est `develop...origin/develop [ahead 15]`, avec seulement des fichiers runtime `.ideai/conversations/*` dirty. Je considère donc `agent-skill-awareness-v2` intégré localement et je reclasse depuis le code actuel, pas depuis la mémoire seule. ### Déjà livré / à considérer fondation acquise 1. **Registre session / singleton agent** - Livré côté application : `TerminalSessions`, `StructuredSessions`, agrégateur `LiveSessions`, `session_for_agent`, `node_for_agent`, `rebind_agent_node`. - Livré côté Tauri/UI : `list_live_agents`, `attach_live_agent`, guards frontend de lancement singleton. - Statut : **fondation livrée**, à durcir uniquement par tests de flux réel. 2. **Messagerie inter-agents / FIFO / réponse synchrone** - Livré : `AgentMailbox`, `InputMediator`, `AgentBusyChanged`, tickets, `idea_reply`, résolution par ticket, timeout/cancel. - `OrchestratorService::ask_agent` et `reply` existent, avec conversation par paire. - Statut : **livré applicativement**, UX encore perfectible. 3. **MCP IdeA-only en flux backend** - Livré : bridge `idea mcp-server`, endpoint app-tauri, serveur MCP, outils `idea_*`, runtime MCP injecté au launch, badge source `mcp/file` côté UI. - Statut : **livré côté infrastructure/app**, reste validation produit en flux réel et polish observabilité. 4. **Handoff / canonical conversation log cross-session / cross-profile** - Plus avancé que la mémoire ne le dit : `domain/src/conversation_log.rs` définit `ConversationLog`, `HandoffStore`, `HandoffSummarizer`, `ProviderSessionStore`. - Infra livrée : `FsConversationLog`, `FsHandoffStore`, `FsProviderSessionStore`, `HeuristicHandoffSummarizer`. - App livrée : `RecordTurn`, injection handoff au `LaunchAgent`, persistance provider session, séparation pair id IdeA vs engine session id. - Statut : **architecture et première implémentation livrées** ; reste qualité de résumé, couverture UX, audit de complétude de tous les chemins de record. 5. **Restrictions profils supportés** - Livré partiellement : profils structurés Claude/Codex, `structured_adapter`, `materializes_idea_bridge`, garde `guard_mcp_bridge_supported`, profils sélectionnables. - Statut : **règle technique présente**, reste formulation produit/UI des capacités et fallbacks. 6. **Agent skill awareness** - Après intégration locale : **livré comme couche de contexte**, sans nouveau store ni DTO. `agent-skills` reste L12, `awareness` reste composition de convention file. ### Encore actif / pas complètement produit 1. **UX conversations / délégations** - Backend riche, frontend encore dispersé entre agents/layout/terminal/chat/events. - Manque une lecture utilisateur claire : qui a demandé quoi, à qui, état FIFO, réponse reçue, conversation par paire, reprise. - Statut : **actif, prochain meilleur chantier**. 2. **Live-state partagé projet** - Il existe un live-state runtime : sessions, busy, mailbox, events. - Il n’existe pas encore un modèle applicatif unifié, consultable et éventuellement persistable, du “travail en cours” projet. - Statut : **partiellement livré en runtime, pas encore comme read-model produit**. 3. **Mise à jour mémoire/contexte automatique pendant la vie d’un agent** - Injection au launch : livrée. - Outils contexte/mémoire et FileGuard : présents. - Promotion automatique, signal/bruit, mise à jour continue : pas livrés. - Statut : **actif mais à repousser après UX**, car il faut d’abord rendre les fils et décisions visibles. 4. **Handoff/canonical log qualité produit** - Socle livré, mais pas encore “expérience de conversation retrouvée” garantie dans tous les cas. - Résumeur heuristique seulement ; UX de consultation/reprise à clarifier. - Statut : **fondation livrée, produit actif**. ## Prochain chantier recommandé Je recommande de reprendre en premier : **UX conversations & délégations, avec un read-model live-state minimal**. Raison : les briques backend existent maintenant. Continuer par mémoire automatique ou nouveaux mécanismes MCP risque d’empiler de l’invisible. Le prochain verrou produit est de rendre l’orchestration compréhensible et opérable : l’utilisateur doit voir ses “employés IA” travailler, attendre, déléguer, répondre et reprendre. Ce chantier peut aussi servir d’audit d’intégration : s’il manque un événement ou une donnée backend, on l’ajoute proprement comme read-model, au lieu de découvrir les trous plus tard. ## Cadrage architectural initial ### Objectif Fournir une vue produit unifiée des conversations et délégations d’un projet : - agents vivants et cellule hôte, - état `idle/busy/limited/starting` si disponible, - délégations en cours et en attente par agent, - dernière requête/réponse utile, - conversation `User↔Agent` ou `Agent↔Agent` associée, - capacité à ouvrir/rattacher la cellule concernée. Le but n’est pas encore de créer un “journal complet” frontend. Le journal canonique existe côté `.ideai/conversations`; le premier lot doit exposer un **état opérationnel scannable**. ### Frontière backend Préférer un use case de lecture applicatif plutôt que de faire recomposer le frontend depuis dix events. Nouveau read-model applicatif proposé : ```rust ProjectWorkState { live_agents: Vec, conversations: Vec, delegations: Vec, } ``` Port/domain à éviter au départ : pas de nouveau store durable tant qu’on lit les registres existants. Le read-model peut composer : - `LiveSessions` / `TerminalSessions` / `StructuredSessions`, - `InputMediator::busy_state`, - `ConversationRegistry`, - `AgentMailbox` si une méthode d’inspection propre est ajoutée, - `ConversationLog` / `HandoffStore` en lecture best-effort pour les résumés. Si inspection mailbox nécessaire, ne pas exposer l’impl `InMemoryMailbox` : ajouter un petit port ségrégué, par exemple `AgentQueueSnapshot`, ou étendre prudemment `AgentMailbox` avec une méthode read-only si l’impact reste faible. ### Frontière Tauri / DTO Ajouter une commande de lecture, pas une mutation : - `get_project_work_state(projectId) -> ProjectWorkStateDto` DTOs camelCase, stables et tolérants aux champs absents : - `LiveAgentStateDto { agentId, name, nodeId?, sessionId?, kind: "pty"|"structured", busy, limited? }` - `DelegationStateDto { ticketId, requesterAgentId?, targetAgentId, taskPreview, status: "queued"|"delivered"|"awaitingReply"|"replied"|"timedOut", source: "mcp"|"file"|"user"?, createdAtMs? }` - `ConversationThreadSummaryDto { conversationId, left, right, lastTurnPreview?, handoffPreview?, updatedAtMs? }` Ne pas exposer tout `log.jsonl` dans ce lot. Préviews seulement. ### Frontière frontend Ajouter une feature dédiée, sans mélanger avec `TerminalView` : - `frontend/src/features/workstate/` ou `features/conversations/` - hook `useProjectWorkState(projectId)` consommant un nouveau `WorkStateGateway` - panneau dans `ProjectsView` ou extension du sidebar Agents : “Work” / “Conversations” Le frontend doit continuer à consommer des ports TS, pas `invoke()` directement : - `domain/index.ts` : types miroirs DTO. - `ports/index.ts` : `WorkStateGateway`. - `adapters/workState.ts` : Tauri invoke. - mock gateway + tests Vitest. ### Ordre d’implémentation recommandé 1. **Lot A — read-model minimal sans nouvelle persistance** - Backend command `get_project_work_state` qui agrège live agents + busy state + conversations connues. - Frontend panneau simple affichant agents live/busy et dernières délégations connues. 2. **Lot B — queue/delegation snapshot propre** - Ajouter l’inspection read-only de mailbox/FIFO si le Lot A ne peut pas montrer les tickets en attente. - Tester FIFO visible : deux demandes vers le même agent apparaissent dans l’ordre. 3. **Lot C — conversation summaries depuis log/handoff** - Lire `ConversationLog::last` et `HandoffStore::load` best-effort. - Afficher preview et reprise sans charger tout le journal. 4. **Lot D — actions UX** - “Ouvrir/rattacher l’agent”, “voir la conversation”, “arrêter”, “copier résumé”, selon capacités déjà existantes. 5. **Lot E — mémoire/contexte automatique** - Seulement après visibilité des fils : proposer/persister les apprentissages utiles depuis les conversations vers mémoire/contexte via FileGuard. ### Contraintes à respecter - Ne pas faire du frontend la source de vérité du live-state. - Ne pas polluer `.ideai/memory/` avec le journal conversationnel. - Ne pas dupliquer les registres session existants. - Garder PTY brut et structured sessions substituables dans le read-model. - Toute écriture mémoire/contexte doit rester derrière les use cases/FileGuard existants. ## Synthèse Le cœur “agent control IDE” est maintenant largement livré côté backend : singleton, mailbox/FIFO, MCP IdeA-only, log/handoff/provider sessions. Le chantier prioritaire n’est plus d’inventer ces mécanismes, mais de les rendre visibles et pilotables. Je reprendrais donc par **UX conversations/délégations + read-model live-state projet minimal**, puis seulement ensuite la mise à jour automatique mémoire/contexte. - **Prompt:** Nous passons à l'implémentation du chantier `UX conversations/délégations + read-model live-state minimal`. Merci de transformer ton cadrage en périmètre de Lot A implémentable maintenant. Donne les contrats exacts backend/Tauri/frontend, les fichiers probables, ce qui est inclus/exclu, et les tests attendus. Objectif: premier lot utile, minimal, sans nouvelle persistance durable si possible. Ne code pas. - **Response:** ## Lot A — `ProjectWorkState` minimal, implémentable maintenant ### Objectif du lot Livrer une première vue utile et read-only du travail agent en cours dans un projet, sans nouvelle persistance durable : - lister les agents du projet avec leur état live, - indiquer cellule/session hôte quand l’agent tourne, - indiquer `idle` / `busy` à partir du médiateur d’entrée, - exposer le ticket courant si busy, - permettre au frontend d’afficher un panneau “Work” simple et scannable. Ce lot ne cherche pas encore à afficher l’historique, les previews de log, ni la file FIFO complète. Il pose le read-model minimal qui rend l’état agent visible. ## Inclus - Nouvelle commande Tauri read-only `get_project_work_state(projectId)`. - Agrégation backend depuis les stores/registres existants : manifest agents, `LiveSessions`, `InputMediator::busy_state`. - DTO frontend miroir + gateway TS. - Panneau frontend minimal dans la sidebar, probablement onglet `Work`. - Tests backend DTO/commande + tests frontend hook/panel. ## Exclus - Aucune nouvelle persistance `.ideai/`. - Pas de lecture `log.jsonl` / `handoff.md` dans le Lot A. - Pas d’inspection complète de la mailbox/FIFO. - Pas de mutation : pas de stop/reply/attach depuis ce panneau dans le premier lot. - Pas de migration de `list_live_agents` existant. - Pas de refonte des panneaux Agents/Terminal/Chat. ## Contrat backend application Le plus minimal peut rester dans `app-tauri` en composition de DTO, mais je recommande un petit use case application pour garder Tauri adapter mince. ### Nouveau module probable - `crates/application/src/workstate/mod.rs` - export dans `crates/application/src/lib.rs` ### Types application proposés ```rust pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, } pub struct GetProjectWorkStateInput { pub project: Project, } pub struct ProjectWorkState { pub agents: Vec, } pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option, pub busy: AgentBusyState, } pub struct LiveWorkSession { pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } pub enum LiveSessionKind { Pty, Structured, } ``` ### Important sur `kind` `LiveSessions::live_agents()` agrège déjà PTY + structured mais ne porte pas le kind. Pour Lot A, deux options : 1. **Option minimale** : omettre `kind` du DTO. Suffisant pour afficher “live”. 2. **Option préférable** : ajouter une méthode read-only à `LiveSessions`, sans casser l’existant : ```rust pub fn live_agent_entries(&self) -> Vec pub struct LiveAgentEntry { pub agent_id: AgentId, pub node_id: NodeId, pub session_id: SessionId, pub kind: LiveSessionKind, } ``` Garder `live_agents()` existant pour compatibilité avec `list_live_agents`. ### Algorithme use case 1. `manifest = contexts.load_manifest(&project).await?` 2. Convertir chaque entry en `Agent` via `entry.to_agent()`. 3. Construire une map `agent_id -> live entry` depuis `LiveSessions`. 4. Pour chaque agent : - `busy = input.busy_state(agent.id)` - `live = live_map.get(agent.id)` - produire `AgentWorkState` 5. Trier par `name` ou conserver l’ordre manifeste. Recommandation : conserver ordre manifeste pour stabilité avec `list_agents`. ## Contrat Tauri ### Nouveau DTO dans `crates/app-tauri/src/dto.rs` ```rust #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProjectWorkStateDto { pub agents: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentWorkStateDto { pub agent_id: String, pub name: String, pub profile_id: String, pub live: Option, pub busy: BusyStateDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LiveWorkSessionDto { pub node_id: String, pub session_id: String, pub kind: LiveSessionKindDto, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub enum LiveSessionKindDto { Pty, Structured, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", tag = "state")] pub enum BusyStateDto { Idle, Busy { ticket: String, since_ms: u64 }, } ``` Si l’équipe veut réduire encore le lot : supprimer `kind` et `LiveSessionKindDto`. ### Nouvelle commande dans `crates/app-tauri/src/commands.rs` ```rust #[tauri::command] pub async fn get_project_work_state( project_id: String, state: State<'_, AppState>, ) -> Result ``` Comportement : - `resolve_project(&project_id, &state).await?` - appeler `state.get_project_work_state.execute(...)` - mapper en DTO Erreurs : - `INVALID` si `projectId` invalide, - `NOT_FOUND` si projet inconnu, - `STORE` si manifeste illisible. ### Wiring `AppState` Fichiers probables : - `crates/app-tauri/src/state.rs` - ajouter `pub get_project_work_state: Arc`. - construire avec `IdeaiContextStore`, `LiveSessions::new(terminal_sessions, structured_sessions)`, `input_mediator`. - `crates/app-tauri/src/lib.rs` - enregistrer `commands::get_project_work_state` dans `invoke_handler`. ## Contrat frontend ### Types dans `frontend/src/domain/index.ts` ```ts export interface ProjectWorkState { agents: AgentWorkState[]; } export interface AgentWorkState { agentId: string; name: string; profileId: string; live?: LiveWorkSession | null; busy: WorkBusyState; } export interface LiveWorkSession { nodeId: string; sessionId: string; kind: "pty" | "structured"; } export type WorkBusyState = | { state: "idle" } | { state: "busy"; ticket: string; sinceMs: number }; ``` Si backend omet `kind`, retirer `kind` ici aussi. ### Port dans `frontend/src/ports/index.ts` ```ts export interface WorkStateGateway { getProjectWorkState(projectId: string): Promise; } export interface Gateways { // existants... workState: WorkStateGateway; } ``` ### Adapter Tauri Nouveau fichier : `frontend/src/adapters/workState.ts` ```ts export class TauriWorkStateGateway implements WorkStateGateway { getProjectWorkState(projectId: string): Promise { return invoke("get_project_work_state", { projectId }); } } ``` Puis wiring : - `frontend/src/adapters/index.ts` : instancier `workState: new TauriWorkStateGateway()`. - `frontend/src/adapters/mock/index.ts` : ajouter `MockWorkStateGateway`. ### Feature frontend Nouveau dossier recommandé : - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/index.ts` - `frontend/src/features/workstate/workstate.test.tsx` Hook : ```ts export interface ProjectWorkStateViewModel { state: ProjectWorkState | null; busy: boolean; error: string | null; refresh: () => Promise; } ``` Refresh initial + refresh sur events existants : - `agentLaunched` - `agentExited` - `agentBusyChanged` - `orchestratorRequestProcessed` - éventuellement `agentProfileChanged` ### UI minimale Dans `ProjectsView.tsx` : - ajouter `SidebarTab = ... | "work"` - ajouter `{ id: "work", label: "Work" }` - afficher `ProjectWorkStatePanel projectId={active.id}` si actif. Contenu minimal du panneau : - ligne par agent : nom, live/offline, busy/idle. - si live : afficher `sessionId` court ou `nodeId` court. - si busy : afficher ticket court + âge approximatif facultatif. - empty state : “No agents yet.” - erreur et bouton refresh. Pas d’actions dans Lot A. Le panneau est observabilité seulement. ## Fichiers probables Backend : - `crates/application/src/workstate/mod.rs` nouveau - `crates/application/src/lib.rs` - `crates/application/src/terminal/registry.rs` si ajout `live_agent_entries()` avec kind - `crates/app-tauri/src/dto.rs` - `crates/app-tauri/src/commands.rs` - `crates/app-tauri/src/state.rs` - `crates/app-tauri/src/lib.rs` Frontend : - `frontend/src/domain/index.ts` - `frontend/src/ports/index.ts` - `frontend/src/adapters/workState.ts` nouveau - `frontend/src/adapters/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/useProjectWorkState.ts` nouveau - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` nouveau - `frontend/src/features/workstate/index.ts` nouveau - `frontend/src/features/workstate/workstate.test.tsx` nouveau - `frontend/src/features/projects/ProjectsView.tsx` ## Tests attendus ### Rust application Nouveau test application, selon conventions existantes : - `cargo test -p application workstate` Cas : 1. Projet avec deux agents, aucun live, aucun busy → deux rows `live=None`, `busy=Idle`. 2. Agent live PTY → row porte `live.session_id`, `live.node_id`, `kind=Pty` si kind inclus. 3. Agent busy → row porte `Busy { ticket, since_ms }`. 4. Agent absent du manifeste mais présent dans registry → ignoré dans le read-model projet. ### Rust app-tauri Selon structure actuelle, ajouter dans tests de wiring/commands : - commande enregistrée dans `invoke_handler`, - DTO sérialise en camelCase : `agentId`, `profileId`, `sinceMs`, `sessionId`, `nodeId`, `kind`. Commandes de vérification probables : - `cargo test -p app-tauri --test orchestrator_wiring` - ou test dédié si les commands sont déjà testées ailleurs. ### Frontend Vitest Nouveau `workstate.test.tsx` : 1. Le panneau affiche `No agents yet.` quand liste vide. 2. Il affiche agent idle/offline. 3. Il affiche agent live + busy avec ticket court. 4. Un event `agentBusyChanged` déclenche refresh. 5. `ProjectsView` expose l’onglet `Work` et rend le panneau pour le projet actif. Commande ciblée : ```bash npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` ### Typecheck ```bash npx tsc --noEmit cargo check -p app-tauri ``` ## Notes de rigueur - Le Lot A doit rester read-only. Les actions “ouvrir/rattacher”, “stop”, “voir conversation” sont Lot D. - Ne pas lire `ConversationLog` dans Lot A : dès qu’on affiche des previews de conversation, on bascule vers Lot C. - Ne pas ajouter une persistance `live_state.json` : le live-state est dérivé des registres runtime existants. - Ne pas dupliquer `list_live_agents`; le nouveau read-model peut l’utiliser conceptuellement, mais il doit ajouter le busy state et le nom agent, donc il mérite sa commande dédiée. - Si `LiveSessions` est difficile à injecter tel quel, injecter les deux registres et agréger dans le use case, mais garder l’agrégation dans `application`, pas dans Tauri ni frontend.