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:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user