diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index 68b8bec..3e44839 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -2541,6 +2541,13 @@ fn append_block(input: &str, block: &str) -> String { /// agent's persona `.md`, then the bodies of its assigned `skills` under a /// `# Skills` section (ARCHITECTURE §14.2). /// +/// A short skill-awareness paragraph is always injected in the orchestration +/// block: it explains that assigned skills are operational workflow context, not +/// magic commands or provider subagents, and points reusable workflow creation to +/// the active IdeA orchestration surface (`idea_create_skill` for MCP profiles, +/// `skill.create` for the file protocol). This awareness deliberately does not +/// inject unassigned skill bodies; assignment remains the context boundary. +/// /// Skills are emitted in the order given (the caller passes them in manifest /// order, making the output deterministic); each is introduced by a `##` header /// carrying its name. When `skills` is empty the section is omitted entirely, so @@ -2616,6 +2623,7 @@ pub(crate) fn compose_convention_file( avec son propre AI Profile, son contexte et sa mémoire.\n\n", ); } + out.push_str(skill_awareness(mcp_enabled)); out.push_str("---\n\n"); if !project_context.trim().is_empty() { @@ -2671,6 +2679,24 @@ pub(crate) fn compose_convention_file( out } +fn skill_awareness(mcp_enabled: bool) -> &'static str { + if mcp_enabled { + "**Skills IdeA** : les workflows assignés apparaissent plus bas dans ce fichier sous la \ + section Skills quand il y en a. Un skill assigné est du contexte opérationnel utilisable \ + quand pertinent : ce n'est ni une commande magique, ni une sous-tâche fournisseur. Si tu \ + identifies un workflow réutilisable à capitaliser, passe par l'orchestration IdeA avec \ + `idea_create_skill`. Les skills non assignés ne sont pas injectés intégralement à tous les \ + agents ; l'assignation reste la frontière.\n\n" + } else { + "**Skills IdeA** : les workflows assignés apparaissent plus bas dans ce fichier sous la \ + section Skills quand il y en a. Un skill assigné est du contexte opérationnel utilisable \ + quand pertinent : ce n'est ni une commande magique, ni une sous-tâche fournisseur. Si tu \ + identifies un workflow réutilisable à capitaliser, passe par l'orchestration IdeA avec le \ + protocole fichier `skill.create`. Les skills non assignés ne sont pas injectés \ + intégralement à tous les agents ; l'assignation reste la frontière.\n\n" + } +} + /// Renders a [`MemoryType`] as its stable lowercase label for the convention-file /// memory section (`user`/`feedback`/`project`/`reference`). #[must_use] @@ -2759,8 +2785,13 @@ mod tests { let root_at = doc.find("/abs/project/root").unwrap(); let persona_at = doc.find("# Persona").unwrap(); assert!(root_at < persona_at, "root header must precede the persona"); - // No skills ⇒ no Skills section. - assert!(!doc.contains("# Skills")); + // No skills ⇒ awareness is present, but no Skills section is opened. + assert!(doc.contains("**Skills IdeA**")); + assert!( + !doc.contains("\n# Skills\n"), + "no assigned skills ⇒ no Skills section: {doc}" + ); + assert!(doc.contains("skill.create")); } #[test] @@ -2812,9 +2843,19 @@ mod tests { // Both skill bodies present, after the persona. assert!(doc.contains("REFAC_BODY")); assert!(doc.contains("REVIEW_BODY")); + let awareness_at = doc.find("**Skills IdeA**").unwrap(); let persona_at = doc.find("# Persona").unwrap(); + let skills_at = doc.find("\n# Skills\n").unwrap(); let refac_at = doc.find("REFAC_BODY").unwrap(); let review_at = doc.find("REVIEW_BODY").unwrap(); + assert!( + awareness_at < persona_at, + "skill awareness belongs to orchestration, before persona" + ); + assert!( + persona_at < skills_at && skills_at < refac_at, + "assigned skill bodies come under the Skills section after persona" + ); assert!(persona_at < refac_at, "skills come after the persona"); // Deterministic order: first assigned skill precedes the second. assert!(refac_at < review_at, "skills emitted in the given order"); @@ -2823,6 +2864,44 @@ mod tests { assert!(doc.contains("## review")); } + #[test] + fn compose_convention_file_skill_awareness_precedes_project_context() { + let doc = compose_convention_file( + "/root", + "# Shared project context\n\nUse pnpm.", + "# Persona\n\nDo X.", + &[], + &[], + None, + false, + ); + + let awareness_at = doc.find("**Skills IdeA**").unwrap(); + let context_at = doc.find("# Contexte projet").unwrap(); + let persona_at = doc.find("# Persona").unwrap(); + assert!( + awareness_at < context_at && context_at < persona_at, + "order must stay root -> orchestration/awareness -> project context -> persona" + ); + } + + #[test] + fn compose_convention_file_skill_awareness_uses_mcp_or_file_creation_path() { + let mcp_doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, true); + assert!(mcp_doc.contains("idea_create_skill")); + assert!( + !mcp_doc.contains("skill.create"), + "MCP awareness must point to the native IdeA tool" + ); + + let file_doc = compose_convention_file("/root", "", "# Persona", &[], &[], None, false); + assert!(file_doc.contains("skill.create")); + assert!( + !file_doc.contains("idea_create_skill"), + "file-protocol awareness must point to skill.create" + ); + } + /// Builds a memory index entry for the convention-file composition tests. fn mem(slug_str: &str, title: &str, hook: &str, kind: MemoryType) -> MemoryIndexEntry { MemoryIndexEntry {