use std::fmt;
use crate::config::AgentMode;
use crate::context::estimate_tokens;
use crate::guardrails::{self, GuardrailProfile};
use crate::personality::{soul_identity_text, PersonalityBand, PersonalityProfile};
use crate::resources::{AgentsMd, Skill, SoulDoc};
use crate::roles::Role;
use crate::tools::ToolRegistry;
#[derive(Debug, Clone)]
pub struct Fact {
pub text: String,
pub verified_ago: String,
}
#[derive(Debug, Clone)]
pub struct Attempt {
pub number: u32,
pub outcome: String,
pub summary: String,
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub name: String,
pub status: String,
pub detail: String,
}
#[derive(Debug, Clone)]
pub struct TaskContext {
pub title: String,
pub description: String,
pub design: Option<String>,
pub acceptance: Option<String>,
pub verify: Option<String>,
pub verify_timeout_secs: Option<u64>,
pub fail_first: bool,
pub notes: Option<String>,
pub attempts: Vec<Attempt>,
pub dependencies: Vec<Dependency>,
pub decisions: Vec<String>,
pub context_paths: Vec<String>,
pub constraints: Vec<String>,
}
#[derive(Debug)]
pub struct AssembledPrompt {
pub text: String,
pub estimated_tokens: u32,
}
impl fmt::Display for AssembledPrompt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text)
}
}
pub struct AssembleParams<'a> {
pub tools: &'a ToolRegistry,
pub agents_md: &'a [AgentsMd],
pub skills: &'a [Skill],
pub facts: &'a [Fact],
pub project_memory_status: Option<&'a str>,
pub personality: Option<&'a PersonalityProfile>,
pub soul: Option<&'a SoulDoc>,
pub task: Option<&'a TaskContext>,
pub role: Option<&'a Role>,
pub mode: &'a AgentMode,
pub memory: Option<&'a str>,
pub user_profile: Option<&'a str>,
pub cwd: Option<&'a std::path::Path>,
pub learning_enabled: bool,
pub guardrail_profile: Option<GuardrailProfile>,
}
pub fn assemble(params: &AssembleParams<'_>) -> AssembledPrompt {
assemble_inner(params)
}
fn assemble_inner(p: &AssembleParams<'_>) -> AssembledPrompt {
let mut parts = Vec::new();
parts.push(identity_layer(
p.tools,
p.role,
p.mode,
p.learning_enabled,
p.personality,
p.soul,
));
let execution_policy = execution_policy_layer();
if !execution_policy.is_empty() {
parts.push(execution_policy);
}
parts.push(environment_layer(p.cwd));
if !p.agents_md.is_empty() {
parts.push(agents_md_layer(p.agents_md));
}
if !p.skills.is_empty() {
parts.push(skills_layer(p.skills, p.mode));
}
if !p.facts.is_empty() {
parts.push(facts_layer(p.facts));
}
if let Some(status) = p.project_memory_status {
if !status.is_empty() {
parts.push(project_memory_status_layer(status));
}
}
if let Some(profile) = p.guardrail_profile {
parts.push(guardrails::guardrails_layer(profile));
}
if let Some(task) = p.task {
parts.push(task_layer(task));
parts.push(headless_execution_layer(task));
}
if let Some(mem) = p.memory {
if !mem.is_empty() {
parts.push(mem.to_string());
}
}
if let Some(user) = p.user_profile {
if !user.is_empty() {
parts.push(user.to_string());
}
}
let text = parts.join("\n\n");
let estimated_tokens = estimate_tokens(&text);
AssembledPrompt {
text,
estimated_tokens,
}
}
fn identity_layer(
tools: &ToolRegistry,
role: Option<&Role>,
mode: &AgentMode,
learning_enabled: bool,
personality: Option<&PersonalityProfile>,
soul: Option<&SoulDoc>,
) -> String {
let mut s = String::new();
if let Some(soul) = soul {
s.push_str(&soul_identity_text(&soul.content));
} else if let Some(personality) = personality {
s.push_str(&personality.identity.render_sentence());
} else {
s.push_str("You are imp, a coding agent.");
}
s.push_str("\n\nAvailable tools:\n");
let defs = match role {
Some(r) if r.readonly => tools.readonly_definitions(),
_ => tools.definitions_for_mode(mode),
};
for def in &defs {
s.push_str(&format!("- {}: {}\n", def.name, def.description));
}
if let Some(soul) = soul {
s.push_str("\n\nSoul:\n");
s.push_str(&soul.content);
s.push('\n');
} else if let Some(personality) = personality {
let working_style = working_style_lines(&personality.sliders);
if !working_style.is_empty() {
s.push_str("\nWorking style:\n");
for line in working_style {
s.push_str("- ");
s.push_str(line);
s.push('\n');
}
}
}
s.push_str("\nTool routing:\n");
s.push_str("- Use `bash` for shell-native search, file discovery, builds, tests, scripts, and package managers; prefer `scan` when code structure or symbols matter.\n");
if defs.iter().any(|def| def.name == "git") {
s.push_str("- Use `git` for local repo/worktree operations; use `bash` for uncovered git commands.\n");
}
if defs.iter().any(|def| def.name == "mana") {
s.push_str("- Prefer native `mana` actions over shell for mana work.\n");
}
s.push_str("- Use `read` before explaining or editing specific files; use `edit`/`write` for file changes.\n");
s.push_str("\nOperating rules:\n");
s.push_str("- Re-check the user's intent each turn; distinguish discussion, planning, implementation, review, and orchestration.\n");
s.push_str("- Ground repository claims in files or tool output inspected in this session; inspect named files, symbols, commands, and errors before acting on them.\n");
s.push_str("- For analysis-only requests, stay read-only. For implementation, make small reversible changes and verify with the narrowest useful check.\n");
s.push_str("- Treat failed commands, compiler errors, and missing evidence as blockers to resolve or report; never claim unverified success.\n");
s.push_str("- Ask one focused question when uncertainty changes scope, risk, architecture, destructive action, or user-visible behavior; otherwise proceed on low-risk local assumptions.\n");
s.push_str("- Keep replies concise and evidence-oriented: what changed or was found, how it was verified, and what remains.\n");
s.push_str("- Use mana when durable work structure, verification, dependencies, retries, decisions, handoff, or recovery matter; make units detailed enough for another agent to execute cold.\n");
s.push_str("- During planning/design, externalize real durable structure only when it changes project/work state the user is actively developing: concrete goals, decompositions, decisions, dependencies, follow-ups, blockers, or handoff context.\n");
s.push_str("- Do not create mana artifacts from explanation-only answers, hypotheticals, commentary about external content, brainstorming with no adopted next step, or conversational asides. When unsure whether discussion became durable work, ask or just answer in chat.\n");
s.push_str("- For real durable structure, use epics/tasks/notes/decisions deliberately, reserve facts for verifiable claims, and avoid noisy mana writes for small one-pass work.\n");
s.push_str("- Update mana after failures or material planning changes before relying on chat memory.\n");
s.push_str("- When working from a mana unit, treat its scope, dependencies, acceptance criteria, and verify command as the execution contract; do not broaden into unrelated cleanup.\n");
s.push_str("- Stop only on verified completion, a real blocker, or a user-facing decision point; mana writes are checkpoints, not proof of completion.\n");
if let Some(role) = role {
if let Some(ref instructions) = role.instructions {
s.push('\n');
s.push_str(instructions);
s.push('\n');
}
}
if let Some(instructions) = mode.instructions() {
s.push('\n');
s.push_str(instructions);
s.push('\n');
}
if learning_enabled {
s.push('\n');
s.push_str(crate::learning::LEARNING_INSTRUCTIONS);
s.push('\n');
}
s
}
fn execution_policy_layer() -> String {
String::new()
}
fn working_style_lines(sliders: &crate::personality::PersonalitySliders) -> Vec<&'static str> {
vec![
autonomy_line(sliders.autonomy),
verbosity_line(sliders.verbosity),
caution_line(sliders.caution),
warmth_line(sliders.warmth),
planning_depth_line(sliders.planning_depth),
"If you find yourself repeating the same action without progress, step back and try a different approach or ask the user for guidance.",
]
}
pub(crate) fn autonomy_line(band: PersonalityBand) -> &'static str {
match band {
PersonalityBand::VeryLow => {
"Ask for confirmation before making consequential decisions or larger changes."
}
PersonalityBand::Low => {
"Prefer confirmation before acting when requirements or consequences are unclear."
}
PersonalityBand::Medium => {
"Act on clear next steps, but ask when requirements are ambiguous."
}
PersonalityBand::High => {
"Act independently by default and ask when blocked, uncertain, or facing a consequential decision. Keep working until the task is fully resolved before yielding."
}
PersonalityBand::VeryHigh => {
"Take initiative aggressively on clear work and only ask when blocked or genuinely uncertain. Keep working until the task is fully resolved before yielding."
}
}
}
pub(crate) fn verbosity_line(band: PersonalityBand) -> &'static str {
match band {
PersonalityBand::VeryLow => "Keep responses terse and strongly action-oriented.",
PersonalityBand::Low => "Keep responses brief and focused on progress.",
PersonalityBand::Medium => {
"Be concise by default, but explain important tradeoffs when useful."
}
PersonalityBand::High => {
"Explain reasoning and tradeoffs when they help the user follow the work."
}
PersonalityBand::VeryHigh => {
"Give fuller explanations of reasoning, tradeoffs, and next steps."
}
}
}
pub(crate) fn caution_line(band: PersonalityBand) -> &'static str {
match band {
PersonalityBand::VeryLow => {
"Move forward with reasonable assumptions when the path is clear."
}
PersonalityBand::Low => "Favor progress over caution when risks are limited and local.",
PersonalityBand::Medium => "Balance steady progress with avoiding avoidable risk.",
PersonalityBand::High => {
"Prefer small, reversible changes and verify assumptions before riskier actions."
}
PersonalityBand::VeryHigh => {
"Be highly conservative with risky changes: verify assumptions and avoid acting on weak evidence."
}
}
}
pub(crate) fn warmth_line(band: PersonalityBand) -> &'static str {
match band {
PersonalityBand::VeryLow => "Use a direct, neutral tone.",
PersonalityBand::Low => "Use a clear, matter-of-fact tone.",
PersonalityBand::Medium => "Use a clear and calm tone.",
PersonalityBand::High => "Use a warm, supportive tone without becoming verbose.",
PersonalityBand::VeryHigh => {
"Use a notably warm, encouraging tone while staying useful and grounded."
}
}
}
pub(crate) fn planning_depth_line(band: PersonalityBand) -> &'static str {
match band {
PersonalityBand::VeryLow => "Favor immediate execution on the most obvious next step.",
PersonalityBand::Low => "Plan lightly, then move quickly into execution.",
PersonalityBand::Medium => "Plan briefly, then execute.",
PersonalityBand::High => "Think through structure and likely consequences before acting.",
PersonalityBand::VeryHigh => {
"Be methodical: think through structure, dependencies, and consequences before acting."
}
}
}
fn environment_layer(cwd: Option<&std::path::Path>) -> String {
let home = std::env::var("HOME").unwrap_or_default();
let cwd_str = cwd.map(|p| p.display().to_string()).unwrap_or_else(|| {
std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_default()
});
let os = std::env::consts::OS;
let today = {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let (y, m, d) = days_to_ymd(days);
format!("{y}-{m:02}-{d:02}")
};
format!("Environment: cwd={cwd_str}, os={os}, home={home}, date={today}")
}
fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
days += 719_468;
let era = days / 146_097;
let doe = days - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn agents_md_layer(agents: &[AgentsMd]) -> String {
let mut s = String::from("# Project Context\n\n");
for agent in agents {
s.push_str(&agent.content);
s.push('\n');
}
s
}
fn skills_layer(skills: &[Skill], _mode: &AgentMode) -> String {
let mut s = String::from(
"Available skills (load with `read ~/.imp/skills/<name>/SKILL.md` when relevant):\n",
);
for skill in skills {
let description = compact_skill_description(&skill.description);
if description.is_empty() {
s.push_str(&format!("- {}\n", skill.name));
} else {
s.push_str(&format!("- {}: {}\n", skill.name, description));
}
}
s
}
fn compact_skill_description(description: &str) -> String {
let normalized = description.split_whitespace().collect::<Vec<_>>().join(" ");
let first_sentence = normalized
.split_once(". ")
.map(|(first, _)| format!("{}.", first))
.unwrap_or(normalized);
truncate_chars(&first_sentence, 120)
}
fn truncate_chars(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let mut truncated = text
.chars()
.take(max_chars.saturating_sub(1))
.collect::<String>();
truncated.push('…');
truncated
}
fn facts_layer(facts: &[Fact]) -> String {
let mut s = String::from("Project facts:\n");
for fact in facts {
s.push_str(&format!(
"- \"{}\" [verified {}]\n",
fact.text, fact.verified_ago
));
}
s
}
fn project_memory_status_layer(status: &str) -> String {
status.to_string()
}
fn task_layer(task: &TaskContext) -> String {
let mut s = String::from("## Task\n");
s.push_str(&format!("Title: {}\n", task.title));
s.push_str(&format!("Description: {}\n", task.description));
if let Some(ref design) = task.design {
if !design.trim().is_empty() {
s.push_str("Design:\n");
s.push_str(design);
s.push('\n');
}
}
if let Some(ref notes) = task.notes {
if !notes.trim().is_empty() {
s.push_str("Notes:\n");
s.push_str(notes);
s.push('\n');
}
}
if let Some(ref acceptance) = task.acceptance {
s.push_str("Acceptance:\n");
s.push_str(acceptance);
s.push('\n');
}
if let Some(ref verify) = task.verify {
s.push_str(&format!("Verify: {}\n", verify));
if let Some(timeout_secs) = task.verify_timeout_secs {
s.push_str(&format!("Verify timeout: {}s\n", timeout_secs));
}
if task.fail_first {
s.push_str("Fail-first: verify was expected to fail before implementation; preserve that contract.\n");
}
s.push_str("Treat the verify command as the primary completion check for this task.\n");
}
if !task.context_paths.is_empty() {
s.push_str("\n## Referenced files\n");
s.push_str("Use these declared file/path hints before broadening the search.\n");
for path in &task.context_paths {
s.push_str(&format!("- {}\n", path));
}
}
if !task.constraints.is_empty() {
s.push_str("\n## Constraints\n");
for constraint in &task.constraints {
s.push_str(&format!("- {}\n", constraint));
}
}
if !task.attempts.is_empty() {
s.push_str("\n## Previous attempts\n");
s.push_str("Do not repeat a failed approach unchanged; use the attempt history to adjust your plan.\n");
for attempt in &task.attempts {
s.push_str(&format!(
"Attempt {} ({}): {}\n",
attempt.number, attempt.outcome, attempt.summary
));
}
}
if !task.dependencies.is_empty() {
s.push_str("\n## Dependencies\n");
s.push_str("Respect dependency state when sequencing work; unresolved dependencies are potential blockers.\n");
for dep in &task.dependencies {
s.push_str(&format!(
"- {} ({}): {}\n",
dep.name, dep.status, dep.detail
));
}
}
if !task.decisions.is_empty() {
s.push_str("\n## Unresolved decisions\n");
s.push_str("These decisions block fully autonomous execution; resolve them or surface them clearly instead of guessing.\n");
for decision in &task.decisions {
s.push_str(&format!("- {}\n", decision));
}
}
s
}
fn headless_execution_layer(task: &TaskContext) -> String {
let mut s = String::from("## Headless execution contract\n");
s.push_str("- You are executing an explicit mana unit, not exploring broadly.\n");
s.push_str("- Treat the unit title, description, notes, acceptance criteria, and verify gate as the source of truth for scope and success.\n");
s.push_str("- Execute the assigned outcome before expanding into adjacent cleanup, refactors, or unrelated improvements.\n");
s.push_str("- Use explicit file references and prefilled context first before searching more broadly.\n");
s.push_str(
"- If the unit includes prior failed attempts, do not retry the same plan unchanged.\n",
);
s.push_str("- If dependency state or prerequisite decisions are unresolved, treat that as a blocker rather than improvising around it.\n");
s.push_str("- Keep progress updates concise and useful. Record meaningful discoveries, blockers, and revised plans with `mana update`.\n");
if task.verify.is_some() {
s.push_str("- If the verify command fails, either fix the issue or report the exact blocker. Do not claim completion anyway.\n");
}
s.push_str("- In batch-verify flows, treat your goal as leaving the unit ready for verify rather than assuming verify already passed.\n");
s.push_str(
"- Respect parent/child structure: finish this unit's outcome, not the whole feature.\n",
);
s
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::Arc;
use crate::personality::{
PersonaFocus, PersonaRole, PersonalityBand, PersonalityIdentity, PersonalityProfile,
PersonalitySliders, VoiceWord, WorkStyleWord,
};
use crate::resources::SoulDoc;
use crate::tools::{Tool, ToolContext, ToolOutput};
use async_trait::async_trait;
struct FakeTool {
name: &'static str,
description: &'static str,
readonly: bool,
}
#[async_trait]
impl Tool for FakeTool {
fn name(&self) -> &str {
self.name
}
fn label(&self) -> &str {
self.name
}
fn description(&self) -> &str {
self.description
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({"type": "object"})
}
fn is_readonly(&self) -> bool {
self.readonly
}
async fn execute(
&self,
_: &str,
_: serde_json::Value,
_: ToolContext,
) -> crate::Result<ToolOutput> {
Ok(ToolOutput::text("ok"))
}
}
fn make_registry() -> ToolRegistry {
let mut reg = ToolRegistry::new();
reg.register(Arc::new(FakeTool {
name: "read",
description: "Read file contents",
readonly: true,
}));
reg.register(Arc::new(FakeTool {
name: "write",
description: "Write content to a file",
readonly: false,
}));
reg.register(Arc::new(FakeTool {
name: "edit",
description: "Edit a file by replacing exact text",
readonly: false,
}));
reg.register(Arc::new(FakeTool {
name: "bash",
description: "Run shell commands",
readonly: false,
}));
reg
}
fn make_skill(name: &str, desc: &str, path: &str) -> Skill {
Skill {
name: name.into(),
description: desc.into(),
path: PathBuf::from(path),
}
}
fn make_agents_md(content: &str) -> AgentsMd {
AgentsMd {
path: PathBuf::from("/project/AGENTS.md"),
content: content.into(),
}
}
fn make_readonly_role() -> Role {
use crate::roles::ToolSet;
Role {
name: "reviewer".into(),
model: None,
thinking_level: None,
tool_set: ToolSet::All,
readonly: true,
instructions: Some("Review code carefully. Do not modify files.".into()),
}
}
fn make_worker_role() -> Role {
use crate::roles::ToolSet;
Role {
name: "worker".into(),
model: None,
thinking_level: None,
tool_set: ToolSet::All,
readonly: false,
instructions: None,
}
}
fn make_personality() -> PersonalityProfile {
PersonalityProfile {
identity: PersonalityIdentity {
name: "Nova".into(),
work_style: WorkStyleWord::Careful,
voice: VoiceWord::Direct,
focus: PersonaFocus::Research,
role: PersonaRole::Assistant,
},
sliders: PersonalitySliders {
autonomy: PersonalityBand::Low,
verbosity: PersonalityBand::Medium,
caution: PersonalityBand::VeryHigh,
warmth: PersonalityBand::High,
planning_depth: PersonalityBand::VeryLow,
},
}
}
fn test_assemble(
tools: &ToolRegistry,
agents_md: &[AgentsMd],
skills: &[Skill],
facts: &[Fact],
personality: Option<&PersonalityProfile>,
task: Option<&TaskContext>,
role: Option<&Role>,
) -> AssembledPrompt {
assemble(&AssembleParams {
tools,
agents_md,
skills,
facts,
project_memory_status: None,
personality,
soul: None,
task,
role,
mode: &AgentMode::Full,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
})
}
#[test]
fn system_prompt_includes_operating_rules() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result.text.contains("Operating rules:"));
assert!(result.text.contains(
"Ground repository claims in files or tool output inspected in this session"
));
assert!(result.text.contains(
"For analysis-only requests, stay read-only. For implementation, make small reversible changes"
));
}
#[test]
fn system_prompt_includes_conversation_time_mana_planning_doctrine() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result.text.contains(
"During planning/design, externalize real durable structure only when it changes project/work state the user is actively developing"
));
assert!(result
.text
.contains("Do not create mana artifacts from explanation-only answers"));
assert!(result.text.contains("epics/tasks/notes/decisions"));
assert!(result.text.contains("reserve facts for verifiable claims"));
assert!(result.text.contains(
"Update mana after failures or material planning changes before relying on chat memory"
));
assert!(result
.text
.contains("mana writes are checkpoints, not proof of completion"));
assert_eq!(
result
.text
.matches("mana writes are checkpoints, not proof of completion")
.count(),
1,
"mana checkpoint guidance should appear once"
);
assert!(!result.text.contains("Mana doctrine:"));
assert!(!result
.text
.contains("between-turn mana update before the substantive reply"));
assert!(!result
.text
.contains("include a concise mana delta summary in the response"));
}
#[test]
fn system_prompt_identity_includes_all_tools() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result.text.contains("You are imp, a coding agent."));
assert!(result.text.contains("- read: Read file contents"));
assert!(result.text.contains("- write: Write content to a file"));
assert!(result
.text
.contains("- edit: Edit a file by replacing exact text"));
assert!(result.text.contains("- bash: Run shell commands"));
}
#[test]
fn system_prompt_mana_guidance_prefers_native_tool_when_available() {
let mut reg = make_registry();
reg.register(Arc::new(FakeTool {
name: "mana",
description: "Manage mana work natively",
readonly: false,
}));
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result
.text
.contains("Prefer native `mana` actions over shell for mana work."));
}
#[test]
fn system_prompt_mana_guidance_omitted_without_mana_tool() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(!result
.text
.contains("Prefer native `mana` actions over shell for mana work."));
}
#[test]
fn system_prompt_no_mana_guidance_or_delegation_in_prompt() {
let mut reg = make_registry();
reg.register(Arc::new(FakeTool {
name: "bash",
description: "Run shell commands",
readonly: false,
}));
reg.register(Arc::new(FakeTool {
name: "mana",
description: "Manage mana work",
readonly: false,
}));
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(
!result.text.contains("Mana guidance:"),
"mana guidance block should not appear in system prompt"
);
assert!(
!result.text.contains("## Mana delegation"),
"delegation guidance should not appear in system prompt"
);
}
#[test]
fn system_prompt_identity_only_when_all_layers_empty() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result.text.contains("You are imp"));
assert!(!result.text.contains("# Project Context"));
assert!(!result.text.contains("Available skills"));
assert!(!result.text.contains("Project facts"));
assert!(!result.text.contains("## Task"));
}
#[test]
fn system_prompt_uses_personality_identity_sentence() {
let reg = make_registry();
let personality = make_personality();
let result = test_assemble(®, &[], &[], &[], Some(&personality), None, None);
assert!(result
.text
.contains("You are Nova, a careful, direct, research assistant."));
}
#[test]
fn system_prompt_renders_personality_working_style_block() {
let reg = make_registry();
let personality = make_personality();
let result = test_assemble(®, &[], &[], &[], Some(&personality), None, None);
assert!(result.text.contains("Working style:"));
assert!(result.text.contains(
"Prefer confirmation before acting when requirements or consequences are unclear."
));
assert!(result
.text
.contains("Be concise by default, but explain important tradeoffs when useful."));
assert!(result.text.contains(
"Be highly conservative with risky changes: verify assumptions and avoid acting on weak evidence."
));
assert!(result
.text
.contains("Use a warm, supportive tone without becoming verbose."));
assert!(result
.text
.contains("Favor immediate execution on the most obvious next step."));
}
#[test]
fn system_prompt_prefers_soul_over_personality_profile() {
let reg = make_registry();
let personality = make_personality();
let soul = SoulDoc {
path: PathBuf::from("/tmp/soul.md"),
content: "# Soul\n\nYou are Sol, a tuned and reflective collaborator.\n\n## Tunables\n\n- Autonomy: Act independently by default.\n".into(),
};
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: None,
personality: Some(&personality),
soul: Some(&soul),
task: None,
role: None,
mode: &AgentMode::Full,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(result
.text
.contains("You are Sol, a tuned and reflective collaborator."));
assert!(result.text.contains("Soul:"));
assert!(result.text.contains("## Tunables"));
assert!(!result.text.contains("Working style:"));
}
#[test]
fn system_prompt_without_soul_keeps_personality_working_style_block() {
let reg = make_registry();
let personality = make_personality();
let result = test_assemble(®, &[], &[], &[], Some(&personality), None, None);
assert!(result.text.contains("Working style:"));
}
#[test]
fn system_prompt_agents_md_included_verbatim() {
let reg = make_registry();
let agents = vec![make_agents_md("# Rules\n\nUse snake_case everywhere.")];
let result = test_assemble(®, &agents, &[], &[], None, None, None);
assert!(result.text.contains("# Project Context"));
assert!(result
.text
.contains("# Rules\n\nUse snake_case everywhere."));
}
#[test]
fn system_prompt_multiple_agents_md_concatenated() {
let reg = make_registry();
let agents = vec![
make_agents_md("Global rules here."),
make_agents_md("Project rules here."),
];
let result = test_assemble(®, &agents, &[], &[], None, None, None);
assert!(result.text.contains("Global rules here."));
assert!(result.text.contains("Project rules here."));
}
#[test]
fn system_prompt_empty_agents_md_skipped() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(!result.text.contains("# Project Context"));
}
#[test]
fn system_prompt_skills_listed_compactly_without_paths() {
let reg = make_registry();
let skills = vec![
make_skill(
"rust",
"Conventions for Rust code. Extra detail that should not be included.",
"/home/.imp/skills/rust/SKILL.md",
),
make_skill(
"testing",
"Write and review tests",
"/home/.imp/skills/testing/SKILL.md",
),
];
let result = test_assemble(®, &[], &skills, &[], None, None, None);
assert!(result.text.contains(
"Available skills (load with `read ~/.imp/skills/<name>/SKILL.md` when relevant):"
));
assert!(result.text.contains("- rust: Conventions for Rust code."));
assert!(result.text.contains("- testing: Write and review tests"));
assert!(!result.text.contains("/home/.imp/skills/rust/SKILL.md"));
assert!(!result
.text
.contains("Extra detail that should not be included"));
}
#[test]
fn system_prompt_does_not_add_mode_aware_mana_skill_trigger() {
let reg = make_registry();
let skills = vec![make_skill(
"mana",
"Coordinate explicit work through mana",
"/home/.imp/skills/mana/SKILL.md",
)];
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &skills,
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Planner,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("- Trigger:"));
assert!(!result.text.contains("Load `mana`"));
}
#[test]
fn system_prompt_orchestrator_does_not_add_mana_skill_trigger() {
let reg = make_registry();
let skills = vec![make_skill(
"mana",
"Coordinate explicit work through mana",
"/home/.imp/skills/mana/SKILL.md",
)];
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &skills,
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Orchestrator,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("- Trigger:"));
assert!(!result.text.contains("Load `mana`"));
}
#[test]
fn system_prompt_worker_does_not_add_mana_basics_trigger() {
let reg = make_registry();
let skills = vec![
make_skill(
"mana",
"Coordinate multi-step work through mana",
"/home/.imp/skills/mana/SKILL.md",
),
make_skill(
"mana-basics",
"Use native mana actions safely and efficiently",
"/home/.imp/skills/mana-basics/SKILL.md",
),
];
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &skills,
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Worker,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("- Trigger:"));
assert!(!result.text.contains("Load `mana-basics`"));
}
#[test]
fn system_prompt_omits_mana_trigger_without_mana_skill() {
let reg = make_registry();
let skills = vec![make_skill(
"rust",
"Conventions for Rust code",
"/home/.imp/skills/rust/SKILL.md",
)];
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &skills,
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Planner,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("- Trigger:"));
}
#[test]
fn system_prompt_reviewer_mode_omits_mana_trigger() {
let reg = make_registry();
let skills = vec![make_skill(
"mana",
"Coordinate multi-step work through mana",
"/home/.imp/skills/mana/SKILL.md",
)];
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &skills,
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Reviewer,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("- Trigger:"));
}
#[test]
fn system_prompt_empty_skills_skipped() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(!result.text.contains("Available skills"));
}
#[test]
fn system_prompt_facts_included() {
let reg = make_registry();
let facts = vec![
Fact {
text: "Uses JWT for auth".into(),
verified_ago: "2h ago".into(),
},
Fact {
text: "Test suite requires Docker".into(),
verified_ago: "1d ago".into(),
},
];
let result = test_assemble(®, &[], &[], &facts, None, None, None);
assert!(result.text.contains("Project facts:"));
assert!(result
.text
.contains("\"Uses JWT for auth\" [verified 2h ago]"));
assert!(result
.text
.contains("\"Test suite requires Docker\" [verified 1d ago]"));
}
#[test]
fn system_prompt_empty_facts_skipped() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(!result.text.contains("Project facts"));
}
#[test]
fn system_prompt_project_memory_status_included() {
let reg = make_registry();
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: Some(
"Project memory status:\nWarnings:\n- STALE: \"Lockfile drift\"\n\nWorking on:\n- [12] Refresh auth flow",
),
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(result.text.contains("Project memory status:"));
assert!(result.text.contains("Warnings:"));
assert!(result.text.contains("Working on:"));
}
#[test]
fn system_prompt_project_memory_status_empty_string_is_skipped() {
let reg = make_registry();
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: Some(""),
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("Project memory status:"));
}
#[test]
fn system_prompt_project_memory_status_included_separately_from_facts() {
let reg = make_registry();
let facts = vec![Fact {
text: "Uses JWT for auth".into(),
verified_ago: "2h ago".into(),
}];
let status =
"Project memory status:\nWarnings:\n- stale fact\n\nWorking on:\n- [7] Fix auth flow";
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &facts,
project_memory_status: Some(status),
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: None,
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
let facts_pos = result.text.find("Project facts:").unwrap();
let status_pos = result.text.find("Project memory status:").unwrap();
assert!(result
.text
.contains("\"Uses JWT for auth\" [verified 2h ago]"));
assert!(result.text.contains("Warnings:"));
assert!(result.text.contains("Working on:"));
assert!(facts_pos < status_pos);
}
#[test]
fn system_prompt_task_context_included() {
let reg = make_registry();
let task = TaskContext {
title: "Fix the failing auth test".into(),
description: "The JWT validation test panics on expired tokens".into(),
design: None,
acceptance: None,
verify: Some("cargo test auth::jwt_test".into()),
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![],
dependencies: vec![],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let result = test_assemble(®, &[], &[], &[], None, Some(&task), None);
assert!(result.text.contains("## Task"));
assert!(result.text.contains("Title: Fix the failing auth test"));
assert!(result
.text
.contains("Description: The JWT validation test panics"));
assert!(result.text.contains("Verify: cargo test auth::jwt_test"));
assert!(result
.text
.contains("Treat the verify command as the primary completion check for this task."));
}
#[test]
fn system_prompt_task_with_attempts() {
let reg = make_registry();
let task = TaskContext {
title: "Fix bug".into(),
description: "Something is broken".into(),
design: None,
acceptance: None,
verify: None,
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![
Attempt {
number: 1,
outcome: "failed".into(),
summary: "Tried X, got error Y".into(),
},
Attempt {
number: 2,
outcome: "failed".into(),
summary: "Tried Z, still broken".into(),
},
],
dependencies: vec![],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let result = test_assemble(®, &[], &[], &[], None, Some(&task), None);
assert!(result.text.contains("## Previous attempts"));
assert!(result.text.contains(
"Do not repeat a failed approach unchanged; use the attempt history to adjust your plan."
));
assert!(result
.text
.contains("Attempt 1 (failed): Tried X, got error Y"));
assert!(result
.text
.contains("Attempt 2 (failed): Tried Z, still broken"));
}
#[test]
fn system_prompt_task_with_dependencies() {
let reg = make_registry();
let task = TaskContext {
title: "Implement feature".into(),
description: "New feature".into(),
design: None,
acceptance: None,
verify: None,
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![],
dependencies: vec![Dependency {
name: "Schema types".into(),
status: "completed".into(),
detail: "defined in src/schema.rs".into(),
}],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let result = test_assemble(®, &[], &[], &[], None, Some(&task), None);
assert!(result.text.contains("## Dependencies"));
assert!(result.text.contains(
"Respect dependency state when sequencing work; unresolved dependencies are potential blockers."
));
assert!(result
.text
.contains("- Schema types (completed): defined in src/schema.rs"));
}
#[test]
fn system_prompt_task_with_notes_and_context_paths() {
let reg = make_registry();
let task = TaskContext {
title: "Fix auth".into(),
description: "Tighten token validation".into(),
design: Some(
"Keep validation logic in the existing auth module; avoid a broader auth rewrite."
.into(),
),
acceptance: None,
verify: Some("cargo test auth".into()),
verify_timeout_secs: Some(30),
fail_first: true,
notes: Some("Prefer touching only auth paths unless necessary".into()),
attempts: vec![],
dependencies: vec![],
decisions: vec![],
context_paths: vec!["src/auth.rs".into(), "tests/auth.rs".into()],
constraints: vec![
"Scope changes to auth-related files unless broader edits are necessary".into(),
],
};
let result = test_assemble(®, &[], &[], &[], None, Some(&task), None);
assert!(result.text.contains("Design:"));
assert!(result
.text
.contains("Keep validation logic in the existing auth module"));
assert!(result.text.contains("Verify timeout: 30s"));
assert!(result
.text
.contains("Fail-first: verify was expected to fail before implementation"));
assert!(result.text.contains("Notes:"));
assert!(result
.text
.contains("Prefer touching only auth paths unless necessary"));
assert!(result.text.contains("## Referenced files"));
assert!(result.text.contains("- src/auth.rs"));
assert!(result.text.contains("- tests/auth.rs"));
assert!(result.text.contains("## Constraints"));
assert!(result
.text
.contains("Scope changes to auth-related files unless broader edits are necessary"));
}
#[test]
fn system_prompt_no_task_skips_layer5() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(!result.text.contains("## Task"));
}
#[test]
fn system_prompt_task_without_verify_omits_verify_line() {
let reg = make_registry();
let task = TaskContext {
title: "Do something".into(),
description: "Details here".into(),
design: None,
acceptance: None,
verify: None,
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![],
dependencies: vec![],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let result = test_assemble(®, &[], &[], &[], None, Some(&task), None);
assert!(result.text.contains("Title: Do something"));
assert!(!result.text.contains("Verify:"));
}
#[test]
fn system_prompt_readonly_role_filters_tools() {
let reg = make_registry();
let role = make_readonly_role();
let result = test_assemble(®, &[], &[], &[], None, None, Some(&role));
assert!(result.text.contains("- read:"));
assert!(!result.text.contains("- write:"));
assert!(!result.text.contains("- edit:"));
}
#[test]
fn system_prompt_role_instructions_appended() {
let reg = make_registry();
let role = make_readonly_role();
let result = test_assemble(®, &[], &[], &[], None, None, Some(&role));
assert!(result
.text
.contains("Review code carefully. Do not modify files."));
}
#[test]
fn system_prompt_worker_role_includes_all_tools() {
let reg = make_registry();
let role = make_worker_role();
let result = test_assemble(®, &[], &[], &[], None, None, Some(&role));
assert!(result.text.contains("- read:"));
assert!(result.text.contains("- write:"));
assert!(result.text.contains("- edit:"));
assert!(result.text.contains("- bash:"));
}
#[test]
fn system_prompt_no_role_instructions_when_none() {
let reg = make_registry();
let role = make_worker_role();
let result = test_assemble(®, &[], &[], &[], None, None, Some(&role));
let lines: Vec<&str> = result.text.lines().collect();
let after_tools = lines.iter().position(|l| l.starts_with("- bash:")).unwrap();
let remaining = &lines[after_tools + 1..];
let next_content = remaining.iter().find(|l| !l.is_empty());
assert!(next_content.is_none() || !next_content.unwrap().contains("Review"));
}
#[test]
fn system_prompt_tracks_estimated_tokens() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
assert!(result.estimated_tokens > 0);
assert!(result.estimated_tokens >= 10);
}
#[test]
fn system_prompt_more_layers_means_more_tokens() {
let reg = make_registry();
let minimal = test_assemble(®, &[], &[], &[], None, None, None);
let agents = vec![make_agents_md(
"Lots of project context here with many words.",
)];
let skills = vec![make_skill(
"rust",
"Rust conventions",
"/skills/rust/SKILL.md",
)];
let facts = vec![Fact {
text: "Uses Postgres".into(),
verified_ago: "1h ago".into(),
}];
let full = test_assemble(®, &agents, &skills, &facts, None, None, None);
assert!(
full.estimated_tokens > minimal.estimated_tokens,
"full ({}) should have more tokens than minimal ({})",
full.estimated_tokens,
minimal.estimated_tokens
);
}
#[test]
fn system_prompt_all_layers_present() {
let reg = make_registry();
let agents = vec![make_agents_md("Be concise.")];
let skills = vec![make_skill(
"rust",
"Rust code conventions",
"/skills/rust/SKILL.md",
)];
let facts = vec![Fact {
text: "Uses SQLite".into(),
verified_ago: "30m ago".into(),
}];
let task = TaskContext {
title: "Add caching".into(),
description: "Add Redis caching layer".into(),
design: None,
acceptance: None,
verify: Some("cargo test cache".into()),
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![Attempt {
number: 1,
outcome: "failed".into(),
summary: "Wrong key format".into(),
}],
dependencies: vec![Dependency {
name: "Config".into(),
status: "done".into(),
detail: "src/config.rs".into(),
}],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let result = test_assemble(®, &agents, &skills, &facts, None, Some(&task), None);
let identity_pos = result.text.find("You are imp").unwrap();
let policy_pos = result.text.find("Operating rules").unwrap();
let context_pos = result.text.find("# Project Context").unwrap();
let skills_pos = result.text.find("Available skills").unwrap();
let facts_pos = result.text.find("Project facts").unwrap();
let task_pos = result.text.find("## Task").unwrap();
assert!(identity_pos < policy_pos, "identity before policy");
assert!(policy_pos < context_pos, "policy before context");
assert!(context_pos < skills_pos, "context before skills");
assert!(skills_pos < facts_pos, "skills before facts");
assert!(facts_pos < task_pos, "facts before task");
}
#[test]
fn system_prompt_display_impl() {
let reg = make_registry();
let result = test_assemble(®, &[], &[], &[], None, None, None);
let displayed = format!("{result}");
assert_eq!(displayed, result.text);
}
#[test]
fn system_prompt_memory_included() {
let reg = make_registry();
let mem = "══════════════════\nMEMORY [50% — 100/200]\n══════════════════\nUser runs macOS";
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: Some(mem),
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(result.text.contains("MEMORY"));
assert!(result.text.contains("User runs macOS"));
}
#[test]
fn system_prompt_user_profile_included() {
let reg = make_registry();
let user =
"══════════════════\nUSER PROFILE [30% — 42/140]\n══════════════════\nPrefers concise";
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: None,
user_profile: Some(user),
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(result.text.contains("USER PROFILE"));
assert!(result.text.contains("Prefers concise"));
}
#[test]
fn system_prompt_empty_memory_skipped() {
let reg = make_registry();
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &[],
skills: &[],
facts: &[],
project_memory_status: None,
personality: None,
soul: None,
task: None,
role: None,
mode: &AgentMode::Full,
memory: Some(""),
user_profile: Some(""),
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
assert!(!result.text.contains("MEMORY"));
assert!(!result.text.contains("USER PROFILE"));
}
#[test]
fn system_prompt_memory_after_all_other_layers() {
let reg = make_registry();
let agents = vec![make_agents_md("Project context.")];
let skills = vec![make_skill("rust", "Rust", "/skills/rust/SKILL.md")];
let facts = vec![Fact {
text: "Uses SQLite".into(),
verified_ago: "1h".into(),
}];
let task = TaskContext {
title: "Fix bug".into(),
description: "Broken".into(),
design: None,
acceptance: None,
verify: None,
verify_timeout_secs: None,
fail_first: false,
notes: None,
attempts: vec![],
dependencies: vec![],
decisions: vec![],
context_paths: vec![],
constraints: vec![],
};
let mem = "══════\nMEMORY [50%]\n══════\nSome fact";
let result = assemble(&AssembleParams {
tools: ®,
agents_md: &agents,
skills: &skills,
facts: &facts,
project_memory_status: None,
personality: None,
soul: None,
task: Some(&task),
role: None,
mode: &AgentMode::Full,
memory: Some(mem),
user_profile: None,
cwd: None,
learning_enabled: false,
guardrail_profile: None,
});
let identity_pos = result.text.find("You are imp").unwrap();
let context_pos = result.text.find("# Project Context").unwrap();
let facts_pos = result.text.find("Project facts").unwrap();
let task_pos = result.text.find("## Task").unwrap();
let memory_pos = result.text.find("MEMORY").unwrap();
assert!(identity_pos < context_pos);
assert!(context_pos < facts_pos);
assert!(facts_pos < task_pos);
assert!(task_pos < memory_pos, "memory should come after task");
}
}