Mise à jour de l'état runtime non-code : flux de conversations (handoff/log.jsonl), layouts, index mémoire MEMORY.md, et nouveau checkpoint checkpoint-delivery-submit-logging-fix.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 KiB
82 KiB
upTo, objective
| upTo | objective |
|---|---|
| 506b5f0c-e7c2-457e-81cf-a22d5ac5ac97 | CADRAGE ARCHITECTURE (pas de code) — Support de la délégation inter-agents (idea_ask_agent/idea_reply) pour les profils CODEX. |
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: Validation d'approche AVANT dev — lot LP4-4 : étendre l'enforcement OS Landlock au chemin structuré (sessions Claude/Codex en mode JSON), aujourd'hui non sandboxé. État actuel (committé, vert) : - Domaine pur :
domain::sandboxavecSandboxPlan,SandboxContext,compile_sandbox_plan(eff, ctx) -> Option<SandboxPlan>, et le portSandboxEnforcer { fn enforce(&self, &SandboxPlan) -> Result<SandboxStatus, SandboxError>; fn kind(); }. Adapters infra :LandlockSandbox(Linux),NoopSandbox,default_enforcer(). - Chemin PTY (crates/infrastructure/src/pty/mod.rs) : OS-enforcé. Technique retenue =spawn_command_sandboxed: un THREAD JETABLE appelleenforcer.enforce(plan)(restriction Landlock irréversible, donc confinée au thread) PUIS spawn le child depuis ce thread → le domaine Landlock est hérité par le child via fork+exec. portable-pty n'expose pas de pre_exec injectable, d'où ce choix ; il évite aussi le piège de l'allocation non async-signal-safe dans un pre_exec post-fork en process multithreadé. - Chemin structuré (crates/infrastructure/src/session/process.rs::drain) : spawn viatokio::process::Command(async), draine stdout ligne-à-ligne jusqu'à EOF.SpawnLine{ command, args, cwd, env, stdin } ne porte aucun plan. C'est l'incarnation « un exec par tour ». Question d'archi à trancher : comment enforcer leSandboxPlansur le child spawné partokio::process::Commanden respectant l'hexagonal ET la sûreté (le truc du thread jetable du PTY ne transpose pas tel quel au spawn async tokio qui forke sur les threads du runtime) ? Pistes à arbitrer : (a)CommandExt::pre_exec(tokio le réexpose) appelant l'enforce dans le child post-fork — simple mais risque async-signal-safety silandlockalloue ; (b) déporter le spawn du tour structuré sur un thread dédié restreint (viaspawn_blocking+ thread jetable comme le PTY), au prix de la nature async du drain ; (c) un helper de spawn sandboxé partagé entre PTY et session (factoriser la technique) ; (d) autre. Livrable attendu (via idea_reply) : la stratégie retenue + justification (sûreté + hexagonal), le contrat à modifier (faut-il queSpawnLineporte le plan ? l'enforcer est-il injecté dans la session factory comme il l'est dans le PTY adapter ?), le découpage des fichiers à toucher, et les invariants/tests que QA devra vérifier. NE code rien. Si une piste est dangereuse (ex. allocation post-fork), dis-le explicitement. - Response: # LP4-4 — Validation d'approche : enforcement Landlock sur le chemin structuré ## Verdict Stratégie retenue : (b) raffiné — spawn du tour structuré sandboxé sur un thread jetable restreint, en spawn
stdsynchrone, exposé à l'async viaspawn_blocking. C'est la transposition directe et sûre de la technique déjà prouvée du PTY. Piste (a)pre_exec(enforce)REJETÉE comme dangereuse (détail plus bas). (c) factorisation : oui sur le principe, non sur le corps — les deux spawns sont trop différents (portable-ptySlavePtyvsstd::process). --- ## Pourquoi PAS (a)pre_exec(enforce)— DANGER explicitetokio::process::Commandréexpose bienCommandExt::pre_exec, mais le closure s'exécute dans l'enfant aprèsfork, avantexec, dans un process multithreadé (les workers du runtime tokio). Aprèsforken contexte multithreadé, seules les opérations async-signal-safe sont permises : tout mutex tenu par un autre thread au moment du fork (typiquement le lock de l'arène malloc) reste verrouillé à jamais dans l'enfant. Orlandlock::Ruleset::…restrict_self()alloue (construction du ruleset,RestrictionStatus, Vec internes) →mallocpost-fork → risque de deadlock de l'enfant. C'est exactement le piège que le commentaire du PTY (spawn_command_sandboxed, lignes 185-206) documente et évite. La cohérence d'archi impose de le rejeter ici aussi. > Note : il existe une variante async-signal-safe de Landlock (construire le ruleset_fd dans le parent, ne faire queprctl(NO_NEW_PRIVS)+landlock_restrict_self(fd)— deux syscalls purs — dans le pre_exec). Mais elle force à scinder le portSandboxEnforceren deux phases (prepare allouant / commit syscall-only) et fait fuiter cette mécanique dans le domaine. Inutile : la technique du thread jetable atteint la même sûreté sans toucher au contrat de port. Je la mentionne pour mémoire, je ne la recommande pas. ## Pourquoi (b) est sûr La technique du PTY ne fait rien dans l'enfant post-fork :enforce(plan)tourne dans le thread jetable parent AVANT le fork ; l'enfant hérite simplement le domaine Landlock (hérité acrossfork, préservé acrossexecve). Zéro code post-fork ⇒ zéro problème d'async-signal-safety. C'est toute l'élégance, et elle transpose telle quelle. La seule difficulté est quetokio::process::Commandforke sur un worker partagé du runtime (qu'on ne peut pas restreindre : la restriction est irréversible → on empoisonnerait le runtime). D'où : pour le chemin sandboxé on abandonne le drain async tokio au profit d'un drainstdsynchrone sur le thread jetable restreint, réconcilié à l'async parspawn_blocking. Le chemin non-sandboxé (sandbox == Noneou pas d'enforcer) reste l'actuel drain async tokio, inchangé (zéro régression, c'est aussi le seul chemin sur non-Linux). Détails de sûreté du thread sandboxé : 1.enforcer.enforce(&plan)restreint CE thread (fail-closed :Err⇒ on échoue le tour, aucun child ne tourne) ; 2.std::process::Command::spawn()depuis ce thread ⇒ l'enfant hérite le domaine ; 3. poser ununsafe { cmd.pre_exec(|| Ok(())) }vide (async-signal-safe) pour forcer le cheminfork+execdéterministe (parité avec portable-pty, lève tout doute vs un éventuelposix_spawnglibc — l'héritage tiendrait de toute façon, mais on ne parie pas) ; 4. drain bloquant ligne-à-ligne stdin/stdout → EOF →wait()→Vec<String>; 5. le thread meurt, emportant sa restriction irréversible ; les autres threads d'IdeA sont intouchés. Timeout : aujourd'huirun_turnenveloppe ledrainasync. En sandboxé, on enveloppe leJoinHandledu thread viatokio::time::timeout; à expiration il faut tuer le child (le thread est bloqué en read). Donc le thread renvoie son killer (pid /Arc<Mutex<Child>>) par unoneshotdès après spawn ; le kill provoque l'EOF ⇒ le read débloque ⇒ le thread finit ⇒ on retourneTimeout. À implémenter proprement (c'est le seul vrai surcoût de machinerie vs l'élégance actuelle). --- ## Contrat à modifier 1.SpawnLineporte le plan (oui) —crates/infrastructure/src/session/process.rs: ajouterpub sandbox: Option<SandboxPlan>. Symétrie avecSpawnSpec.sandbox.SpawnLineest un DTO infra (pas domaine), donc OK. 2. L'enforcer est injecté dans la factory (oui, comme le PTY adapter) —StructuredSessionFactorygagne un champOption<Arc<dyn SandboxEnforcer>>+ un builderwith_sandbox_enforcer(...), jumeau exact dePortablePtyAdapter::with_sandbox_enforcer. Pas dans la signature du port : c'est une dépendance de composition root, par-instance, pas par-appel. 3. Le plan traverse le port par-appel — leSandboxPlandépend des permissions résolues de l'agent, il est calculé par-lancement danslifecycle.rsstep 5d (spec.sandbox). Il doit donc passer dansAgentSessionFactory::start: ajoutersandbox: Option<&SandboxPlan>.SandboxPlanest un type domaine (domain::sandbox) franchissant un port domaine — cohérent (déjà le cas viaSpawnSpec.sandboxsurAgentRuntime). Extension O/C : une seule vraie impl + les fakes. Flux complet :lifecycle.rs(calculespec.sandbox) → passe le plan àlaunch_structured(qui ne reçoit passpecaujourd'hui) →factory.start(..., sandbox)→ la factory appariesandbox(plan, par-appel) + son enforcer (par-instance) et les passe àClaudeSdkSession::new/CodexExecSession::new→ l'adapter les stocke, remplitSpawnLine.sandbox, et passe l'enforcer àrun_turn. 4.run_turn—crates/infrastructure/src/session/process.rs: signaturerun_turn(spec, enforcer: Option<&Arc<dyn SandboxEnforcer>>, timeout). Sispec.sandbox.is_some() && enforcer.is_some()⇒ drain sandboxé (thread restreint) ; sinon ⇒drainasync actuel strictement inchangé. --- ## Découpage des fichiers à toucher -crates/domain/src/ports.rs:537—AgentSessionFactory::start: +sandbox: Option<&SandboxPlan>. -crates/infrastructure/src/session/process.rs— champSpawnLine.sandbox;run_turnreçoit l'enforcer ; nouveaudrain_sandboxed(thread jetable + std spawn + pre_exec vide + timeout par kill). -crates/infrastructure/src/session/factory.rs— champOption<Arc<dyn SandboxEnforcer>>+with_sandbox_enforcer;startapparie plan+enforcer et les injecte dans les ctors d'adapters. -crates/infrastructure/src/session/claude.rs(build_spawn_line~L187,send~L195) &codex.rs(~L162/L195) — stocker plan+enforcer, remplirSpawnLine.sandbox, passer l'enforcer àrun_turn. -crates/application/src/agent/lifecycle.rs—launch_structured(~L1620) reçoit le plan (spec.sandbox) et le relaie àfactory.start. -crates/app-tauri/src/state.rs:408—StructuredSessionFactory::new().with_sandbox_enforcer(infrastructure::default_enforcer()). - Fakes à mettre à jour (nouvelle signaturestart) :crates/domain/tests/structured_session_d0.rs,crates/application/tests/structured_launch_d3.rs,crates/application/tests/orchestrator_service.rs. --- ## Invariants & tests pour la QA 1. Parité avec le PTY (le test pivot) : réplique depty_spawn_enforces_sandbox_plan_end_to_endcôté structuré, avec un fake CLI /shqui émet une ligne JSONL et tente d'écrire hors grant (doit être bloqué kernel) et dans grant (doit réussir). Zéro token (aucun vrai claude/codex). Gardé derrièrelandlock_is_enforced()(skip propre sur kernel sans Landlock). 2. Companion négatif : même factory+enforcer maisspec.sandbox == None⇒ l'écriture hors-grant réussit (prouve que le blocage vient du plan, pas d'une restriction ambiante). 3. Fail-closed : postureDenysur kernel sans Landlock ⇒enforceErr⇒run_turnrenvoie une erreur et aucun child ne tourne (assert : le marqueur de sortie du fake CLI n'existe pas). 4. No-op par défaut :eff == None⇒compile_sandbox_planNone⇒spec.sandbox None⇒ chemin async tokio actuel, comportement natif (régression nulle). Vérifier que la suite structurée existante (conformance, D0/D3) reste verte sans modification de comportement. 5. Confinement de l'irréversibilité : après un tour sandboxé, un autre tour (autre session, plan plus large) sur le même process IdeA n'est PAS affecté par la restriction du tour précédent (le thread jetable est mort). Test : deux tours successifs avec des grants disjoints, chacun voit son propre périmètre. 6. Timeout sous sandbox : un fake CLI qui ne ferme jamais stdout ⇒run_turn(timeout)tue le child et renvoieTimeout, sans laisser de zombie ni de thread bloqué. 7. Resume préservé : leconversation_idest toujours capté correctement après un tour sandboxé (la restriction FS ne casse pas le parsing dusession_id). ## ⚠️ Risque produit à signaler (hors mécanisme, mais bloquant à valider) Les CLI structurées (claude/codex = binaires Node) ont des besoins FS ambiants bien plus lourds qu'unsh: lecture de~/.claude/~/.codex(credentials, cache de session pour le resume),node_modules, libs système, écriture de caches temporaires.compile_sandbox_planne fence en écriture/lecture que les classes explicitement posées et garde les lectures globalement ouvertes — mais si une policy pose unDeny/posture restrictive touchant$HOME, le resume peut casser (la CLI n'écrit plus son cache de session). Recommandation : LP4-4 livre le mécanisme validé au fake CLI (zéro token, points 1-7), et on flag qu'une validation e2e manuelle d'un vrai tour claude/codex sous un plan représentatif doit confirmer que le run dir + le home de la CLI restent atteignables avant d'activer le sandbox en prod pour le chemin structuré. C'est un sujet de composition du plan (run_dir reachability, déjà réservé dansSandboxContext.run_dirmais non consommé par la traduction pure), pas du mécanisme d'enforcement — à traiter en lot suivant si besoin. --- Résumé : (b) avec thread jetable restreint + std spawn +spawn_blocking, fail-closed identique au PTY ;SpawnLine.sandboxajouté ; enforcer injecté par construction dans la factory ; plan passé par-appel viaAgentSessionFactory::start(.., sandbox). (a) rejetée (alloc post-fork). Ne rien coder avant ce GO — QA doit pouvoir s'appuyer sur les 7 invariants ci-dessus. - Prompt: Arbitrage LP4-4 — une divergence est remontée par DevBackend sur ton cadrage, elle touche un invariant crate-wide. Verdict requis. Ton cadrage demandait, dans
drain_sandboxed(chemin structuré), ununsafe { cmd.pre_exec(|| Ok(())) }VIDE pour forcer std sur le chemin déterministe fork+exec (parité avec portable-pty). Blocage : la crateinfrastructureest#. Unforbidne peut pas être relâché par un#[allow]local. Le PTY n'avait jamais eu besoin d'unsafe car il hérite le fork+exec du pre_exec INTERNE de portable-pty ; sur le chemin structuré on pilotestd::processen direct, donc le pre_exec serait à nous → unsafe → interdit. DevBackend a retenu (et livré, build+tests verts) l'option (a) : RETIRER le pre_exec. Son argument :landlock_restrict_selfrestreint le thread courant et toute sa descendance ; l'héritage est assuré par le noyau à travers fork/clone/vfork ET préservé par execve, Y COMPRIS via posix_spawn (= clone+execve), car l'enforcement vit au niveau des credentials de la tâche, hors d'atteinte de l'espace utilisateur. Le fork+exec n'était donc qu'une garantie de DÉTERMINISME du syscall de spawn, pas de sécurité ; enforce()-avant-spawn-sur-le-même-thread suffit. Les deux options sur la table : (a) rester comme livré : zéro unsafe, héritage noyau (choix par défaut de DevBackend). (b) passer lib.rs de#![forbid(unsafe_code)]à#![deny(unsafe_code)]+#[allow(unsafe_code)]scoppé sur drain_sandboxed pour rétablir le pre_exec vide. Question : valides-tu (a) — l'héritage de la restriction Landlock est-il bien garanti par le noyau indépendamment de fork+exec vs posix_spawn, rendant le pre_exec inutile à la sécurité — ou exiges-tu (b) (relâcher l'invariant unsafe-code de la crate) ? Si (a), confirme qu'on garde#![forbid(unsafe_code)]intact. Réponds avec le verdict + justification courte ; c'est juste cet arbitrage, pas une re-revue complète. - Response: # Verdict LP4-4 : (a) validée — on garde
#![forbid(unsafe_code)]intact DevBackend a raison, et son choix est même cohérent avec mon cadrage initial : j'y avais explicitement écrit que l'héritage tient sans le pre_exec (« even without it inheritance holds across posix_spawn's clone »), le pre_exec vide n'étant qualifié que d'« assurance de déterminisme », jamais de garantie de sécurité. Le retirer ne retire donc aucune propriété de sûreté. ## Justification technique (l'héritage est bien garanti par le noyau) - L'enforcement Landlock est attaché aux credentials de la tâche (le domaine Landlock vit dans lacred, sous NO_NEW_PRIVS — querestrict_selfpose lui-même). C'est de l'état noyau, hors d'atteinte de l'espace utilisateur. -landlock_restrict_selfrestreint le thread appelant ; tout enfant créé par ce thread hérite ses credentials → donc le domaine Landlock. L'héritage se fait à traversfork/clone/vforket est préservé parexecve. -posix_spawn(glibc) =clone(CLONE_VM|CLONE_VFORK)+execveémis depuis le thread appelant dans le même process : l'enfant hérite les creds exactement comme via fork+exec. Le mécanisme de spawn ne change que le déterminisme du syscall, pas l'héritage. La seule précondition de sûreté — le syscall de spawn doit être émis par le thread restreint — est respectée par le design :enforce(plan)puisCommand::spawn()sur le même thread jetable. ✔ ## Confirmation empirique décisive Le test e2e de parité (écriture hors-grant bloquée kernel / dans-grant réussie) passe sur le chemin structuré avec le spawn réellement utilisé. C'est la preuve in vivo que l'héritage tient indépendamment de fork+exec vs posix_spawn — exactement l'invariant en débat. Build + suite verts ⇒ rien à rouvrir. ## Arbitrage de l'invariant crate-wide Relâcher#![forbid(unsafe_code)]→deny + #[allow]scoppé pour ne récupérer qu'une garantie de déterminisme de spawn (sans valeur de sécurité) serait un mauvais échange : on dégraderait une protection statique crate-wide pour une commodité nulle côté sûreté. Non. Décision : on reste en (a).crates/infrastructure/src/lib.rs:12#![forbid(unsafe_code)]reste INTACT. Pas de pre_exec, pas d'unsafe. Une seule exigence pour QA, à garder verte en permanence (déjà couverte par le test de parité) : l'invariant « écriture hors-grant bloquée sur le chemin structuré » est le garde-fou qui détecterait toute régression d'héritage si le mécanisme de spawn venait à changer. Tant qu'il est vert, (a) est prouvée. - 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_infoest 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 profilrate_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 agentStalled(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 commeRateLimited { until: Option<Instant> }. Tout le savoir spécifique modèle reste confiné aux adapters/profils. REPRISE (model-agnostique, briques existantes) : pivot sur leconversation_iddu moteur +--resumenatif, déjà câblés (session/claude.rs build_spawn_line + application/agent/resume.rs ListResumableAgents). Le--resumeporte 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_eventClaude, 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 —Instantrejeté. Le domaine parlei64époche-ms (viaClock::now_millis),Instantest non sérialisable/monotone/absent. ⇒RateLimited { resets_at_ms: Option<i64> }. Déviation assumée de ta proposition. 2. T2 — pas de regex dans le domaine.prompt_ready_patterna été choisi littéral exprès pour éviter la dépregex. ⇒ le domaine stocke la donnée (RateLimitPattern{pattern,…}), le moteur regex + parsing d'heure vivent en infra (regexajouté au seulCargo.tomld'infrastructure). 3. T3 —Clockne réveille pas. Il donne l'heure, pas un timer. ⇒ 1 seul nouveau portScheduler(arm/cancel), tout le reste réutilise l'existant. 4. T4 —RateLimitednon terminal. Le contratReplyStreamdit « seulFinalest terminal » etclaude.rsrompt surFinal. ⇒RateLimiteds'intercale commeHeartbeat. Point d'intégration : un tour clos sansFinalparce que limité ne doit pas devenirAgentSessionError::Io—drain_boundeddoit le traiter en fin gracieuse. 5. T5 — niveau 3 dépend du lot 2.ReadinessSignal::Stalledest 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 ; adapterTokioScheduler(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éutiliseAgentSessionFactory::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 : VOSessionLimit+ fn pureplan_resume). - infra :session/claude.rs(lireresetsAt→époche-ms au lieu de jeter en Heartbeat, ligne ~90),session/codex.rs(si signal Codex),ratelimit/(NEW :RateLimitParserregex 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), commandescancel_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 portScheduler+TokioScheduler→ LS4SessionLimitService+ 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 existantListResumableAgentsreprend (popup, pas d'auto). Cohérent. - Auto + annulable =Scheduler::arm/cancel+ eventsAgentResumeScheduled/AgentResumeCancelled. ## Spikes (§21.10) Format réel deresetsAt(é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 (1SessionLimitvivante/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 → aucunDomainEvent::AgentRateLimited/ResumeScheduled/Resumed/RateLimitSuspectedn'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 ?), instancierSessionLimitService::new(clock, scheduler, events, resumer). QuelSchedulerconcret (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émenterAgentResumer::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 voisinesHandoffProvider/ProviderSessionProvidermentionnées dans session_limit.rs. 3. Tap détection niveau 1 (structuré) : où le fluxReplyStream/ReplyEventdes sessions structurées est consommé aujourd'hui (le « pump » qui draine les tours — cf. chat.rschunk_from_event), et comment y intercepterReplyEvent::RateLimited{resets_at_ms}pour appelerservice.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 uninfrastructure::RateLimitParser(règle de sélectionratelimit::applies(profile)) pour le nourrir des fragments et router unSessionLimitdétecté vers le service. Respect de l'anti-double-détection §21.10-4. 5. Drain du Scheduler : commentTokioSchedulerremet lesScheduledTask::ResumeAgentéchus (canal de remise) et où câbler le récepteur qui appelleservice.execute_resume(task)sur le runtime Tokio. 6. Commande Tauri d'annulation : exposercancel_resume(agent_id)comme#[tauri::command](la fenêtre annulable côté UI l'appellera en LS8) — où l'enregistrer dans legenerate_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 laScheduledTaskéchue danstx; le drain détientrx. - 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électionratelimit::applies(profile) -> bool(=structured_adapter.is_none() && rate_limit_pattern.is_some()). Réexportésinfrastructure::RateLimitParser/infrastructure::ratelimit::applies. - LS6 a déjà câblé les 6 variantesDomainEvent::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, dansAppState::build(à partir de la ligne 355). Réutiliser les adapters déjà construits en tête debuild: -clock(l.358,SystemClock, implémenteClock::now_millis) → caster enArc<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 parspawn_relay). Séquence d'instanciation (à placer après la construction delaunch_agentl.668-700 et deproject_storel.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 champpub session_limit_service: Arc<SessionLimitService>à la structAppState(déclaration vers l.326, à côté deorchestrator_service) et le renvoyer dans le littéral final (l.967-1055). LeArcest partagé par : (a) la commandecancel_resume(§6), (b) la tâche de drain (§5), (c) les taps de détection N1/N2 (§3/§4) qui appellenton_rate_limited. Leresume_rxn'entre pas dansAppState: il est moved dans la tâche de drain spawné à l'intérieur debuild(§5), exactement commesweep_stalled(l.899-911). Imports à ajouter en tête destate.rs:application::{SessionLimitService, AgentResumer},domain::ports::{Scheduler, ScheduledTask},infrastructure::TokioScheduler. --- ## 2. PortAgentResumer→LaunchAgent(adapter app-tauri) Nouvel adapter danscrates/app-tauri/src/state.rs, à côté des passerelles stateless existantesAppHandoffProvider(l.100-109) /AppProviderSessionProvider(l.119-128) /AppRecordTurnProvider(l.81-90) — même patronimpl application::Trait for AppXxx.impl application::AgentResumer for AppAgentResumer { async fn resume(agent_id, node_id, conversation_id, resume_prompt) -> Result<(), AppError> }recompose unLaunchAgentInputet appelleself.launch_agent.execute(...)(le mêmeArc<LaunchAgent>que la commandelaunch_agent, l.1140). C'estLaunchAgentqui, viawith_handoff_provider/with_provider_session_provider(l.687-695) et le routage §17.4, applique déjàSessionPlan::Resumequand unconversation_idest présent (chemin de reprise P7/P8b/§15). Leresume_prompt(constanteapplication::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 premiersend. À 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::resumeetScheduledTask::ResumeAgentne portent pas deproject_id, alors queLaunchAgentInputexige unProjectcomplet +rows/cols+mcp_runtime(cf. commande l.1126-1152). Le service est volontairement model/projet-agnostique. Il faut donc que l'adapter résolve leProjectà partir de l'agent_id. Recommandation :AppAgentResumerdétient unArc<Mutex<HashMap<AgentId, ResumeContext>>>(ResumeContext = { project: Project, rows: u16, cols: u16 }) alimenté par le chemin de lancement (commandelaunch_agent, l.1140, oùproject/rows/colssont en main) et lu au moment du resume. Lemcp_runtimeest recalculé dansresumeà partir deproject.idviacrate::mcp_endpoint::{idea_exe_path, mcp_endpoint}(recette identique à l.1126-1133). Évite un scan coûteux deproject_store.list_projects+ manifestes.store_portreste 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'estListResumableAgentsqui reprend, pas le scheduler). Dépendances capturées par l'adapter :Arc<LaunchAgent>,Arc<dyn ProjectStore>(repli), le registreResumeContextpartagé. --- ## 3. Tap détection niveau 1 (structuré) Fichier :crates/app-tauri/src/commands.rs, fnagent_send, boucle de pump l.1283-1294. C'est le seul drain actif d'unReplyStreamstructuré (for event in stream { ... }). Aujourd'huichunk_from_event(crates/app-tauri/src/chat.rs:186-193) mappeReplyEvent::RateLimited→ None (jeté). Le tap : avant d'appelerchunk_from_event, faire unif 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'auFinal). Récupération denode_id+agent_id: le pump ne connaît quesid: SessionId. Le registrestate.structured_sessions(crates/application/src/terminal/registry.rs:297) mappeSessionId → (agent_id, node_id)— mais il manque un accès par session_id. Ajouter une petite méthodemeta_for_session(&SessionId) -> Option<(AgentId, NodeId)>surStructuredSessions(jumeau trivial delive_agentsl.388, lookup direct dansentries).conversation_id:StructuredEntryne le porte pas ; passerNone(le resume dégrade proprement sans id, contratScheduledTask.conversation_id: Option) ou, plus précis, le lire viaproviders.json(passerelleAppProviderSessionProvider) — optionnel,Noneest acceptable pour LS7. État actuel : ce tap est DORMANT. En composition B-2 la fabrique structurée est décâblée (launch_agentretombe toujours sur PTY, l.645-653 ; orchestrateur sans.with_structured, l.947-958). Aucun agent n'a de session structurée vivante ⇒agent_sendn'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_sendastate: State<AppState>⇒Arc::clone(&state.session_limit_service)avant lethread::spawn(l.1282) et le move dans le thread. --- ## 4. Tap détection niveau 2 (PTY) Fichier :crates/app-tauri/src/commands.rs, fnlaunch_agent, branche PTY l.1165-1190 (leif output.structured.is_none()+ lethread::spawndu 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 leAgentProfilede l'agent et appelerinfrastructure::ratelimit::applies(&profile).appliesimpose déjàstructured_adapter.is_none()(or on est dans la brancheoutput.structured.is_none()⇒ cohérent) etrate_limit_pattern.is_some(). Besoin de wiring : la commandelaunch_agentne charge pas le profil. Deux options — (a) exposer leAgentProfile(ou au minimum leRateLimitPattern) résolu surLaunchAgentOutput(LaunchAgent::executele 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)(retourneOption⇒ 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 bouclefor chunk in stream(l.1177), aprèssend_output, décoder le fragment (String::from_utf8_lossy) et appelerparser.detect(&text, clock.now_millis()). SurSome(SessionLimit)⇒service.on_rate_limited(agent_id, node_id, conversation_id, limit.resets_at_ms). Iciagent_id,node_id(l.1115) etconversation_id(request.conversation_id, l.1150) sont déjà en main dans la commande ⇒ les cloner avant lethread::spawn. IdemArc::clone(&state.session_limit_service)et unArc<dyn Clock>(ajouter un champclockàAppState, ou réutiliserSystemMillisClock). Anti-double-détection : garantie par construction —appliesne renvoietrueque pour les agents sans adapter structuré ; un agent structuré (N1) n'arme jamais de parser N2. Un seul tap actif par agent. Fragilité fragmentation :detectreç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 SchedulerTokioScheduler(crates/infrastructure/src/scheduler/mod.rs:62-85) pousse laScheduledTaskéchue dans sontx(canalmpscnon borné). Le récepteurresume_rx(créé en §1) est drainé dans une tâche détachée spawné à l'intérieur deAppState::build, sur le patron exact du sweepersweep_stalled(crates/app-tauri/src/state.rs:899-911) —tauri::async_runtime::spawn(et pastokio::spawn:buildtourne dans le hooksetupsans 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 */ } }. LeArc<SessionLimitService>etresume_rxsont moved dans la closure.execute_resumedésarme l'entrée puis appelleAgentResumer::resumepuis publieAgentResumed(déjà relayé par LS6). La tâche vit autant que l'app (le canal se ferme au drop duTokioScheduler/AppState). --- ## 6. Commande Tauricancel_resumeDé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_idexiste déjà, cf. l.1111/1215.) Retourne lebool(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, danstauri::generate_handler — ajoutercommands::cancel_resume,à côté des commandes agent (p.ex. aprèscommands::change_agent_profile, l.166). LS8 (UI fenêtre annulable) l'appellera viainvoke("cancel_resume", { agentId }). --- ## Ordre d'implémentation recommandé 1. Service + scheduler + drain (§1 + §5) avec unAgentResumerstub (renvoieOk(())) ⇒ valide queon_rate_limitedarme, que le canal remet la tâche, queexecute_resume/AgentResumedpartent. 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 leRateLimitPattern/AgentProfilesurLaunchAgentOutput. 4.AppAgentResumerréel (§2) + registreResumeContextalimenté parlaunch_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ésolutionagent_id → Projectau resume (§2) : le gros risque. Vérifier qu'un resume après fermeture/réouverture de projet ne plante pas (repliNone/erreur propre, jamais de panique). Tester resume quand leResumeContextest absent. - Cancel « pile au tir » (service l.188-213, schedulercancell.87-100) : sous runtime multi-thread,cancelpeut renvoyerfalsecar la tâche vient de tirer ⇒ pas d'AgentResumeCancelled, la reprise suit son cours. UI LS8 doit tolérer unfalse(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 ⇒disarmpuis ré-arm ; vérifier qu'on n'empile pas deuxScheduledTasket qu'un seulAgentResumedsort. - Fragmentation PTY (§4) : motif coupé entre chunks (best-effort en LS7, buffer glissant en repli si raté). - Injection duresume_prompten 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 avecrelease_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/EventBusne portent aucune I/O ni regex. ✅ - Regex + parsing d'heure confinés infra :RateLimitParserettimeparsevivent danscrates/infrastructure/src/ratelimit|timeparse; ils produisent une valeur domaine (SessionLimit). Aucuneregexne franchit la frontière. ✅ - Service applicatif pur-ports :SessionLimitServicene dépend que de traits (Clock/Scheduler/EventBus/AgentResumer). ✅ - Composition root seul à connaître le concret :TokioScheduler,RateLimitParser,AppAgentResumer, le canalmpscet les taps sont tous dansapp-tauri(state.rs/commands.rs/lib.rs), jamais ailleurs.AgentResumerest un port applicatif implémenté à la racine, exactement commeHandoffProvider/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 Tauricancel_resume. GAP constaté sur le niveau 3 : l'événementagentRateLimitSuspected(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 typeset_resume_at(agentId, epochMs)qui arme le mêmeSessionLimitService(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::Humanexiste 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 deResumePlan::HumanFallbackdit : « 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 (cheminListResumableAgents) 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::Scheduledcouvrent déjà tout. Une heure saisie par l'utilisateur est fonctionnellement uneSessionLimitde sourceHumanavecresets_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 PASon_rate_limited(sémantique « signal de détection auto »). Ajoute une entrée dédiée, en réutilisant strictement la brancheScheduledexistante :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 brancheResumePlan::Scheduleddeon_rate_limited(publishAgentRateLimited{Some}→disarm→scheduler.arm(ResumeAgent)→ mémoriser leScheduleId→ publishAgentResumeScheduled), avecSessionLimit::new(Some(resets_at_ms), now, RateLimitSource::Human). Factorise la branche en unfn 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_resumeetcancel_resumerestent 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 decancel_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ésoudrenode_idetconversation_idcôté backend depuis la registry (le front n'a que l'agent_id;agentRateLimitSuspectedne porte que ça) : -node_idviaTerminalSessions::node_for_agent(&id)(ou la registry unifiée). SiNone⇒ErrorDtoINVALID/NOT_FOUND (l'agent n'a plus de cellule vivante — la saisie n'a pas de cible). -conversation_idvia la session structuréesession_for_agent(&id).map(|s| s.conversation_id())(best-effortNonetoléré, comme tout le chemin de reprise dégradé). Puisstate.session_limit_service.confirm_human_resume(id, node_id, conv_id, resets_at_ms). Enregistre la commande danslib.rsà côté decancel_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 deAgentResumeScheduled(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) }puisAgentResumeScheduled { agent_id, fire_at_ms }. Le front les consomme déjà. La transition « suspecté → programmé » se fait donc sans code de présentation neuf :AgentResumeScheduledest le pivot qui retire l'état « heure inconnue » et réutilise le rendu nominal. Annulation ⇒AgentResumeCancelled(inchangé). Reprise ⇒AgentResumed(inchangé).AgentRateLimitSuspectedreste 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_resumeclampe ànow⇒ reprise quasi-immédiate. Test à ajouter. - Re-saisie / second suspect :confirm_human_resumedoit passer par le mêmedisarmque 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 parcancel_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 mergefeature/* → 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émentalhandoff.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 deidea_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écrithead.texten un seulhandle.write(encoder.encode(head.text)), attendsubmitDelayMspuis écrit\r; chemin headlesscrates/infrastructure/src/input/mod.rs::make_headless_sinkécrit aussitext.as_bytes()d'un coup puis submit.makeTerminalHandlesérialise déjà les invokes etPortablePtyAdapter::writefaitwrite_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 testsuseWritePortalqui 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 execdepuis le run dir isolé et doit permettre l’écriture dans le project root quand les permissions résolues donnent workspace-write. Proposition: faire transiterPreparedContext.project_root/équivalent depuisStructuredSessionFactory::startversCodexExecSession, puis ajouter--add-dir <projectRoot>à la lignecodex execen workspace-write, sans ajouter--ask-for-approvalcarcodex execle 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_rootdans un DTO/VO de contexte préparé (PreparedContext.project_rootou é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::startpeut 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 <projectRoot>dansCodexExecSessionest au bon endroit siCodexExecSessionest l’adapter infrastructure/presentation-backend spécifique à Codex CLI. Le flag--add-direst un détail technique de l’adapter Codex, donc il ne doit pas remonter dans le domaine. - Conditionner cet ajout àworkspace-writeest 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-approvalest correct sicodex execle 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 decodex exec,--add-dir,--sandbox, ni des noms de flags Codex. Ces détails restent dans l’adapter Codex. - Ne pas laisserCodexExecSessionrecalculer 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-dirdoit être ajouté uniquement pour leprojectRootvalidé/résolu du projet et uniquement quand le mode effectif estworkspace-write. - Ne pas confondrecwdet périmètre d’écriture : le cwd peut rester le run dir isolé pour l’exécution/session, tandis queproject_rootdevient 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 commeworkspace_roots,writable_roots,project_root, ouExecutionScope { cwd, project_root, sandbox_mode }. Puis l’adapter Codex traduitExecutionScope + workspace-writeencodex 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 diagnosticcrates/application/src/diag.rspour le rendez-vous inter-agents. - Ne pas stash/switch. Premier chantier logique: finirorchestrator-designationsur 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 courantorchestrator-designationa 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 jourarchitect.mdmaintenant ou attendre la fin de la stabilisation. Ne code pas. Reponds avecidea_replysi 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-designationest intégré localement surdevelop(develop ahead origin, AppImage 0.3.0 produite). Le prochain chantier recommandé par Git estfeature/agent-skill-awareness, mais il faut d'abord trancher son chevauchement avec une branche/initiativefeature/agent-skillsmentionnée dans les notes. Merci de cadrer précisément: 1) objectif produit/technique deagent-skill-awareness, 2) différence avecagent-skillssi 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é :developest ahead origin et contient déjà L12agent-skillsdans le code courant (Skill,SkillStore, CRUD, assignation agent↔skill, injection des bodies dans le convention file). La branchefeature/agent-skill-awarenessdiverge avant l’intégrationorchestrator-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 depuisdevelop. ## 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 parname + description; - outil MCP read-onlyidea_skill_read(name)pour charger le body complet à la demande ; - champ optionneldescriptionsurSkill, avec fallback déterministe viaeffective_description(); - rétrocompat des anciensskills/index.jsonsansdescription; - conserver un mode non-MCP compatible. Point à trancher avec Architecture : soit garder l’ancien dump complet pourmcp == None, soit intégrer cette découverte au futur binaire universelidea/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” danscompose_convention_file. Attention :ARCHITECTURE.mdrécent pousse une décision plus structurante : remplacer la prose libre d’orchestration par un skill built-inOrchestration 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 deagent-skill-awarenessdoit rester : “découverte et lecture des skills assignés”. ## 2) Différence avecfeature/agent-skillsagent-skillsvisible dans le repo = L12, déjà intégré fonctionnellement : - domaine :Skill,SkillScope::{Global, Project},SkillRef,Agent.skills/ManifestEntry.skills; - port :SkillStore; - infra :FsSkillStoreglobal + projet ; - application :CreateSkill,UpdateSkill,ListSkills,DeleteSkill,AssignSkillToAgent,UnassignSkillFromAgent; - backend Tauri + DTO ; - frontend :features/skills,SkillGateway, assignation dansAgentsPanel; - lancement :LaunchAgentrésout les skills assignés et injecte leurs bodies danscompose_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 viaidea_skill_read; - ajoutedescriptioncomme méta courte, pas une nouvelle famille de skills. Donc ce n’est pas un doublon deagent-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 : - ajouterdescription: Option<String>àSkillavec#[serde(default)]; - ajouterSkill::with_descriptionetSkill::effective_description(); - préserverwith_content()en conservant la description ; - éventuellement ajouterOrchestratorCommand::ReadSkill { name, requester? }et actionskill.readdansOrchestratorRequest::validatesi on garde le routage MCP via modèle orchestrator. Backend Application : -CreateSkillInputreçoitdescription: Option<String>; -UpdateSkilldoit idéalement pouvoir modifierdescriptionaussi, pas seulementcontent, sinon le frontend ne peut pas éditer la méta ; - créerReadSkilluse case read-only sur le port existantSkillStore, 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 : - modifiercompose_convention_filepour les profils MCP : section# Skills disponiblesen amont, lignes déterministes**name** — description, mentionnantidea_skill_read(name=...); - garder ordre déterministe des skills assignés selon le manifest ; - décider comportement non-MCP. Option conservatrice : garder le dump# Skillscomplet seulement en non-MCP pour zéro régression. Option cible architecture : passer par skill built-in + futur CLIidea, mais c’est un autre lot. Infrastructure / MCP : - ajouteridea_skill_readau catalogue MCP et au mapping tool →OrchestratorCommand::ReadSkill; - dispatcher dansOrchestratorServiceversReadSkill, retour inline Markdown ; - câbler dansstate.rs/composition root ; - mettre à jour les tests de compteur/catalogue MCP, actuellement sensibles aux nombres fixes. App-Tauri DTO/commands : -SkillDtobénéficie du champ automatiquement siSkillsérialise camelCase ; -CreateSkillRequestDtoetUpdateSkillRequestDtodoivent porterdescription?: string | null; - commandes UI existantescreate_skill/update_skillrestent les mêmes noms. Frontend : -domain Skillajoutedescription?: string | null; -CreateSkillInputajoutedescription?: string;updateSkilldoit permettre de passer description + content, pas seulement content ; -SkillEditorajoute un champ court “Description” ; en edit, description éditable ; -SkillsPanelpeut 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.mdou note dédiée : préciser que L12 crée/assigne/injecte, et queagent-skill-awarenessajoute 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 implslist/get/savetransportent simplement le champ. -FsSkillStoreindex : ajouterdescriptiondansIndexEntry,#[serde(default)], écriture camelCase. Le body reste dansmd/<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 completdescription + contentpour rester aligné UI. - nouveau use caseReadSkill { SkillStore }: input{ name, project_root }, outputMarkdownDocou DTO{ name, scope, contentMd }si on veut plus de traçabilité. Branche historique renvoyait seulementMarkdownDoc; acceptable pour MCP. -OrchestratorRequest: action/typeskill.read, champnamerequis. -OrchestratorCommand::ReadSkill { name, requester? }. Lerequesterest utile si la couche MCP veut garder la symétrie avec context/memory, mais le use case n’en a pas besoin. - MCP toolidea_skill_read: input{ name: string }, output contenu Markdown inline. - DTO Tauri/TS :Skill.description?: string | null, create/update requests. À ne pas créer : - pas deSkillAwarenessStore; - 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 deorchestrator-designation; merge brut très risqué ; - conflit conceptuel avec la décision architectureSkillScope::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 depuisdevelop, par exemplefeature/agent-skill-awareness-v2; ne cherry-pick que si les hunks sont triviaux, sinon réimplémenter depuis la note. Garderfeature/agent-skill-awarenesscomme référence de design/test, pas comme base de merge. 2. DevBackend T1 : domaine + serde + tests purs (description, fallback,with_contentconserve description, legacy JSON). 3. DevBackend T2 :FsSkillStore, use cases create/update/list, DTO Tauri. Tests store legacy + roundtrip. 4. DevBackend T3 :ReadSkill+OrchestratorCommand::ReadSkill+ validationskill.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_fileawareness. Tests : MCP avec skills affiche# Skills disponibles, descriptions, appelidea_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 futurSkillScope::Builtin. 9. QA T8 :cargo test --workspace,pnpm test, puis e2e AppImage : agent neuf + skill assigné + profil MCP voit# Skills disponibles, appelleidea_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 depuisdevelop, avec périmètre serré et sans avaler le vieux diff. - Prompt: Reprise chantier
feature/agent-skill-awareness-v2après hotfix livraison délégation/logs submit. Contexte: branche actuellefeature/agent-skill-awareness-v2; dirty attendu avec fichiers runtime.ideai/*et changements code du hotfix listés dans la mémoirecheckpoint-delivery-submit-logging-fix. Le checkpoint précédent disait de cadreragent-skill-awarenessvsagent-skillsavant 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, scopesGlobal|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# Skillssont 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_skillcôté MCP, ouskill.createcô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 flouawarenessvsskills: 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 deProject root, orchestration, contexte projet, persona, skills, mémoire, handoff. - Ajouter une sous-section courte, stable et testable, par exemple dans# Orchestration IdeAou 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# Skillssi 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 outilsidea_create_skill/ outils natifs IdeA. -mcp_enabled=false: mentionner le protocole fichierskill.create. À ne pas toucher pour le MVP : - Pas de nouveauSkillStore. - Pas de nouveau port domaine. - Pas de nouveau DTO Tauri. - Pas de mutation du manifeste. - Pas de changement des invariantsSkill,SkillRef,Agent.skills. - Pas de scopeBuiltintant 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éintroduireBuiltinserait 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/deletereste la seule abstraction de persistance. -LaunchAgent::resolve_skillsreste best-effort pour les dangling refs : un skill supprimé ne bloque pas le launch. -compose_convention_filereste 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> }ouListSkillsavec scope requis. - Alias protocole :skill.list. - Outil MCP :idea_list_skills(scope?). -OrchestratorServiceréutilise leSkillStoreexistant et renvoie une liste compacte{ id, name, scope }, pascontentMdpar défaut. Ce serait un lot séparé, parce que cela touchedomain/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. Modifiercompose_convention_fileuniquement - Ajouter un helper pur si utile, par exemplecompose_skill_awareness(mcp_enabled: bool) -> &'static str. - Ne pas déplacer la section# Skillsni 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 applicationagent/lifecycle. - Puiscargo test -p application agentou 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émenterskill.list/idea_list_skillscomme nouvelle entrée orchestrateur compacte. ### Point d’attention Le fichierARCHITECTURE.mdcontient encore des sections historiques sur un vieux planBuiltin skill + idea CLI + inbox/outbox, mais l’état réel du code est déjà surAskAgent,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.