feat(skills): the skills section comes right after the identity paragraph, not last
deploy / build (push) Canceled after 0s
deploy / test (push) Canceled after 2m12s

`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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-18 22:36:45 -05:00
co-authored by Claude Opus 5
parent 4e342d1ff7
commit a50c41a9c2
+41 -1
View File
@@ -915,7 +915,47 @@ pub fn compose_turn_prompt(
// is also what the scorer reads the arm back from. Two copies of this // 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. // sentence is two chances for the reader to stop recognising the writer.
let preamble = crate::skill_delivery::preamble(mode); 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 8790% 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). /// Parse `role=alias,role=alias` into a map (blank/malformed entries skipped).