# IdeA — Cartographie d'Architecture > Document de référence produit par l'**Agent Architecture**. > Fait autorité sur les frontières, ports, adapters, modules et conventions. > Toute feature DOIT être validée contre ce document avant développement. > Architecture **Hexagonale (Ports & Adapters)** + **SOLID**, stricte. > > Stack non négociable : Tauri v2 (shell) · Rust (cœur hexagonal) · TypeScript + React (UI) · xterm.js + portable-pty (terminaux) · git2/libgit2 · russh/ssh2 · wsl.exe. > > **Principe fondateur** : IdeA est un IDE 100 % IA dont le rôle est de **refléter fidèlement la façon dont on travaille avec des IAs** — sans jamais dépendre des commandes, flags ou conventions d'un modèle en particulier. Les deux abstractions de premier rang sont les **Agents** (instances IA à rôle/contexte définis) et les **Skills** (workflows réutilisables). Ces deux concepts sont gérés par IdeA de façon universelle : un utilisateur qui passe de Claude Code à Gemini CLI ou Codex retrouve exactement les mêmes Agents et Skills — seul le moteur d'exécution change. --- ## 1. Principes : SOLID + Hexagonal, appliqués concrètement ### 1.1 Règle de dépendance (la seule qui compte) ``` ┌─────────────────────────────────────────────┐ │ Le sens des dépendances │ │ │ Présentation ─► Application ─► Domaine ◄─ Infrastructure │ (React/Tauri) (use cases) (pur) (adapters) │ │ │ └─────────────────────────────────────────────┘ ``` - **Le Domaine ne dépend de RIEN** : ni Tauri, ni tokio, ni git2, ni portable-pty, ni serde (le moins possible — voir §1.4). Il ne contient que des entités, value objects, règles métier et **traits = ports**. - **L'Application** dépend du Domaine. Elle orchestre les use cases en parlant **uniquement aux ports** (traits), jamais aux adapters concrets. - **L'Infrastructure** dépend du Domaine et de l'Application (elle implémente les ports). Elle contient tous les détails techniques (PTY, FS, git, SSH, WSL, stores). - **La Présentation** (Tauri commands + React) dépend de l'Application. Les commandes Tauri sont des **adapters entrants (driving adapters)** ; les impl de ports sont des **adapters sortants (driven adapters)**. Aucune flèche ne pointe **vers** la présentation ou l'infrastructure. L'inversion de dépendance (le **D** de SOLID) est matérialisée par les traits définis dans le domaine et implémentés dehors. ### 1.2 SOLID, point par point, traduit IdeA | Principe | Application concrète | |---|---| | **S** — Single Responsibility | Un use case = une intention métier (`LaunchAgent`, `SyncAgentWithTemplate`). Un adapter = une techno (`Git2Repository` ne fait que du git). Le `LayoutNode` ne gère que la topologie, pas le rendu. | | **O** — Open/Closed | Ajouter une IA = ajouter un **profil déclaratif** (donnée), pas du code. Ajouter un mode distant = nouvel adapter `RemoteHost` sans toucher aux use cases. Ajouter une stratégie d'injection de contexte = nouvelle variante d'enum + handler, use case inchangé. | | **L** — Liskov | Tout `RemoteHost` (local, SSH, WSL) est substituable : un use case marche identiquement quelle que soit l'impl. Les contrats (pré/postconditions) des ports sont documentés et respectés par chaque adapter. | | **I** — Interface Segregation | Ports **fins et ciblés** : `ProcessSpawner`, `FileSystem`, `PtyPort` séparés plutôt qu'un `System` fourre-tout. Un use case ne reçoit que les ports qu'il consomme. | | **D** — Dependency Inversion | Domaine définit les traits ; infra les implémente ; l'application reçoit des `Arc` par **injection** (composition root dans la couche Tauri). | ### 1.3 Hexagonal côté Frontend (React aussi) L'hexagonal ne s'arrête pas à Rust. Côté React on applique le même découpage : - **Domaine UI / modèles de vue** : types TS purs (miroir des DTO), logique de présentation pure (ex. calcul de tailles de cellules d'un `LayoutNode`), testable sans React ni Tauri. - **Ports UI** : interfaces TS (`AgentGateway`, `TerminalGateway`, `ProjectGateway`, `LayoutGateway`, `GitGateway`, `RemoteGateway`) décrivant **ce dont l'UI a besoin**, indépendamment du transport. - **Adapters UI** : implémentation des ports via `@tauri-apps/api` (`invoke` pour commands, `listen` pour events). Remplaçables par des **mocks** en test/Storybook. - **Présentation** : composants React, hooks, state (Zustand/Redux) qui consomment les ports UI, jamais `invoke()` en direct. Bénéfice : le frontend est testable et développable sans backend (adapters mock), et la frontière IPC est centralisée en un seul endroit. ### 1.4 Domaine pur vs adapters — règle pratique Rust - Le crate `domain` est **`#![no_std]`-friendly d'esprit** (pas imposé), sans dépendance I/O. Tolérance pragmatique : `serde` est autorisé **uniquement** pour dériver la (dé)sérialisation des entités persistées (manifeste, layout, profils), car c'est une contrainte métier de format, pas un détail technique d'I/O. Les **traits/ports** y vivent. Pas de `tokio`, pas de `std::process`, pas de `std::fs`. - Tout ce qui touche le monde réel (`std::fs`, `Command`, sockets, libgit2, PTY) vit **exclusivement** dans `infrastructure`. --- ## 2. Découpage en couches & frontière Rust ↔ Tauri ↔ React ``` ┌───────────────────────────────────────────────────────────────────────┐ │ PRÉSENTATION (Frontend) — TypeScript + React + xterm.js │ │ features/* · ui-ports (gateways) · tauri-adapters (invoke/listen) │ └───────────────────────────────┬───────────────────────────────────────┘ │ IPC Tauri (commands ⇄ events, JSON) ┌───────────────────────────────▼───────────────────────────────────────┐ │ PRÉSENTATION (Backend) — crate `app-tauri` (DRIVING ADAPTER) │ │ #[tauri::command] handlers · event emitters · COMPOSITION ROOT (DI) │ │ PTY byte-stream bridge ⇄ xterm.js │ └───────────────────────────────┬───────────────────────────────────────┘ │ appels de use cases (Arc) ┌───────────────────────────────▼───────────────────────────────────────┐ │ APPLICATION — crate `application` │ │ Use cases / services · DTOs · orchestration · transactions métier │ │ Dépend UNIQUEMENT des ports (traits) du domaine │ └───────────────────────────────┬───────────────────────────────────────┘ │ implémente / consomme ┌───────────────────────────────▼───────────────────────────────────────┐ │ DOMAINE — crate `domain` (PUR, sans I/O) │ │ Entities · Value Objects · Invariants · PORTS (traits) · DomainEvents │ └───────────────────────────────▲───────────────────────────────────────┘ │ implémentent les ports (DRIVEN ADAPTERS) ┌───────────────────────────────┴───────────────────────────────────────┐ │ INFRASTRUCTURE — crate `infrastructure` │ │ portable-pty · git2 · russh/ssh2 · wsl.exe · fs local · md/json store │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Frontière IPC Tauri — deux directions - **Commands (Frontend → Backend, request/response)** : `invoke("create_project", {...})`. Le handler `#[tauri::command]` désérialise le DTO, appelle le use case, renvoie un `Result`. **Stateless** côté forme : tout l'état vit dans des services managés via `tauri::State`. - **Events (Backend → Frontend, push)** : flux PTY (octets/base64), changements de statut d'agent, fin de processus, progrès git, drift de template détecté. Émis via `app_handle.emit(...)` / channels Tauri. L'`EventBus` domaine est relayé vers ces events Tauri par un adapter dans `app-tauri`. > **Décision** : le flux PTY haute fréquence passe par des **Tauri Channels** (`tauri::ipc::Channel`) plutôt que des events globaux, pour la perf et l'isolement par session terminal. --- ## 3. Modèle de domaine ### 3.1 Vue d'ensemble (relations) ``` Workspace 1───* Window 1───* Tab 1───1 Project │ │ │ 1 ├──* Agent ─────? AgentTemplate (origine) │ │ │ 1 │ 1 │ └──1 AgentProfile (runtime IA, par réf id) LayoutTree ├──1 GitRepository (LayoutNode récursif) ├──1 RemoteHost (Local | Ssh | Wsl) │ feuilles └──1 AgentManifest (.ideai/agents.json) ▼ TerminalSession 1───? AgentSession 1───1 Agent │ └──? CellBinding (0 ou 1 cellule visible) ``` ### 3.2 Entités & Value Objects (avec invariants) **`ProjectId`, `AgentId`, `TemplateId`, `ProfileId`, `SessionId`, `WindowId`, `TabId`, `NodeId`** — VO `newtype(Uuid)` ou string typée. Invariant : non vide, immuable. **`Project`** (entité, racine d'agrégat projet) - Champs : `id`, `name`, `root: ProjectPath`, `remote: RemoteRef`, `created_at`. - Invariants : `root` doit être un chemin **absolu et valide pour son `RemoteRef`** ; deux projets ne peuvent partager le même `(remote, root)`. **`ProjectPath`** (VO) — chemin absolu normalisé, conscient de la plateforme cible (POSIX vs Windows vs WSL `/mnt/...`). **`Agent`** (entité) - Champs : `id`, `name`, `context: AgentContextRef` (chemin du `.md` dans `.ideai/`), `profile_id: ProfileId`, `origin: AgentOrigin` (`Scratch` | `FromTemplate { template_id, synced_version }`), `synchronized: bool`. - Invariants : `synchronized == true` ⇒ `origin == FromTemplate{..}` (on ne peut pas synchroniser un agent créé from scratch). `context` doit exister à l'activation. `profile_id` doit référencer un `AgentProfile` connu. **`AgentSession`** (entité — exécution active d'un agent IdeA) - Champs : `id`, `agent_id`, `profile_id`, `terminal_session_id`, `requested_by: AgentRequester` (`User` | `Agent { agent_id, session_id? }`), `task: Option`, `visibility: AgentVisibility`, `status` (`Starting|Running|WaitingForUser|Failed|Stopped|Exited{code}`). - Invariants : une session active référence **un seul** `Agent` et **un seul** `AgentProfile` résolu au lancement ; elle peut être visible dans **0 ou 1** cellule. Le profil de l'agent demandeur ne contraint jamais le profil de l'agent cible. **`AgentVisibility` / `CellBinding`** (VO) ``` AgentVisibility = | Background | Visible { node_id: NodeId } ``` - Invariants : une cellule peut afficher 0 ou 1 `AgentSession` active ; une `AgentSession` active peut être attachée à 0 ou 1 cellule visible. Fermer une cellule **détache** la session (`Visible` → `Background`) sans arrêter le process. Ouvrir une cellule sur une session déjà active **réattache** la session et affiche le travail en cours. **`AgentTemplate`** (entité, store global) - Champs : `id`, `name`, `content_md: MarkdownDoc`, `version: TemplateVersion`, `default_profile_id`. - Invariants : `version` **monotone croissante** ; toute modification du `content_md` ⇒ `version + 1` (voir §8). **`AgentProfile`** (entité de config runtime IA — le port `AgentRuntime` est paramétré par elle) - Champs : `id`, `name`, `command: String`, `args: Vec`, `context_injection: ContextInjection`, `detect: Option`, `cwd_template: String` (vaut toujours `"{agentRunDir}"` — voir §9.1 et §14.1). - Invariants : `command` non vide ; cohérence de `ContextInjection` (voir VO ci-dessous). **`ContextInjection`** (VO, enum — cœur du moteur IA flexible) ``` ContextInjection = | ConventionFile { target: String } // ex. "CLAUDE.md" / "AGENTS.md" / "GEMINI.md" | Flag { flag: String } // ex. "--context-file {path}" ou "-f" | Stdin // pipe du contenu md sur stdin | Env { var: String } // ex. "AGENT_CONTEXT_FILE" ``` - Invariants : `ConventionFile.target` est un nom de fichier relatif (pas de `..`, pas absolu) ; `Env.var` est un identifiant d'env valide ; `Flag.flag` non vide. **`TerminalSession`** (entité) - Champs : `id`, `node_id: Option` (cellule visible qui l'héberge, absente si arrière-plan), `cwd: ProjectPath`, `kind: SessionKind` (`Plain` | `Agent { session_id }`), `pty_size: PtySize { rows, cols }`, `status` (`Starting|Running|Exited{code}`). - Invariants : une cellule (feuille de layout) héberge **au plus une** `TerminalSession` active. Une session agent peut conserver son PTY sans cellule visible. `pty_size.rows>0 && cols>0`. **`LayoutNode` / `LayoutTree`** (VO récursif — voir §7 pour le détail complet) - Invariants : poids relatifs strictement positifs ; somme normalisable ; pas de fusion qui chevauche deux conteneurs distincts ; un `Leaf` référence 0 ou 1 `SessionId`. **`RemoteHost`** (VO de stratégie de localisation — abstrait Local/SSH/WSL) ``` RemoteRef = | Local | Ssh { host, port, user, auth: SshAuth, remote_root } | Wsl { distro: String } ``` - Invariants : `Ssh.port` ∈ 1..=65535 ; `Wsl.distro` non vide ; pour `Ssh`/`Wsl`, les chemins projet sont interprétés côté distant. **`GitRepository`** (entité) - Champs : `project_id`, `root`, `current_branch`, `is_dirty`. - Invariants : `root` contient (ou contiendra après init) un `.git`. État dérivé, rafraîchi via le port. **`AgentManifest`** (entité — image en mémoire de `.ideai/agents.json`) - Champs : `entries: Vec`. - Invariants : `synchronized ⇒ template_id.is_some() && synced_template_version.is_some()` ; `md_path` unique ; cohérence avec les `Agent` chargés. **`Workspace` / `Window` / `Tab`** (entités de présentation persistée) - `Workspace` = ensemble des fenêtres d'une session utilisateur. - `Window` = fenêtre OS ; possède un `LayoutTree` **par onglet actif** et une liste de `Tab`. - `Tab` = onglet ⇔ **un `Project`** (1:1). - Invariants : un `Project` ouvert apparaît dans **exactement un** `Tab` à la fois (le drag déplace, ne duplique pas) ; un `Window` a ≥ 1 `Tab` ou est fermée. **`Skill`** (entité) - Champs : `id`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope` (`Global` | `Project`). - Invariants : `name` non vide ; `content_md` non vide. - Un agent référence 0..N skills (dans l'`AgentManifest`). Les skills assignés sont injectés dans son convention file à l'activation. **`DomainEvent`** (enum) — `ProjectCreated`, `AgentLaunched`, `AgentSessionAttached`, `AgentSessionDetached`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `SkillAssigned`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}`, `OrchestratorRequest{requester_id, action}` (ce dernier souvent court-circuité vers un Channel). --- ## 4. Ports (traits du domaine) > Signatures **conceptuelles** (Rust idiomatique, `async` via `async_trait` ou retours `Future` ; erreurs typées par port). « Consommé par » = use cases. « Implémenté par » = adapters de §5. ### `AgentRuntime` - **Rôle** : lancer/piloter la CLI d'une IA selon un `AgentProfile`, en gérant l'injection du contexte `.md`. - **Signature** : ```rust trait AgentRuntime { fn detect(&self, profile: &AgentProfile) -> Result; fn prepare_invocation(&self, profile: &AgentProfile, ctx: &PreparedContext, cwd: &ProjectPath) -> Result; // commande + args + plan d'injection (fichier/flag/stdin/env) } ``` - **Consommé par** : `LaunchAgent`, `DetectProfilesUseCase` (first-run). - **Implémenté par** : `CliAgentRuntime` (un seul adapter générique piloté par le profil déclaratif — c'est l'**Open/Closed**). La diversité des IA = données, pas code. ### `PtyPort` (alias domaine de `TerminalSessionPort`) - **Rôle** : ouvrir un pseudo-terminal, lire/écrire, redimensionner, tuer. - **Signature** : ```rust trait PtyPort { async fn spawn(&self, spec: SpawnSpec, size: PtySize) -> Result; fn write(&self, h: &PtyHandle, data: &[u8]) -> Result<(), PtyError>; fn resize(&self, h: &PtyHandle, size: PtySize) -> Result<(), PtyError>; fn subscribe_output(&self, h: &PtyHandle) -> OutputStream; // flux d'octets async fn kill(&self, h: &PtyHandle) -> Result; } ``` - **Consommé par** : `OpenTerminal`, `LaunchAgent`, `CloseTerminal`. - **Implémenté par** : `PortablePtyAdapter` (local), `SshPtyAdapter` (PTY distant via russh exec/shell), `WslPtyAdapter` (PTY via `wsl.exe`). Sélection par stratégie `RemoteRef` (Liskov). ### `RemoteHost` - **Rôle** : abstraction de la **localisation d'exécution** (local / SSH / WSL) : exécuter une commande, ouvrir un PTY, accéder au FS, dans le bon contexte. - **Signature** : ```rust trait RemoteHost { fn kind(&self) -> RemoteKind; async fn connect(&self) -> Result<(), RemoteError>; fn file_system(&self) -> Arc; fn process_spawner(&self) -> Arc; fn pty(&self) -> Arc; } ``` - **Consommé par** : tous les use cases qui touchent un projet (résolvent leurs ports via le `RemoteHost` du projet → **transparence local/distant**). - **Implémenté par** : `LocalHost`, `SshHost` (russh/ssh2), `WslHost` (wsl.exe). C'est la **stratégie** qui unifie les 3 modes. ### `ProcessSpawner` - **Rôle** : lancer un process **non interactif** et récupérer sortie/exit (ex. `detect`, commandes git hors libgit2, scripts). - **Signature** : `async fn run(&self, spec: SpawnSpec) -> Result;` - **Consommé par** : `DetectProfilesUseCase`, services divers. - **Implémenté par** : `LocalProcessSpawner`, `SshProcessSpawner`, `WslProcessSpawner`. ### `FileSystem` - **Rôle** : lecture/écriture/listing/symlink, neutre vis-à-vis de la localisation. - **Signature** : ```rust trait FileSystem { async fn read(&self, p: &RemotePath) -> Result, FsError>; async fn write(&self, p: &RemotePath, data: &[u8]) -> Result<(), FsError>; async fn exists(&self, p: &RemotePath) -> Result; async fn create_dir_all(&self, p: &RemotePath) -> Result<(), FsError>; async fn list(&self, p: &RemotePath) -> Result, FsError>; async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError>; } ``` - **Consommé par** : `AgentContextStore`, `ProjectStore`, injection `conventionFile`, etc. - **Implémenté par** : `LocalFileSystem` (std::fs/tokio::fs), `SshFileSystem` (SFTP), `WslFileSystem` (via `wsl.exe` ou chemins `\\wsl$`). ### `TemplateStore` - **Rôle** : CRUD des `AgentTemplate` dans le store global IDE + versioning. - **Signature** : `list / get / save / delete / bump_version`. - **Consommé par** : `CreateTemplate`, `UpdateTemplate`, `CreateAgentFromTemplate`, `SyncAgentWithTemplate`. - **Implémenté par** : `FsTemplateStore` (md + index json dans le dossier de données app). ### `ProjectStore` - **Rôle** : persistance de la liste des projets connus, workspaces, windows, tabs, layouts. - **Signature** : `list_projects / load_project / save_project / save_workspace / load_workspace`. - **Consommé par** : `CreateProject`, `OpenProject`, persistance fenêtres/onglets/layout. - **Implémenté par** : `FsProjectStore` (json dans données app pour le registre ; layout par projet dans `.ideai/`). ### `AgentContextStore` - **Rôle** : lire/écrire les `.md` d'agents **et** le manifeste `.ideai/agents.json` (au sein du projet, via le `FileSystem` du `RemoteHost`). - **Signature** : ```rust trait AgentContextStore { async fn read_context(&self, project: &Project, agent: &AgentId) -> Result; async fn write_context(&self, project: &Project, agent: &AgentId, md: &MarkdownDoc) -> Result<(), StoreError>; async fn load_manifest(&self, project: &Project) -> Result; async fn save_manifest(&self, project: &Project, m: &AgentManifest) -> Result<(), StoreError>; } ``` - **Consommé par** : `CreateAgent*`, `LaunchAgent`, `SyncAgentWithTemplate`. - **Implémenté par** : `IdeaiContextStore` (compose `FileSystem`, écrit `.ideai/`). ### `GitRepository` - **Rôle** : opérations git du projet. - **Signature** : `status / stage / unstage / commit / branches / checkout / current_branch / diff / log / pull / push / clone / init`. - **Consommé par** : use cases Git. - **Implémenté par** : `Git2Repository` (libgit2, local) ; sur SSH/WSL, `RemoteGitRepository` délègue à git CLI via `ProcessSpawner` quand libgit2 ne peut pas atteindre le FS distant (point ouvert §13). ### `EventBus` - **Rôle** : publier/souscrire les `DomainEvent` (découple émetteurs et présentation). - **Signature** : `fn publish(&self, e: DomainEvent); fn subscribe(&self) -> EventStream;` - **Consommé par** : tous use cases (publient) ; l'adapter Tauri (souscrit → relaye en events/channels IPC). - **Implémenté par** : `TokioBroadcastEventBus` (in-process), relayé par `TauriEventRelay`. ### `Clock` & `IdGenerator` (ports utilitaires — testabilité) - **Rôle** : éliminer le non-déterminisme (`now()`, `uuid`) du domaine/application. - **Implémenté par** : `SystemClock` / `UuidGenerator` (prod), `FixedClock` / `SeqIdGenerator` (tests). --- ## 5. Adapters (impl concrètes par port) | Port | Adapter(s) | Techno | Notes | |---|---|---|---| | `AgentRuntime` | `CliAgentRuntime` | piloté par `AgentProfile` | Construit `SpawnSpec` + plan d'injection. Un seul adapter, N profils. | | `PtyPort` | `PortablePtyAdapter` | portable-pty | Local. Stream octets → Channel Tauri. | | | `SshPtyAdapter` | russh (channel shell/exec + pty req) | Distant SSH. | | | `WslPtyAdapter` | `wsl.exe -d ` + portable-pty | PTY dans la distro. | | `RemoteHost` | `LocalHost` / `SshHost` / `WslHost` | — / russh,ssh2 / wsl.exe | Stratégie ; fabrique FS/Spawner/PTY adaptés. | | `ProcessSpawner` | `LocalProcessSpawner` | std/tokio `Command` | | | | `SshProcessSpawner` | russh exec | | | | `WslProcessSpawner` | `wsl.exe` | | | `FileSystem` | `LocalFileSystem` | tokio::fs | | | | `SshFileSystem` | SFTP (ssh2/russh-sftp) | | | | `WslFileSystem` | `\\wsl$\` / `wsl.exe cat`… | | | `TemplateStore` | `FsTemplateStore` | tokio::fs + serde_json | Dossier données app. | | `ProjectStore` | `FsProjectStore` | tokio::fs + serde_json | Registre projets + workspace. | | `AgentContextStore` | `IdeaiContextStore` | compose `FileSystem` | Écrit `.ideai/`. | | `GitRepository` | `Git2Repository` | git2 | Local. | | | `RemoteGitRepository` | git CLI via `ProcessSpawner` | SSH/WSL fallback. | | `EventBus` | `TokioBroadcastEventBus` (+ `TauriEventRelay`) | tokio::broadcast | Relais vers IPC. | | `Clock`/`IdGenerator` | `SystemClock`/`UuidGenerator` | std/uuid | Mocks en test. | **Adapters entrants (driving)** : handlers `#[tauri::command]` (frontend → app) + `TauriEventRelay` (app → frontend). Côté UI : `tauri-adapters` implémentant les gateways TS. --- ## 6. Use cases / services applicatifs > Chaque use case : un struct `XxxUseCase` portant ses ports en `Arc`, une méthode `execute(input: XxxInput) -> Result`. **Single Responsibility**. Aucune dépendance à Tauri. | Use case | Rôle | Ports consommés | |---|---|---| | `CreateProject` | Crée un projet (project root), init `.ideai/`, registre. | `ProjectStore`, `FileSystem`, `IdGenerator`, `EventBus` | | `OpenProject` | Charge projet, manifeste, layout, résout `RemoteHost`. | `ProjectStore`, `AgentContextStore`, `RemoteHost` | | `CloseProject` / `CloseTab` | Persiste l'état, libère PTYs. | `ProjectStore`, `PtyPort`, `EventBus` | | `DetectProfiles` (first-run) | Teste `detect` de chaque profil candidat. | `AgentRuntime`, `ProcessSpawner` | | `ConfigureProfiles` | Enregistre profils choisis/édités/custom. | `TemplateStore`/profile store, `FileSystem` | | `CreateAgentFromScratch` | Crée agent + `.md`, met à jour manifeste. | `AgentContextStore`, `IdGenerator` | | `CreateAgentFromTemplate` | Copie le `content_md` du template → agent ; lie origine + version + `synchronized`. | `TemplateStore`, `AgentContextStore` | | `UpdateTemplate` | Modifie un template, **bump version**, signale drift aux agents liés. | `TemplateStore`, `EventBus` | | `DetectAgentDrift` | Compare `synced_template_version` vs `template.version`. | `TemplateStore`, `AgentContextStore` | | `SyncAgentWithTemplate` | Applique la MAJ template→agent si `synchronized`. | `TemplateStore`, `AgentContextStore`, `EventBus` | | `RequestAgentWork` | Demande structurée utilisateur/agent pour faire travailler un agent IdeA cible, sans subagent natif fournisseur. | `AgentContextStore`, `AgentSessionStore`, `AgentRequestQueue`, `EventBus` | | `LaunchAgentSession` / `LaunchAgent` | Résout profil+contexte+mémoire, prépare injection, crée ou reprend une session agent, spawn CLI si nécessaire. | `AgentRuntime`, `AgentContextStore`, `AgentSessionStore`, `RemoteHost`→`PtyPort`/`FileSystem`, `EventBus` | | `AttachAgentSessionToCell` / `DetachAgentSessionFromCell` / `MoveAgentSessionToCell` | Rend visible, détache ou déplace une session active dans la grille sans tuer le process. | `AgentSessionStore`, `ProjectStore`, `EventBus` | | `ListAgentSessions` / `ObserveAgentSession` | Liste les sessions visibles/arrière-plan et observe leur état/logs. | `AgentSessionStore`, `EventBus` | | `StopAgentSession` | Arrête explicitement une session agent et son PTY. | `AgentSessionStore`, `PtyPort`, `EventBus` | | `OpenTerminal` | Ouvre un PTY simple dans une cellule. | `RemoteHost`→`PtyPort`, `EventBus` | | `WriteToTerminal` / `ResizeTerminal` / `CloseTerminal` | I/O PTY. | `PtyPort` | | `MutateLayout` (split/merge/resize/move) | Applique une opération sur le `LayoutTree` (logique **pure** dans le domaine, persistée ici). | `ProjectStore` (persistance) | | `ConnectRemote` (SSH/WSL) | Établit la connexion, valide l'accès au root. | `RemoteHost`, `FileSystem` | | `MoveTabToNewWindow` | Détache un onglet → nouvelle fenêtre (réaffectation `WindowId`). | `ProjectStore`, `EventBus` | | Use cases Git | `GitStatus`, `GitCommit`, `GitCheckout`, `GitPush`, … | `GitRepository`, `EventBus` | --- ## 7. Modèle de layout terminal (grille tableur récursive + fusion) ### 7.1 Structure de données La grille « type tableur, lignes/colonnes imbriquées indépendamment + fusion » est modélisée par un **arbre de splits récursif** où chaque conteneur définit son propre découpage. La **fusion** est obtenue nativement : fusionner = ne pas subdiviser une zone (un `Leaf` couvre plusieurs « cellules visuelles » d'un parent voisin). Pour le cas Excel pur (fusion arbitraire chevauchant la grille), on superpose un modèle **GridContainer** avec spans. ```rust enum LayoutNode { Leaf(LeafCell), Split(SplitContainer), Grid(GridContainer), } struct LeafCell { id: NodeId, session: Option, // 0 ou 1 terminal } struct SplitContainer { // découpage simple binaire/n-aire pondéré id: NodeId, direction: Direction, // Row (colonnes) | Column (lignes) children: Vec, // ordre = gauche→droite / haut→bas } struct WeightedChild { node: LayoutNode, weight: f32 } // poids = part redimensionnable struct GridContainer { // grille tableur avec fusion (spans) id: NodeId, col_weights: Vec, // largeurs de colonnes row_weights: Vec, // hauteurs de lignes cells: Vec, // placements avec spans (fusion) } struct GridCell { node: LayoutNode, // récursif : une cellule peut re-contenir un Split/Grid row: u16, col: u16, row_span: u16, // ≥1 ; >1 = cellules fusionnées verticalement col_span: u16, // ≥1 ; >1 = cellules fusionnées horizontalement } ``` - **Lignes/colonnes indépendantes par zone** : chaque `SplitContainer`/`GridContainer` a ses propres poids ⇒ pas de grille uniforme rigide. - **Imbrication** : un enfant peut être un nouveau `Split`/`Grid` ⇒ « N colonnes dans une ligne, M lignes dans une colonne » de façon arbitraire. - **Fusion** : `row_span`/`col_span` dans `GridContainer` (modèle tableur fidèle) **ou** simplement un `Leaf` plus grand via `SplitContainer` (cas courant). Le domaine supporte les deux ; l'UI choisit la représentation selon l'interaction. ### 7.2 Invariants (validés dans le domaine, testables sans I/O) - Tous les `weight > 0`. Les poids sont **relatifs** (l'UI normalise pour le rendu). - Dans un `GridContainer` : aucune superposition de spans ; toute la surface couverte ; `row+row_span ≤ rows`, `col+col_span ≤ cols`. - Un `SessionId` n'apparaît que dans **un seul** `Leaf`. - Pour une session agent, retirer le `SessionId` d'un `Leaf` détache l'affichage seulement ; l'arrêt du process passe par `StopAgentSession`/`CloseTerminal`, jamais par la fermeture visuelle de cellule. - Les opérations `split`, `merge`, `resize`, `move` sont des **fonctions pures** `LayoutTree -> Result` (immutabilité ⇒ testabilité, undo/redo facile). ### 7.3 Sérialisation & persistance - Sérialisé en **JSON** (serde, `tag`/`content` pour l'enum) → `.ideai/layout.json` (par projet, donc voyage avec le projet, y compris distant). - Le `Workspace`/`Window`/`Tab` (organisation des fenêtres OS) est persisté côté **store global IDE** (machine-local, pas dans le projet) car lié à l'écran de l'utilisateur, pas au code. --- ## 8. Synchronisation template → agents ### 8.1 Versioning - `AgentTemplate.version: u64` monotone. **`UpdateTemplate` incrémente** la version à chaque changement de `content_md`. Un hash du contenu (`content_hash`) est aussi stocké pour détecter les éditions hors-app. - Chaque `ManifestEntry` d'agent lié garde `synced_template_version` = version du template **au dernier sync réussi**. ### 8.2 Détection de drift ``` drift(agent) = agent.synchronized && agent.origin == FromTemplate{ template_id, .. } && template_store.get(template_id).version > entry.synced_template_version ``` `DetectAgentDrift` est lancé à `OpenProject` et après chaque `UpdateTemplate` ; émet `AgentDriftDetected { agent_id, from, to }` → badge UI. ### 8.3 Application de la MAJ (`SyncAgentWithTemplate`) ``` 1. Charger template (version courante) + manifeste projet. 2. Pour chaque agent ciblé avec synchronized==true : a. Stratégie de MAJ = REMPLACEMENT du .md par content_md du template (le contexte d'un agent synchronisé est "possédé" par le template). → Variante future : merge 3-way si l'agent a un bloc local marqué. b. write_context(agent, template.content_md) c. entry.synced_template_version = template.version 3. save_manifest. publish(AgentSynced{..}). ``` ### 8.4 Agents non synchronisés - `synchronized == false` : ne reçoivent **jamais** de MAJ auto. Ils gardent leur `.md` libre. On peut afficher « une nouvelle version du template existe » (info) mais aucune écriture n'a lieu sans action explicite (qui basculerait `synchronized` ou ferait un sync ponctuel one-shot). - Agents `Scratch` : aucun lien template, hors périmètre de sync. --- ## 9. Stockage & arborescence des fichiers ### 9.1 Dans le projet — `.ideai/` (voyage avec le code, versionnable) ``` / ├── .ideai/ │ ├── agents.json # AgentManifest (mapping md ↔ template ↔ sync ↔ version) │ ├── layout.json # LayoutTree de l'onglet (sérialisé) │ ├── project.json # méta projet local (nom, profil par défaut, remote ref) │ ├── CONTEXT.md # contexte projet partagé, injecté à tous les agents/profils │ ├── memory/ │ │ ├── MEMORY.md # index dérivé de rappel mémoire │ │ ├── .md # notes mémoire, source de vérité │ │ └── .index/ # index vectoriel dérivé/reconstructible │ ├── agents/ │ │ ├── reviewer.md # contexte d'un agent de projet │ │ ├── backend-dev.md │ │ └── ... │ ├── skills/ │ │ ├── code-review.md # contexte d'un skill (voir §14.2) │ │ ├── simplify.md │ │ └── ... │ └── run/ │ ├── / # cwd isolé par agent actif (créé à l'activation) │ │ └── CLAUDE.md # fichier de convention généré par IdeA (profil-dépendant) │ └── ... └── (aucun CONTEXT.md/CLAUDE.md/AGENTS.md/GEMINI.md utilisé par IdeA à la racine — voir §14.1) ``` **Règle de désinstallation projet** : tout artefact projet utilisé par IdeA vit sous `.ideai/`. Si l'utilisateur ne veut plus utiliser IdeA sur un projet, il supprime `.ideai/` : contexte projet, agents, skills projet, mémoire, layouts, requêtes d'orchestration et dossiers d'exécution disparaissent ensemble. Les stores machine-locaux (profils globaux, templates globaux, registre des projets récents) ne sont pas des artefacts du projet. **Schéma `agents.json`** : ```json { "version": 1, "agents": [ { "id": "a3f1...", "name": "Backend Dev", "md": "agents/backend-dev.md", "profileId": "claude-code", "origin": { "type": "fromTemplate", "templateId": "tpl-backend", "syncedTemplateVersion": 4 }, "synchronized": true }, { "id": "b7c2...", "name": "Ad-hoc", "md": "agents/adhoc.md", "profileId": "codex-cli", "origin": { "type": "scratch" }, "synchronized": false } ] } ``` ### 9.2 Store global IDE (données app, hors projet, machine-local) Emplacement résolu via Tauri path API (`AppData`/`~/.local/share/IdeA`/`~/Library/Application Support/IdeA`). ``` /IdeA/ ├── profiles.json # AgentProfile[] configurés (first-run + custom + édités) ├── settings.json # préférences IDE ├── workspace.json # Workspace/Window/Tab + quel projet dans quel onglet (machine-local) └── templates/ ├── index.json # [{id, name, version, contentHash, defaultProfileId}] └── md/ ├── tpl-backend.md ├── tpl-reviewer.md └── ... ``` **Schéma `profiles.json` (item)** : exactement le profil déclaratif de CONTEXT.md §9 (`id, name, command, args, contextInjection{strategy,target/flag/var}, detect, cwd`). **Formats** : contextes & templates en **Markdown** ; tout le reste en **JSON** (serde). Pas de base de données : fichiers plats, simples, diffables, portables (AppImage friendly). > **Note** : `.ideai/run/` contient des répertoires d'exécution éphémères (créés à l'activation, nettoyés à la fermeture). Leur contenu (convention files générés) ne doit **pas** être versionné dans git — ajouter `.ideai/run/` au `.gitignore` du projet. --- ## 10. Arborescence du repo ### 10.1 Décision : workspace Cargo **multi-crate** **Multi-crate** retenu (vs mono-crate) pour **forcer** la règle de dépendance à la compilation : le crate `domain` ne peut littéralement pas dépendre de `infrastructure` si ce n'est pas dans son `Cargo.toml`. C'est la garantie mécanique de l'hexagonal (mieux qu'une convention). Coût : un peu de cérémonie de workspace — acceptable et même souhaitable ici vu le découpage en lots/agents (§12). ``` IdeA/ ├── Cargo.toml # [workspace] members ├── ARCHITECTURE.md ├── CONTEXT.md ├── crates/ │ ├── domain/ # PUR : entities, VO, ports (traits), domain events, layout logic │ │ └── src/{project,agent,template,profile,terminal,layout,remote,git,ports,events}.rs │ ├── application/ # use cases, DTOs, AppError ; dépend de domain │ │ └── src/{project,agent,template,terminal,layout,remote,git}/ │ ├── infrastructure/ # adapters ; dépend de domain (+ application pour DTO si besoin) │ │ └── src/{pty,fs,process,remote,git,store,runtime,eventbus}/ │ └── app-tauri/ # binaire Tauri : commands, events, COMPOSITION ROOT (DI) │ ├── src/{commands,events,state,main.rs} │ ├── tauri.conf.json │ ├── build.rs │ └── icons/, bundle (NSIS + AppImage) ├── frontend/ # TypeScript + React (Vite) │ ├── package.json, vite.config.ts, index.html │ └── src/ │ ├── domain/ # types & logique de vue purs (miroir DTO, calc layout) │ ├── ports/ # gateways TS (interfaces) : AgentGateway, TerminalGateway, ... │ ├── adapters/ # impl gateways via @tauri-apps/api (invoke/listen/Channel) │ │ └── mock/ # impl mock pour dev/test/storybook │ ├── features/ # par feature : projects, agents, templates, terminals, layout, git, remote, first-run │ │ └── /{components,hooks,store,index.ts} │ ├── shared/ # ui kit, xterm wrapper, design system │ └── app/ # bootstrap, routing, providers (DI des adapters) └── docs/ # ADRs, schémas ``` `app-tauri` = **seul** endroit qui connaît tous les crates : il instancie les adapters concrets et injecte dans les use cases (composition root). Personne d'autre ne fait de `new ConcreteAdapter`. --- ## 11. Stratégie de tests | Couche | Type de test | Comment / où | |---|---|---| | `domain` | **Unitaires purs** (sans I/O, sans async) | `#[cfg(test)] mod tests` par module. Invariants d'entités, opérations de layout (split/merge/resize), détection de drift, validation `ContextInjection`. Déterministe via `FixedClock`/`SeqIdGenerator`. | | `application` | **Unitaires avec ports mockés** | Chaque use case testé avec des **mocks de ports** (`mockall` ou fakes manuels). Ex. `LaunchAgent` vérifie qu'il appelle `prepare_invocation` puis `pty.spawn` avec le bon `cwd` et plan d'injection. **Aucun vrai PTY/FS/git.** | | `infrastructure` | **Tests d'intégration ciblés** | Par adapter : `LocalFileSystem` sur tmpdir, `Git2Repository` sur repo temporaire, `PortablePtyAdapter` lance `echo`. SSH/WSL : tests `#[ignore]` gated derrière feature/env (CI conditionnelle). | | `app-tauri` | Tests des commands (mapping DTO ↔ use case) | Wiring testé avec use cases réels + adapters in-memory. | | Frontend `domain`/`ports` | **Vitest** (unitaires purs) | Logique de vue, calc tailles cellules, réducteurs de state. | | Frontend `features` | **React Testing Library** + **gateways mock** | Composants testés avec adapters mock ⇒ **sans backend**. | | E2E (plus tard) | Playwright / `tauri-driver` | Smoke tests des parcours clés. | **Clé de testabilité** : grâce aux **ports**, le domaine et l'application se testent **100 % sans I/O**. C'est l'argument central de l'hexagonal et le socle du cycle dev↔test (chaque agent dev appairé à un agent test, cf. CONTEXT §3). Règle d'or : une feature n'est verte que quand `cargo test -p ` et `vitest` passent. --- ## 12. Découpage en lots/features livrables > Chaque lot = périmètre autonome, validable par le cycle dev/test, confiable à **un binôme (agent dev + agent test)**. Ordonnés par dépendance. | # | Lot | Contenu | Crates/zones | |---|---|---|---| | L0 | **Socle domaine & ports** | Entities, VO, **tous les traits ports**, domain events, `AppError`. Aucun adapter. | `domain` (+ ports utilitaires) | | L1 | **Composition root & IPC** | `app-tauri` : DI, registre de commands/events, bridge PTY↔Channel, gateways TS + adapters Tauri + mocks. | `app-tauri`, `frontend/ports`+`adapters` | | L2 | **Projets & stockage** | `CreateProject`/`OpenProject`/`CloseProject`, `FsProjectStore`, `LocalFileSystem`, init `.ideai/`. UI projets/onglets. | `application/project`, `infrastructure/{fs,store}`, `frontend/features/projects` | | L3 | **Terminaux & PTY (local)** | `PtyPort` + `PortablePtyAdapter`, use cases terminal, wrapper xterm.js, flux Channel. | `infrastructure/pty`, `application/terminal`, `frontend/features/terminals` | | L4 | **Layout tableur** | Logique pure `LayoutTree` (déjà en L0 partiellement), `MutateLayout`, persistance `layout.json`, UI grille redimensionnable + fusion. | `domain/layout`, `application/layout`, `frontend/features/layout` | | L5 | **Profils IA & runtime** | `AgentProfile`, `CliAgentRuntime`, `DetectProfiles`, first-run wizard, `profiles.json`. | `infrastructure/runtime`, `application/agent`, `frontend/features/first-run` | | L6 | **Agents & contextes** | `AgentContextStore`/`IdeaiContextStore`, CRUD agents, `LaunchAgent` (injection + spawn + cellule). | `application/agent`, `infrastructure/store`, `frontend/features/agents` | | L7 | **Templates & synchro** | `TemplateStore`, versioning, `DetectAgentDrift`, `SyncAgentWithTemplate`. UI templates + badges drift. | `application/template`, `infrastructure/store`, `frontend/features/templates` | | L8 | **Git** | `GitRepository`/`Git2Repository`, use cases git, UI git. | `infrastructure/git`, `application/git`, `frontend/features/git` | | L9 | **Remote (SSH + WSL)** | `RemoteHost` stratégie, `SshHost`/`WslHost`, adapters FS/PTY/Spawner distants, `RemoteGitRepository`. UI connexion. | `infrastructure/remote`, `application/remote`, `frontend/features/remote` | | L10 | **Fenêtres & multi-window** | `Workspace`/`Window`/`Tab`, `MoveTabToNewWindow`, drag d'onglet → nouvelle fenêtre OS Tauri. | `application`, `app-tauri`, `frontend/app` | | L11 | **Packaging & livraison** | Tauri bundle : NSIS `setup.exe`, **AppImage** multi-distro, CI Linux+Windows. | `app-tauri`, CI | | L12 | **Skills** | Entité `Skill`, `SkillStore`, CRUD skills global+projet, assignation agent↔skills, injection dans convention file à l'activation. UI onglet Skills. | `domain/skill`, `application/skill`, `infrastructure/store`, `frontend/features/skills` | | L13 | **OrchestratorApi** | File-watcher `.ideai/requests/`, port `OrchestratorApi`, adapter `FsOrchestratorAdapter`, protocole `agent.run`/`agent.stop`/`agent.attach`/`agent.detach`/`agent.message`, sessions agent visibles ou arrière-plan. | `domain/agent`, `application/agent`, `infrastructure/orchestrator`, `app-tauri`, `frontend/features/agents` | | L14 | **Mémoire** | Base de connaissance projet model-agnostic. Modèle 2 étages : `.md` source de vérité (port `MemoryStore`), rappel adaptatif (port `MemoryRecall`), embeddings déclaratifs (port `Embedder`), bascule auto sur seuil. Découpé en sous-lots **A/B/C** (voir §14.5). | `domain/memory`, `application/memory`, `infrastructure/store`, `frontend/features/memory` | --- ## 14. Décisions d'architecture figées (2026-06-06) ### 14.1 Isolation du cwd par agent — résolution de la collision de contexte **Problème** : plusieurs agents du même profil (ex. deux instances Claude Code) sur le même project root produisaient une collision — le fichier de convention (`CLAUDE.md`, `AGENTS.md`…) est un emplacement fixe unique à la racine. **Décision** : le cwd du PTY d'un agent n'est **jamais** le project root. C'est `.ideai/run//`, un dossier créé par IdeA à l'activation et nettoyé à la fermeture. **Convention file généré par IdeA** : IdeA écrit dans ce dossier le fichier conventionnel attendu par le profil (`CLAUDE.md`, `AGENTS.md`, etc.). Ce fichier contient : 1. Le **chemin absolu du project root** (pour que l'agent sache où opérer). 2. Le contrat d'**orchestration IdeA** (délégation via `.ideai/requests`, pas via les subagents natifs du fournisseur). 3. Le **contexte projet partagé** (`.ideai/CONTEXT.md`), si présent. 4. La **persona/rôle** de l'agent (son `.md` dans `.ideai/agents/`). 5. Les **skills actifs** assignés à cet agent (voir §14.2). 6. Le **rappel mémoire** du projet (index/hooks), si présent (voir §14.5.4). **Avantages** : - Zéro collision entre agents, même N instances du même profil. - Universel : fonctionne pour toute CLI qui lit un fichier de convention depuis son cwd — aucun flag ou commande propre à un modèle. - Zéro dépendance à git (git est optionnel — supprimer un repo ne casse rien). **Impact sur `AgentProfile.cwd_template`** : la valeur est toujours `"{agentRunDir}"`, jamais `"{projectRoot}"`. La connaissance du project root passe par le *contenu* du convention file, pas par le cwd. --- ### 14.2 Skills — abstraction universelle de workflows réutilisables **Définition** : un **Skill** est un workflow/comportement réutilisable qu'on peut assigner à un agent. Exemples : `code-review`, `simplify`, `run-tests`, `explain`. C'est l'équivalent universel des slash-commands de Claude Code — mais sans dépendance à la syntaxe `/command` d'un modèle particulier. **Stockage** : - Skills globaux (templates) : `/IdeA/skills/` (store global IDE, réutilisables entre projets). - Skills de projet : `.ideai/skills/.md` (spécifiques au projet). **Injection** : les skills assignés à un agent sont **inclus dans son convention file** généré par IdeA au moment de l'activation. L'agent reçoit donc ses skills comme du contexte textuel — aucun mécanisme CLI propriétaire. **Entité `Skill`** (à ajouter au domaine) : - Champs : `id`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope` (`Global` | `Project`). - Un agent peut avoir 0..N skills assignés (stocké dans l'`AgentManifest`). **Port `SkillStore`** : CRUD skills globaux + skills projet (compose `FileSystem`/store global selon le scope). --- ### 14.3 OrchestratorApi — IdeA orchestre les agents, pas les CLIs fournisseurs **Objectif** : qu'un agent ou l'utilisateur puisse demander à IdeA de faire travailler un autre agent IdeA, visible dans la grille ou en arrière-plan, sans passer par les subagents natifs d'un fournisseur IA. **Décision** : IdeA est l'**orchestrateur unique** du cycle de vie des agents. Un agent Claude, Codex, Gemini ou custom ne lance jamais directement un subagent natif de son fournisseur. Il écrit une demande d'orchestration IdeA ; IdeA résout l'agent cible, son `AgentProfile`, son contexte, ses skills et sa mémoire, puis lance ou réattache la session via le runtime adapté au profil cible. Conséquence : le profil IA de l'agent demandeur ne contraint pas le profil IA de l'agent cible. Exemple valide : ```text Main = Codex Architect = Claude DevBackend = Codex Ask = Gemini ``` Si `Main` demande à `Architect` de travailler, IdeA lance/réattache `Architect` avec son profil Claude. `Main` ne connaît ni la commande Claude ni son format de contexte. **Mécanisme** : file-watching sur `.ideai/requests//`. L'agent écrit un fichier JSON de requête stable, consommé par IdeA : ```json { "type": "agent.run", "requestedBy": "Main", "targetAgent": "Architect", "task": "Analyser la décision d'architecture multi-agents", "visibility": "background", "attachToCell": null } ``` IdeA détecte le fichier, exécute les use cases d'orchestration, écrit une réponse, puis archive ou supprime la requête traitée. Le même chemin applicatif est utilisé depuis l'UI. **Contrat session/cellule** : - `Agent` = définition stable (contexte `.ideai/agents/.md`, profil IA, origine template). - `AgentSession` = exécution active d'un agent. - `CellBinding` = affichage optionnel d'une session dans une cellule. Invariants : - une `AgentSession` active peut être attachée à **0 ou 1** cellule visible ; - une cellule peut afficher **0 ou 1** session active ; - fermer une cellule détache la session (`Visible` → `Background`) et ne tue jamais l'agent ; - ouvrir une cellule sur une session déjà active réattache la cellule à cette session et montre le travail en cours ; - arrêter une session est une action explicite (`agent.stop`), distincte de la fermeture UI. **Port `OrchestratorApi`** (adapter entrant, driven by file-watcher) : surveille `.ideai/requests/`, désérialise les commandes, les traduit en appels de use cases (`RequestAgentWork`, `LaunchAgentSession`, `AttachAgentSessionToCell`, `DetachAgentSessionFromCell`, `StopAgentSession`…). Implémenté dans `infrastructure/orchestrator`. **Actions supportées (v2 cible)** : - `agent.run` : lance ou reprend une session pour un agent cible ; `visibility` vaut `background` ou `visible`. - `agent.stop` : arrête explicitement une session agent et son PTY. - `agent.attach` : attache une session active à une cellule. - `agent.detach` : détache une session active vers l'arrière-plan. - `agent.message` : transmet une tâche/message à une session existante. - `agent.update_context` : demande une mise à jour de contexte via les use cases IdeA, pas par écriture sauvage hors store. - `skill.create` : crée un skill réutilisable comme l'UI (use case `CreateSkill`). Exemple `skill.create` : ```json { "type": "skill.create", "name": "deploy", "context": "# Étapes de déploiement…", "scope": "project" } ``` **Instruction injectée aux agents** : les convention files générés (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…) doivent contenir une règle explicite : pour déléguer une tâche, l'agent utilise le protocole IdeA `.ideai/requests` et n'utilise pas les subagents natifs du fournisseur. Cela garantit que l'IDE garde l'identité des agents, leur mémoire, leur contexte et leur observabilité UI. **Impact UI/UX** : le layout n'est pas la source de vérité du travail agent. La grille affiche des vues attachées aux sessions. L'UI doit exposer un registre des sessions visibles et arrière-plan, avec actions ouvrir dans une cellule, détacher, déplacer, arrêter, et afficher la relation `requestedBy`/`targetAgent` quand elle existe. --- ### 14.4 Git = intégration optionnelle, zéro dépendance fonctionnelle Git est un **outil posé par-dessus l'IDE**, pas un socle. Supprimer le repo git d'un projet ne doit casser aucune feature d'IdeA (agents, terminaux, layout, skills, orchestration). Les use cases git (L8) sont un module indépendant ; rien d'autre n'en dépend. Cette contrainte s'applique à toute future décision de conception. --- ### 14.5 Système de mémoire — base de connaissance projet model-agnostic (L14) **Objectif** : doter chaque projet d'une **mémoire persistante** (préférences, décisions, feedback, références) que les agents lisent et enrichissent, **sans jamais dépendre d'un modèle ou d'une CLI** (principe fondateur). La mémoire est commune au project root, partagée par tous les agents (décision : mémoire au niveau projet, pas par `run/`). #### Modèle 2 étages (hybride) - **Étage 1 — fichiers `.md` (source de vérité)** : chaque note = un `.md` (frontmatter YAML `name`/`description`/`metadata.type` + corps Markdown) sous `.ideai/memory/.md`. Un index agrégé `MEMORY.md` (une ligne `- [Titre](slug.md) — hook` par note) est **dérivé**, reconstructible. Les notes se référencent par liens `[[slug]]`. C'est le **seul** étage qui fait foi. - **Étage 2 — index de rappel sémantique vectoriel (dérivé)** : embeddings des notes, **jamais source de vérité**, reconstructible et supprimable à tout moment. Sert uniquement à accélérer/cibler le rappel quand la mémoire devient trop grosse pour être lue intégralement. #### Bascule étage 1 ↔ étage 2 (automatique, sur seuil objectif) Le **rappel** (port `MemoryRecall`) est adaptatif : tant que la taille de la mémoire reste sous un **budget de tokens** configurable, l'adapter **naïf** lit `MEMORY.md` intégralement (zéro dépendance). Au-dessus du seuil, l'adapter **vectoriel** prend le relais. La bascule est une décision objective (taille vs budget), pas un choix manuel. **Défaut : `none`** (rappel naïf, aucun embedder) — esprit Linux « rien d'imposé, tout fonctionnel ». #### Ports (frontières domaine) - **`MemoryStore`** *(fait — LOT A étage 1)* : CRUD des `.md` + index `MEMORY.md` dérivé + `resolve_links` (ignore les liens cassés). Adapter `FsMemoryStore` (compose `FileSystem`, location-neutral). Erreurs `MemoryError`. - **`MemoryRecall`** *(LOT B)* : rappel adaptatif d'un sous-ensemble pertinent de notes pour une requête. Deux adapters substituables (Liskov) : `NaiveMemoryRecall` (lecture intégrale via `MemoryStore`) et, plus tard, `VectorMemoryRecall` (étage 2). - **`Embedder`** *(LOT C)* : production de vecteurs d'embedding, décrit par des **profils déclaratifs façon CLI LLM** (§9). Stratégies : `localOnnx` / `localServer` / `api` / `none`. `none` = pas d'embedder ⇒ rappel naïf forcé. #### Conformité hexagonale `MemoryStore`/`MemoryRecall`/`Embedder` sont des **traits du domaine** ; les adapters vivent en `infrastructure`. L'application ne parle qu'aux ports. Le domaine `memory` reste I/O-free (le format YAML/index est **possédé par l'adapter** `FsMemoryStore`, pas par le domaine). La mémoire est **indépendante de git** (§14.4) et du moteur IA. #### Découpage en sous-lots (binôme dev+test par sous-lot) - **LOT A — Étage 1 `.md`** : *domaine + adapter faits et verts.* Reste à livrer : **use cases `application/memory`** + **câblage app-tauri** (commandes + DTO ; les events `Memory*` et leurs DTO existent déjà). Voir §14.5.1. - **LOT B — Rappel adaptatif** : port `MemoryRecall`, adapter `NaiveMemoryRecall`, use case `RecallMemory`. Voir §14.5.2. - **LOT C — Étage 2 vectoriel** : port `Embedder`, profils déclaratifs `embedder.json`, adapter `VectorMemoryRecall`, logique de bascule sur seuil. Voir §14.5.3. - **Sous-lot d'intégration L14 ↔ L6** : injection du rappel mémoire dans le convention file à l'activation d'un agent (`LaunchAgent` compose `MemoryRecall`). Voir §14.5.4. ##### 14.5.1 LOT A (fin) — contrats application + app-tauri **Use cases `application/memory`** (miroir de `application/skill`, chacun `Arc`, `execute(Input) -> Result`) : ```rust // CreateMemory — crée/écrit une note + upsert index. Émet MemorySaved. struct CreateMemoryInput { project_root: ProjectPath, name: String, // slug brut → MemorySlug::new description: String, r#type: MemoryType, content: String } struct CreateMemoryOutput { memory: Memory } // UpdateMemory — remplace une note existante (revalide invariants). Émet MemorySaved. struct UpdateMemoryInput { project_root: ProjectPath, slug: MemorySlug, description: String, r#type: MemoryType, content: String } struct UpdateMemoryOutput { memory: Memory } // ListMemories — liste les notes (pilotée par l'index). struct ListMemoriesInput { project_root: ProjectPath } struct ListMemoriesOutput { memories: Vec } // GetMemory — une note par slug. struct GetMemoryInput { project_root: ProjectPath, slug: MemorySlug } struct GetMemoryOutput { memory: Memory } // DeleteMemory — supprime une note (retire la ligne d'index). Émet MemoryDeleted. struct DeleteMemoryInput { project_root: ProjectPath, slug: MemorySlug } // -> () // ReadMemoryIndex — lit MEMORY.md structuré (pour l'affichage graphique de la mémoire). struct ReadMemoryIndexInput { project_root: ProjectPath } struct ReadMemoryIndexOutput { entries: Vec } // ResolveMemoryLinks — liens [[slug]] sortants résolus (liens cassés ignorés). struct ResolveMemoryLinksInput { project_root: ProjectPath, slug: MemorySlug } struct ResolveMemoryLinksOutput { links: Vec } ``` > **Décision** : on expose les **7** use cases (CRUD complet + `ReadMemoryIndex` + `ResolveMemoryLinks`). `ReadMemoryIndex` alimente la vue graphique de la mémoire ; `ResolveMemoryLinks` alimente la navigation par liens. `CreateMemory`/`UpdateMemory` portent en plus `Arc`. **Émission d'events** (ports `MemoryStore` + `EventBus` ; events déjà définis dans `domain::events`) : | Use case | Event émis | |---|---| | `CreateMemory`, `UpdateMemory` | `DomainEvent::MemorySaved { slug }` | | `DeleteMemory` | `DomainEvent::MemoryDeleted { slug }` | | `ListMemories`, `GetMemory`, `ReadMemoryIndex`, `ResolveMemoryLinks` | *(aucun — lectures)* | > `MemoryIndexRebuilt { project_id }` n'est **pas** émis par les use cases CRUD (l'index est réécrit en interne par le store à chaque save/delete, pas un rebuild distinct). Le réserver à un futur use case `RebuildMemoryIndex` (reconstruction explicite depuis les `.md`) — non requis pour clore LOT A. **Mapping `From for AppError`** (à ajouter dans `application/src/error.rs`, calqué sur `From`) : ```rust impl From for AppError { fn from(e: MemoryError) -> Self { match e { MemoryError::NotFound => Self::NotFound("memory note".to_owned()), MemoryError::Frontmatter(m) => Self::Invalid(m), // donnée malformée = invariant other /* Io | Serialization */ => Self::Store(other.to_string()), } } } ``` **Commandes app-tauri ↔ DTO** (miroir des commandes skills ; `resolve_project(project_id)` → `project.root` ; slug parsé via `MemorySlug::new` qui renvoie `INVALID` si invalide) : | Commande Tauri | Request DTO (camelCase) | Réponse | |---|---|---| | `create_memory` | `{ projectId, name, description, type, content }` | `MemoryDto` | | `update_memory` | `{ projectId, slug, description, type, content }` | `MemoryDto` | | `list_memories` | `{ projectId }` | `MemoryListDto` | | `get_memory` | `{ projectId, slug }` | `MemoryDto` | | `delete_memory` | `{ projectId, slug }` | `()` | | `read_memory_index` | `{ projectId }` | `MemoryIndexDto` | | `resolve_memory_links` | `{ projectId, slug }` | `MemoryLinksDto` | DTO de réponse (`Memory`/`MemoryIndexEntry`/`MemoryLink` dérivent déjà `Serialize` côté domaine pour les types persistés ; sinon mapping explicite façon `SkillDto`) : `MemoryDto(Memory)`, `MemoryListDto(Vec)`, `MemoryIndexDto(Vec)`, `MemoryLinksDto(Vec)` (slugs cibles). Enregistrer les 7 commandes dans `tauri::generate_handler!` (`app-tauri/src/lib.rs`). Câbler les 7 use cases dans le composition root (`state.rs`) : `FsMemoryStore` (déjà exporté) injecté en `Arc`, partagé par tous les use cases, `EventBus` pour Create/Update/Delete. > Note d'implémentation : `FsMemoryStore` prend le `root` **par appel** (comme `SkillStore`) ⇒ **une seule** instance store partagée, comme pour les skills. ##### 14.5.2 LOT B — port `MemoryRecall` + adapter naïf ```rust /// Rappel adaptatif d'un sous-ensemble pertinent de notes pour une requête. #[async_trait] pub trait MemoryRecall: Send + Sync { /// Renvoie les notes les plus pertinentes pour `query`, limitées à `budget`. /// Contrat : best-effort, jamais d'erreur bloquante sur mémoire vide /// (renvoie une liste vide) ; l'adapter naïf ignore la pertinence et /// renvoie l'index/les notes dans l'ordre, tronqué au budget. async fn recall( &self, root: &ProjectPath, query: &MemoryQuery, ) -> Result, MemoryError>; } pub struct MemoryQuery { pub text: String, // requête (souvent le contexte courant de l'agent) pub token_budget: usize, // budget au-delà duquel l'étage 2 prendrait le relais } ``` - **Adapter `NaiveMemoryRecall`** (`infrastructure`) : compose `Arc`, lit `read_index` et tronque au budget. Aucune dépendance externe. C'est le **défaut**. - **Use case `RecallMemory`** (`application/memory`) : `RecallMemoryInput { project_root, text, token_budget } -> RecallMemoryOutput { entries: Vec }`, port `Arc`. Pas d'event. - **Contrat de substituabilité (Liskov)** : `NaiveMemoryRecall` et `VectorMemoryRecall` (LOT C) sont interchangeables ; un budget nul ⇒ liste vide ; mémoire absente ⇒ liste vide, jamais d'erreur. ##### 14.5.3 LOT C — port `Embedder` + profils + adapter vectoriel + bascule ```rust /// Produit des vecteurs d'embedding, piloté par un profil déclaratif (façon §9). #[async_trait] pub trait Embedder: Send + Sync { fn id(&self) -> &str; // ex. "local-onnx-minilm" async fn embed(&self, texts: &[String]) -> Result>, EmbedderError>; fn dimension(&self) -> usize; // taille des vecteurs produits } ``` - **Profils déclaratifs** `embedder.json` (store global IDE, comme `profiles.json`) : `{ id, name, strategy: "localOnnx"|"localServer"|"api"|"none", model?, endpoint?, apiKeyEnv?, dimension }`. **Open/Closed** : ajouter un moteur d'embedding = une donnée, pas du code. - **Adapter `VectorMemoryRecall`** (`infrastructure`) : compose `Arc` + `Arc` + un store de vecteurs dérivé sous `.ideai/memory/.index/` (reconstructible, ajouté au `.gitignore` au même titre que `run/`). Implémente `MemoryRecall`. - **Logique de bascule** (dans le use case `RecallMemory` ou un `AdaptiveMemoryRecall` qui compose les deux adapters) : si taille mémoire ≤ `token_budget` **ou** stratégie embedder = `none` ⇒ `NaiveMemoryRecall` ; sinon ⇒ `VectorMemoryRecall`. Décision objective, testable sans I/O (la mesure de taille est une fonction pure de l'index). - **`EmbedderError`** : nouveau type d'erreur par port (façon `MemoryError`), mappé en `AppError` (`none`/indisponible ⇒ dégrade vers naïf, jamais d'échec dur du rappel). - **Garde-fou produit** : défaut `none` ⇒ LOT C **n'impose aucune dépendance** ; un utilisateur sans embedder a une mémoire pleinement fonctionnelle (étage 1 + rappel naïf). ##### 14.5.4 Injection de la mémoire à l'activation d'un agent (intégration L14 ↔ L6) **Problème** : la mémoire (`MemoryStore` + `MemoryRecall`) existe et est consultable par commandes, mais **aucun agent ne la lit**. À l'activation d'un agent, IdeA génère le convention file (`CLAUDE.md`/`AGENTS.md`…) dans le run dir et y injecte la persona + les skills assignés (§14.1/§14.2) ; il faut y ajouter le **rappel mémoire** du projet, exactement comme les skills. **Décision (où injecter)** : le rappel est composé dans la **même fonction pure** `compose_convention_file`, qui devient : ```rust pub(crate) fn compose_convention_file( project_root: &str, agent_md: &str, skills: &[Skill], memory: &[MemoryIndexEntry], // nouveau : rappel mémoire (peut être vide) ) -> String ``` - On passe les **`MemoryIndexEntry`** déjà résolus (pas une `&str` pré-rendue ni le port) : la fonction reste **pure et I/O-free**, donc unit-testable sans fake, cohérent avec le traitement des skills (le port est appelé en amont, pas dans la fonction de composition). - `LaunchAgent` gagne **une nouvelle dépendance port** `recall: Arc` (déjà un port domaine, déjà câblé en `NaiveMemoryRecall` dans `state.rs`). Aucun couplage à un adapter concret : Liskov garantit qu'un `VectorMemoryRecall`/`AdaptiveMemoryRecall` (LOT C) se substitue sans toucher à `LaunchAgent`. - La résolution suit le **modèle `resolve_skills`** : une méthode `resolve_memory(&self, root) -> Vec` best-effort appelée juste avant `apply_injection`, dont le résultat est passé à `apply_injection` puis à `compose_convention_file`. Signature interne : ```rust async fn resolve_memory(&self, root: &ProjectPath) -> Result, AppError>; // puis : async fn apply_injection( &self, project: &Project, context_rel_path: &str, content: &MarkdownDoc, skills: &[Skill], memory: &[MemoryIndexEntry], // nouveau spec: &mut SpawnSpec, ) -> Result<(), AppError>; ``` **Décision (quoi injecter)** : l'**index/les hooks**, pas le corps des notes. On rappelle via `MemoryRecall::recall` (donc `read_index` tronqué au budget pour l'adapter naïf), et on rend une section : ```markdown --- # Mémoire projet - [Titre](slug.md) — hook (type) - … ``` Une ligne par `MemoryIndexEntry` (`title`, `slug`, `hook`, `r#type`). C'est le « léger par défaut » : l'agent reçoit les **pointeurs** vers le savoir projet (et peut lire le `.md` cible via son cwd = project root logique) ; l'étage 2 (corps/sémantique) reste hors convention file. Cohérent avec « léger par défaut, étage 2 seulement au-delà du seuil ». - **Budget** : constante `application` `const AGENT_MEMORY_RECALL_BUDGET: usize = 2_048;` (tokens approx.), passée dans `MemoryQuery { text, token_budget }`. Valeur **interne et documentée**, pas encore exposée en config (évolutif : pourra devenir un champ de réglage projet plus tard sans changer le contrat). `text` = la persona de l'agent (`content.as_str()`) : sans pertinence sémantique pour le naïf, mais déjà la bonne requête pour le vectoriel (LOT C) — zéro refactor au passage étage 2. **Décision (best-effort / dégradation)** : mémoire vide, absente, ou budget produisant 0 entrée ⇒ **aucune section `# Mémoire projet`** (omise entièrement, comme `# Skills` quand `skills` est vide) ⇒ document identique à l'actuel. `resolve_memory` ne **bloque jamais** un launch : le contrat de `MemoryRecall` est déjà « mémoire absente ⇒ liste vide, jamais d'erreur » ; une éventuelle `AppError::Store` inattendue est traitée comme les skills (best-effort — on dégrade vers liste vide plutôt que d'échouer l'activation). Confirmé : **un projet sans `.ideai/memory/` lance ses agents exactement comme aujourd'hui**. **Décision (universalité)** : l'injection passe **uniquement par le contenu du convention file** (`ContextInjectionPlan::File`), donc valable pour **toute stratégie `conventionFile`** (Claude/Codex/Gemini…), sans flag ni commande propriétaire. Pour `env`/`stdin`/`args` : **pas d'injection mémoire pour l'instant**, strictement **aligné sur les skills** (lesquels ne sont composés que dans la branche `File` de `apply_injection`). Rationale : l'uniformité avec les skills prime ; étendre aux autres stratégies serait une décision séparée (et pour `env`, la mémoire n'a pas de fichier unique à pointer). À tracer comme point ouvert si un profil non-`conventionFile` devait bénéficier du rappel. **Conformité hexagonale/SOLID** : `LaunchAgent` ne parle qu'à des **ports** (`+ Arc`) ; la composition reste une **fonction pure** ; aucun adapter concret référencé ; ISP respectée (une dépendance de plus, pour la seule tranche « rappel »). Substituabilité LOT C gratuite. **Découpage dev/test (ordre)** : 1. `crates/application/src/agent/lifecycle.rs` — **dev** : - ajouter le champ `recall: Arc` au struct `LaunchAgent` + paramètre dans `new` (en fin de liste, après `ids`) ; - `const AGENT_MEMORY_RECALL_BUDGET: usize = 2_048;` ; - `async fn resolve_memory(&self, root: &ProjectPath) -> Result, AppError>` (best-effort, dégrade vers `vec![]`) ; - `execute` : appeler `resolve_memory` après `resolve_skills` et passer le résultat à `apply_injection` ; - `apply_injection` : nouvel argument `memory: &[MemoryIndexEntry]`, transmis à `compose_convention_file` (branche `File` uniquement) ; - `compose_convention_file` : nouvel argument `memory`, section `# Mémoire projet` (omise si vide), rendu d'une ligne par entrée. 2. `crates/application/src/agent/lifecycle.rs` (`#[cfg(test)]`) — **test** : étendre les tests purs de `compose_convention_file` (section présente/ordonnée ; absente si vide ; document inchangé sans mémoire). 3. `crates/app-tauri/src/state.rs` — **dev** : passer `Arc::clone(&memory_recall_port)` (existant l.509-510, à hisser avant la construction de `LaunchAgent` l.379) en dernier argument de `LaunchAgent::new`. 4. **Câblage des tests existants** (`LaunchAgent::new` à 6 sites) — **test/dev** : fournir un fake `MemoryRecall` (un `FakeRecall` renvoyant `vec![]` par défaut, configurable pour un cas non-vide). Sites à mettre à jour : - `crates/application/tests/agent_lifecycle.rs` (×5, dont l'helper de construction l.666) ; - `crates/application/tests/orchestrator_service.rs` (l.407) ; - `crates/infrastructure/tests/orchestrator_watcher.rs` (l.298). 5. **Test d'intégration** (`agent_lifecycle.rs`) — **test** : avec un `FakeRecall` non-vide, asserter que le convention file écrit par `FakeFs` contient la section `# Mémoire projet` et les hooks ; avec recall vide, asserter son absence (document identique au baseline persona+skills). **Note de réutilisation** : la résolution préfère le port `MemoryRecall` (rappel borné) plutôt qu'un `read_index` brut, pour hériter directement de la bascule étage 1/étage 2 (LOT C) sans retoucher `LaunchAgent`. ##### 14.5.5 Câblage final & UI mémoire — clôture du sujet mémoire Deux dernières pièces ferment L14 : le **câblage adaptatif backend** (rendre la bascule étage 1↔2 « live » au composition root, défaut `none` conservé) et le **contrat `MemoryGateway` + panneau mémoire** côté front (miroir du modèle skills L12). **Pièce 1 — wiring `AdaptiveMemoryRecall` dans `app-tauri` (backend).** - Décision : `state.rs` câble désormais `Arc` via un **helper de composition** `build_memory_recall(fs, memory_store_port, embedder_profile) -> Arc` plutôt que `NaiveMemoryRecall` en dur. Le profil embedder est **chargé depuis `embedder.json` global** via `FsEmbedderProfileStore` (déjà existant), avec **fallback `EmbedderProfile::none()`** si le fichier est absent ou vide — esprit « rien d'imposé, zéro dépendance ». - **Défaut strictement identique au naïf** : pour `EmbedderProfile::none()`, `embedder_from_profile` retourne `None`. Dans ce cas le helper renvoie **directement `NaiveMemoryRecall`** (pas d'`AdaptiveMemoryRecall`, pas de `VectorMemoryRecall`, donc `StubEmbedder` jamais instancié). L'`AdaptiveMemoryRecall` n'est construit **que** lorsqu'un embedder concret existe (stratégie ≠ `none`) ; sa logique `should_use_vector` garantit de toute façon le repli naïf tant que la mémoire ne dépasse pas le budget, et le repli best-effort si l'embedder échoue (Liskov). Comportement par défaut = byte-for-byte le naïf actuel (couvert par `adaptive_none_strategy_matches_naive_exactly`). - **Instance unique partagée** : le `Arc` produit est injecté **à l'identique** dans `LaunchAgent` (injection §14.5.4) **et** dans `RecallMemory` (commande `recall_memory`) — une seule instance, donc l'UI et l'activation d'agent voient le même rappel. Aucune régression sur les 7 commandes mémoire ni sur les tests existants : seul le type concret derrière le port change, et il reste `NaiveMemoryRecall` par défaut. - **Chargement async dans un `build` sync** : `embedder.json` est lu via `tauri::async_runtime::block_on` dans `AppState::build` (déjà appelé dans un contexte `setup` Tauri), cohérent avec le `block_on` du hook de shutdown. Le composition root reste le seul endroit qui touche au runtime. - Conformité : `LaunchAgent`/`RecallMemory` ne dépendent que du port `MemoryRecall` ; `build_memory_recall` est la seule fonction qui connaît les adapters concrets (DIP). Zéro dépendance lourde tirée au défaut. **Pièce 2 — contrat `MemoryGateway` + panneau mémoire (frontend).** - Décision : un nouveau port UI `MemoryGateway` (dans `ports/index.ts`), **miroir des 7 commandes backend** (`create_memory`/`update_memory`/`list_memories`/`get_memory`/`delete_memory`/`read_memory_index`/`resolve_memory_links`) + `recall_memory` (optionnel UI). Identité = **slug** (kebab-case), pas d'UUID. Payloads camelCase, `type ∈ user|feedback|project|reference`. Adapter `TauriMemoryGateway` (`adapters/memory.ts`) + `MockMemoryGateway` (`adapters/mock/index.ts`), enregistrés dans `Gateways`. - Types domaine TS ajoutés (`domain/index.ts`) : `MemoryType` (`"user"|"feedback"|"project"|"reference"`), `Memory` (`{ name; description; type; content }`, miroir `MemoryDto`), `MemoryIndexEntry` (`{ slug; title; hook; type }`), `MemoryLink` (alias `string` cible, miroir `MemoryLinksDto`). - UI : feature `features/memory/` calquée sur `features/skills/` — `useMemory.ts` (view-model : liste depuis l'index, create/update/delete, resolve links), `MemoryPanel.tsx` (liste l'index, boutons New/Edit/Delete), `MemoryEditor.tsx` (slug+description+type+contenu en create, contenu+description+type en edit), `index.ts`, `memory.test.tsx`. Montée dans `ProjectsView` via un nouvel onglet sidebar `"memory"`. Périmètre **sobre**, aligné `SkillsPanel`, sans design system avancé. Affichage des liens `[[ ]]` via `resolveLinks` dans l'éditeur (lecture seule). - Conformité : aucun couplage UI↔Tauri hors `TauriMemoryGateway` ; les composants ne consomment que `MemoryGateway` (DIP), testables avec `MockMemoryGateway`. Impacts tests : étendre le mock gateway, ajouter `memory.test.tsx`, et compléter le test `ProjectsView` (nouvel onglet + montage du panneau). > **Sujet mémoire (L14) clos** une fois ces deux pièces livrées et vertes : domaine + adapters (A/B/C), use cases + commandes (LOT A/B), injection à l'activation (§14.5.4), bascule adaptative live (§14.5.5 pièce 1) et UI complète (§14.5.5 pièce 2). Évolutions ultérieures (vrais embedders ONNX/HTTP derrière feature, réglage du budget/seuil en config projet) restent des follow-ups indépendants, hors périmètre de clôture. #### Anomalies de conformité relevées sur l'existant (à traiter par les agents dev) 1. **Doublon `DomainError::MalformedFrontmatter` vs `MemoryError::Frontmatter`** : `error.rs` définit `DomainError::MalformedFrontmatter { reason }`, mais le domaine `memory.rs` ne l'utilise jamais (il lève `EmptyField`/`InvalidSlug`) et l'adapter parse via `MemoryError::Frontmatter`. La variante `DomainError::MalformedFrontmatter` est **morte**. **Action** : la supprimer (le parsing de frontmatter est une responsabilité d'adapter → `MemoryError::Frontmatter`), sauf si un futur parseur de frontmatter *dans le domaine* est prévu (il ne l'est pas — le domaine reste format-neutral, cf. doc-module `memory.rs`). Anomalie mineure, sans impact fonctionnel. 2. **`MemoryError` non encore mappé dans `AppError`** : normal (la couche application mémoire n'existe pas encore) ; couvert par LOT A (§14.5.1). 3. **`MemoryIndexRebuilt` / `MemorySaved` / `MemoryDeleted` déjà câblés bout-en-bout** (domaine event + DTO `app-tauri` + relais) mais **non encore émis** faute de use cases : attendu, résolu par LOT A. RAS sur le sens des dépendances : domaine I/O-free, adapter compose `FileSystem`, application ne référence que les ports. **Conforme hexagonal/SOLID.** --- ## 13. Risques techniques & points ouverts (spikes) 1. **PTY cross-platform** : portable-pty + xterm.js OK sur les 3 OS, mais signaux/resize/exit codes diffèrent (Windows ConPTY). **Spike** L3. 2. **AppImage multi-distro** : libgit2/openssl/glibc liés dynamiquement → risque de non-portabilité. **Spike** : vendoring statique (`git2` features, `rustls` pour russh au lieu d'OpenSSL), test sur ≥3 distros (Ubuntu/Fedora/Arch). L11. 3. **Drag d'onglet entre fenêtres Tauri** : Tauri v2 multi-webview/multi-window + DnD natif inter-fenêtres est délicat (le DnD HTML ne traverse pas les fenêtres OS). **Spike** : protocole « detach » (créer une `WebviewWindow`, transférer l'état via store + event, fermer l'onglet source). L10. 4. **Git sur FS distant** : libgit2 ne lit pas un FS SSH/WSL directement. Décision : **fallback git CLI** (`RemoteGitRepository`) côté distant via `ProcessSpawner`. À valider (perf, parsing). L9. 5. **Synchro temps réel UI ↔ PTY** : volume d'octets élevé ; backpressure des Channels Tauri, throttling/coalescing côté front. **Spike** L3. 6. ~~**Injection `conventionFile`** : symlink vs copie du `.md` vers `CLAUDE.md`/`AGENTS.md` ; conflits si fichier existant, .gitignore, droits Windows (symlinks).~~ **Résolu (§14.1)** : cwd isolé par agent dans `.ideai/run//` — plus de conflit à la racine, convention file généré par copie simple. 7. **SSH auth** : agent/clé/mot de passe/known_hosts ; choix russh (rustls) vs ssh2 (libssh2/OpenSSL — impacte point 2). Décision à figer début L9. 8. **WSL chemins** : conversion `/mnt/c/...` ↔ `\\wsl$\...`, distros multiples, perf I/O cross-boundary. Spike L9. 9. **Détection d'édition hors-app** des `.md`/templates (content hash) et résolution de conflit lors du sync. L7. --- *Document maintenu par l'Agent Architecture — base du jalon « cadrage architecture » avant tout code applicatif.*