feat(skills): injecte un paragraphe d'awareness skills dans le fichier de convention

compose_convention_file émet désormais, dans le bloc d'orchestration (avant le
contexte projet et la persona), un court paragraphe expliquant qu'un skill
assigné est du contexte opérationnel — ni commande magique, ni sous-tâche
fournisseur — et oriente la capitalisation de workflows réutilisables vers la
surface d'orchestration active : `idea_create_skill` en profil MCP, le protocole
fichier `skill.create` sinon.

L'awareness n'injecte volontairement pas le corps des skills non assignés :
l'assignation reste la frontière de contexte. Tests de composition (présence,
ordre root → orchestration → contexte → persona, branche MCP vs fichier) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 10:57:11 +02:00
parent e832af5428
commit befff76de8

View File

@ -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 {