diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index 7f2022a..c755a19 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -2624,6 +2624,33 @@ pub(crate) fn compose_convention_file( résultat. Ne réponds **jamais** uniquement en texte : seul `idea_reply` \ débloque l'agent qui t'a sollicité.\n\n", ); + // Capacités IdeA (feature skill-awareness) : briefing à HAUTE ALTITUDE, + // télégraphique, injecté à CHAQUE lancement (coût token → pas d'exemples ni de + // schémas). On expose la CAPACITÉ (jamais le contenu) : contexte projet géré par + // IdeA, mémoire durable partagée, skills. But : un agent neuf ne réinvente pas son + // propre contexte/mémoire/workflow alors qu'IdeA les fournit déjà. + out.push_str( + "**Contexte projet** : le contexte projet d'IdeA EST le contexte du projet — \ + n'improvise JAMAIS ton propre fichier de contexte/config. Lis-le avec \ + `idea_context_read` (sans `target`). Pour le faire évoluer, soumets une \ + proposition via `idea_context_propose` : le contexte projet global est \ + single-writer (réservé à l'orchestrateur), donc une proposition sans `target` \ + n'est PAS appliquée directement, elle est enregistrée pour validation. Pour \ + écrire le `.md` d'un agent précis : `idea_update_context` (ou \ + `idea_context_propose` avec `target`).\n\n", + ); + out.push_str( + "**Mémoire durable projet** : partagée entre TOUS les agents du projet. Lis-la \ + avec `idea_memory_read` (sans `slug` = index agrégé, ou un slug pour une note), \ + écris une note stable avec `idea_memory_write`. N'invente pas ton propre \ + stockage de mémoire.\n\n", + ); + out.push_str( + "**Skills** : workflows réutilisables. Ceux qui te sont assignés te sont listés \ + plus haut dans ce fichier ; charge le détail d'un skill avec \ + `idea_skill_read(name=…)` ; crée-en un avec `idea_create_skill`. Ne réinvente \ + pas un workflow déjà couvert par un skill.\n\n", + ); } else { out.push_str( "Pour déléguer une tâche à un autre agent, n'utilise jamais les subagents \ @@ -2631,6 +2658,23 @@ pub(crate) fn compose_convention_file( `.ideai/requests//` ; IdeA lancera ou réattachera l'agent cible \ avec son propre AI Profile, son contexte et sa mémoire.\n\n", ); + // Capacités IdeA, surface SANS MCP : mêmes concepts décrits UNIQUEMENT via les + // fichiers/dossiers `.ideai/` — aucun nom d'outil `idea_*` ici (cloisonnement + // strict des deux surfaces). Télégraphique, capacité (jamais contenu). + out.push_str( + "Le contexte projet partagé vit dans `.ideai/CONTEXT.md` (géré par IdeA) — ne \ + crée pas ton propre fichier de contexte/config, c'est CELUI-LÀ le contexte du \ + projet.\n\n", + ); + out.push_str( + "La mémoire durable partagée entre tous les agents vit dans `.ideai/memory/` \ + (notes `.md` + index `MEMORY.md`). N'invente pas ton propre stockage de \ + mémoire.\n\n", + ); + out.push_str( + "Les skills sont des workflows réutilisables, stockés comme des `.md` gérés par \ + IdeA. Ne réinvente pas un workflow déjà couvert par un skill.\n\n", + ); } out.push_str(skill_awareness(mcp_enabled)); out.push_str(memory_awareness()); @@ -3011,7 +3055,10 @@ mod tests { ); // The affordance section is present. - assert!(doc.contains("# Skills disponibles"), "MCP skills section present"); + assert!( + doc.contains("# Skills disponibles"), + "MCP skills section present" + ); assert!(doc.contains("idea_skill_read"), "points to the read tool"); // Affordance lines: `**name** — `. assert!(doc.contains("**refactor** — Refactors code")); @@ -3033,10 +3080,12 @@ mod tests { #[test] fn compose_convention_file_mcp_mode_without_skills_omits_section() { - // MCP mode + zero skills ⇒ no `# Skills disponibles` section at all. + // MCP mode + zero skills ⇒ no `# Skills disponibles` affordance section at all. + // NB : l'outil `idea_skill_read` reste évoqué par le briefing « capacités IdeA » + // (toujours présent), donc on n'asserte plus son absence — seulement celle de la + // section d'affordances, qui, elle, dépend bien des skills assignés. let doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); assert!(!doc.contains("# Skills disponibles")); - assert!(!doc.contains("idea_skill_read")); } /// Builds a memory index entry for the convention-file composition tests. @@ -3202,6 +3251,158 @@ mod tests { ); } + #[test] + fn compose_convention_file_mcp_briefs_all_three_capabilities_even_when_empty() { + // Cœur du besoin (feature skill-awareness) : le briefing « capacités IdeA » + // est INCONDITIONNEL. Projet/agent neuf et vide (zéro skill, zéro mémoire, + // contexte vide) ⇒ les 3 capacités (contexte, mémoire, skills) DOIVENT quand + // même être décrites, avec les noms des outils MCP correspondants. + let doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); + + // Capacité contexte projet. + assert!( + doc.contains("idea_context_read"), + "brief must surface idea_context_read even with empty content" + ); + assert!(doc.contains("idea_context_propose")); + assert!( + doc.contains("idea_update_context"), + "brief must surface idea_update_context for per-agent .md writes" + ); + // Capacité mémoire durable partagée. + assert!(doc.contains("idea_memory_read")); + assert!(doc.contains("idea_memory_write")); + // Capacité skills. + assert!(doc.contains("idea_skill_read")); + assert!(doc.contains("idea_create_skill")); + } + + #[test] + fn compose_convention_file_mcp_context_brief_is_single_writer_honest() { + // La prose contexte ne doit PAS laisser croire qu'une proposition globale est + // appliquée directement. On asserte sur la nuance réellement écrite par le dev : + // « single-writer » + le fait qu'une proposition sans `target` est enregistrée + // « pour validation » (et donc « n'est PAS appliquée directement »). + let doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); + + assert!( + doc.contains("single-writer"), + "context brief must name the single-writer constraint" + ); + assert!( + doc.contains("validation"), + "context brief must say a proposal is recorded for validation" + ); + assert!( + doc.contains("n'est PAS appliquée directement"), + "context brief must be honest that a global proposal isn't auto-applied" + ); + } + + #[test] + fn compose_convention_file_non_mcp_briefs_capabilities_via_ideai_files_no_tools() { + // Surface SANS MCP : mêmes 3 capacités décrites UNIQUEMENT via les fichiers + // `.ideai/` — et AUCUN nom d'outil `idea_*` (cloisonnement strict des surfaces). + let doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, false); + + // Capacités décrites par leurs fichiers/dossiers. + assert!( + doc.contains(".ideai/CONTEXT.md"), + "non-MCP brief must point to the .ideai/CONTEXT.md context file" + ); + assert!( + doc.contains(".ideai/memory/"), + "non-MCP brief must point to the .ideai/memory/ store" + ); + assert!( + doc.contains("MEMORY.md"), + "non-MCP brief must name the MEMORY.md index" + ); + + // AUCUN nom d'outil `idea_*` ne fuit (asserts forts sur les noms d'OUTILS, pas + // sur la sous-chaîne brute `idea` qui matcherait `.ideai/`). + assert!( + !doc.contains("idea_context"), + "no idea_context* tool in file surface" + ); + assert!( + !doc.contains("idea_memory"), + "no idea_memory* tool in file surface" + ); + assert!( + !doc.contains("idea_skill"), + "no idea_skill* tool in file surface" + ); + assert!( + !doc.contains("idea_create"), + "no idea_create* tool in file surface" + ); + assert!( + !doc.contains("idea_ask_agent"), + "no idea_ask_agent in file surface" + ); + assert!( + !doc.contains("idea_update_context"), + "no idea_update_context in file surface" + ); + } + + #[test] + fn compose_convention_file_surfaces_stay_cloistered_on_capabilities() { + // Cloisonnement : la prose MCP ne mentionne pas le protocole fichier + // `.ideai/requests`, et la prose fichier ne mentionne aucun nom d'outil. + let mcp = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); + let file = compose_convention_file("/root", "", "# Persona", &[], &[], None, false); + + assert!( + !mcp.contains(".ideai/requests"), + "MCP capability brief must not leak the file delegation protocol" + ); + // Aucun nom d'outil `idea_*` dans la surface fichier (recoupe le test dédié, + // gardé vert ici sur l'ensemble des outils des 3 capacités + délégation). + for tool in [ + "idea_context_read", + "idea_context_propose", + "idea_update_context", + "idea_memory_read", + "idea_memory_write", + "idea_skill_read", + "idea_create_skill", + "idea_ask_agent", + "idea_reply", + ] { + assert!( + !file.contains(tool), + "non-MCP surface must not mention the tool {tool}" + ); + } + } + + #[test] + fn compose_convention_file_capability_brief_precedes_the_persona_in_both_modes() { + // Ordre : le briefing capacités apparaît AVANT le persona dans les 2 modes. + // On ancre le brief sur un marqueur stable propre à chaque surface. + let mcp = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); + let mcp_brief_at = mcp + .find("idea_context_read") + .expect("MCP capability brief present"); + let mcp_persona_at = mcp.find("# Persona").expect("persona present"); + assert!( + mcp_brief_at < mcp_persona_at, + "MCP capability brief must come before the persona" + ); + + let file = compose_convention_file("/root", "", "# Persona", &[], &[], None, false); + let file_brief_at = file + .find(".ideai/CONTEXT.md") + .expect("file capability brief present"); + let file_persona_at = file.find("# Persona").expect("persona present"); + assert!( + file_brief_at < file_persona_at, + "file capability brief must come before the persona" + ); + } + #[test] fn mcp_declaration_with_runtime_points_the_bridge_at_the_real_endpoint() { // B-0 — a PTY-launched MCP-capable CLI (Claude/Codex REPL) auto-discovers the