From 2332b7f815bc5e797675ea9366130ea8866b50d7 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 6 Jun 2026 16:15:19 +0200 Subject: [PATCH] fix: fix some ui displays and features miss implemented --- .claude/worktrees/agent-a2650e91d2bd39ca2 | 1 + .claude/worktrees/agent-a4227a8f495123597 | 1 + .claude/worktrees/agent-a78df266a0bbaf5c3 | 1 + .claude/worktrees/agent-aeb1e862ef04b991b | 1 + .ideai/agents.json | 19 + .ideai/agents/architect.md | 601 ++++++++++++++++++++++ .ideai/agents/main.md | 187 +++++++ .ideai/layouts.json | 54 ++ .ideai/project.json | 9 + ARCHITECTURE.md | 98 +++- CLAUDE.md | 187 +++++++ agents-dev/L12-skills.md | 42 ++ agents-dev/L13-orchestrator.md | 35 ++ agents-dev/README.md | 2 + crates/app-tauri/src/events.rs | 19 + crates/domain/src/agent.rs | 45 +- crates/domain/src/events.rs | 11 +- crates/domain/src/ids.rs | 4 + crates/domain/src/lib.rs | 6 +- crates/domain/src/skill.rs | 111 ++++ crates/domain/tests/entities.rs | 122 ++++- crates/domain/tests/serde_roundtrip.rs | 57 +- 22 files changed, 1599 insertions(+), 14 deletions(-) create mode 160000 .claude/worktrees/agent-a2650e91d2bd39ca2 create mode 160000 .claude/worktrees/agent-a4227a8f495123597 create mode 160000 .claude/worktrees/agent-a78df266a0bbaf5c3 create mode 160000 .claude/worktrees/agent-aeb1e862ef04b991b create mode 100644 .ideai/agents.json create mode 100644 .ideai/agents/architect.md create mode 100644 .ideai/agents/main.md create mode 100644 .ideai/layouts.json create mode 100644 .ideai/project.json create mode 100644 CLAUDE.md create mode 100644 agents-dev/L12-skills.md create mode 100644 agents-dev/L13-orchestrator.md create mode 100644 crates/domain/src/skill.rs diff --git a/.claude/worktrees/agent-a2650e91d2bd39ca2 b/.claude/worktrees/agent-a2650e91d2bd39ca2 new file mode 160000 index 0000000..9736c42 --- /dev/null +++ b/.claude/worktrees/agent-a2650e91d2bd39ca2 @@ -0,0 +1 @@ +Subproject commit 9736c4242423319bc1fe065b6623b7ad6930cbbf diff --git a/.claude/worktrees/agent-a4227a8f495123597 b/.claude/worktrees/agent-a4227a8f495123597 new file mode 160000 index 0000000..33edbad --- /dev/null +++ b/.claude/worktrees/agent-a4227a8f495123597 @@ -0,0 +1 @@ +Subproject commit 33edbad713abab84a12bcda76803e8a213db70a3 diff --git a/.claude/worktrees/agent-a78df266a0bbaf5c3 b/.claude/worktrees/agent-a78df266a0bbaf5c3 new file mode 160000 index 0000000..0660f52 --- /dev/null +++ b/.claude/worktrees/agent-a78df266a0bbaf5c3 @@ -0,0 +1 @@ +Subproject commit 0660f52e2bfba93030abb846446aaa18e6be49cc diff --git a/.claude/worktrees/agent-aeb1e862ef04b991b b/.claude/worktrees/agent-aeb1e862ef04b991b new file mode 160000 index 0000000..9736c42 --- /dev/null +++ b/.claude/worktrees/agent-aeb1e862ef04b991b @@ -0,0 +1 @@ +Subproject commit 9736c4242423319bc1fe065b6623b7ad6930cbbf diff --git a/.ideai/agents.json b/.ideai/agents.json new file mode 100644 index 0000000..85ffe54 --- /dev/null +++ b/.ideai/agents.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "agents": [ + { + "agentId": "a6ced819-b893-4213-b003-9e9dc79b9641", + "name": "Main", + "mdPath": "agents/main.md", + "profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4", + "synchronized": false + }, + { + "agentId": "dce19c75-9669-4e45-b8de-9950025157da", + "name": "Architect", + "mdPath": "agents/architect.md", + "profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4", + "synchronized": false + } + ] +} diff --git a/.ideai/agents/architect.md b/.ideai/agents/architect.md new file mode 100644 index 0000000..c2c06a6 --- /dev/null +++ b/.ideai/agents/architect.md @@ -0,0 +1,601 @@ +# 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. + +--- + +## 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───? Agent (si lancé par un agent) +``` + +### 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. + +**`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` (ex. `"{projectRoot}"`). +- 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` (cellule du layout qui l'héberge), `cwd: ProjectPath`, `kind: SessionKind` (`Plain` | `Agent { agent_id }`), `pty_size: PtySize { rows, cols }`, `status` (`Starting|Running|Exited{code}`). +- Invariants : une cellule (feuille de layout) héberge **au plus une** `TerminalSession` active. `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. + +**`DomainEvent`** (enum) — `ProjectCreated`, `AgentLaunched`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}` (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` | +| `LaunchAgent` | Résout profil+contexte, prépare injection, ouvre cellule PTY au bon `cwd`, spawn CLI. | `AgentRuntime`, `AgentContextStore`, `RemoteHost`→`PtyPort`/`FileSystem`, `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`. +- 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) +│ └── agents/ +│ ├── reviewer.md # contexte d'un agent de projet +│ ├── backend-dev.md +│ └── ... +└── (CLAUDE.md / AGENTS.md / GEMINI.md générés/symlinkés à l'activation si conventionFile) +``` + +**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). + +--- + +## 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 | + +--- + +## 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). À cadrer L6. +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.* diff --git a/.ideai/agents/main.md b/.ideai/agents/main.md new file mode 100644 index 0000000..d0c97f4 --- /dev/null +++ b/.ideai/agents/main.md @@ -0,0 +1,187 @@ +# IdeA — Contexte & Méthode de travail + +> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA. +> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici. + +--- + +## 1. Mon rôle : chef d'orchestre, pas développeur + +Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail. +Je suis responsable de : + +- Découper le travail en tâches claires et autonomes. +- Attribuer chaque tâche aux bons agents. +- Garantir que le cycle de développement/test est respecté. +- Faire respecter les principes d'architecture (SOLID, Hexagonal). +- Maintenir la cohérence globale du projet et de ce document. +- Arbitrer et valider avant toute action irréversible ou sortante. + +--- + +## 2. Les agents + +### 2.1 Agent Architecture (1 pour tout le projet) +- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**. +- Définit les frontières (domaine / application / infrastructure), les ports, les contrats. +- Valide que chaque nouvelle feature respecte la structure avant son développement. +- Tient à jour la cartographie d'architecture et les conventions. + +### 2.2 Agents de Développement +- Écrivent le code des features. +- Respectent strictement l'architecture définie par l'agent Architecture. +- Code **propre, structuré, stable**. +- Reçoivent les rapports d'erreurs des agents de test et corrigent. + +### 2.3 Agents de Test +- **Chaque agent de développement est appairé avec un agent de test dédié.** +- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées. +- Produisent un **rapport d'erreurs** clair quand un test échoue. +- Re-testent après chaque correction. + +--- + +## 3. Le cycle de développement (boucle obligatoire) + +Pour **chaque** feature implémentée ou modifiée : + +``` +1. Agent Architecture → valide le découpage et les contrats (ports/interfaces) +2. Agent Développement → écrit le code +3. Agent Test → écrit les tests unitaires + les exécute +4a. Tests OK → feature validée, on passe à la suite +4b. Tests KO → rapport d'erreurs → retour à l'agent Développement + → correction → retour à l'étape 3 (boucle jusqu'au vert) +``` + +**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas. +Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle. + +--- + +## 4. Principes de code + +- **SOLID** appliqué au maximum. +- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...). +- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe. +- Tests unitaires systématiques ; couverture des features critiques. +- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif. + +--- + +## 5. Vision produit : IdeA + +**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.** + +### Fonctionnalités clés +- **Multi-projets en parallèle** : un **onglet par projet**. +- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement. +- **Agents par projet** : chaque projet a ses propres agents. +- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets. +- **Création d'agents** : depuis zéro ou à partir d'un template. +- **Synchronisation template → agents** : option « garder l'agent à jour ». + Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour. +- **Contextes d'agents stockés en `.md`** (toujours). +- **Création de projet** = définition de son **project root**. + +### Intégrations +- **Git** intégré. +- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH. +- **Développement WSL** : travailler sur une WSL depuis Windows. + +### Plateformes & livraison +- Cible : **macOS, Linux, Windows**. +- Première phase de compilation : **Linux et Windows**. +- Livraison : + - **Windows** : `setup.exe`. + - **Linux** : **AppImage** (doit fonctionner sur les différentes distributions). + +--- + +## 6. Stack technique (validée) + +- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs). +- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations). +- **Frontend / UI** : **TypeScript + React**. +- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust). +- **Git** : **libgit2** via `git2` (Rust). +- **SSH** : `russh` / `ssh2` (Rust). +- **WSL** : invocation de `wsl.exe` depuis le backend. + +## 7. Layout des terminaux (exigence produit) + +Disposition en **grille redimensionnable de type tableur (Excel)** : + +- Splits redimensionnables horizontaux **et** verticaux. +- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone. +- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur. +- Chaque cellule de la grille héberge un terminal. +- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture. + +## 8. Stockage des contextes & liaison aux templates + +- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet). +- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root. + *(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)* +- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet : + - le `.md` de l'agent, + - le template d'origine (le cas échéant), + - `synchronized: true/false`, + - la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible). +- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ. + +## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`) + +Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA : + +1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`). +2. **Stratégie d'injection du contexte `.md`** : + - `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…). + - `flag` : passer le chemin via un argument. + - `stdin` : piper le contenu. + - `env` : passer via variable d'environnement. + +Exemple de profil : +```json +{ + "id": "claude-code", + "name": "Claude Code", + "command": "claude", + "args": [], + "contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" }, + "detect": "claude --version", + "cwd": "{projectRoot}" +} +``` + +**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message). + +**Règles produit :** +- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut. +- Les commandes des profils sont **pré-remplies mais éditables**. +- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA. + +**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil. + +## 10. Fenêtres & onglets + +- **Par défaut : un onglet par projet** (comme les IDE classiques). +- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet. +- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets. + +## 11. Feuille de route + +1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**. +2. Puis MVP incrémental selon le cycle dev/test de la section 3. + +## 12. Autonomie d'exécution dans le projet + +L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**. + +- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**. +- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…). +- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite. + +--- + +*Dernière mise à jour : 2026-06-05* diff --git a/.ideai/layouts.json b/.ideai/layouts.json new file mode 100644 index 0000000..265e58d --- /dev/null +++ b/.ideai/layouts.json @@ -0,0 +1,54 @@ +{ + "version": 1, + "activeId": "9188db80-8535-4786-a20b-3c9a36b222e3", + "layouts": [ + { + "id": "9188db80-8535-4786-a20b-3c9a36b222e3", + "name": "Default", + "kind": "terminal", + "tree": { + "root": { + "type": "split", + "node": { + "id": "8aca2f93-1a9b-4693-9bba-9a01e130a48c", + "direction": "row", + "children": [ + { + "node": { + "type": "leaf", + "node": { + "id": "d8a86eb1-cd4d-4937-b900-4989da7c868d", + "agent": "a6ced819-b893-4213-b003-9e9dc79b9641" + } + }, + "weight": 1.0 + }, + { + "node": { + "type": "leaf", + "node": { + "id": "6c5be5e7-a54b-468c-a2e2-8ec853629d5e" + } + }, + "weight": 1.0 + } + ] + } + } + } + }, + { + "id": "40ea4fa9-25b3-410b-9d3e-6350834b421b", + "name": "Git Graph", + "kind": "gitGraph", + "tree": { + "root": { + "type": "leaf", + "node": { + "id": "c840bfdd-3330-46a3-b727-0799f6853e72" + } + } + } + } + ] +} diff --git a/.ideai/project.json b/.ideai/project.json new file mode 100644 index 0000000..0d13865 --- /dev/null +++ b/.ideai/project.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "id": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "name": "IdeA", + "remote": { + "kind": "local" + }, + "createdAt": 1780702317785 +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c2c06a6..e6ea822 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,6 +6,8 @@ > 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. --- @@ -134,7 +136,7 @@ Workspace 1───* Window 1───* Tab 1───1 Project - 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` (ex. `"{projectRoot}"`). +- 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) @@ -177,7 +179,12 @@ RemoteRef = - `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. -**`DomainEvent`** (enum) — `ProjectCreated`, `AgentLaunched`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}` (ce dernier souvent court-circuité vers un Channel). +**`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`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `SkillAssigned`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}`, `OrchestratorRequest{requester_id, action}` (ce dernier souvent court-circuité vers un Channel). --- @@ -451,11 +458,19 @@ drift(agent) = │ ├── 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) -│ └── agents/ -│ ├── reviewer.md # contexte d'un agent de projet -│ ├── backend-dev.md +│ ├── 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) │ └── ... -└── (CLAUDE.md / AGENTS.md / GEMINI.md générés/symlinkés à l'activation si conventionFile) +└── (aucun CLAUDE.md/AGENTS.md/GEMINI.md à la racine — jamais — voir §14.1) ``` **Schéma `agents.json`** : @@ -504,6 +519,8 @@ Emplacement résolu via Tauri path API (`AppData`/`~/.local/share/IdeA`/`~/Libra **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 @@ -581,6 +598,73 @@ IdeA/ | 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`, actions `spawn_agent`/`stop_agent`/`update_agent_context`. | `infrastructure/orchestrator`, `application/agent`, `app-tauri` | + +--- + +## 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. La **persona/rôle** de l'agent (son `.md` dans `.ideai/agents/`). +2. Le **chemin absolu du project root** (pour que l'agent sache où opérer). +3. Les **skills actifs** assignés à cet agent (voir §14.2). +4. Une référence au **contexte projet partagé** si présent. + +**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 — spawn d'agents depuis un agent ou depuis l'UI + +**Objectif** : qu'un agent orchestrateur puisse demander à IdeA de créer un nouvel agent (visible dans la grille et dans l'onglet Agents), exactement comme le ferait l'utilisateur via l'UI. + +**Mécanisme** : file-watching sur `.ideai/requests//`. L'orchestrateur écrit un fichier JSON de requête : +```json +{ "action": "spawn_agent", "name": "dev-backend", "profile": "claude-code", "context": "agents/dev-backend.md" } +``` +IdeA détecte le fichier, exécute la même logique que `LaunchAgent` déclenché depuis l'UI, crée la cellule terminal, inscrit l'agent dans l'onglet Agents, puis supprime le fichier et écrit une réponse. + +**Règle** : l'orchestrateur ne spawne jamais lui-même un process CLI — il **délègue à IdeA**. IdeA reste l'unique source de vérité du cycle de vie des agents. + +**Port `OrchestratorApi`** (adapter entrant, driven by file-watcher) : surveille `.ideai/requests/`, désérialise les commandes, les traduit en appels de use cases (`LaunchAgent`, `StopAgent`…). Implémenté dans `infrastructure/orchestrator`. + +**Actions supportées (v1)** : `spawn_agent`, `stop_agent`, `update_agent_context`. + +--- + +### 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. --- @@ -591,7 +675,7 @@ IdeA/ 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). À cadrer L6. +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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d0c97f4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,187 @@ +# IdeA — Contexte & Méthode de travail + +> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA. +> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici. + +--- + +## 1. Mon rôle : chef d'orchestre, pas développeur + +Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail. +Je suis responsable de : + +- Découper le travail en tâches claires et autonomes. +- Attribuer chaque tâche aux bons agents. +- Garantir que le cycle de développement/test est respecté. +- Faire respecter les principes d'architecture (SOLID, Hexagonal). +- Maintenir la cohérence globale du projet et de ce document. +- Arbitrer et valider avant toute action irréversible ou sortante. + +--- + +## 2. Les agents + +### 2.1 Agent Architecture (1 pour tout le projet) +- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**. +- Définit les frontières (domaine / application / infrastructure), les ports, les contrats. +- Valide que chaque nouvelle feature respecte la structure avant son développement. +- Tient à jour la cartographie d'architecture et les conventions. + +### 2.2 Agents de Développement +- Écrivent le code des features. +- Respectent strictement l'architecture définie par l'agent Architecture. +- Code **propre, structuré, stable**. +- Reçoivent les rapports d'erreurs des agents de test et corrigent. + +### 2.3 Agents de Test +- **Chaque agent de développement est appairé avec un agent de test dédié.** +- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées. +- Produisent un **rapport d'erreurs** clair quand un test échoue. +- Re-testent après chaque correction. + +--- + +## 3. Le cycle de développement (boucle obligatoire) + +Pour **chaque** feature implémentée ou modifiée : + +``` +1. Agent Architecture → valide le découpage et les contrats (ports/interfaces) +2. Agent Développement → écrit le code +3. Agent Test → écrit les tests unitaires + les exécute +4a. Tests OK → feature validée, on passe à la suite +4b. Tests KO → rapport d'erreurs → retour à l'agent Développement + → correction → retour à l'étape 3 (boucle jusqu'au vert) +``` + +**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas. +Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle. + +--- + +## 4. Principes de code + +- **SOLID** appliqué au maximum. +- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...). +- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe. +- Tests unitaires systématiques ; couverture des features critiques. +- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif. + +--- + +## 5. Vision produit : IdeA + +**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.** + +### Fonctionnalités clés +- **Multi-projets en parallèle** : un **onglet par projet**. +- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement. +- **Agents par projet** : chaque projet a ses propres agents. +- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets. +- **Création d'agents** : depuis zéro ou à partir d'un template. +- **Synchronisation template → agents** : option « garder l'agent à jour ». + Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour. +- **Contextes d'agents stockés en `.md`** (toujours). +- **Création de projet** = définition de son **project root**. + +### Intégrations +- **Git** intégré. +- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH. +- **Développement WSL** : travailler sur une WSL depuis Windows. + +### Plateformes & livraison +- Cible : **macOS, Linux, Windows**. +- Première phase de compilation : **Linux et Windows**. +- Livraison : + - **Windows** : `setup.exe`. + - **Linux** : **AppImage** (doit fonctionner sur les différentes distributions). + +--- + +## 6. Stack technique (validée) + +- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs). +- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations). +- **Frontend / UI** : **TypeScript + React**. +- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust). +- **Git** : **libgit2** via `git2` (Rust). +- **SSH** : `russh` / `ssh2` (Rust). +- **WSL** : invocation de `wsl.exe` depuis le backend. + +## 7. Layout des terminaux (exigence produit) + +Disposition en **grille redimensionnable de type tableur (Excel)** : + +- Splits redimensionnables horizontaux **et** verticaux. +- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone. +- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur. +- Chaque cellule de la grille héberge un terminal. +- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture. + +## 8. Stockage des contextes & liaison aux templates + +- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet). +- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root. + *(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)* +- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet : + - le `.md` de l'agent, + - le template d'origine (le cas échéant), + - `synchronized: true/false`, + - la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible). +- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ. + +## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`) + +Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA : + +1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`). +2. **Stratégie d'injection du contexte `.md`** : + - `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…). + - `flag` : passer le chemin via un argument. + - `stdin` : piper le contenu. + - `env` : passer via variable d'environnement. + +Exemple de profil : +```json +{ + "id": "claude-code", + "name": "Claude Code", + "command": "claude", + "args": [], + "contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" }, + "detect": "claude --version", + "cwd": "{projectRoot}" +} +``` + +**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message). + +**Règles produit :** +- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut. +- Les commandes des profils sont **pré-remplies mais éditables**. +- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA. + +**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil. + +## 10. Fenêtres & onglets + +- **Par défaut : un onglet par projet** (comme les IDE classiques). +- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet. +- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets. + +## 11. Feuille de route + +1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**. +2. Puis MVP incrémental selon le cycle dev/test de la section 3. + +## 12. Autonomie d'exécution dans le projet + +L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**. + +- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**. +- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…). +- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite. + +--- + +*Dernière mise à jour : 2026-06-05* diff --git a/agents-dev/L12-skills.md b/agents-dev/L12-skills.md new file mode 100644 index 0000000..0a32fd3 --- /dev/null +++ b/agents-dev/L12-skills.md @@ -0,0 +1,42 @@ +# L12 — Skills + +**Binôme :** `dev-skills` / `test-skills` +**Zones :** `domain/skill`, `application/skill`, `infrastructure/store`, `frontend/features/skills` +**Dépendances amont :** L0, L1, L5, L6 (convention file généré à l'activation), L7 (store global réutilisé). + +## Objectif +Modéliser les **Skills** : workflows réutilisables (équivalent universel des slash-commands, sans dépendance à la syntaxe `/command` d'un modèle). Stockage global IDE + projet, assignation agent↔skills, **injection des skills assignés dans le convention file** généré à l'activation de l'agent. Cf. ARCHITECTURE §14.2. + +## Périmètre (DEV) +- **Domaine** : entité `Skill { id, name, content_md: MarkdownDoc, scope: SkillScope(Global|Project) }`. Invariants : `name` non vide, `content_md` non vide. Event `SkillAssigned`. +- **Port `SkillStore`** : CRUD skills globaux (`/IdeA/skills/`) + skills projet (`.ideai/skills/.md`), résolution selon `scope` (compose `FileSystem`/store global comme L7). +- **AgentManifest** : étendre pour porter la liste `skills: Vec` assignés à chaque agent (0..N). +- **Use cases** (`application/skill`) : `CreateSkill`, `UpdateSkill`, `DeleteSkill`, `ListSkills(scope)`, `AssignSkillToAgent`, `UnassignSkillFromAgent`. +- **Injection** : à l'activation (fil L6), composer le convention file en concaténant persona agent + chemin project root + **skills assignés** (lus via `SkillStore`). Pas de mécanisme CLI propriétaire. +- **Front** : onglet/section Skills (liste globale + projet, CRUD, éditeur md), assignation skills↔agent dans `AgentsPanel`. + +## Périmètre (TEST) +- `Skill` rejette `name`/`content_md` vides. +- `SkillStore` : CRUD round-trip en tmpdir pour les deux scopes ; un skill `Project` n'apparaît pas dans le scope `Global` et inversement. +- `AssignSkillToAgent` / `UnassignSkillFromAgent` : mutent l'`AgentManifest`, émettent `SkillAssigned`, idempotents (pas de doublon). +- **Injection** : le convention file généré contient bien le `content_md` des skills assignés et **rien** des skills non assignés ; ordre déterministe. +- Front : CRUD skills + assignation via gateway mock (RTL) ; garde-fou « no direct invoke ». + +## Definition of Done +- `cargo test` (skill/store/app) + `vitest` verts ; cycle manuel : créer un skill, l'assigner à un agent, l'activer → le skill apparaît dans le convention file de `.ideai/run//`. +- DoD commune (cf. README) respectée ; zéro régression. + +## Avancement + +### ✅ Domaine (vert) +- **Entité `Skill`** (`domain/skill.rs`) : `id: SkillId`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope(Global|Project)`. Constructeur validant (`name` + `content_md` non vides), `with_content` re-valide l'invariant. +- **`SkillRef { skill_id, scope }`** : référence d'assignation portée par l'agent ; `From<&Skill>`. +- **`SkillId`** ajouté (`ids.rs`), event **`SkillAssigned { agent_id, skill_id, assigned }`** (`events.rs`), DTO + arm de mapping côté `app-tauri` (`events.rs`). +- **`Agent`** étendu : champ `skills: Vec` (serde `default`), méthodes `assign_skill` (idempotent), `unassign_skill`, `with_skills` (dédup). **`ManifestEntry`** : champ `skills` (serde `default` + `skip_serializing_if` → rétrocompat des manifests pré-L12) ; `from_agent`/`to_agent` préservent les skills. +- **Tests** : 8 invariants (`entities.rs`) + 3 serde dont rétrocompat d'un manifest legacy sans clé `skills` (`serde_roundtrip.rs`). `cargo test -p domain` vert ; `cargo test --workspace` vert (0 régression) ; clippy clean. + +### ⏳ Reste à faire +- Port `SkillStore` (`domain/ports.rs`) + adapter `FsSkillStore` (`infrastructure/store`). +- Use cases `application/skill` : CRUD + `AssignSkillToAgent`/`UnassignSkillFromAgent`. +- Injection des skills assignés dans le convention file à l'activation (fil L6). +- IPC `app-tauri` + front `features/skills`. diff --git a/agents-dev/L13-orchestrator.md b/agents-dev/L13-orchestrator.md new file mode 100644 index 0000000..dcfa968 --- /dev/null +++ b/agents-dev/L13-orchestrator.md @@ -0,0 +1,35 @@ +# L13 — OrchestratorApi + +**Binôme :** `dev-orchestrator` / `test-orchestrator` +**Zones :** `infrastructure/orchestrator`, `application/agent`, `app-tauri` +**Dépendances amont :** L0, L1, L6 (`LaunchAgent`/`StopAgent`), L12 (`update_agent_context` peut toucher les skills). + +## Objectif +Permettre à un **agent orchestrateur** de demander à IdeA de créer/arrêter/mettre à jour un agent — **exactement** comme l'utilisateur via l'UI. L'orchestrateur ne spawne jamais lui-même un process CLI : il **délègue à IdeA**, unique source de vérité du cycle de vie des agents. Cf. ARCHITECTURE §14.3. + +## Périmètre (DEV) +- **Port `OrchestratorApi`** (adapter *entrant*, driven by file-watcher) : surveille `.ideai/requests//`, désérialise les requêtes JSON, les traduit en appels de use cases. +- **Adapter `FsOrchestratorAdapter`** (`infrastructure/orchestrator`) : file-watching (`notify`), parse, dispatch, **supprime le fichier de requête** et **écrit une réponse** (succès/erreur) à côté. +- **Actions v1** : `spawn_agent` (→ `LaunchAgent`), `stop_agent` (→ `StopAgent`), `update_agent_context` (réécrit le `.md` de l'agent ± skills). +- **Event** : `OrchestratorRequest { requester_id, action }`. +- **Schéma requête** : + ```json + { "action": "spawn_agent", "name": "dev-backend", "profile": "claude-code", "context": "agents/dev-backend.md" } + ``` +- Le résultat d'un `spawn_agent` est **identique** à un lancement UI : cellule terminal créée, agent inscrit dans l'onglet Agents. +- **Composition root** (`app-tauri`) : démarrer le watcher, brancher sur les use cases existants ; arrêt propre à la fermeture. + +## Périmètre (TEST) +- Désérialisation : requêtes valides → action typée ; requête malformée → réponse d'erreur, **pas de crash**, pas de spawn. +- `spawn_agent` invoque `LaunchAgent` avec les bons args (use case mocké) ; idempotence sur double-dépôt du même fichier (traité une fois). +- Après traitement : fichier de requête **supprimé**, fichier de réponse écrit avec le bon statut. +- `stop_agent` / `update_agent_context` : mappent vers les bons use cases ; cible inexistante → erreur propre. +- Watcher : un fichier déposé dans `.ideai/requests//` est détecté (test d'intégration tmpdir + `notify`). + +## Definition of Done +- `cargo test` (orchestrator/app) verts ; cycle manuel : un agent écrit un fichier de requête `spawn_agent` → un nouvel agent apparaît dans la grille et l'onglet Agents, fichier consommé + réponse écrite. +- Garde-fou : l'orchestrateur ne lance **aucun** process directement (vérifié par revue + absence de `ProcessSpawner` dans le chemin orchestrateur). +- DoD commune respectée ; zéro régression ; git reste optionnel (rien dans ce lot n'en dépend, §14.4). + +## Avancement +⬜ À démarrer. Cadrage figé dans ARCHITECTURE §14.3. diff --git a/agents-dev/README.md b/agents-dev/README.md index 9824f5b..e878dd9 100644 --- a/agents-dev/README.md +++ b/agents-dev/README.md @@ -26,6 +26,8 @@ Un fichier `Lx-*.md` par binôme décrit son périmètre, ses ports/adapters, se | L9 | [L9-remote.md](L9-remote.md) | 🟡 **socle vert** (LocalHost + ConnectRemote, Liskov) · SSH/WSL gated à venir | | L10 | [L10-windows.md](L10-windows.md) | 🟡 **backend + IPC verts** (move-tab + `WebviewWindow`) · détach UI fait avec la refonte L11 | | L11 | [L11-packaging.md](L11-packaging.md) | 🟡 AppImage Linux **OK** · refonte disposition IDE en cours · Windows/CI à venir | +| L12 | [L12-skills.md](L12-skills.md) | ⬜ **Skills** — entité + store + assignation + injection convention file + UI | +| L13 | [L13-orchestrator.md](L13-orchestrator.md) | ⬜ **OrchestratorApi** — file-watcher, spawn depuis agent ou UI, même résultat | ## Cycle dev ↔ test (obligatoire, cf. CONTEXT §3) diff --git a/crates/app-tauri/src/events.rs b/crates/app-tauri/src/events.rs index b3532d6..458fd27 100644 --- a/crates/app-tauri/src/events.rs +++ b/crates/app-tauri/src/events.rs @@ -73,6 +73,16 @@ pub enum DomainEventDto { /// Version synced to. to: u64, }, + /// A skill was assigned to (or unassigned from) an agent. + #[serde(rename_all = "camelCase")] + SkillAssigned { + /// Agent id. + agent_id: String, + /// Skill id. + skill_id: String, + /// `true` if assigned, `false` if unassigned. + assigned: bool, + }, /// A tab's layout changed. #[serde(rename_all = "camelCase")] LayoutChanged { @@ -138,6 +148,15 @@ impl From<&DomainEvent> for DomainEventDto { agent_id: agent_id.to_string(), to: to.get(), }, + DomainEvent::SkillAssigned { + agent_id, + skill_id, + assigned, + } => Self::SkillAssigned { + agent_id: agent_id.to_string(), + skill_id: skill_id.to_string(), + assigned: *assigned, + }, DomainEvent::LayoutChanged { project_id } => Self::LayoutChanged { project_id: project_id.to_string(), }, diff --git a/crates/domain/src/agent.rs b/crates/domain/src/agent.rs index a8a80bc..8ba060d 100644 --- a/crates/domain/src/agent.rs +++ b/crates/domain/src/agent.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::error::DomainError; use crate::ids::{AgentId, ProfileId, TemplateId}; +use crate::skill::SkillRef; use crate::template::TemplateVersion; /// Origin of an agent: created from scratch, or derived from a template. @@ -65,6 +66,10 @@ pub struct Agent { pub origin: AgentOrigin, /// Whether the agent tracks its template (only valid for template origins). pub synchronized: bool, + /// Skills assigned to this agent, injected into its convention file at + /// activation (ARCHITECTURE §14.2). Empty by default. + #[serde(default)] + pub skills: Vec, } impl Agent { @@ -98,8 +103,37 @@ impl Agent { profile_id, origin, synchronized, + skills: Vec::new(), }) } + + /// Returns a copy of this agent carrying the given assigned skills, + /// deduplicated by `skill_id` (keeping first occurrence). + #[must_use] + pub fn with_skills(mut self, skills: Vec) -> Self { + self.skills = Vec::new(); + for skill in skills { + self.assign_skill(skill); + } + self + } + + /// Assigns a skill to this agent. Idempotent: re-assigning the same + /// `skill_id` is a no-op (returns `false`); a new assignment returns `true`. + pub fn assign_skill(&mut self, skill: SkillRef) -> bool { + if self.skills.iter().any(|s| s.skill_id == skill.skill_id) { + return false; + } + self.skills.push(skill); + true + } + + /// Removes a skill assignment by id. Returns `true` if a skill was removed. + pub fn unassign_skill(&mut self, skill_id: crate::ids::SkillId) -> bool { + let before = self.skills.len(); + self.skills.retain(|s| s.skill_id != skill_id); + self.skills.len() != before + } } /// One entry in the project agent manifest (`.ideai/agents.json`). @@ -135,6 +169,10 @@ pub struct ManifestEntry { /// Template version recorded at the last sync. #[serde(default, skip_serializing_if = "Option::is_none")] pub synced_template_version: Option, + /// Skills assigned to this agent (ARCHITECTURE §14.2). Defaults to empty for + /// backward-compatible deserialisation of pre-L12 manifests. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub skills: Vec, } impl ManifestEntry { @@ -172,6 +210,7 @@ impl ManifestEntry { template_id, synchronized, synced_template_version, + skills: Vec::new(), }) } @@ -196,6 +235,7 @@ impl ManifestEntry { template_id, synchronized: agent.synchronized, synced_template_version, + skills: agent.skills.clone(), } } @@ -213,14 +253,15 @@ impl ManifestEntry { }, _ => AgentOrigin::Scratch, }; - Agent::new( + Ok(Agent::new( self.agent_id, self.name.clone(), self.md_path.clone(), self.profile_id, origin, self.synchronized, - ) + )? + .with_skills(self.skills.clone())) } } diff --git a/crates/domain/src/events.rs b/crates/domain/src/events.rs index 3c5f501..ff10044 100644 --- a/crates/domain/src/events.rs +++ b/crates/domain/src/events.rs @@ -1,7 +1,7 @@ //! Domain events published on the [`crate::ports::EventBus`] and relayed to the //! presentation layer (ARCHITECTURE §3.2). -use crate::ids::{AgentId, ProjectId, SessionId, TemplateId}; +use crate::ids::{AgentId, ProjectId, SessionId, SkillId, TemplateId}; use crate::template::TemplateVersion; /// Events emitted by the domain/application as state changes occur. @@ -54,6 +54,15 @@ pub enum DomainEvent { /// Version it was brought up to. to: TemplateVersion, }, + /// A skill was assigned to (or unassigned from) an agent. + SkillAssigned { + /// The agent whose skill set changed. + agent_id: AgentId, + /// The skill involved. + skill_id: SkillId, + /// `true` if assigned, `false` if unassigned. + assigned: bool, + }, /// A tab's layout changed. LayoutChanged { /// The project whose layout changed. diff --git a/crates/domain/src/ids.rs b/crates/domain/src/ids.rs index cf2260f..00fb82c 100644 --- a/crates/domain/src/ids.rs +++ b/crates/domain/src/ids.rs @@ -68,6 +68,10 @@ typed_id!( /// Identifies an [`crate::profile::AgentProfile`]. ProfileId ); +typed_id!( + /// Identifies a [`crate::skill::Skill`]. + SkillId +); typed_id!( /// Identifies a [`crate::terminal::TerminalSession`]. SessionId diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 7701ce1..d15d421 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -41,6 +41,7 @@ pub mod ports; pub mod profile; pub mod project; pub mod remote; +pub mod skill; pub mod template; pub mod terminal; @@ -53,13 +54,16 @@ mod validation; pub use error::DomainError; pub use ids::{ - AgentId, LayoutId, NodeId, ProfileId, ProjectId, SessionId, TabId, TemplateId, WindowId, + AgentId, LayoutId, NodeId, ProfileId, ProjectId, SessionId, SkillId, TabId, TemplateId, + WindowId, }; pub use project::{Project, ProjectPath}; pub use agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry}; +pub use skill::{Skill, SkillRef, SkillScope}; + pub use template::{AgentTemplate, TemplateVersion}; pub use profile::{AgentProfile, ContextInjection}; diff --git a/crates/domain/src/skill.rs b/crates/domain/src/skill.rs new file mode 100644 index 0000000..450cd9c --- /dev/null +++ b/crates/domain/src/skill.rs @@ -0,0 +1,111 @@ +//! Skill entity — reusable, model-agnostic workflows assignable to agents. +//! +//! A [`Skill`] is IdeA's universal equivalent of a CLI's slash-command, but +//! without any dependency on a particular model's `/command` syntax +//! (ARCHITECTURE §14.2). Assigned skills are injected as plain text into the +//! agent's generated convention file at activation — there is no proprietary +//! CLI mechanism involved. + +use serde::{Deserialize, Serialize}; + +use crate::error::DomainError; +use crate::ids::SkillId; +use crate::markdown::MarkdownDoc; + +/// Where a skill lives, which also selects the store used to resolve it. +/// +/// - [`SkillScope::Global`] skills are stored in the global IDE store +/// (`/IdeA/skills/`) and reusable across projects. +/// - [`SkillScope::Project`] skills are stored under `.ideai/skills/` and are +/// specific to one project. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SkillScope { + /// Reusable across projects (global IDE store). + Global, + /// Specific to a single project (`.ideai/skills/`). + Project, +} + +/// A reusable workflow assignable to one or more agents. +/// +/// Invariants enforced here: +/// - `name` non-empty, +/// - `content_md` non-empty (an empty skill carries no behaviour). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Skill { + /// Stable identifier. + pub id: SkillId, + /// Display name (also used as the `.md` file stem on disk). + pub name: String, + /// Markdown body — the workflow injected into an agent's convention file. + pub content_md: MarkdownDoc, + /// Scope (selects the backing store). + pub scope: SkillScope, +} + +impl Skill { + /// Builds a validated skill. + /// + /// # Errors + /// - [`DomainError::EmptyField`] if `name` is empty, + /// - [`DomainError::EmptyField`] if `content_md` is empty. + pub fn new( + id: SkillId, + name: impl Into, + content_md: MarkdownDoc, + scope: SkillScope, + ) -> Result { + let name = name.into(); + crate::validation::non_empty(&name, "skill.name")?; + if content_md.is_empty() { + return Err(DomainError::EmptyField { + field: "skill.content_md", + }); + } + Ok(Self { + id, + name, + content_md, + scope, + }) + } + + /// Returns a copy of this skill with replaced content, re-validating the + /// non-empty invariant. + /// + /// # Errors + /// [`DomainError::EmptyField`] if `content_md` is empty. + pub fn with_content(&self, content_md: MarkdownDoc) -> Result { + Skill::new(self.id, self.name.clone(), content_md, self.scope) + } +} + +/// A reference from an agent to one assigned skill. +/// +/// Stored in the [`crate::agent::ManifestEntry`]: an agent carries 0..N of these. +/// The `scope` is kept alongside the id so the application layer knows which +/// store to resolve the skill from without a global lookup. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillRef { + /// The assigned skill. + pub skill_id: SkillId, + /// Scope of the assigned skill (selects its store). + pub scope: SkillScope, +} + +impl SkillRef { + /// Builds a reference to an assigned skill. + #[must_use] + pub const fn new(skill_id: SkillId, scope: SkillScope) -> Self { + Self { skill_id, scope } + } +} + +impl From<&Skill> for SkillRef { + fn from(skill: &Skill) -> Self { + Self::new(skill.id, skill.scope) + } +} diff --git a/crates/domain/tests/entities.rs b/crates/domain/tests/entities.rs index 272ed5b..abf666b 100644 --- a/crates/domain/tests/entities.rs +++ b/crates/domain/tests/entities.rs @@ -5,8 +5,8 @@ mod helpers; use domain::{ Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, DomainError, - ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectPath, PtySize, RemoteRef, SshAuth, - TemplateId, TemplateVersion, + ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectPath, PtySize, RemoteRef, Skill, SkillId, + SkillRef, SkillScope, SshAuth, TemplateId, TemplateVersion, }; use helpers::{AtomicSeqIdGenerator, FixedClock}; use uuid::Uuid; @@ -409,3 +409,121 @@ fn manifest_unique_md_paths_ok() { .unwrap(); assert!(AgentManifest::new(1, vec![e1, e2]).is_ok()); } + +// --------------------------------------------------------------------------- +// Skill invariants (L12, ARCHITECTURE §14.2) +// --------------------------------------------------------------------------- + +fn skill_id(n: u128) -> SkillId { + SkillId::from_uuid(Uuid::from_u128(n)) +} + +#[test] +fn skill_valid_construction() { + let s = Skill::new( + skill_id(1), + "code-review", + MarkdownDoc::new("review the diff"), + SkillScope::Global, + ); + assert!(s.is_ok()); +} + +#[test] +fn skill_rejects_empty_name() { + let err = Skill::new( + skill_id(1), + "", + MarkdownDoc::new("body"), + SkillScope::Project, + ) + .unwrap_err(); + assert_eq!(err, DomainError::EmptyField { field: "skill.name" }); +} + +#[test] +fn skill_rejects_empty_content() { + let err = Skill::new(skill_id(1), "x", MarkdownDoc::new(""), SkillScope::Global).unwrap_err(); + assert_eq!( + err, + DomainError::EmptyField { + field: "skill.content_md" + } + ); +} + +#[test] +fn skill_with_content_revalidates() { + let s = Skill::new(skill_id(1), "x", MarkdownDoc::new("a"), SkillScope::Global).unwrap(); + assert!(s.with_content(MarkdownDoc::new("b")).is_ok()); + assert!(s.with_content(MarkdownDoc::new("")).is_err()); +} + +#[test] +fn agent_assign_skill_is_idempotent() { + let mut a = Agent::new( + agent_id(1), + "dev", + "agents/dev.md", + profile_id(), + AgentOrigin::Scratch, + false, + ) + .unwrap(); + let r = SkillRef::new(skill_id(7), SkillScope::Global); + assert!(a.assign_skill(r)); // first assignment + assert!(!a.assign_skill(r)); // duplicate ignored + assert_eq!(a.skills, vec![r]); +} + +#[test] +fn agent_unassign_skill() { + let mut a = Agent::new( + agent_id(1), + "dev", + "agents/dev.md", + profile_id(), + AgentOrigin::Scratch, + false, + ) + .unwrap(); + let r = SkillRef::new(skill_id(7), SkillScope::Project); + a.assign_skill(r); + assert!(a.unassign_skill(skill_id(7))); + assert!(!a.unassign_skill(skill_id(7))); // already gone + assert!(a.skills.is_empty()); +} + +#[test] +fn agent_with_skills_dedups() { + let r = SkillRef::new(skill_id(7), SkillScope::Global); + let a = Agent::new( + agent_id(1), + "dev", + "agents/dev.md", + profile_id(), + AgentOrigin::Scratch, + false, + ) + .unwrap() + .with_skills(vec![r, r]); + assert_eq!(a.skills, vec![r]); +} + +#[test] +fn manifest_entry_preserves_skills_through_agent_roundtrip() { + let r = SkillRef::new(skill_id(7), SkillScope::Project); + let agent = Agent::new( + agent_id(1), + "dev", + "agents/dev.md", + profile_id(), + AgentOrigin::Scratch, + false, + ) + .unwrap() + .with_skills(vec![r]); + let entry = ManifestEntry::from_agent(&agent); + assert_eq!(entry.skills, vec![r]); + assert_eq!(entry.to_agent().unwrap(), agent); +} diff --git a/crates/domain/tests/serde_roundtrip.rs b/crates/domain/tests/serde_roundtrip.rs index 5e472db..b497af7 100644 --- a/crates/domain/tests/serde_roundtrip.rs +++ b/crates/domain/tests/serde_roundtrip.rs @@ -6,7 +6,7 @@ mod helpers; use domain::{ Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, Direction, LayoutNode, LayoutTree, LeafCell, ManifestEntry, MarkdownDoc, Project, ProjectPath, RemoteRef, - SplitContainer, SshAuth, TemplateVersion, WeightedChild, + Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth, TemplateVersion, WeightedChild, }; use helpers::{node, session}; use uuid::Uuid; @@ -229,6 +229,61 @@ fn manifest_roundtrip_and_camel_case() { assert!(!json.contains("\"templateId\":null"), "json was {json}"); } +// --------------------------------------------------------------------------- +// Skill (L12) — round-trip, camelCase scope tag, manifest skills back-compat +// --------------------------------------------------------------------------- + +fn sid(n: u128) -> SkillId { + SkillId::from_uuid(Uuid::from_u128(n)) +} + +#[test] +fn skill_roundtrip_and_camel_case_scope() { + let s = Skill::new(sid(1), "code-review", MarkdownDoc::new("body"), SkillScope::Global).unwrap(); + assert_eq!(roundtrip(&s), s); + let json = serde_json::to_string(&s).unwrap(); + assert!(json.contains("\"scope\":\"global\""), "json was {json}"); + assert!(json.contains("\"contentMd\""), "json was {json}"); + + let p = Skill::new(sid(2), "simplify", MarkdownDoc::new("b"), SkillScope::Project).unwrap(); + let pj = serde_json::to_string(&p).unwrap(); + assert!(pj.contains("\"scope\":\"project\""), "json was {pj}"); +} + +#[test] +fn manifest_entry_skills_roundtrip_and_camel_case() { + let entry = ManifestEntry::from_agent( + &Agent::new( + aid(1), + "dev", + "agents/dev.md", + profid(9), + AgentOrigin::Scratch, + false, + ) + .unwrap() + .with_skills(vec![SkillRef::new(sid(5), SkillScope::Project)]), + ); + assert_eq!(roundtrip(&entry), entry); + let json = serde_json::to_string(&entry).unwrap(); + assert!(json.contains("\"skillId\""), "json was {json}"); + assert!(json.contains("\"scope\":\"project\""), "json was {json}"); +} + +#[test] +fn manifest_entry_without_skills_omits_field_and_deserialises() { + // An entry with no skills must not emit "skills" (skip_serializing_if), + // and a pre-L12 manifest JSON (no skills key) must deserialise to empty. + let entry = + ManifestEntry::new(aid(1), "dev", "agents/dev.md", profid(9), None, false, None).unwrap(); + let json = serde_json::to_string(&entry).unwrap(); + assert!(!json.contains("\"skills\""), "json was {json}"); + + let legacy = r#"{"agentId":"00000000-0000-0000-0000-000000000001","name":"dev","mdPath":"agents/dev.md","profileId":"00000000-0000-0000-0000-000000000009","synchronized":false}"#; + let parsed: ManifestEntry = serde_json::from_str(legacy).unwrap(); + assert!(parsed.skills.is_empty()); +} + // --------------------------------------------------------------------------- // LayoutTree (tagged enum: type/node) // ---------------------------------------------------------------------------