From a50c41a9c2af088189537be9370a55a4fce2f4fb Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Fri, 18 Sep 2026 22:36:45 -0500 Subject: [PATCH] feat(skills): the skills section comes right after the identity paragraph, not last MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `compose_turn_prompt` appended `# Your skills` after the task, the tool list, the workspace rules and the marker contract — 87–90% of the way into a 6 KB prompt. It is the one section that asks the agent to do something BEFORE it starts (read a procedure), and on the three `index`-arm runs the agents' narratives never mentioned it. Position was the untested lever in the retrieval work; this puts the section second, after "You are the … agent" and before "Task:", and measures it on the next mission. The readers (`mode_in_prompt`, `skill_was_indexed`, `skill_names_in`) match lines, not offsets, so every stored prompt still scores. Two tests pin the order. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz --- crates/cm-api/src/topology_exec.rs | 42 +++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/cm-api/src/topology_exec.rs b/crates/cm-api/src/topology_exec.rs index 45b23e7..c682fab 100644 --- a/crates/cm-api/src/topology_exec.rs +++ b/crates/cm-api/src/topology_exec.rs @@ -915,7 +915,47 @@ pub fn compose_turn_prompt( // is also what the scorer reads the arm back from. Two copies of this // sentence is two chances for the reader to stop recognising the writer. let preamble = crate::skill_delivery::preamble(mode); - format!("{base}\n\n# Your skills\n\n{preamble}\n\n{skills}") + let section = format!("# Your skills\n\n{preamble}\n\n{skills}"); + // After the identity paragraph and BEFORE the task. This section is the + // one part of the prompt that asks the agent to do something before it + // starts — read a procedure — and until 2026-09-18 it was appended last, + // after the task, the tool list, the workspace rules and the marker + // contract, sitting 87–90% of the way into a 6 KB prompt. On the three + // `index`-arm runs the narratives never mentioned it at all. Position is + // the untested lever; this is the test. + match base.split_once("\n\n") { + Some((identity, rest)) if identity.starts_with("You are ") => { + format!("{identity}\n\n{section}\n\n{rest}") + } + _ => format!("{section}\n\n{base}"), + } +} + +#[cfg(test)] +mod prompt_order_tests { + use super::compose_turn_prompt; + use crate::skill_delivery::Mode; + + /// The section comes right after the identity paragraph, before the task — + /// not appended after everything else. + #[test] + fn skills_come_after_identity_and_before_the_task() { + let base = "You are the \"x\" agent. Do your part.\n\nTask: MISSION: y\n\nTOOLS AVAILABLE"; + let p = compose_turn_prompt(base, Some("--- SKILL: a ---\nbody"), Mode::Files); + let i_id = p.find("You are the").unwrap(); + let i_sk = p.find("# Your skills").unwrap(); + let i_task = p.find("Task: MISSION").unwrap(); + assert!(i_id < i_sk && i_sk < i_task, "order was identity={i_id} skills={i_sk} task={i_task}\n{p}"); + assert!(p.ends_with("TOOLS AVAILABLE"), "the base's tail is untouched"); + } + + /// A base with no identity paragraph still gets the section first. + #[test] + fn skills_lead_when_there_is_no_identity_paragraph() { + let p = compose_turn_prompt("Task: y", Some("--- SKILL: a ---\nbody"), Mode::Files); + assert!(p.starts_with("# Your skills"), "{p}"); + assert!(p.ends_with("Task: y")); + } } /// Parse `role=alias,role=alias` into a map (blank/malformed entries skipped).