feat(skill-awareness): T+ — brief « capacités IdeA » inconditionnel dans le contexte d'agent

Enrichit `compose_convention_file` pour que TOUT agent neuf, de tout
projet, soit briefé sur l'ensemble des capacités IdeA — plus seulement la
délégation. Briefing à haute altitude, télégraphique, injecté à chaque
lancement (sobriété token : capacité exposée, jamais le contenu).

Les 3 capacités sont décrites sur les 2 surfaces, strictement cloisonnées :
- MCP : via les outils `idea_context_read`/`_propose`/`idea_update_context`
  (contexte projet single-writer, proposition globale enregistrée pour
  validation, pas auto-appliquée), `idea_memory_read`/`_write` (mémoire
  durable partagée), `idea_skill_read`/`idea_create_skill` (skills).
- Sans MCP : mêmes concepts via les seuls fichiers `.ideai/` (CONTEXT.md,
  memory/ + MEMORY.md, skills `.md`), aucun nom d'outil `idea_*` ne fuit.

But : un agent ne réinvente pas son propre contexte/mémoire/workflow alors
qu'IdeA les fournit déjà.

5 tests dédiés (briefing inconditionnel même à vide, honnêteté single-writer
du contexte, cloisonnement des surfaces, ordre brief-avant-persona). 1 assert
existant adapté (`..._without_skills_omits_section` : `idea_skill_read` est
désormais toujours évoqué par le brief ; l'intention reste gardée par
l'assert sur l'absence de la section `# Skills disponibles`).
`cargo test -p application` = 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 19:45:13 +02:00
parent faa9692fc4
commit a9941c01d7

View File

@ -2624,6 +2624,33 @@ pub(crate) fn compose_convention_file(
résultat. Ne réponds **jamais** uniquement en texte : seul `idea_reply` \ résultat. Ne réponds **jamais** uniquement en texte : seul `idea_reply` \
débloque l'agent qui t'a sollicité.\n\n", 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 { } else {
out.push_str( out.push_str(
"Pour déléguer une tâche à un autre agent, n'utilise jamais les subagents \ "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/<ton-agent>/` ; IdeA lancera ou réattachera l'agent cible \ `.ideai/requests/<ton-agent>/` ; IdeA lancera ou réattachera l'agent cible \
avec son propre AI Profile, son contexte et sa mémoire.\n\n", 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(skill_awareness(mcp_enabled));
out.push_str(memory_awareness()); out.push_str(memory_awareness());
@ -3011,7 +3055,10 @@ mod tests {
); );
// The affordance section is present. // 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"); assert!(doc.contains("idea_skill_read"), "points to the read tool");
// Affordance lines: `**name** — <effective description>`. // Affordance lines: `**name** — <effective description>`.
assert!(doc.contains("**refactor** — Refactors code")); assert!(doc.contains("**refactor** — Refactors code"));
@ -3033,10 +3080,12 @@ mod tests {
#[test] #[test]
fn compose_convention_file_mcp_mode_without_skills_omits_section() { 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); let doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true);
assert!(!doc.contains("# Skills disponibles")); assert!(!doc.contains("# Skills disponibles"));
assert!(!doc.contains("idea_skill_read"));
} }
/// Builds a memory index entry for the convention-file composition tests. /// 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] #[test]
fn mcp_declaration_with_runtime_points_the_bridge_at_the_real_endpoint() { 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 // B-0 — a PTY-launched MCP-capable CLI (Claude/Codex REPL) auto-discovers the