Skip to main content

imp_core/
system_prompt.rs

1use std::fmt;
2
3use crate::config::AgentMode;
4use crate::context::estimate_tokens;
5use crate::guardrails::{self, GuardrailProfile};
6use crate::personality::{soul_identity_text, PersonalityBand, PersonalityProfile};
7use crate::resources::{AgentsMd, Skill, SoulDoc};
8use crate::roles::Role;
9use crate::tools::ToolRegistry;
10
11/// A project fact from mana-core.
12#[derive(Debug, Clone)]
13pub struct Fact {
14    pub text: String,
15    pub verified_ago: String,
16}
17
18/// Previous attempt info for task context.
19#[derive(Debug, Clone)]
20pub struct Attempt {
21    pub number: u32,
22    pub outcome: String,
23    pub summary: String,
24}
25
26/// Dependency info for task context.
27#[derive(Debug, Clone)]
28pub struct Dependency {
29    pub name: String,
30    pub status: String,
31    pub detail: String,
32}
33
34/// Task context for headless/task mode (Layer 5).
35#[derive(Debug, Clone)]
36pub struct TaskContext {
37    pub title: String,
38    pub description: String,
39    pub design: Option<String>,
40    pub acceptance: Option<String>,
41    pub verify: Option<String>,
42    pub verify_timeout_secs: Option<u64>,
43    pub fail_first: bool,
44    pub notes: Option<String>,
45    pub attempts: Vec<Attempt>,
46    pub dependencies: Vec<Dependency>,
47    pub decisions: Vec<String>,
48    pub context_paths: Vec<String>,
49    pub constraints: Vec<String>,
50}
51
52/// Result of system prompt assembly, including size tracking.
53#[derive(Debug)]
54pub struct AssembledPrompt {
55    pub text: String,
56    pub estimated_tokens: u32,
57}
58
59impl fmt::Display for AssembledPrompt {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(&self.text)
62    }
63}
64
65/// All inputs needed to assemble a system prompt.
66pub struct AssembleParams<'a> {
67    pub tools: &'a ToolRegistry,
68    pub agents_md: &'a [AgentsMd],
69    pub skills: &'a [Skill],
70    pub facts: &'a [Fact],
71    pub project_memory_status: Option<&'a str>,
72    pub personality: Option<&'a PersonalityProfile>,
73    pub soul: Option<&'a SoulDoc>,
74    pub task: Option<&'a TaskContext>,
75    pub role: Option<&'a Role>,
76    pub mode: &'a AgentMode,
77    pub memory: Option<&'a str>,
78    pub user_profile: Option<&'a str>,
79    pub cwd: Option<&'a std::path::Path>,
80    /// Whether to include learning instructions in the system prompt.
81    pub learning_enabled: bool,
82    /// Resolved guardrail profile (None = guardrails disabled).
83    pub guardrail_profile: Option<GuardrailProfile>,
84}
85
86/// Assemble the system prompt from seven layers.
87///
88/// - Layer 1: Identity + tool descriptions (+ role instructions if any)
89/// - Layer 1.25: Execution policy
90/// - Layer 1.5: Environment context
91/// - Layer 2: Project context from AGENTS.md files
92/// - Layer 3: Skills index
93/// - Layer 4: Mana facts (skipped if empty)
94/// - Layer 4.25: Compact project memory status (skipped if empty)
95/// - Layer 5: Task context (only in headless/task mode)
96/// - Layer 6: Agent memory (if present)
97pub fn assemble(params: &AssembleParams<'_>) -> AssembledPrompt {
98    assemble_inner(params)
99}
100
101fn assemble_inner(p: &AssembleParams<'_>) -> AssembledPrompt {
102    let mut parts = Vec::new();
103
104    // Layer 1: Identity + tool descriptions
105    parts.push(identity_layer(
106        p.tools,
107        p.role,
108        p.mode,
109        p.learning_enabled,
110        p.personality,
111        p.soul,
112    ));
113
114    // Layer 1.25: Execution policy (currently folded into identity operating rules)
115    let execution_policy = execution_policy_layer();
116    if !execution_policy.is_empty() {
117        parts.push(execution_policy);
118    }
119
120    // Layer 1.5: Environment context
121    parts.push(environment_layer(p.cwd));
122
123    // Layer 2: Project context from AGENTS.md
124    if !p.agents_md.is_empty() {
125        parts.push(agents_md_layer(p.agents_md));
126    }
127
128    // Layer 3: Skills index
129    if !p.skills.is_empty() {
130        parts.push(skills_layer(p.skills, p.mode));
131    }
132
133    // Layer 4: Mana facts
134    if !p.facts.is_empty() {
135        parts.push(facts_layer(p.facts));
136    }
137
138    // Layer 4.25: Compact project memory status
139    if let Some(status) = p.project_memory_status {
140        if !status.is_empty() {
141            parts.push(project_memory_status_layer(status));
142        }
143    }
144
145    // Layer 4.5: Engineering guardrails (when enabled)
146    if let Some(profile) = p.guardrail_profile {
147        parts.push(guardrails::guardrails_layer(profile));
148    }
149
150    // Layer 5: Task context (headless mode only)
151    if let Some(task) = p.task {
152        parts.push(task_layer(task));
153        parts.push(headless_execution_layer(task));
154    }
155
156    // Layer 6: Agent memory
157    if let Some(mem) = p.memory {
158        if !mem.is_empty() {
159            parts.push(mem.to_string());
160        }
161    }
162    if let Some(user) = p.user_profile {
163        if !user.is_empty() {
164            parts.push(user.to_string());
165        }
166    }
167
168    let text = parts.join("\n\n");
169    let estimated_tokens = estimate_tokens(&text);
170
171    AssembledPrompt {
172        text,
173        estimated_tokens,
174    }
175}
176
177fn identity_layer(
178    tools: &ToolRegistry,
179    role: Option<&Role>,
180    mode: &AgentMode,
181    learning_enabled: bool,
182    personality: Option<&PersonalityProfile>,
183    soul: Option<&SoulDoc>,
184) -> String {
185    let mut s = String::new();
186    if let Some(soul) = soul {
187        s.push_str(&soul_identity_text(&soul.content));
188    } else if let Some(personality) = personality {
189        s.push_str(&personality.identity.render_sentence());
190    } else {
191        s.push_str("You are imp, a coding agent.");
192    }
193    s.push_str("\n\nAvailable tools:\n");
194
195    let defs = match role {
196        Some(r) if r.readonly => tools.readonly_definitions(),
197        _ => tools.definitions_for_mode(mode),
198    };
199
200    for def in &defs {
201        s.push_str(&format!("- {}: {}\n", def.name, def.description));
202    }
203
204    if let Some(soul) = soul {
205        s.push_str("\n\nSoul:\n");
206        s.push_str(&soul.content);
207        s.push('\n');
208    } else if let Some(personality) = personality {
209        let working_style = working_style_lines(&personality.sliders);
210        if !working_style.is_empty() {
211            s.push_str("\nWorking style:\n");
212            for line in working_style {
213                s.push_str("- ");
214                s.push_str(line);
215                s.push('\n');
216            }
217        }
218    }
219
220    s.push_str("\nTool routing:\n");
221    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");
222    if defs.iter().any(|def| def.name == "git") {
223        s.push_str("- Use `git` for local repo/worktree operations; use `bash` for uncovered git commands.\n");
224    }
225    if defs.iter().any(|def| def.name == "mana") {
226        s.push_str("- Prefer native `mana` actions over shell for mana work.\n");
227    }
228    s.push_str("- Use `read` before explaining or editing specific files; use `edit`/`write` for file changes.\n");
229
230    s.push_str("\nOperating rules:\n");
231    s.push_str("- Re-check the user's intent each turn; distinguish discussion, planning, implementation, review, and orchestration.\n");
232    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");
233    s.push_str("- For analysis-only requests, stay read-only. For implementation, make small reversible changes and verify with the narrowest useful check.\n");
234    s.push_str("- Treat failed commands, compiler errors, and missing evidence as blockers to resolve or report; never claim unverified success.\n");
235    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");
236    s.push_str("- Keep replies concise and evidence-oriented: what changed or was found, how it was verified, and what remains.\n");
237    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");
238    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");
239    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");
240    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");
241    s.push_str("- Update mana after failures or material planning changes before relying on chat memory.\n");
242    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");
243    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");
244
245    // Append role instructions after identity layer
246    if let Some(role) = role {
247        if let Some(ref instructions) = role.instructions {
248            s.push('\n');
249            s.push_str(instructions);
250            s.push('\n');
251        }
252    }
253
254    // Append mode instructions if present
255    if let Some(instructions) = mode.instructions() {
256        s.push('\n');
257        s.push_str(instructions);
258        s.push('\n');
259    }
260
261    // Append learning instructions when enabled
262    if learning_enabled {
263        s.push('\n');
264        s.push_str(crate::learning::LEARNING_INSTRUCTIONS);
265        s.push('\n');
266    }
267
268    s
269}
270
271fn execution_policy_layer() -> String {
272    String::new()
273}
274
275fn working_style_lines(sliders: &crate::personality::PersonalitySliders) -> Vec<&'static str> {
276    vec![
277        autonomy_line(sliders.autonomy),
278        verbosity_line(sliders.verbosity),
279        caution_line(sliders.caution),
280        warmth_line(sliders.warmth),
281        planning_depth_line(sliders.planning_depth),
282        "If you find yourself repeating the same action without progress, step back and try a different approach or ask the user for guidance.",
283    ]
284}
285
286pub(crate) fn autonomy_line(band: PersonalityBand) -> &'static str {
287    match band {
288        PersonalityBand::VeryLow => {
289            "Ask for confirmation before making consequential decisions or larger changes."
290        }
291        PersonalityBand::Low => {
292            "Prefer confirmation before acting when requirements or consequences are unclear."
293        }
294        PersonalityBand::Medium => {
295            "Act on clear next steps, but ask when requirements are ambiguous."
296        }
297        PersonalityBand::High => {
298            "Act independently by default and ask when blocked, uncertain, or facing a consequential decision. Keep working until the task is fully resolved before yielding."
299        }
300        PersonalityBand::VeryHigh => {
301            "Take initiative aggressively on clear work and only ask when blocked or genuinely uncertain. Keep working until the task is fully resolved before yielding."
302        }
303    }
304}
305
306pub(crate) fn verbosity_line(band: PersonalityBand) -> &'static str {
307    match band {
308        PersonalityBand::VeryLow => "Keep responses terse and strongly action-oriented.",
309        PersonalityBand::Low => "Keep responses brief and focused on progress.",
310        PersonalityBand::Medium => {
311            "Be concise by default, but explain important tradeoffs when useful."
312        }
313        PersonalityBand::High => {
314            "Explain reasoning and tradeoffs when they help the user follow the work."
315        }
316        PersonalityBand::VeryHigh => {
317            "Give fuller explanations of reasoning, tradeoffs, and next steps."
318        }
319    }
320}
321
322pub(crate) fn caution_line(band: PersonalityBand) -> &'static str {
323    match band {
324        PersonalityBand::VeryLow => {
325            "Move forward with reasonable assumptions when the path is clear."
326        }
327        PersonalityBand::Low => "Favor progress over caution when risks are limited and local.",
328        PersonalityBand::Medium => "Balance steady progress with avoiding avoidable risk.",
329        PersonalityBand::High => {
330            "Prefer small, reversible changes and verify assumptions before riskier actions."
331        }
332        PersonalityBand::VeryHigh => {
333            "Be highly conservative with risky changes: verify assumptions and avoid acting on weak evidence."
334        }
335    }
336}
337
338pub(crate) fn warmth_line(band: PersonalityBand) -> &'static str {
339    match band {
340        PersonalityBand::VeryLow => "Use a direct, neutral tone.",
341        PersonalityBand::Low => "Use a clear, matter-of-fact tone.",
342        PersonalityBand::Medium => "Use a clear and calm tone.",
343        PersonalityBand::High => "Use a warm, supportive tone without becoming verbose.",
344        PersonalityBand::VeryHigh => {
345            "Use a notably warm, encouraging tone while staying useful and grounded."
346        }
347    }
348}
349
350pub(crate) fn planning_depth_line(band: PersonalityBand) -> &'static str {
351    match band {
352        PersonalityBand::VeryLow => "Favor immediate execution on the most obvious next step.",
353        PersonalityBand::Low => "Plan lightly, then move quickly into execution.",
354        PersonalityBand::Medium => "Plan briefly, then execute.",
355        PersonalityBand::High => "Think through structure and likely consequences before acting.",
356        PersonalityBand::VeryHigh => {
357            "Be methodical: think through structure, dependencies, and consequences before acting."
358        }
359    }
360}
361
362fn environment_layer(cwd: Option<&std::path::Path>) -> String {
363    let home = std::env::var("HOME").unwrap_or_default();
364    let cwd_str = cwd.map(|p| p.display().to_string()).unwrap_or_else(|| {
365        std::env::current_dir()
366            .map(|p| p.display().to_string())
367            .unwrap_or_default()
368    });
369    let os = std::env::consts::OS;
370    let today = {
371        use std::time::{SystemTime, UNIX_EPOCH};
372        let secs = SystemTime::now()
373            .duration_since(UNIX_EPOCH)
374            .unwrap_or_default()
375            .as_secs();
376        let days = secs / 86400;
377        // Simple date calculation
378        let (y, m, d) = days_to_ymd(days);
379        format!("{y}-{m:02}-{d:02}")
380    };
381    format!("Environment: cwd={cwd_str}, os={os}, home={home}, date={today}")
382}
383
384/// Convert days since Unix epoch to (year, month, day).
385fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
386    // Civil days algorithm (Howard Hinnant)
387    days += 719_468;
388    let era = days / 146_097;
389    let doe = days - era * 146_097;
390    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
391    let y = yoe + era * 400;
392    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
393    let mp = (5 * doy + 2) / 153;
394    let d = doy - (153 * mp + 2) / 5 + 1;
395    let m = if mp < 10 { mp + 3 } else { mp - 9 };
396    let y = if m <= 2 { y + 1 } else { y };
397    (y, m, d)
398}
399
400fn agents_md_layer(agents: &[AgentsMd]) -> String {
401    let mut s = String::from("# Project Context\n\n");
402    for agent in agents {
403        s.push_str(&agent.content);
404        s.push('\n');
405    }
406    s
407}
408
409fn skills_layer(skills: &[Skill], _mode: &AgentMode) -> String {
410    let mut s = String::from(
411        "Available skills (load with `read ~/.imp/skills/<name>/SKILL.md` when relevant):\n",
412    );
413    for skill in skills {
414        let description = compact_skill_description(&skill.description);
415        if description.is_empty() {
416            s.push_str(&format!("- {}\n", skill.name));
417        } else {
418            s.push_str(&format!("- {}: {}\n", skill.name, description));
419        }
420    }
421    s
422}
423
424fn compact_skill_description(description: &str) -> String {
425    let normalized = description.split_whitespace().collect::<Vec<_>>().join(" ");
426    let first_sentence = normalized
427        .split_once(". ")
428        .map(|(first, _)| format!("{}.", first))
429        .unwrap_or(normalized);
430    truncate_chars(&first_sentence, 120)
431}
432
433fn truncate_chars(text: &str, max_chars: usize) -> String {
434    if text.chars().count() <= max_chars {
435        return text.to_string();
436    }
437
438    let mut truncated = text
439        .chars()
440        .take(max_chars.saturating_sub(1))
441        .collect::<String>();
442    truncated.push('…');
443    truncated
444}
445
446fn facts_layer(facts: &[Fact]) -> String {
447    let mut s = String::from("Project facts:\n");
448    for fact in facts {
449        s.push_str(&format!(
450            "- \"{}\" [verified {}]\n",
451            fact.text, fact.verified_ago
452        ));
453    }
454    s
455}
456
457fn project_memory_status_layer(status: &str) -> String {
458    status.to_string()
459}
460
461fn task_layer(task: &TaskContext) -> String {
462    let mut s = String::from("## Task\n");
463    s.push_str(&format!("Title: {}\n", task.title));
464    s.push_str(&format!("Description: {}\n", task.description));
465    if let Some(ref design) = task.design {
466        if !design.trim().is_empty() {
467            s.push_str("Design:\n");
468            s.push_str(design);
469            s.push('\n');
470        }
471    }
472    if let Some(ref notes) = task.notes {
473        if !notes.trim().is_empty() {
474            s.push_str("Notes:\n");
475            s.push_str(notes);
476            s.push('\n');
477        }
478    }
479    if let Some(ref acceptance) = task.acceptance {
480        s.push_str("Acceptance:\n");
481        s.push_str(acceptance);
482        s.push('\n');
483    }
484    if let Some(ref verify) = task.verify {
485        s.push_str(&format!("Verify: {}\n", verify));
486        if let Some(timeout_secs) = task.verify_timeout_secs {
487            s.push_str(&format!("Verify timeout: {}s\n", timeout_secs));
488        }
489        if task.fail_first {
490            s.push_str("Fail-first: verify was expected to fail before implementation; preserve that contract.\n");
491        }
492        s.push_str("Treat the verify command as the primary completion check for this task.\n");
493    }
494
495    if !task.context_paths.is_empty() {
496        s.push_str("\n## Referenced files\n");
497        s.push_str("Use these declared file/path hints before broadening the search.\n");
498        for path in &task.context_paths {
499            s.push_str(&format!("- {}\n", path));
500        }
501    }
502
503    if !task.constraints.is_empty() {
504        s.push_str("\n## Constraints\n");
505        for constraint in &task.constraints {
506            s.push_str(&format!("- {}\n", constraint));
507        }
508    }
509
510    if !task.attempts.is_empty() {
511        s.push_str("\n## Previous attempts\n");
512        s.push_str("Do not repeat a failed approach unchanged; use the attempt history to adjust your plan.\n");
513        for attempt in &task.attempts {
514            s.push_str(&format!(
515                "Attempt {} ({}): {}\n",
516                attempt.number, attempt.outcome, attempt.summary
517            ));
518        }
519    }
520
521    if !task.dependencies.is_empty() {
522        s.push_str("\n## Dependencies\n");
523        s.push_str("Respect dependency state when sequencing work; unresolved dependencies are potential blockers.\n");
524        for dep in &task.dependencies {
525            s.push_str(&format!(
526                "- {} ({}): {}\n",
527                dep.name, dep.status, dep.detail
528            ));
529        }
530    }
531
532    if !task.decisions.is_empty() {
533        s.push_str("\n## Unresolved decisions\n");
534        s.push_str("These decisions block fully autonomous execution; resolve them or surface them clearly instead of guessing.\n");
535        for decision in &task.decisions {
536            s.push_str(&format!("- {}\n", decision));
537        }
538    }
539
540    s
541}
542
543fn headless_execution_layer(task: &TaskContext) -> String {
544    let mut s = String::from("## Headless execution contract\n");
545    s.push_str("- You are executing an explicit mana unit, not exploring broadly.\n");
546    s.push_str("- Treat the unit title, description, notes, acceptance criteria, and verify gate as the source of truth for scope and success.\n");
547    s.push_str("- Execute the assigned outcome before expanding into adjacent cleanup, refactors, or unrelated improvements.\n");
548    s.push_str("- Use explicit file references and prefilled context first before searching more broadly.\n");
549    s.push_str(
550        "- If the unit includes prior failed attempts, do not retry the same plan unchanged.\n",
551    );
552    s.push_str("- If dependency state or prerequisite decisions are unresolved, treat that as a blocker rather than improvising around it.\n");
553    s.push_str("- Keep progress updates concise and useful. Record meaningful discoveries, blockers, and revised plans with `mana update`.\n");
554    if task.verify.is_some() {
555        s.push_str("- If the verify command fails, either fix the issue or report the exact blocker. Do not claim completion anyway.\n");
556    }
557    s.push_str("- In batch-verify flows, treat your goal as leaving the unit ready for verify rather than assuming verify already passed.\n");
558    s.push_str(
559        "- Respect parent/child structure: finish this unit's outcome, not the whole feature.\n",
560    );
561    s
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use std::path::PathBuf;
568    use std::sync::Arc;
569
570    use crate::personality::{
571        PersonaFocus, PersonaRole, PersonalityBand, PersonalityIdentity, PersonalityProfile,
572        PersonalitySliders, VoiceWord, WorkStyleWord,
573    };
574    use crate::resources::SoulDoc;
575    use crate::tools::{Tool, ToolContext, ToolOutput};
576    use async_trait::async_trait;
577
578    // -- Test tool helpers --
579
580    struct FakeTool {
581        name: &'static str,
582        description: &'static str,
583        readonly: bool,
584    }
585
586    #[async_trait]
587    impl Tool for FakeTool {
588        fn name(&self) -> &str {
589            self.name
590        }
591        fn label(&self) -> &str {
592            self.name
593        }
594        fn description(&self) -> &str {
595            self.description
596        }
597        fn parameters(&self) -> serde_json::Value {
598            serde_json::json!({"type": "object"})
599        }
600        fn is_readonly(&self) -> bool {
601            self.readonly
602        }
603        async fn execute(
604            &self,
605            _: &str,
606            _: serde_json::Value,
607            _: ToolContext,
608        ) -> crate::Result<ToolOutput> {
609            Ok(ToolOutput::text("ok"))
610        }
611    }
612
613    fn make_registry() -> ToolRegistry {
614        let mut reg = ToolRegistry::new();
615        reg.register(Arc::new(FakeTool {
616            name: "read",
617            description: "Read file contents",
618            readonly: true,
619        }));
620        reg.register(Arc::new(FakeTool {
621            name: "write",
622            description: "Write content to a file",
623            readonly: false,
624        }));
625        reg.register(Arc::new(FakeTool {
626            name: "edit",
627            description: "Edit a file by replacing exact text",
628            readonly: false,
629        }));
630        reg.register(Arc::new(FakeTool {
631            name: "bash",
632            description: "Run shell commands",
633            readonly: false,
634        }));
635        reg
636    }
637
638    fn make_skill(name: &str, desc: &str, path: &str) -> Skill {
639        Skill {
640            name: name.into(),
641            description: desc.into(),
642            path: PathBuf::from(path),
643        }
644    }
645
646    fn make_agents_md(content: &str) -> AgentsMd {
647        AgentsMd {
648            path: PathBuf::from("/project/AGENTS.md"),
649            content: content.into(),
650        }
651    }
652
653    fn make_readonly_role() -> Role {
654        use crate::roles::ToolSet;
655        Role {
656            name: "reviewer".into(),
657            model: None,
658            thinking_level: None,
659            tool_set: ToolSet::All,
660            readonly: true,
661            instructions: Some("Review code carefully. Do not modify files.".into()),
662        }
663    }
664
665    fn make_worker_role() -> Role {
666        use crate::roles::ToolSet;
667        Role {
668            name: "worker".into(),
669            model: None,
670            thinking_level: None,
671            tool_set: ToolSet::All,
672            readonly: false,
673            instructions: None,
674        }
675    }
676
677    fn make_personality() -> PersonalityProfile {
678        PersonalityProfile {
679            identity: PersonalityIdentity {
680                name: "Nova".into(),
681                work_style: WorkStyleWord::Careful,
682                voice: VoiceWord::Direct,
683                focus: PersonaFocus::Research,
684                role: PersonaRole::Assistant,
685            },
686            sliders: PersonalitySliders {
687                autonomy: PersonalityBand::Low,
688                verbosity: PersonalityBand::Medium,
689                caution: PersonalityBand::VeryHigh,
690                warmth: PersonalityBand::High,
691                planning_depth: PersonalityBand::VeryLow,
692            },
693        }
694    }
695
696    /// Test helper: shorthand for assemble() with no memory/user_profile.
697    fn test_assemble(
698        tools: &ToolRegistry,
699        agents_md: &[AgentsMd],
700        skills: &[Skill],
701        facts: &[Fact],
702        personality: Option<&PersonalityProfile>,
703        task: Option<&TaskContext>,
704        role: Option<&Role>,
705    ) -> AssembledPrompt {
706        assemble(&AssembleParams {
707            tools,
708            agents_md,
709            skills,
710            facts,
711            project_memory_status: None,
712            personality,
713            soul: None,
714            task,
715            role,
716            mode: &AgentMode::Full,
717            memory: None,
718            user_profile: None,
719            cwd: None,
720            learning_enabled: false,
721            guardrail_profile: None,
722        })
723    }
724
725    // -- Layer 1: Identity --
726
727    #[test]
728    fn system_prompt_includes_operating_rules() {
729        let reg = make_registry();
730        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
731        assert!(result.text.contains("Operating rules:"));
732        assert!(result.text.contains(
733            "Ground repository claims in files or tool output inspected in this session"
734        ));
735        assert!(result.text.contains(
736            "For analysis-only requests, stay read-only. For implementation, make small reversible changes"
737        ));
738    }
739
740    #[test]
741    fn system_prompt_includes_conversation_time_mana_planning_doctrine() {
742        let reg = make_registry();
743        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
744        assert!(result.text.contains(
745            "During planning/design, externalize real durable structure only when it changes project/work state the user is actively developing"
746        ));
747        assert!(result
748            .text
749            .contains("Do not create mana artifacts from explanation-only answers"));
750        assert!(result.text.contains("epics/tasks/notes/decisions"));
751        assert!(result.text.contains("reserve facts for verifiable claims"));
752        assert!(result.text.contains(
753            "Update mana after failures or material planning changes before relying on chat memory"
754        ));
755        assert!(result
756            .text
757            .contains("mana writes are checkpoints, not proof of completion"));
758        assert_eq!(
759            result
760                .text
761                .matches("mana writes are checkpoints, not proof of completion")
762                .count(),
763            1,
764            "mana checkpoint guidance should appear once"
765        );
766        assert!(!result.text.contains("Mana doctrine:"));
767        assert!(!result
768            .text
769            .contains("between-turn mana update before the substantive reply"));
770        assert!(!result
771            .text
772            .contains("include a concise mana delta summary in the response"));
773    }
774
775    #[test]
776    fn system_prompt_identity_includes_all_tools() {
777        let reg = make_registry();
778        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
779        assert!(result.text.contains("You are imp, a coding agent."));
780        assert!(result.text.contains("- read: Read file contents"));
781        assert!(result.text.contains("- write: Write content to a file"));
782        assert!(result
783            .text
784            .contains("- edit: Edit a file by replacing exact text"));
785        assert!(result.text.contains("- bash: Run shell commands"));
786    }
787
788    #[test]
789    fn system_prompt_mana_guidance_prefers_native_tool_when_available() {
790        let mut reg = make_registry();
791        reg.register(Arc::new(FakeTool {
792            name: "mana",
793            description: "Manage mana work natively",
794            readonly: false,
795        }));
796
797        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
798        assert!(result
799            .text
800            .contains("Prefer native `mana` actions over shell for mana work."));
801    }
802
803    #[test]
804    fn system_prompt_mana_guidance_omitted_without_mana_tool() {
805        let reg = make_registry();
806        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
807        assert!(!result
808            .text
809            .contains("Prefer native `mana` actions over shell for mana work."));
810    }
811
812    #[test]
813    fn system_prompt_no_mana_guidance_or_delegation_in_prompt() {
814        // Extended mana guidance lives in native `mana guide`/`mana template` affordances.
815        // Verify large legacy prompt blocks no longer appear regardless of tool availability.
816        let mut reg = make_registry();
817        reg.register(Arc::new(FakeTool {
818            name: "bash",
819            description: "Run shell commands",
820            readonly: false,
821        }));
822        reg.register(Arc::new(FakeTool {
823            name: "mana",
824            description: "Manage mana work",
825            readonly: false,
826        }));
827
828        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
829        assert!(
830            !result.text.contains("Mana guidance:"),
831            "mana guidance block should not appear in system prompt"
832        );
833        assert!(
834            !result.text.contains("## Mana delegation"),
835            "delegation guidance should not appear in system prompt"
836        );
837    }
838
839    #[test]
840    fn system_prompt_identity_only_when_all_layers_empty() {
841        let reg = make_registry();
842        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
843        // Should have identity but no section headers for missing layers
844        assert!(result.text.contains("You are imp"));
845        assert!(!result.text.contains("# Project Context"));
846        assert!(!result.text.contains("Available skills"));
847        assert!(!result.text.contains("Project facts"));
848        assert!(!result.text.contains("## Task"));
849    }
850
851    #[test]
852    fn system_prompt_uses_personality_identity_sentence() {
853        let reg = make_registry();
854        let personality = make_personality();
855        let result = test_assemble(&reg, &[], &[], &[], Some(&personality), None, None);
856        assert!(result
857            .text
858            .contains("You are Nova, a careful, direct, research assistant."));
859    }
860
861    #[test]
862    fn system_prompt_renders_personality_working_style_block() {
863        let reg = make_registry();
864        let personality = make_personality();
865        let result = test_assemble(&reg, &[], &[], &[], Some(&personality), None, None);
866        assert!(result.text.contains("Working style:"));
867        assert!(result.text.contains(
868            "Prefer confirmation before acting when requirements or consequences are unclear."
869        ));
870        assert!(result
871            .text
872            .contains("Be concise by default, but explain important tradeoffs when useful."));
873        assert!(result.text.contains(
874            "Be highly conservative with risky changes: verify assumptions and avoid acting on weak evidence."
875        ));
876        assert!(result
877            .text
878            .contains("Use a warm, supportive tone without becoming verbose."));
879        assert!(result
880            .text
881            .contains("Favor immediate execution on the most obvious next step."));
882    }
883
884    #[test]
885    fn system_prompt_prefers_soul_over_personality_profile() {
886        let reg = make_registry();
887        let personality = make_personality();
888        let soul = SoulDoc {
889            path: PathBuf::from("/tmp/soul.md"),
890            content: "# Soul\n\nYou are Sol, a tuned and reflective collaborator.\n\n## Tunables\n\n- Autonomy: Act independently by default.\n".into(),
891        };
892        let result = assemble(&AssembleParams {
893            tools: &reg,
894            agents_md: &[],
895            skills: &[],
896            facts: &[],
897            project_memory_status: None,
898            personality: Some(&personality),
899            soul: Some(&soul),
900            task: None,
901            role: None,
902            mode: &AgentMode::Full,
903            memory: None,
904            user_profile: None,
905            cwd: None,
906            learning_enabled: false,
907            guardrail_profile: None,
908        });
909        assert!(result
910            .text
911            .contains("You are Sol, a tuned and reflective collaborator."));
912        assert!(result.text.contains("Soul:"));
913        assert!(result.text.contains("## Tunables"));
914        assert!(!result.text.contains("Working style:"));
915    }
916
917    #[test]
918    fn system_prompt_without_soul_keeps_personality_working_style_block() {
919        let reg = make_registry();
920        let personality = make_personality();
921        let result = test_assemble(&reg, &[], &[], &[], Some(&personality), None, None);
922        assert!(result.text.contains("Working style:"));
923    }
924
925    // -- Layer 2: AGENTS.md --
926
927    #[test]
928    fn system_prompt_agents_md_included_verbatim() {
929        let reg = make_registry();
930        let agents = vec![make_agents_md("# Rules\n\nUse snake_case everywhere.")];
931        let result = test_assemble(&reg, &agents, &[], &[], None, None, None);
932        assert!(result.text.contains("# Project Context"));
933        assert!(result
934            .text
935            .contains("# Rules\n\nUse snake_case everywhere."));
936    }
937
938    #[test]
939    fn system_prompt_multiple_agents_md_concatenated() {
940        let reg = make_registry();
941        let agents = vec![
942            make_agents_md("Global rules here."),
943            make_agents_md("Project rules here."),
944        ];
945        let result = test_assemble(&reg, &agents, &[], &[], None, None, None);
946        assert!(result.text.contains("Global rules here."));
947        assert!(result.text.contains("Project rules here."));
948    }
949
950    #[test]
951    fn system_prompt_empty_agents_md_skipped() {
952        let reg = make_registry();
953        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
954        assert!(!result.text.contains("# Project Context"));
955    }
956
957    // -- Layer 3: Skills --
958
959    #[test]
960    fn system_prompt_skills_listed_compactly_without_paths() {
961        let reg = make_registry();
962        let skills = vec![
963            make_skill(
964                "rust",
965                "Conventions for Rust code. Extra detail that should not be included.",
966                "/home/.imp/skills/rust/SKILL.md",
967            ),
968            make_skill(
969                "testing",
970                "Write and review tests",
971                "/home/.imp/skills/testing/SKILL.md",
972            ),
973        ];
974        let result = test_assemble(&reg, &[], &skills, &[], None, None, None);
975        assert!(result.text.contains(
976            "Available skills (load with `read ~/.imp/skills/<name>/SKILL.md` when relevant):"
977        ));
978        assert!(result.text.contains("- rust: Conventions for Rust code."));
979        assert!(result.text.contains("- testing: Write and review tests"));
980        assert!(!result.text.contains("/home/.imp/skills/rust/SKILL.md"));
981        assert!(!result
982            .text
983            .contains("Extra detail that should not be included"));
984    }
985
986    #[test]
987    fn system_prompt_does_not_add_mode_aware_mana_skill_trigger() {
988        let reg = make_registry();
989        let skills = vec![make_skill(
990            "mana",
991            "Coordinate explicit work through mana",
992            "/home/.imp/skills/mana/SKILL.md",
993        )];
994        let result = assemble(&AssembleParams {
995            tools: &reg,
996            agents_md: &[],
997            skills: &skills,
998            facts: &[],
999            project_memory_status: None,
1000            personality: None,
1001            soul: None,
1002            task: None,
1003            role: None,
1004            mode: &AgentMode::Planner,
1005            memory: None,
1006            user_profile: None,
1007            cwd: None,
1008            learning_enabled: false,
1009            guardrail_profile: None,
1010        });
1011
1012        assert!(!result.text.contains("- Trigger:"));
1013        assert!(!result.text.contains("Load `mana`"));
1014    }
1015
1016    #[test]
1017    fn system_prompt_orchestrator_does_not_add_mana_skill_trigger() {
1018        let reg = make_registry();
1019        let skills = vec![make_skill(
1020            "mana",
1021            "Coordinate explicit work through mana",
1022            "/home/.imp/skills/mana/SKILL.md",
1023        )];
1024        let result = assemble(&AssembleParams {
1025            tools: &reg,
1026            agents_md: &[],
1027            skills: &skills,
1028            facts: &[],
1029            project_memory_status: None,
1030            personality: None,
1031            soul: None,
1032            task: None,
1033            role: None,
1034            mode: &AgentMode::Orchestrator,
1035            memory: None,
1036            user_profile: None,
1037            cwd: None,
1038            learning_enabled: false,
1039            guardrail_profile: None,
1040        });
1041
1042        assert!(!result.text.contains("- Trigger:"));
1043        assert!(!result.text.contains("Load `mana`"));
1044    }
1045
1046    #[test]
1047    fn system_prompt_worker_does_not_add_mana_basics_trigger() {
1048        let reg = make_registry();
1049        let skills = vec![
1050            make_skill(
1051                "mana",
1052                "Coordinate multi-step work through mana",
1053                "/home/.imp/skills/mana/SKILL.md",
1054            ),
1055            make_skill(
1056                "mana-basics",
1057                "Use native mana actions safely and efficiently",
1058                "/home/.imp/skills/mana-basics/SKILL.md",
1059            ),
1060        ];
1061        let result = assemble(&AssembleParams {
1062            tools: &reg,
1063            agents_md: &[],
1064            skills: &skills,
1065            facts: &[],
1066            project_memory_status: None,
1067            personality: None,
1068            soul: None,
1069            task: None,
1070            role: None,
1071            mode: &AgentMode::Worker,
1072            memory: None,
1073            user_profile: None,
1074            cwd: None,
1075            learning_enabled: false,
1076            guardrail_profile: None,
1077        });
1078
1079        assert!(!result.text.contains("- Trigger:"));
1080        assert!(!result.text.contains("Load `mana-basics`"));
1081    }
1082
1083    #[test]
1084    fn system_prompt_omits_mana_trigger_without_mana_skill() {
1085        let reg = make_registry();
1086        let skills = vec![make_skill(
1087            "rust",
1088            "Conventions for Rust code",
1089            "/home/.imp/skills/rust/SKILL.md",
1090        )];
1091        let result = assemble(&AssembleParams {
1092            tools: &reg,
1093            agents_md: &[],
1094            skills: &skills,
1095            facts: &[],
1096            project_memory_status: None,
1097            personality: None,
1098            soul: None,
1099            task: None,
1100            role: None,
1101            mode: &AgentMode::Planner,
1102            memory: None,
1103            user_profile: None,
1104            cwd: None,
1105            learning_enabled: false,
1106            guardrail_profile: None,
1107        });
1108
1109        assert!(!result.text.contains("- Trigger:"));
1110    }
1111
1112    #[test]
1113    fn system_prompt_reviewer_mode_omits_mana_trigger() {
1114        let reg = make_registry();
1115        let skills = vec![make_skill(
1116            "mana",
1117            "Coordinate multi-step work through mana",
1118            "/home/.imp/skills/mana/SKILL.md",
1119        )];
1120        let result = assemble(&AssembleParams {
1121            tools: &reg,
1122            agents_md: &[],
1123            skills: &skills,
1124            facts: &[],
1125            project_memory_status: None,
1126            personality: None,
1127            soul: None,
1128            task: None,
1129            role: None,
1130            mode: &AgentMode::Reviewer,
1131            memory: None,
1132            user_profile: None,
1133            cwd: None,
1134            learning_enabled: false,
1135            guardrail_profile: None,
1136        });
1137
1138        assert!(!result.text.contains("- Trigger:"));
1139    }
1140
1141    #[test]
1142    fn system_prompt_empty_skills_skipped() {
1143        let reg = make_registry();
1144        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
1145        assert!(!result.text.contains("Available skills"));
1146    }
1147
1148    // -- Layer 4: Mana facts --
1149
1150    #[test]
1151    fn system_prompt_facts_included() {
1152        let reg = make_registry();
1153        let facts = vec![
1154            Fact {
1155                text: "Uses JWT for auth".into(),
1156                verified_ago: "2h ago".into(),
1157            },
1158            Fact {
1159                text: "Test suite requires Docker".into(),
1160                verified_ago: "1d ago".into(),
1161            },
1162        ];
1163        let result = test_assemble(&reg, &[], &[], &facts, None, None, None);
1164        assert!(result.text.contains("Project facts:"));
1165        assert!(result
1166            .text
1167            .contains("\"Uses JWT for auth\" [verified 2h ago]"));
1168        assert!(result
1169            .text
1170            .contains("\"Test suite requires Docker\" [verified 1d ago]"));
1171    }
1172
1173    #[test]
1174    fn system_prompt_empty_facts_skipped() {
1175        let reg = make_registry();
1176        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
1177        assert!(!result.text.contains("Project facts"));
1178    }
1179
1180    #[test]
1181    fn system_prompt_project_memory_status_included() {
1182        let reg = make_registry();
1183        let result = assemble(&AssembleParams {
1184            tools: &reg,
1185            agents_md: &[],
1186            skills: &[],
1187            facts: &[],
1188            project_memory_status: Some(
1189                "Project memory status:\nWarnings:\n- STALE: \"Lockfile drift\"\n\nWorking on:\n- [12] Refresh auth flow",
1190            ),
1191            personality: None,
1192            soul: None,
1193            task: None,
1194            role: None,
1195            mode: &AgentMode::Full,
1196            memory: None,
1197            user_profile: None,
1198            cwd: None,
1199            learning_enabled: false,
1200            guardrail_profile: None,
1201        });
1202        assert!(result.text.contains("Project memory status:"));
1203        assert!(result.text.contains("Warnings:"));
1204        assert!(result.text.contains("Working on:"));
1205    }
1206
1207    #[test]
1208    fn system_prompt_project_memory_status_empty_string_is_skipped() {
1209        let reg = make_registry();
1210        let result = assemble(&AssembleParams {
1211            tools: &reg,
1212            agents_md: &[],
1213            skills: &[],
1214            facts: &[],
1215            project_memory_status: Some(""),
1216            personality: None,
1217            soul: None,
1218            task: None,
1219            role: None,
1220            mode: &AgentMode::Full,
1221            memory: None,
1222            user_profile: None,
1223            cwd: None,
1224            learning_enabled: false,
1225            guardrail_profile: None,
1226        });
1227        assert!(!result.text.contains("Project memory status:"));
1228    }
1229
1230    #[test]
1231    fn system_prompt_project_memory_status_included_separately_from_facts() {
1232        let reg = make_registry();
1233        let facts = vec![Fact {
1234            text: "Uses JWT for auth".into(),
1235            verified_ago: "2h ago".into(),
1236        }];
1237        let status =
1238            "Project memory status:\nWarnings:\n- stale fact\n\nWorking on:\n- [7] Fix auth flow";
1239        let result = assemble(&AssembleParams {
1240            tools: &reg,
1241            agents_md: &[],
1242            skills: &[],
1243            facts: &facts,
1244            project_memory_status: Some(status),
1245            personality: None,
1246            soul: None,
1247            task: None,
1248            role: None,
1249            mode: &AgentMode::Full,
1250            memory: None,
1251            user_profile: None,
1252            cwd: None,
1253            learning_enabled: false,
1254            guardrail_profile: None,
1255        });
1256
1257        let facts_pos = result.text.find("Project facts:").unwrap();
1258        let status_pos = result.text.find("Project memory status:").unwrap();
1259        assert!(result
1260            .text
1261            .contains("\"Uses JWT for auth\" [verified 2h ago]"));
1262        assert!(result.text.contains("Warnings:"));
1263        assert!(result.text.contains("Working on:"));
1264        assert!(facts_pos < status_pos);
1265    }
1266
1267    // -- Layer 5: Task context --
1268
1269    #[test]
1270    fn system_prompt_task_context_included() {
1271        let reg = make_registry();
1272        let task = TaskContext {
1273            title: "Fix the failing auth test".into(),
1274            description: "The JWT validation test panics on expired tokens".into(),
1275            design: None,
1276            acceptance: None,
1277            verify: Some("cargo test auth::jwt_test".into()),
1278            verify_timeout_secs: None,
1279            fail_first: false,
1280            notes: None,
1281            attempts: vec![],
1282            dependencies: vec![],
1283            decisions: vec![],
1284            context_paths: vec![],
1285            constraints: vec![],
1286        };
1287        let result = test_assemble(&reg, &[], &[], &[], None, Some(&task), None);
1288        assert!(result.text.contains("## Task"));
1289        assert!(result.text.contains("Title: Fix the failing auth test"));
1290        assert!(result
1291            .text
1292            .contains("Description: The JWT validation test panics"));
1293        assert!(result.text.contains("Verify: cargo test auth::jwt_test"));
1294        assert!(result
1295            .text
1296            .contains("Treat the verify command as the primary completion check for this task."));
1297    }
1298
1299    #[test]
1300    fn system_prompt_task_with_attempts() {
1301        let reg = make_registry();
1302        let task = TaskContext {
1303            title: "Fix bug".into(),
1304            description: "Something is broken".into(),
1305            design: None,
1306            acceptance: None,
1307            verify: None,
1308            verify_timeout_secs: None,
1309            fail_first: false,
1310            notes: None,
1311            attempts: vec![
1312                Attempt {
1313                    number: 1,
1314                    outcome: "failed".into(),
1315                    summary: "Tried X, got error Y".into(),
1316                },
1317                Attempt {
1318                    number: 2,
1319                    outcome: "failed".into(),
1320                    summary: "Tried Z, still broken".into(),
1321                },
1322            ],
1323            dependencies: vec![],
1324            decisions: vec![],
1325            context_paths: vec![],
1326            constraints: vec![],
1327        };
1328        let result = test_assemble(&reg, &[], &[], &[], None, Some(&task), None);
1329        assert!(result.text.contains("## Previous attempts"));
1330        assert!(result.text.contains(
1331            "Do not repeat a failed approach unchanged; use the attempt history to adjust your plan."
1332        ));
1333        assert!(result
1334            .text
1335            .contains("Attempt 1 (failed): Tried X, got error Y"));
1336        assert!(result
1337            .text
1338            .contains("Attempt 2 (failed): Tried Z, still broken"));
1339    }
1340
1341    #[test]
1342    fn system_prompt_task_with_dependencies() {
1343        let reg = make_registry();
1344        let task = TaskContext {
1345            title: "Implement feature".into(),
1346            description: "New feature".into(),
1347            design: None,
1348            acceptance: None,
1349            verify: None,
1350            verify_timeout_secs: None,
1351            fail_first: false,
1352            notes: None,
1353            attempts: vec![],
1354            dependencies: vec![Dependency {
1355                name: "Schema types".into(),
1356                status: "completed".into(),
1357                detail: "defined in src/schema.rs".into(),
1358            }],
1359            decisions: vec![],
1360            context_paths: vec![],
1361            constraints: vec![],
1362        };
1363        let result = test_assemble(&reg, &[], &[], &[], None, Some(&task), None);
1364        assert!(result.text.contains("## Dependencies"));
1365        assert!(result.text.contains(
1366            "Respect dependency state when sequencing work; unresolved dependencies are potential blockers."
1367        ));
1368        assert!(result
1369            .text
1370            .contains("- Schema types (completed): defined in src/schema.rs"));
1371    }
1372
1373    #[test]
1374    fn system_prompt_task_with_notes_and_context_paths() {
1375        let reg = make_registry();
1376        let task = TaskContext {
1377            title: "Fix auth".into(),
1378            description: "Tighten token validation".into(),
1379            design: Some(
1380                "Keep validation logic in the existing auth module; avoid a broader auth rewrite."
1381                    .into(),
1382            ),
1383            acceptance: None,
1384            verify: Some("cargo test auth".into()),
1385            verify_timeout_secs: Some(30),
1386            fail_first: true,
1387            notes: Some("Prefer touching only auth paths unless necessary".into()),
1388            attempts: vec![],
1389            dependencies: vec![],
1390            decisions: vec![],
1391            context_paths: vec!["src/auth.rs".into(), "tests/auth.rs".into()],
1392            constraints: vec![
1393                "Scope changes to auth-related files unless broader edits are necessary".into(),
1394            ],
1395        };
1396        let result = test_assemble(&reg, &[], &[], &[], None, Some(&task), None);
1397        assert!(result.text.contains("Design:"));
1398        assert!(result
1399            .text
1400            .contains("Keep validation logic in the existing auth module"));
1401        assert!(result.text.contains("Verify timeout: 30s"));
1402        assert!(result
1403            .text
1404            .contains("Fail-first: verify was expected to fail before implementation"));
1405        assert!(result.text.contains("Notes:"));
1406        assert!(result
1407            .text
1408            .contains("Prefer touching only auth paths unless necessary"));
1409        assert!(result.text.contains("## Referenced files"));
1410        assert!(result.text.contains("- src/auth.rs"));
1411        assert!(result.text.contains("- tests/auth.rs"));
1412        assert!(result.text.contains("## Constraints"));
1413        assert!(result
1414            .text
1415            .contains("Scope changes to auth-related files unless broader edits are necessary"));
1416    }
1417
1418    #[test]
1419    fn system_prompt_no_task_skips_layer5() {
1420        let reg = make_registry();
1421        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
1422        assert!(!result.text.contains("## Task"));
1423    }
1424
1425    #[test]
1426    fn system_prompt_task_without_verify_omits_verify_line() {
1427        let reg = make_registry();
1428        let task = TaskContext {
1429            title: "Do something".into(),
1430            description: "Details here".into(),
1431            design: None,
1432            acceptance: None,
1433            verify: None,
1434            verify_timeout_secs: None,
1435            fail_first: false,
1436            notes: None,
1437            attempts: vec![],
1438            dependencies: vec![],
1439            decisions: vec![],
1440            context_paths: vec![],
1441            constraints: vec![],
1442        };
1443        let result = test_assemble(&reg, &[], &[], &[], None, Some(&task), None);
1444        assert!(result.text.contains("Title: Do something"));
1445        assert!(!result.text.contains("Verify:"));
1446    }
1447
1448    // -- Role-aware assembly --
1449
1450    #[test]
1451    fn system_prompt_readonly_role_filters_tools() {
1452        let reg = make_registry();
1453        let role = make_readonly_role();
1454        let result = test_assemble(&reg, &[], &[], &[], None, None, Some(&role));
1455        // Should include readonly tools
1456        assert!(result.text.contains("- read:"));
1457        // Should NOT include write tools
1458        assert!(!result.text.contains("- write:"));
1459        assert!(!result.text.contains("- edit:"));
1460    }
1461
1462    #[test]
1463    fn system_prompt_role_instructions_appended() {
1464        let reg = make_registry();
1465        let role = make_readonly_role();
1466        let result = test_assemble(&reg, &[], &[], &[], None, None, Some(&role));
1467        assert!(result
1468            .text
1469            .contains("Review code carefully. Do not modify files."));
1470    }
1471
1472    #[test]
1473    fn system_prompt_worker_role_includes_all_tools() {
1474        let reg = make_registry();
1475        let role = make_worker_role();
1476        let result = test_assemble(&reg, &[], &[], &[], None, None, Some(&role));
1477        assert!(result.text.contains("- read:"));
1478        assert!(result.text.contains("- write:"));
1479        assert!(result.text.contains("- edit:"));
1480        assert!(result.text.contains("- bash:"));
1481    }
1482
1483    #[test]
1484    fn system_prompt_no_role_instructions_when_none() {
1485        let reg = make_registry();
1486        let role = make_worker_role();
1487        let result = test_assemble(&reg, &[], &[], &[], None, None, Some(&role));
1488        // Worker has no instructions, so the prompt shouldn't have extra instruction text
1489        let lines: Vec<&str> = result.text.lines().collect();
1490        let after_tools = lines.iter().position(|l| l.starts_with("- bash:")).unwrap();
1491        // Next non-empty line after the last tool should be end of identity layer
1492        // (no instructions appended)
1493        let remaining = &lines[after_tools + 1..];
1494        let next_content = remaining.iter().find(|l| !l.is_empty());
1495        assert!(next_content.is_none() || !next_content.unwrap().contains("Review"));
1496    }
1497
1498    // -- Size tracking --
1499
1500    #[test]
1501    fn system_prompt_tracks_estimated_tokens() {
1502        let reg = make_registry();
1503        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
1504        assert!(result.estimated_tokens > 0);
1505        // Rough check: the text is at least ~100 chars, so >= 25 tokens
1506        assert!(result.estimated_tokens >= 10);
1507    }
1508
1509    #[test]
1510    fn system_prompt_more_layers_means_more_tokens() {
1511        let reg = make_registry();
1512
1513        let minimal = test_assemble(&reg, &[], &[], &[], None, None, None);
1514
1515        let agents = vec![make_agents_md(
1516            "Lots of project context here with many words.",
1517        )];
1518        let skills = vec![make_skill(
1519            "rust",
1520            "Rust conventions",
1521            "/skills/rust/SKILL.md",
1522        )];
1523        let facts = vec![Fact {
1524            text: "Uses Postgres".into(),
1525            verified_ago: "1h ago".into(),
1526        }];
1527
1528        let full = test_assemble(&reg, &agents, &skills, &facts, None, None, None);
1529
1530        assert!(
1531            full.estimated_tokens > minimal.estimated_tokens,
1532            "full ({}) should have more tokens than minimal ({})",
1533            full.estimated_tokens,
1534            minimal.estimated_tokens
1535        );
1536    }
1537
1538    // -- Full assembly --
1539
1540    #[test]
1541    fn system_prompt_all_layers_present() {
1542        let reg = make_registry();
1543        let agents = vec![make_agents_md("Be concise.")];
1544        let skills = vec![make_skill(
1545            "rust",
1546            "Rust code conventions",
1547            "/skills/rust/SKILL.md",
1548        )];
1549        let facts = vec![Fact {
1550            text: "Uses SQLite".into(),
1551            verified_ago: "30m ago".into(),
1552        }];
1553        let task = TaskContext {
1554            title: "Add caching".into(),
1555            description: "Add Redis caching layer".into(),
1556            design: None,
1557            acceptance: None,
1558            verify: Some("cargo test cache".into()),
1559            verify_timeout_secs: None,
1560            fail_first: false,
1561            notes: None,
1562            attempts: vec![Attempt {
1563                number: 1,
1564                outcome: "failed".into(),
1565                summary: "Wrong key format".into(),
1566            }],
1567            dependencies: vec![Dependency {
1568                name: "Config".into(),
1569                status: "done".into(),
1570                detail: "src/config.rs".into(),
1571            }],
1572            decisions: vec![],
1573            context_paths: vec![],
1574            constraints: vec![],
1575        };
1576
1577        let result = test_assemble(&reg, &agents, &skills, &facts, None, Some(&task), None);
1578
1579        // All layers present in order
1580        let identity_pos = result.text.find("You are imp").unwrap();
1581        let policy_pos = result.text.find("Operating rules").unwrap();
1582        let context_pos = result.text.find("# Project Context").unwrap();
1583        let skills_pos = result.text.find("Available skills").unwrap();
1584        let facts_pos = result.text.find("Project facts").unwrap();
1585        let task_pos = result.text.find("## Task").unwrap();
1586
1587        assert!(identity_pos < policy_pos, "identity before policy");
1588        assert!(policy_pos < context_pos, "policy before context");
1589        assert!(context_pos < skills_pos, "context before skills");
1590        assert!(skills_pos < facts_pos, "skills before facts");
1591        assert!(facts_pos < task_pos, "facts before task");
1592    }
1593
1594    #[test]
1595    fn system_prompt_display_impl() {
1596        let reg = make_registry();
1597        let result = test_assemble(&reg, &[], &[], &[], None, None, None);
1598        let displayed = format!("{result}");
1599        assert_eq!(displayed, result.text);
1600    }
1601
1602    // -- Layer 6: Agent Memory --
1603
1604    #[test]
1605    fn system_prompt_memory_included() {
1606        let reg = make_registry();
1607        let mem = "══════════════════\nMEMORY [50% — 100/200]\n══════════════════\nUser runs macOS";
1608        let result = assemble(&AssembleParams {
1609            tools: &reg,
1610            agents_md: &[],
1611            skills: &[],
1612            facts: &[],
1613            project_memory_status: None,
1614            personality: None,
1615            soul: None,
1616            task: None,
1617            role: None,
1618            mode: &AgentMode::Full,
1619            memory: Some(mem),
1620            user_profile: None,
1621            cwd: None,
1622            learning_enabled: false,
1623            guardrail_profile: None,
1624        });
1625        assert!(result.text.contains("MEMORY"));
1626        assert!(result.text.contains("User runs macOS"));
1627    }
1628
1629    #[test]
1630    fn system_prompt_user_profile_included() {
1631        let reg = make_registry();
1632        let user =
1633            "══════════════════\nUSER PROFILE [30% — 42/140]\n══════════════════\nPrefers concise";
1634        let result = assemble(&AssembleParams {
1635            tools: &reg,
1636            agents_md: &[],
1637            skills: &[],
1638            facts: &[],
1639            project_memory_status: None,
1640            personality: None,
1641            soul: None,
1642            task: None,
1643            role: None,
1644            mode: &AgentMode::Full,
1645            memory: None,
1646            user_profile: Some(user),
1647            cwd: None,
1648            learning_enabled: false,
1649            guardrail_profile: None,
1650        });
1651        assert!(result.text.contains("USER PROFILE"));
1652        assert!(result.text.contains("Prefers concise"));
1653    }
1654
1655    #[test]
1656    fn system_prompt_empty_memory_skipped() {
1657        let reg = make_registry();
1658        let result = assemble(&AssembleParams {
1659            tools: &reg,
1660            agents_md: &[],
1661            skills: &[],
1662            facts: &[],
1663            project_memory_status: None,
1664            personality: None,
1665            soul: None,
1666            task: None,
1667            role: None,
1668            mode: &AgentMode::Full,
1669            memory: Some(""),
1670            user_profile: Some(""),
1671            cwd: None,
1672            learning_enabled: false,
1673            guardrail_profile: None,
1674        });
1675        assert!(!result.text.contains("MEMORY"));
1676        assert!(!result.text.contains("USER PROFILE"));
1677    }
1678
1679    #[test]
1680    fn system_prompt_memory_after_all_other_layers() {
1681        let reg = make_registry();
1682        let agents = vec![make_agents_md("Project context.")];
1683        let skills = vec![make_skill("rust", "Rust", "/skills/rust/SKILL.md")];
1684        let facts = vec![Fact {
1685            text: "Uses SQLite".into(),
1686            verified_ago: "1h".into(),
1687        }];
1688        let task = TaskContext {
1689            title: "Fix bug".into(),
1690            description: "Broken".into(),
1691            design: None,
1692            acceptance: None,
1693            verify: None,
1694            verify_timeout_secs: None,
1695            fail_first: false,
1696            notes: None,
1697            attempts: vec![],
1698            dependencies: vec![],
1699            decisions: vec![],
1700            context_paths: vec![],
1701            constraints: vec![],
1702        };
1703        let mem = "══════\nMEMORY [50%]\n══════\nSome fact";
1704        let result = assemble(&AssembleParams {
1705            tools: &reg,
1706            agents_md: &agents,
1707            skills: &skills,
1708            facts: &facts,
1709            project_memory_status: None,
1710            personality: None,
1711            soul: None,
1712            task: Some(&task),
1713            role: None,
1714            mode: &AgentMode::Full,
1715            memory: Some(mem),
1716            user_profile: None,
1717            cwd: None,
1718            learning_enabled: false,
1719            guardrail_profile: None,
1720        });
1721
1722        let identity_pos = result.text.find("You are imp").unwrap();
1723        let context_pos = result.text.find("# Project Context").unwrap();
1724        let facts_pos = result.text.find("Project facts").unwrap();
1725        let task_pos = result.text.find("## Task").unwrap();
1726        let memory_pos = result.text.find("MEMORY").unwrap();
1727
1728        assert!(identity_pos < context_pos);
1729        assert!(context_pos < facts_pos);
1730        assert!(facts_pos < task_pos);
1731        assert!(task_pos < memory_pos, "memory should come after task");
1732    }
1733}