Files
IdeA/.ideai/conversations/7a2f446c-2efa-0c56-08dd-07cdc5cac19b/handoff.md
2026-06-20 18:10:48 +02:00

26 lines
89 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
upTo: 001f112c-4ece-4f49-a7cc-140ee76eabec
objective: CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour
---
**Objectif :** CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. ## Contexte / gap déjà localisé Le round-trip inter-agent fonctionne pour
- **Prompt:** 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<ScheduledTask>, clock: Arc<dyn Clock>)`. 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<Self>`, `detect(text, now_ms) -> Option<SessionLimit>`, 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<dyn Clock>` comme partout (cf. `Arc::clone(&clock) as Arc<dyn Clock>`). - `event_bus` (l.357) → `events_port` (l.369, `Arc<dyn EventBus>`), 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::<ScheduledTask>();` 2. `let scheduler = Arc::new(TokioScheduler::new(resume_tx, Arc::clone(&clock) as Arc<dyn Clock>)) as Arc<dyn Scheduler>;` 3. `let resumer = Arc::new(AppAgentResumer::new(Arc::clone(&launch_agent), Arc::clone(&store_port), <resume_ctx_registry>)) as Arc<dyn application::AgentResumer>;` (cf. §2). 4. `let session_limit_service = Arc::new(SessionLimitService::new(Arc::clone(&clock) as Arc<dyn Clock>, scheduler, Arc::clone(&events_port), resumer));` **Cycle de vie / partage :** ajouter un champ `pub session_limit_service: Arc<SessionLimitService>` à 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<LaunchAgent>` 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<Mutex<HashMap<AgentId, ResumeContext>>>` (`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<LaunchAgent>`, `Arc<dyn ProjectStore>` (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<AppState>``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<dyn Clock>` (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<SessionLimitService>` 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<bool, ErrorDto>` : `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<String>, 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 lutilisateur crée beaucoup dagents il atteint une limite de requests/login. De plus un agent jamais lancé peut échouer lors dune délégation car il nest pas authentifié. Questions: 1) quelle solution respecte larchitecture hexagonale/SOLID pour partager lauth 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 dagent ? 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 <projectRoot>` à 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, cest conforme à larchitecture 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 dexécution du projet, pas un détail Tauri/Codex. - `StructuredSessionFactory::start` peut transmettre cette donnée vers ladapter runtime concret : cest précisément le rôle de la couche application/composition dorchestrer les ports avec le contexte résolu. - Ajouter `--add-dir <projectRoot>` dans `CodexExecSession` est au bon endroit si `CodexExecSession` est ladapter infrastructure/presentation-backend spécifique à Codex CLI. Le flag `--add-dir` est un détail technique de ladapter Codex, donc il ne doit pas remonter dans le domaine. - Conditionner cet ajout à `workspace-write` est cohérent : lintention métier est “la session peut écrire dans le workspace résolu”, limplé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 ladapter 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. Ladapter 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 lexé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. LUI demande un lancement dagent/session ; la politique de sandbox reste dans les services/adapters backend. Formulation recommandée côté modèle : plutôt quun champ trop Codex-spécifique, utiliser un nom neutre comme `workspace_roots`, `writable_roots`, `project_root`, ou `ExecutionScope { cwd, project_root, sandbox_mode }`. Puis ladapter Codex traduit `ExecutionScope + workspace-write` en `codex exec --add-dir <projectRoot>`.
- **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 linté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 quil possède des skills assignés et comment les utiliser, sans que lutilisateur doive le lui rappeler. Aujourdhui L12 injecte le contenu complet des skills dans `# Skills`, mais cest bas dans le contexte, lu comme documentation, et pas présenté comme affordance/action disponible. Objectif technique minimal : transformer linjection “dump de contenu” en modèle “à la MCP” pour les profils MCP : - section haute altitude `# Skills disponibles`, juste après lorchestration/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 lancien 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 dorchestration 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 nest pas un doublon de `agent-skills`; cest une amélioration dergonomie 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<String>` à `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<String>` ; - `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 dabord, 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 cest 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 laligner 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<String> }` 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/<id>.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 nen 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 lUI. ## 5) Risques et ordre dimplé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 quaprè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 à lexé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 à lactivation. - **`agent-skill-awareness`** = rendre lagent 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é nest pas un nouveau store, ni une nouvelle UI CRUD, ni un mécanisme de slash-commands fournisseur. Cest 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 dagent, 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 lagent identifie un workflow réutilisable, il doit passer par lorchestration 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 : lassignation 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 lagent 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 lordre 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 : larchitecture historique en parle, mais le code courant a déjà une prose dorchestration 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**. LUI 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 lagent”. 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 lagent/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<SkillScope> }` 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`, ladapter MCP / watcher, et éventuellement les DTO de réponse. Ce nest pas nécessaire pour corriger lawareness des skills assignés. ### Ordre dimplé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 linterdiction 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 lordre. - 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 dattention 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 lancien plan. Le bon axe est : **améliorer la conscience contextuelle des agents sur les skills existants**, pas refaire lorchestration.
- **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 nexiste 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 dun 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 dabord 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 dempiler de linvisible. Le prochain verrou produit est de rendre lorchestration compréhensible et opérable : lutilisateur doit voir ses “employés IA” travailler, attendre, déléguer, répondre et reprendre. Ce chantier peut aussi servir daudit dintégration : sil manque un événement ou une donnée backend, on lajoute 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 dun 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 nest 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<LiveAgentState>, conversations: Vec<ConversationThreadSummary>, delegations: Vec<DelegationState>, } ``` Port/domain à éviter au départ : pas de nouveau store durable tant quon lit les registres existants. Le read-model peut composer : - `LiveSessions` / `TerminalSessions` / `StructuredSessions`, - `InputMediator::busy_state`, - `ConversationRegistry`, - `AgentMailbox` si une méthode dinspection propre est ajoutée, - `ConversationLog` / `HandoffStore` en lecture best-effort pour les résumés. Si inspection mailbox nécessaire, ne pas exposer limpl `InMemoryMailbox` : ajouter un petit port ségrégué, par exemple `AgentQueueSnapshot`, ou étendre prudemment `AgentMailbox` avec une méthode read-only si limpact 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 dimplé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 linspection 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 lordre. 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 lagent”, “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 nest plus dinventer 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 lagent tourne, - indiquer `idle` / `busy` à partir du médiateur dentrée, - exposer le ticket courant si busy, - permettre au frontend dafficher un panneau “Work” simple et scannable. Ce lot ne cherche pas encore à afficher lhistorique, 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 dinspection 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<dyn AgentContextStore>, live: Arc<LiveSessions>, input: Arc<dyn InputMediator>, } pub struct GetProjectWorkStateInput { pub project: Project, } pub struct ProjectWorkState { pub agents: Vec<AgentWorkState>, } pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option<LiveWorkSession>, 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 lexistant : ```rust pub fn live_agent_entries(&self) -> Vec<LiveAgentEntry> 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 lordre 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<AgentWorkStateDto>, } #[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<LiveWorkSessionDto>, 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<ProjectWorkStateDto, ErrorDto> ``` 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<GetProjectWorkState>`. - 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<ProjectWorkState>; } 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<ProjectWorkState> { return invoke<ProjectWorkState>("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<void>; } ``` 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 dactions 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 longlet `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 quon 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 lutiliser 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 lagré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<Slot>` 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 lautorité busy/livraison, pas lautorité 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<QueuedTicketSnapshot>; } ``` 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<QueuedTicketSnapshot>` directement à `AgentMailbox`, avec impl par défaut vide. Mais cest 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<dyn AgentContextStore>, live: Arc<LiveSessions>, input: Arc<dyn InputMediator>, queue: Arc<dyn AgentQueueSnapshot>, } ``` Étendre les structs : ```rust pub struct AgentWorkState { pub agent_id: AgentId, pub name: String, pub profile_id: ProfileId, pub live: Option<LiveWorkSession>, pub busy: AgentBusyState, pub tickets: Vec<AgentTicketState>, } 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 denvoyer tout le prompt au panneau. Recommandation : trim + remplacer whitespace runs par espace + couper à 160 caractères. Garder `task_len` pour indiquer quil y a plus. Important : conserver lordre 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<LiveWorkSessionDto>, pub busy: AgentBusyState, pub tickets: Vec<AgentTicketStateDto>, } #[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<InMemoryMailbox>` déjà créé au composition root ; - le passer à `GetProjectWorkState::new(...)` comme `Arc<dyn AgentQueueSnapshot>` ; - continuer à passer le même `Arc<InMemoryMailbox>` au `MediatedInbox` et à lorchestrateur 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<dyn AgentMailbox>; let queue_snapshot = Arc::clone(&inmemory_mailbox) as Arc<dyn AgentQueueSnapshot>; ``` 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<ProjectWorkState>; ``` 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 daction 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 cest le signal existant le plus proche dun 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 dannulation de ticket depuis UI. - Pas de résolution manuelle depuis UI. - Pas de persistance queue dans `.ideai/`. - Pas de lecture `ConversationLog` / `handoff.md`. - Pas dhistorique des tickets terminés. - Pas de statut `delivered` : `delegation_delivered` est aujourdhui un log/ack best-effort, pas une donnée persistée dans la mailbox. Ne pas lexposer comme état tant quil na 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`. Cest 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 sil doit apparaître comme ticket ou être exclu du snapshot. Pour le MVP UX “délégations”, je recommande dafficher uniquement les tickets dont `source == Agent`. Si léquipe veut afficher aussi les entrées humaines, le libellé UI doit parler de “tickets dentré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 lordre 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 lordre `#1`, `#2`. 3. Agent sans tickets naffiche 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é dimplémentation Le Lot B est une extension de projection : `InMemoryMailbox` expose une vue snapshot, `GetProjectWorkState` lagrège par agent manifeste, Tauri sérialise `tickets`, le panneau Work les affiche. Aucun nouveau stockage, aucune nouvelle queue, aucune mutation UX.