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#[derive(Debug, Clone)]
13pub struct Fact {
14 pub text: String,
15 pub verified_ago: String,
16}
17
18#[derive(Debug, Clone)]
20pub struct Attempt {
21 pub number: u32,
22 pub outcome: String,
23 pub summary: String,
24}
25
26#[derive(Debug, Clone)]
28pub struct Dependency {
29 pub name: String,
30 pub status: String,
31 pub detail: String,
32}
33
34#[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#[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
65pub 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 pub learning_enabled: bool,
82 pub guardrail_profile: Option<GuardrailProfile>,
84}
85
86pub 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 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 let execution_policy = execution_policy_layer();
116 if !execution_policy.is_empty() {
117 parts.push(execution_policy);
118 }
119
120 parts.push(environment_layer(p.cwd));
122
123 if !p.agents_md.is_empty() {
125 parts.push(agents_md_layer(p.agents_md));
126 }
127
128 if !p.skills.is_empty() {
130 parts.push(skills_layer(p.skills, p.mode));
131 }
132
133 if !p.facts.is_empty() {
135 parts.push(facts_layer(p.facts));
136 }
137
138 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 if let Some(profile) = p.guardrail_profile {
147 parts.push(guardrails::guardrails_layer(profile));
148 }
149
150 if let Some(task) = p.task {
152 parts.push(task_layer(task));
153 parts.push(headless_execution_layer(task));
154 }
155
156 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 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 if let Some(instructions) = mode.instructions() {
256 s.push('\n');
257 s.push_str(instructions);
258 s.push('\n');
259 }
260
261 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 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
384fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
386 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 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 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 #[test]
728 fn system_prompt_includes_operating_rules() {
729 let reg = make_registry();
730 let result = test_assemble(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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 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(®, &[], &[], &[], 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(®, &[], &[], &[], None, None, None);
843 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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: ®,
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(®, &[], &[], &[], Some(&personality), None, None);
922 assert!(result.text.contains("Working style:"));
923 }
924
925 #[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(®, &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(®, &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(®, &[], &[], &[], None, None, None);
954 assert!(!result.text.contains("# Project Context"));
955 }
956
957 #[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(®, &[], &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: ®,
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: ®,
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: ®,
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: ®,
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: ®,
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(®, &[], &[], &[], None, None, None);
1145 assert!(!result.text.contains("Available skills"));
1146 }
1147
1148 #[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(®, &[], &[], &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(®, &[], &[], &[], 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: ®,
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: ®,
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: ®,
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 #[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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], None, Some(&task), None);
1444 assert!(result.text.contains("Title: Do something"));
1445 assert!(!result.text.contains("Verify:"));
1446 }
1447
1448 #[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(®, &[], &[], &[], None, None, Some(&role));
1455 assert!(result.text.contains("- read:"));
1457 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(®, &[], &[], &[], 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(®, &[], &[], &[], 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(®, &[], &[], &[], None, None, Some(&role));
1488 let lines: Vec<&str> = result.text.lines().collect();
1490 let after_tools = lines.iter().position(|l| l.starts_with("- bash:")).unwrap();
1491 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 #[test]
1501 fn system_prompt_tracks_estimated_tokens() {
1502 let reg = make_registry();
1503 let result = test_assemble(®, &[], &[], &[], None, None, None);
1504 assert!(result.estimated_tokens > 0);
1505 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(®, &[], &[], &[], 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(®, &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 #[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(®, &agents, &skills, &facts, None, Some(&task), None);
1578
1579 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(®, &[], &[], &[], None, None, None);
1598 let displayed = format!("{result}");
1599 assert_eq!(displayed, result.text);
1600 }
1601
1602 #[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: ®,
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: ®,
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: ®,
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: ®,
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}