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