1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use imp_llm::ThinkingLevel;
5use serde::{Deserialize, Serialize};
6
7use crate::error::Result;
8use crate::guardrails::GuardrailConfig;
9use crate::hooks::HookDef;
10use crate::personality::PersonalityConfig;
11use crate::roles::RoleDef;
12use crate::storage;
13use crate::tools::web::types::WebConfig;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17#[serde(rename_all = "kebab-case")]
18pub enum AgentMode {
19 #[default]
21 Full,
22 Worker,
24 Orchestrator,
26 Planner,
28 Reviewer,
30 Auditor,
32}
33
34const WORKER_TOOLS: &[&str] = &[
35 "read", "scan", "web", "recall", "write", "edit", "shell", "git", "mana", "ask",
36];
37const ORCHESTRATOR_TOOLS: &[&str] = &[
38 "read", "scan", "web", "recall", "mana", "git", "ask", "spawn",
39];
40const PLANNER_TOOLS: &[&str] = &["read", "scan", "web", "recall", "git", "mana", "ask"];
41const REVIEWER_TOOLS: &[&str] = &["read", "scan", "web", "recall", "git", "ask"];
42const AUDITOR_TOOLS: &[&str] = &["read", "scan", "web", "recall", "git", "mana"];
43
44const WORKER_MANA_ACTIONS: &[&str] = &[
45 "show",
46 "update",
47 "status",
48 "list",
49 "logs",
50 "next",
51 "verify",
52 "notes_append",
53];
54const ORCHESTRATOR_MANA_ACTIONS: &[&str] = &[
55 "status",
56 "list",
57 "show",
58 "create",
59 "close",
60 "update",
61 "run",
62 "run_state",
63 "evaluate",
64 "claim",
65 "release",
66 "logs",
67 "agents",
68 "next",
69 "tree",
70 "reopen",
71 "verify",
72 "fail",
73 "delete",
74 "dep_add",
75 "dep_remove",
76 "fact_create",
77 "fact_verify",
78 "notes_append",
79 "decision_add",
80 "decision_resolve",
81];
82const PLANNER_MANA_ACTIONS: &[&str] = &[
83 "status",
84 "list",
85 "show",
86 "create",
87 "update",
88 "next",
89 "tree",
90 "dep_add",
91 "dep_remove",
92 "fact_create",
93 "notes_append",
94 "decision_add",
95 "decision_resolve",
96];
97const AUDITOR_MANA_ACTIONS: &[&str] = &[
98 "status",
99 "list",
100 "show",
101 "logs",
102 "agents",
103 "next",
104 "tree",
105 "verify",
106 "fact_verify",
107];
108
109impl AgentMode {
110 pub fn allowed_tool_names(&self) -> &'static [&'static str] {
112 match self {
113 AgentMode::Full => &[],
114 AgentMode::Worker => WORKER_TOOLS,
115 AgentMode::Orchestrator => ORCHESTRATOR_TOOLS,
116 AgentMode::Planner => PLANNER_TOOLS,
117 AgentMode::Reviewer => REVIEWER_TOOLS,
118 AgentMode::Auditor => AUDITOR_TOOLS,
119 }
120 }
121
122 pub fn allows_tool(&self, name: &str) -> bool {
124 match self {
125 AgentMode::Full => true,
126 _ => self.allowed_tool_names().contains(&name),
127 }
128 }
129
130 pub fn allowed_mana_actions(&self) -> &'static [&'static str] {
132 match self {
133 AgentMode::Full | AgentMode::Reviewer => &[],
134 AgentMode::Worker => WORKER_MANA_ACTIONS,
135 AgentMode::Orchestrator => ORCHESTRATOR_MANA_ACTIONS,
136 AgentMode::Planner => PLANNER_MANA_ACTIONS,
137 AgentMode::Auditor => AUDITOR_MANA_ACTIONS,
138 }
139 }
140
141 pub fn allows_mana_action(&self, action: &str) -> bool {
143 match self {
144 AgentMode::Full => true,
145 AgentMode::Reviewer => false,
146 _ => self.allowed_mana_actions().contains(&action),
147 }
148 }
149
150 pub fn from_name(s: &str) -> Option<Self> {
155 match s.to_lowercase().as_str() {
156 "full" => Some(AgentMode::Full),
157 "worker" => Some(AgentMode::Worker),
158 "orchestrator" => Some(AgentMode::Orchestrator),
159 "planner" => Some(AgentMode::Planner),
160 "reviewer" => Some(AgentMode::Reviewer),
161 "auditor" => Some(AgentMode::Auditor),
162 _ => None,
163 }
164 }
165
166 pub fn instructions(&self) -> Option<&'static str> {
168 match self {
169 AgentMode::Full => None,
170 AgentMode::Worker => Some(
171 "You are a worker agent. Your job is to implement the assigned unit as specified and stay within its scope. \
172 You may read files, write files, and run shell commands. Inspect the relevant files before making claims or changes, \
173 use fast scoped checks for local feedback while implementing, and record meaningful progress or failure context with `mana update`. \
174 Do not declare success if commands or checks fail; report the exact blocker and the next useful action. \
175 Treat mana units as execution contracts: use their scope, dependencies, acceptance criteria, and verify gate before broadening the work. \
176 You may not create, run, or close mana units — final verification and closure belong to the orchestrator workflow.",
177 ),
178 AgentMode::Orchestrator => Some(
179 "You are an orchestrator agent. Use mana as your primary execution substrate for non-trivial work. \
180 Inspect mana state before making claims about work status, avoid duplicating or fragmenting existing units, and enrich existing units when that is cleaner than creating new ones. \
181 Write detailed units, split larger efforts into child units with dependencies, dispatch workers through mana, and own the final verification, retry, and closure workflow. \
182 Use the full mana unit vocabulary when it helps: acceptance criteria, labels, dependencies, paths, requires, produces, decisions, and feature boundaries. \
183 Encode unresolved questions as decisions instead of burying ambiguity in prose. \
184 When the conversation itself is producing durable plans, architecture, migrations, or implementation structure, externalize that structure into mana during the conversation rather than waiting until the end. \
185 Prefer native mana actions, including scope-aware and append-style updates, over shell or direct file edits for maintaining the work graph. \
186 You may not read or write files directly — spawn worker agents via mana for all file work. \
187 Update units with concrete failure context and do not retry unchanged failed plans. \
188 You are responsible for unit structure, completeness, and verify quality.",
189 ),
190 AgentMode::Planner => Some(
191 "You are a planner agent. Your job is to decompose work into mana units. \
192 Read enough code and context to ground the plan, cite concrete files or constraints when they matter, \
193 and make dependencies, sequencing, acceptance criteria, and verify commands explicit. \
194 Write worker-ready unit descriptions that include current state, concrete steps, file paths with intent, embedded context, scope boundaries, and what not to do. \
195 Record unresolved questions as decisions when autonomous execution would otherwise require guessing. \
196 Externalize durable planning structure into mana during the conversation, not only after the plan is complete. \
197 Prefer append-style mana updates to keep the graph current as ideas sharpen. \
198 You may read files and create units, but you may not run them — \
199 a human or orchestrator will approve execution.",
200 ),
201 AgentMode::Reviewer => Some(
202 "You are a reviewer agent. Your job is to read code and report findings. \
203 Ground findings in inspected code, cite exact files or symbols when useful, and distinguish confirmed issues from possible concerns. \
204 You may not write files, run commands, or use mana.",
205 ),
206 AgentMode::Auditor => Some(
207 "You are an auditor agent. Your job is to inspect code and mana state \
208 and produce structured reports. Ground conclusions in inspected evidence, cite the relevant files or mana objects, \
209 and clearly separate facts, risks, and open questions. You may read files and mana status, \
210 but you may not modify anything.",
211 ),
212 }
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
218#[serde(rename_all = "kebab-case")]
219pub enum ShellBackend {
220 #[default]
222 Sh,
223 Rush,
226 RushDaemon,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
233pub struct ShellConfig {
234 #[serde(default)]
236 pub backend: ShellBackend,
237 #[serde(default)]
239 pub command: Option<String>,
240}
241
242impl Default for ShellConfig {
243 fn default() -> Self {
244 Self {
245 backend: ShellBackend::Sh,
246 command: None,
247 }
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct LuaCapabilityPolicy {
254 pub allow_native_tool_calls: bool,
255 pub allow_shell_exec: bool,
256 pub allow_http: bool,
257 pub allow_secrets: bool,
258 pub allowed_env: HashSet<String>,
259}
260
261impl Default for LuaCapabilityPolicy {
262 fn default() -> Self {
263 Self {
264 allow_native_tool_calls: true,
265 allow_shell_exec: false,
266 allow_http: false,
267 allow_secrets: false,
268 allowed_env: HashSet::new(),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
275pub struct LuaConfig {
276 pub allow_native_tool_calls: Option<bool>,
278 pub allow_shell_exec: Option<bool>,
280 pub allow_http: Option<bool>,
282 pub allow_secrets: Option<bool>,
284 pub allowed_env: Option<Vec<String>>,
286}
287
288impl LuaConfig {
289 #[must_use]
290 pub fn resolve_policy(&self, mode: AgentMode) -> LuaCapabilityPolicy {
291 let mut policy = LuaCapabilityPolicy::default();
292 if matches!(mode, AgentMode::Worker) {
295 policy.allow_secrets = self.allow_secrets.unwrap_or(false);
296 }
297 if let Some(value) = self.allow_native_tool_calls {
298 policy.allow_native_tool_calls = value;
299 }
300 if let Some(value) = self.allow_shell_exec {
301 policy.allow_shell_exec = value;
302 }
303 if let Some(value) = self.allow_http {
304 policy.allow_http = value;
305 }
306 if let Some(value) = self.allow_secrets {
307 policy.allow_secrets = value;
308 }
309 if let Some(values) = &self.allowed_env {
310 policy.allowed_env = values.iter().cloned().collect();
311 }
312 policy
313 }
314}
315
316#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
318pub struct SecretsConfig {
319 #[serde(default)]
320 pub commands: CommandSecretsConfig,
321}
322
323#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
324pub struct CommandSecretsConfig {
325 #[serde(default)]
326 pub enabled: bool,
327 #[serde(default)]
328 pub allowed: Vec<SecretEnvBindingPolicy>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
332pub struct SecretEnvBindingPolicy {
333 pub provider: String,
334 pub field: String,
335 pub env: String,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
340pub struct Config {
341 pub model: Option<String>,
343
344 pub thinking: Option<ThinkingLevel>,
346
347 pub max_tokens: Option<u32>,
349
350 pub max_turns: Option<u32>,
352
353 pub tools: Option<Vec<String>>,
355
356 #[serde(default)]
358 pub roles: HashMap<String, RoleDef>,
359
360 #[serde(default)]
362 pub hooks: Vec<HookDef>,
363
364 #[serde(default)]
366 pub context: ContextConfig,
367
368 #[serde(default)]
370 pub shell: ShellConfig,
371
372 #[serde(default)]
374 pub guardrails: GuardrailConfig,
375
376 #[serde(default)]
378 pub mode: AgentMode,
379
380 #[serde(default)]
383 pub enabled_models: Option<Vec<String>>,
384
385 pub theme: Option<String>,
387
388 #[serde(default)]
390 pub learning: LearningConfig,
391
392 #[serde(default)]
394 pub ui: UiConfig,
395
396 #[serde(default)]
398 pub web: WebConfig,
399
400 #[serde(default)]
402 pub lua: LuaConfig,
403
404 #[serde(default)]
406 pub secrets: SecretsConfig,
407
408 #[serde(default)]
410 pub personality: PersonalityConfig,
411}
412
413#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "lowercase")]
418pub enum SidebarStyle {
419 #[default]
421 Inspector,
422 Stream,
424 Split,
426}
427
428#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
430#[serde(rename_all = "lowercase")]
431pub enum ToolOutputDisplay {
432 Full,
434 #[default]
436 Compact,
437 Collapsed,
439}
440
441#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(rename_all = "kebab-case")]
444pub enum ChatToolDisplay {
445 Interleaved,
447 #[default]
449 Summary,
450 Hidden,
452}
453
454#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "lowercase")]
457pub enum AnimationLevel {
458 None,
460 Spinner,
462 #[default]
464 #[serde(alias = "full")]
465 Minimal,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
470#[serde(rename_all = "kebab-case")]
471pub enum ContinuePolicy {
472 #[default]
474 Disabled,
475 Conservative,
477 Balanced,
479 Aggressive,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
485pub struct UiConfig {
486 #[serde(default)]
488 pub sidebar_style: SidebarStyle,
489
490 #[serde(default)]
492 pub tool_output: ToolOutputDisplay,
493
494 #[serde(default = "default_tool_output_lines")]
496 pub tool_output_lines: usize,
497
498 #[serde(default = "default_read_max_lines")]
501 pub read_max_lines: usize,
502
503 #[serde(default = "default_sidebar_width")]
505 pub sidebar_width: u16,
506
507 #[serde(default = "default_true")]
509 pub word_wrap: bool,
510
511 #[serde(default)]
513 pub animations: AnimationLevel,
514
515 #[serde(default)]
517 pub hide_tools_in_chat: bool,
518
519 #[serde(default)]
521 pub chat_tool_display: ChatToolDisplay,
522
523 #[serde(default = "default_true")]
525 pub auto_open_sidebar: bool,
526
527 #[serde(default = "default_sidebar_auto_open_width")]
529 pub sidebar_auto_open_width: u16,
530
531 #[serde(default = "default_thinking_lines")]
533 pub thinking_lines: usize,
534
535 #[serde(default = "default_streaming_lines")]
537 pub streaming_lines: usize,
538
539 #[serde(default = "default_mouse_scroll_lines")]
541 pub mouse_scroll_lines: usize,
542
543 #[serde(default = "default_keyboard_scroll_lines")]
545 pub keyboard_scroll_lines: usize,
546
547 #[serde(default)]
550 #[doc(hidden)]
551 pub mouse_capture: bool,
552
553 #[serde(default)]
555 pub show_timestamps: bool,
556
557 #[serde(default = "default_true")]
559 pub show_cost: bool,
560
561 #[serde(default = "default_true")]
563 pub show_context_usage: bool,
564
565 #[serde(default = "default_true")]
568 pub notify_on_agent_complete: bool,
569
570 #[serde(default)]
573 pub continue_policy: ContinuePolicy,
574}
575
576fn default_tool_output_lines() -> usize {
577 10
578}
579fn default_read_max_lines() -> usize {
580 500
581}
582fn default_sidebar_width() -> u16 {
583 40
584}
585fn default_sidebar_auto_open_width() -> u16 {
586 120
587}
588fn default_thinking_lines() -> usize {
589 5
590}
591fn default_streaming_lines() -> usize {
592 5
593}
594fn default_mouse_scroll_lines() -> usize {
595 3
596}
597fn default_keyboard_scroll_lines() -> usize {
598 20
599}
600
601impl Default for UiConfig {
602 fn default() -> Self {
603 Self {
604 sidebar_style: SidebarStyle::default(),
605 tool_output: ToolOutputDisplay::default(),
606 tool_output_lines: default_tool_output_lines(),
607 read_max_lines: default_read_max_lines(),
608 sidebar_width: default_sidebar_width(),
609 word_wrap: default_true(),
610 animations: AnimationLevel::default(),
611 hide_tools_in_chat: false,
612 chat_tool_display: ChatToolDisplay::default(),
613 auto_open_sidebar: default_true(),
614 sidebar_auto_open_width: default_sidebar_auto_open_width(),
615 thinking_lines: default_thinking_lines(),
616 streaming_lines: default_streaming_lines(),
617 mouse_scroll_lines: default_mouse_scroll_lines(),
618 keyboard_scroll_lines: default_keyboard_scroll_lines(),
619 mouse_capture: false,
620 show_timestamps: false,
621 show_cost: true,
622 show_context_usage: true,
623 notify_on_agent_complete: true,
624 continue_policy: ContinuePolicy::Disabled,
625 }
626 }
627}
628
629impl UiConfig {
630 pub fn effective_chat_tool_display(&self) -> ChatToolDisplay {
631 if self.hide_tools_in_chat && self.sidebar_style != SidebarStyle::Inspector {
632 ChatToolDisplay::Hidden
633 } else if self.sidebar_style == SidebarStyle::Inspector {
634 ChatToolDisplay::Summary
635 } else {
636 self.chat_tool_display
637 }
638 }
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
643pub struct LearningConfig {
644 #[serde(default = "default_true")]
646 pub enabled: bool,
647
648 #[serde(default = "default_nudge_threshold")]
650 pub skill_nudge_threshold: u32,
651
652 #[serde(default = "default_memory_limit")]
654 pub memory_char_limit: usize,
655
656 #[serde(default = "default_user_limit")]
658 pub user_char_limit: usize,
659}
660
661fn default_true() -> bool {
662 true
663}
664fn default_nudge_threshold() -> u32 {
665 8
666}
667fn default_memory_limit() -> usize {
668 2200
669}
670fn default_user_limit() -> usize {
671 1400
672}
673
674impl Default for LearningConfig {
675 fn default() -> Self {
676 Self {
677 enabled: default_true(),
678 skill_nudge_threshold: default_nudge_threshold(),
679 memory_char_limit: default_memory_limit(),
680 user_char_limit: default_user_limit(),
681 }
682 }
683}
684
685#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
686#[serde(rename_all = "kebab-case")]
687pub enum AutoCompactionMode {
688 #[default]
690 Disabled,
691 NearThreshold,
693 Aggressive,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
698pub struct AutoCompactionConfig {
699 #[serde(default)]
701 pub mode: AutoCompactionMode,
702}
703
704impl Default for AutoCompactionConfig {
705 fn default() -> Self {
706 Self {
707 mode: AutoCompactionMode::Disabled,
708 }
709 }
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
713pub struct ContextConfig {
714 pub observation_mask_threshold: f64,
716
717 pub mask_window: usize,
719
720 #[serde(default)]
722 pub auto_compaction: AutoCompactionConfig,
723}
724
725impl Default for ContextConfig {
726 fn default() -> Self {
727 Self {
728 observation_mask_threshold: 0.6,
729 mask_window: 10,
730 auto_compaction: AutoCompactionConfig::default(),
731 }
732 }
733}
734
735impl Config {
736 pub fn load(path: &Path) -> Result<Self> {
738 if !path.exists() {
739 return Ok(Self::default());
740 }
741 let content = std::fs::read_to_string(path)?;
742 let config: Config = toml::from_str(&content)?;
743 Ok(config)
744 }
745
746 pub fn resolve(user_config_dir: &Path, project_dir: Option<&Path>) -> Result<Self> {
748 let mut config = Self::default();
749
750 let user_path = user_config_dir.join("config.toml");
752 if user_path.exists() {
753 let user = Self::load(&user_path)?;
754 config.merge(user);
755 }
756
757 if let Some(project) = project_dir {
759 let project_path = project.join(".imp").join("config.toml");
760 if project_path.exists() {
761 let project = Self::load(&project_path)?;
762 config.merge(project);
763 }
764 }
765
766 if let Ok(model) = std::env::var("IMP_MODEL") {
768 config.model = Some(model);
769 }
770 if let Ok(thinking) = std::env::var("IMP_THINKING") {
771 config.thinking = parse_thinking_level(&thinking);
772 }
773 if let Ok(max_tokens) = std::env::var("IMP_MAX_TOKENS") {
774 if let Ok(parsed) = max_tokens.parse::<u32>() {
775 config.max_tokens = Some(parsed);
776 }
777 }
778 if let Ok(mode) = std::env::var("IMP_MODE") {
779 if let Some(m) = parse_agent_mode(&mode) {
780 config.mode = m;
781 }
782 }
783 if let Ok(provider) = std::env::var("IMP_WEB_PROVIDER") {
784 config.web.search_provider = match provider.to_lowercase().as_str() {
785 "tavily" => Some(crate::tools::web::types::SearchProvider::Tavily),
786 "exa" => Some(crate::tools::web::types::SearchProvider::Exa),
787 "linkup" => Some(crate::tools::web::types::SearchProvider::Linkup),
788 "perplexity" => Some(crate::tools::web::types::SearchProvider::Perplexity),
789 _ => config.web.search_provider,
790 };
791 }
792
793 Ok(config)
794 }
795
796 fn merge(&mut self, other: Config) {
797 if other.model.is_some() {
798 self.model = other.model;
799 }
800 if other.thinking.is_some() {
801 self.thinking = other.thinking;
802 }
803 if other.max_tokens.is_some() {
804 self.max_tokens = other.max_tokens;
805 }
806 if other.max_turns.is_some() {
807 self.max_turns = other.max_turns;
808 }
809 if other.tools.is_some() {
810 self.tools = other.tools;
811 }
812 if other.context != ContextConfig::default() {
813 self.context = other.context;
814 }
815 if other.shell != ShellConfig::default() {
816 self.shell = other.shell;
817 }
818 self.guardrails.merge(other.guardrails);
819 if other.mode != AgentMode::default() {
820 self.mode = other.mode;
821 }
822 if other.enabled_models.is_some() {
823 self.enabled_models = other.enabled_models;
824 }
825 if other.theme.is_some() {
826 self.theme = other.theme;
827 }
828 if other.learning != LearningConfig::default() {
829 self.learning = other.learning;
830 }
831 if other.ui != UiConfig::default() {
832 self.ui = other.ui;
833 }
834 if other.web != WebConfig::default() {
835 self.web = other.web;
836 }
837 if other.lua != LuaConfig::default() {
838 self.lua = other.lua;
839 }
840 if other.secrets != SecretsConfig::default() {
841 self.secrets = other.secrets;
842 }
843 if other.personality != PersonalityConfig::default() {
844 self.personality.merge(other.personality);
845 }
846 self.roles.extend(other.roles);
847 self.hooks.extend(other.hooks);
848 }
849
850 pub fn user_config_dir() -> PathBuf {
852 storage::global_root()
853 }
854
855 pub fn session_dir() -> PathBuf {
857 storage::global_sessions_dir()
858 }
859
860 pub fn save(&self, path: &Path) -> Result<()> {
862 if let Some(parent) = path.parent() {
863 std::fs::create_dir_all(parent)?;
864 }
865 let content =
866 toml::to_string_pretty(self).map_err(|e| crate::error::Error::Config(e.to_string()))?;
867 std::fs::write(path, content)?;
868 Ok(())
869 }
870
871 pub fn user_config_path() -> PathBuf {
873 storage::global_config_path()
874 }
875}
876
877fn parse_agent_mode(s: &str) -> Option<AgentMode> {
878 AgentMode::from_name(s)
879}
880
881fn parse_thinking_level(s: &str) -> Option<ThinkingLevel> {
882 match s.to_lowercase().as_str() {
883 "off" => Some(ThinkingLevel::Off),
884 "minimal" => Some(ThinkingLevel::Minimal),
885 "low" => Some(ThinkingLevel::Low),
886 "medium" => Some(ThinkingLevel::Medium),
887 "high" => Some(ThinkingLevel::High),
888 "xhigh" => Some(ThinkingLevel::XHigh),
889 _ => None,
890 }
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896 use std::fs;
897 use tempfile::TempDir;
898
899 #[test]
900 fn config_default_values() {
901 let config = Config::default();
902 assert!(config.model.is_none());
903 assert!(config.thinking.is_none());
904 assert!(config.max_tokens.is_none());
905 assert!(config.max_turns.is_none());
906 assert!(config.tools.is_none());
907 assert_eq!(config.ui.read_max_lines, 500);
908 assert_eq!(config.ui.sidebar_style, SidebarStyle::Inspector);
909 assert_eq!(config.ui.chat_tool_display, ChatToolDisplay::Summary);
910 assert_eq!(config.ui.tool_output, ToolOutputDisplay::Compact);
911 assert_eq!(config.web, WebConfig::default());
912 assert_eq!(config.personality, PersonalityConfig::default());
913 assert!(config.roles.is_empty());
914 assert!(config.hooks.is_empty());
915 assert!((config.context.observation_mask_threshold - 0.6).abs() < f64::EPSILON);
916 assert_eq!(config.context.mask_window, 10);
917 assert_eq!(
918 config.context.auto_compaction.mode,
919 AutoCompactionMode::Disabled
920 );
921 assert_eq!(config.guardrails, GuardrailConfig::default());
922 }
923
924 #[test]
925 fn inspector_sidebar_keeps_tool_calls_in_chat_summary() {
926 let mut ui = UiConfig {
927 sidebar_style: SidebarStyle::Inspector,
928 chat_tool_display: ChatToolDisplay::Interleaved,
929 ..Default::default()
930 };
931 assert_eq!(ui.effective_chat_tool_display(), ChatToolDisplay::Summary);
932
933 ui.chat_tool_display = ChatToolDisplay::Hidden;
934 ui.hide_tools_in_chat = true;
935 assert_eq!(ui.effective_chat_tool_display(), ChatToolDisplay::Summary);
936
937 ui.sidebar_style = SidebarStyle::Stream;
938 assert_eq!(ui.effective_chat_tool_display(), ChatToolDisplay::Hidden);
939 }
940
941 #[test]
942 fn config_load_from_toml() {
943 let dir = TempDir::new().unwrap();
944 let config_path = dir.path().join("config.toml");
945 fs::write(
946 &config_path,
947 r#"
948model = "sonnet"
949thinking = "high"
950max_tokens = 2048
951max_turns = 50
952tools = ["read", "write", "bash"]
953
954[guardrails]
955enabled = true
956level = "enforce"
957profile = "zig"
958critical_paths = ["src/**"]
959after_write = ["zig fmt --check ."]
960
961[context]
962observation_mask_threshold = 0.5
963mask_window = 5
964
965[shell]
966command = "zsh"
967
968[web]
969search_provider = "exa"
970"#,
971 )
972 .unwrap();
973
974 let config = Config::load(&config_path).unwrap();
975 assert_eq!(config.model.as_deref(), Some("sonnet"));
976 assert_eq!(config.thinking, Some(ThinkingLevel::High));
977 assert_eq!(config.max_tokens, Some(2048));
978 assert_eq!(config.max_turns, Some(50));
979 assert_eq!(config.tools.as_ref().unwrap().len(), 3);
980 assert_eq!(config.guardrails.enabled, Some(true));
981 assert_eq!(config.ui.read_max_lines, 500);
982 assert_eq!(
983 config.guardrails.profile,
984 Some(crate::guardrails::GuardrailProfile::Zig)
985 );
986 assert_eq!(
987 config.guardrails.after_write,
988 Some(vec!["zig fmt --check .".into()])
989 );
990 assert_eq!(config.shell.command.as_deref(), Some("zsh"));
991 assert_eq!(
992 config.web.search_provider,
993 Some(crate::tools::web::types::SearchProvider::Exa)
994 );
995 assert!((config.context.observation_mask_threshold - 0.5).abs() < f64::EPSILON);
996 assert_eq!(config.context.mask_window, 5);
997 assert_eq!(
998 config.context.auto_compaction.mode,
999 AutoCompactionMode::Disabled
1000 );
1001 }
1002
1003 #[test]
1004 fn config_load_missing_file_returns_default() {
1005 let dir = TempDir::new().unwrap();
1006 let config_path = dir.path().join("nonexistent.toml");
1007 let config = Config::load(&config_path).unwrap();
1008 assert!(config.model.is_none());
1009 }
1010
1011 #[test]
1012 fn config_loads_personality_section() {
1013 let dir = TempDir::new().unwrap();
1014 let config_path = dir.path().join("config.toml");
1015 fs::write(
1016 &config_path,
1017 r#"
1018[personality.profile.identity]
1019name = "Nova"
1020work_style = "careful"
1021voice = "clear"
1022focus = "research"
1023role = "assistant"
1024
1025[personality.profile.sliders]
1026autonomy = "low"
1027verbosity = "high"
1028caution = "very-high"
1029warmth = "high"
1030planning_depth = "very-high"
1031
1032[personality.profiles]
1033active = "researcher"
1034
1035[personality.profiles.saved.researcher.identity]
1036name = "Nova"
1037work_style = "careful"
1038voice = "clear"
1039focus = "research"
1040role = "assistant"
1041"#,
1042 )
1043 .unwrap();
1044
1045 let config = Config::load(&config_path).unwrap();
1046 assert_eq!(config.personality.profile.identity.name, "Nova");
1047 assert_eq!(
1048 config.personality.profile.identity.render_sentence(),
1049 "You are Nova, a careful, clear, research assistant."
1050 );
1051 assert_eq!(
1052 config.personality.profiles.active.as_deref(),
1053 Some("researcher")
1054 );
1055 assert!(config.personality.profiles.saved.contains_key("researcher"));
1056 }
1057
1058 #[test]
1059 fn config_merge_personality_project_overrides_user_and_keeps_saved_profiles() {
1060 let mut user = Config::default();
1061 user.personality.profile.identity.name = "imp".into();
1062 user.personality.profiles.active = Some("builder".into());
1063 user.personality.profiles.saved.insert(
1064 "builder".into(),
1065 crate::personality::PersonalityProfile::default(),
1066 );
1067
1068 let mut project = Config::default();
1069 project.personality.profile.identity.name = "Patch".into();
1070 project.personality.profiles.active = Some("reviewer".into());
1071 project.personality.profiles.saved.insert(
1072 "reviewer".into(),
1073 crate::personality::PersonalityProfile::default(),
1074 );
1075
1076 user.merge(project);
1077
1078 assert_eq!(user.personality.profile.identity.name, "Patch");
1079 assert_eq!(
1080 user.personality.profiles.active.as_deref(),
1081 Some("reviewer")
1082 );
1083 assert!(user.personality.profiles.saved.contains_key("builder"));
1084 assert!(user.personality.profiles.saved.contains_key("reviewer"));
1085 }
1086
1087 #[test]
1088 fn config_merge_project_overrides_user() {
1089 let mut user = Config {
1090 model: Some("haiku".into()),
1091 max_tokens: Some(1024),
1092 max_turns: Some(20),
1093 ..Default::default()
1094 };
1095
1096 let project = Config {
1097 model: Some("sonnet".into()),
1098 max_tokens: None,
1099 max_turns: None, ..Default::default()
1101 };
1102
1103 user.merge(project);
1104 assert_eq!(user.model.as_deref(), Some("sonnet"));
1105 assert_eq!(user.max_tokens, Some(1024));
1106 assert_eq!(user.max_turns, Some(20));
1107 }
1108
1109 #[test]
1110 fn config_merge_roles_extend() {
1111 let mut base = Config::default();
1112 base.roles.insert(
1113 "worker".into(),
1114 RoleDef {
1115 model: Some("haiku".into()),
1116 thinking: None,
1117 tools: None,
1118 readonly: false,
1119 instructions: None,
1120 max_turns: None,
1121 },
1122 );
1123
1124 let overlay = Config {
1125 roles: {
1126 let mut m = HashMap::new();
1127 m.insert(
1128 "reviewer".into(),
1129 RoleDef {
1130 model: Some("sonnet".into()),
1131 thinking: Some(ThinkingLevel::High),
1132 tools: None,
1133 readonly: true,
1134 instructions: None,
1135 max_turns: None,
1136 },
1137 );
1138 m
1139 },
1140 ..Default::default()
1141 };
1142
1143 base.merge(overlay);
1144 assert!(base.roles.contains_key("worker"));
1145 assert!(base.roles.contains_key("reviewer"));
1146 }
1147
1148 #[test]
1149 fn config_merge_hooks_extend() {
1150 let mut base = Config::default();
1151 base.hooks.push(HookDef {
1152 event: "after_file_write".into(),
1153 match_pattern: None,
1154 action: "log".into(),
1155 command: None,
1156 blocking: false,
1157 threshold: None,
1158 });
1159
1160 let overlay = Config {
1161 hooks: vec![HookDef {
1162 event: "before_tool_call".into(),
1163 match_pattern: None,
1164 action: "block".into(),
1165 command: None,
1166 blocking: true,
1167 threshold: None,
1168 }],
1169 ..Default::default()
1170 };
1171
1172 base.merge(overlay);
1173 assert_eq!(base.hooks.len(), 2);
1174 }
1175
1176 #[test]
1177 fn config_merge_context_overrides_default() {
1178 let mut base = Config::default();
1179
1180 let overlay = Config {
1181 context: ContextConfig {
1182 observation_mask_threshold: 0.5,
1183 mask_window: 5,
1184 auto_compaction: AutoCompactionConfig {
1185 mode: AutoCompactionMode::NearThreshold,
1186 },
1187 },
1188 ..Default::default()
1189 };
1190
1191 base.merge(overlay);
1192 assert!((base.context.observation_mask_threshold - 0.5).abs() < f64::EPSILON);
1193 assert_eq!(base.context.mask_window, 5);
1194 assert_eq!(
1195 base.context.auto_compaction.mode,
1196 AutoCompactionMode::NearThreshold
1197 );
1198 }
1199
1200 #[test]
1201 fn config_merge_includes_theme_learning_and_lua() {
1202 let mut base = Config::default();
1203 let overlay = Config {
1204 theme: Some("light".into()),
1205 learning: LearningConfig {
1206 enabled: false,
1207 skill_nudge_threshold: 3,
1208 memory_char_limit: 1000,
1209 user_char_limit: 700,
1210 },
1211 lua: LuaConfig {
1212 allow_native_tool_calls: Some(false),
1213 allow_shell_exec: Some(true),
1214 allow_http: None,
1215 allow_secrets: None,
1216 allowed_env: Some(vec!["HOME".into()]),
1217 },
1218 ..Default::default()
1219 };
1220
1221 base.merge(overlay);
1222
1223 assert_eq!(base.theme.as_deref(), Some("light"));
1224 assert_eq!(base.learning.skill_nudge_threshold, 3);
1225 assert!(!base.learning.enabled);
1226 assert_eq!(base.lua.allow_native_tool_calls, Some(false));
1227 assert_eq!(base.lua.allow_shell_exec, Some(true));
1228 assert_eq!(base.lua.allowed_env, Some(vec!["HOME".into()]));
1229 }
1230
1231 #[test]
1232 fn config_merge_guardrails_preserves_unspecified_fields() {
1233 let mut base = Config::default();
1234 base.guardrails.enabled = Some(true);
1235 base.guardrails.profile = Some(crate::guardrails::GuardrailProfile::Rust);
1236 base.guardrails.critical_paths = Some(vec!["src/**".into()]);
1237
1238 let mut overlay = Config::default();
1239 overlay.guardrails.level = Some(crate::guardrails::GuardrailLevel::Enforce);
1240 overlay.guardrails.after_write = Some(vec!["cargo test".into()]);
1241
1242 base.merge(overlay);
1243
1244 assert_eq!(base.guardrails.enabled, Some(true));
1245 assert_eq!(
1246 base.guardrails.profile,
1247 Some(crate::guardrails::GuardrailProfile::Rust)
1248 );
1249 assert_eq!(base.guardrails.critical_paths, Some(vec!["src/**".into()]));
1250 assert_eq!(
1251 base.guardrails.level,
1252 Some(crate::guardrails::GuardrailLevel::Enforce)
1253 );
1254 assert_eq!(base.guardrails.after_write, Some(vec!["cargo test".into()]));
1255 }
1256
1257 #[test]
1258 fn config_resolve_user_then_project() {
1259 std::env::remove_var("IMP_MODEL");
1261 std::env::remove_var("IMP_THINKING");
1262
1263 let dir = TempDir::new().unwrap();
1264 let user_dir = dir.path().join("user");
1265 let project_dir = dir.path().join("project");
1266 fs::create_dir_all(&user_dir).unwrap();
1267 fs::create_dir_all(project_dir.join(".imp")).unwrap();
1268
1269 fs::write(
1271 user_dir.join("config.toml"),
1272 r#"
1273model = "haiku"
1274max_turns = 20
1275
1276[context]
1277observation_mask_threshold = 0.55
1278mask_window = 9
1279
1280[context.auto_compaction]
1281mode = "disabled"
1282"#,
1283 )
1284 .unwrap();
1285
1286 fs::write(
1288 project_dir.join(".imp").join("config.toml"),
1289 r#"
1290model = "sonnet"
1291
1292[context]
1293observation_mask_threshold = 0.5
1294mask_window = 5
1295
1296[context.auto_compaction]
1297mode = "disabled"
1298"#,
1299 )
1300 .unwrap();
1301
1302 let config = Config::resolve(&user_dir, Some(&project_dir)).unwrap();
1303 assert_eq!(config.model.as_deref(), Some("sonnet"));
1304 assert_eq!(config.max_turns, Some(20));
1305 assert!((config.context.observation_mask_threshold - 0.5).abs() < f64::EPSILON);
1306 assert_eq!(config.context.mask_window, 5);
1307 }
1308
1309 #[test]
1310 fn config_resolve_env_overrides() {
1311 let mut config = Config {
1315 model: Some("haiku".into()),
1316 thinking: Some(ThinkingLevel::Low),
1317 max_tokens: Some(2048),
1318 ..Default::default()
1319 };
1320
1321 let env_model = "opus";
1323 config.model = Some(env_model.into());
1324
1325 let env_thinking = "high";
1327 config.thinking = parse_thinking_level(env_thinking);
1328
1329 let env_max_tokens = "1024";
1331 config.max_tokens = env_max_tokens.parse::<u32>().ok();
1332
1333 assert_eq!(config.model.as_deref(), Some("opus"));
1334 assert_eq!(config.thinking, Some(ThinkingLevel::High));
1335 assert_eq!(config.max_tokens, Some(1024));
1336 }
1337
1338 #[test]
1339 fn config_resolve_missing_files_uses_defaults() {
1340 let dir = TempDir::new().unwrap();
1341 let config = Config::resolve(dir.path(), None).unwrap();
1342 assert!(config.model.is_none());
1343 assert!(config.thinking.is_none());
1344 assert!(config.max_tokens.is_none());
1345 assert!(config.max_turns.is_none());
1346 }
1347
1348 #[test]
1349 fn config_load_with_roles_and_hooks() {
1350 let dir = TempDir::new().unwrap();
1351 let config_path = dir.path().join("config.toml");
1352 fs::write(
1353 &config_path,
1354 r#"
1355model = "sonnet"
1356
1357[roles.coder]
1358model = "opus"
1359thinking = "high"
1360readonly = false
1361
1362[roles.reader]
1363readonly = true
1364
1365[[hooks]]
1366event = "after_file_write"
1367action = "log"
1368blocking = false
1369"#,
1370 )
1371 .unwrap();
1372
1373 let config = Config::load(&config_path).unwrap();
1374 assert_eq!(config.roles.len(), 2);
1375 assert!(config.roles.contains_key("coder"));
1376 assert!(config.roles.contains_key("reader"));
1377 assert_eq!(config.roles["coder"].model.as_deref(), Some("opus"));
1378 assert!(config.roles["reader"].readonly);
1379 assert_eq!(config.hooks.len(), 1);
1380 assert_eq!(config.hooks[0].event, "after_file_write");
1381 }
1382
1383 #[test]
1384 fn config_parse_thinking_levels() {
1385 assert_eq!(parse_thinking_level("off"), Some(ThinkingLevel::Off));
1386 assert_eq!(
1387 parse_thinking_level("minimal"),
1388 Some(ThinkingLevel::Minimal)
1389 );
1390 assert_eq!(parse_thinking_level("low"), Some(ThinkingLevel::Low));
1391 assert_eq!(parse_thinking_level("medium"), Some(ThinkingLevel::Medium));
1392 assert_eq!(parse_thinking_level("high"), Some(ThinkingLevel::High));
1393 assert_eq!(parse_thinking_level("xhigh"), Some(ThinkingLevel::XHigh));
1394 assert_eq!(parse_thinking_level("OFF"), Some(ThinkingLevel::Off));
1395 assert_eq!(parse_thinking_level("High"), Some(ThinkingLevel::High));
1396 assert_eq!(parse_thinking_level("invalid"), None);
1397 assert_eq!(parse_thinking_level(""), None);
1398 }
1399
1400 #[test]
1401 fn config_partial_toml_fills_defaults() {
1402 let dir = TempDir::new().unwrap();
1403 let config_path = dir.path().join("config.toml");
1404 fs::write(
1405 &config_path,
1406 r#"
1407model = "sonnet"
1408"#,
1409 )
1410 .unwrap();
1411
1412 let config = Config::load(&config_path).unwrap();
1413 assert_eq!(config.model.as_deref(), Some("sonnet"));
1414 assert!(config.thinking.is_none());
1416 assert!(config.max_tokens.is_none());
1417 assert!(config.max_turns.is_none());
1418 assert!((config.context.observation_mask_threshold - 0.6).abs() < f64::EPSILON);
1419 }
1420
1421 #[test]
1424 fn agent_mode_default_is_full() {
1425 let config = Config::default();
1426 assert_eq!(config.mode, AgentMode::Full);
1427 assert_eq!(AgentMode::default(), AgentMode::Full);
1428 }
1429
1430 #[test]
1431 fn lua_config_resolves_capability_policy() {
1432 let config = LuaConfig {
1433 allow_native_tool_calls: Some(false),
1434 allow_shell_exec: Some(true),
1435 allow_http: Some(true),
1436 allow_secrets: Some(true),
1437 allowed_env: Some(vec!["OPENAI_API_KEY".to_string(), "HOME".to_string()]),
1438 };
1439
1440 let policy = config.resolve_policy(AgentMode::Worker);
1441 assert!(!policy.allow_native_tool_calls);
1442 assert!(policy.allow_shell_exec);
1443 assert!(policy.allow_http);
1444 assert!(policy.allow_secrets);
1445 assert!(policy.allowed_env.contains("OPENAI_API_KEY"));
1446 assert!(policy.allowed_env.contains("HOME"));
1447 }
1448
1449 #[test]
1450 fn worker_lua_policy_preserves_configured_secret_access() {
1451 let enabled = LuaConfig {
1452 allow_secrets: Some(true),
1453 ..Default::default()
1454 };
1455 assert!(enabled.resolve_policy(AgentMode::Worker).allow_secrets);
1456
1457 let disabled = LuaConfig {
1458 allow_secrets: Some(false),
1459 ..Default::default()
1460 };
1461 assert!(!disabled.resolve_policy(AgentMode::Worker).allow_secrets);
1462
1463 assert!(
1464 !LuaConfig::default()
1465 .resolve_policy(AgentMode::Worker)
1466 .allow_secrets
1467 );
1468 }
1469
1470 #[test]
1471 fn agent_mode_full_allows_all_tools() {
1472 let mode = AgentMode::Full;
1473 assert!(mode.allows_tool("anything"));
1474 assert!(mode.allows_tool("read"));
1475 assert!(mode.allows_tool("shell"));
1476 assert!(mode.allows_tool("nonexistent_future_tool"));
1477 assert_eq!(mode.allowed_tool_names(), &[] as &[&str]);
1478 }
1479
1480 #[test]
1481 fn agent_mode_orchestrator_allows_read() {
1482 let mode = AgentMode::Orchestrator;
1483 assert!(mode.allows_tool("read"));
1484 assert!(mode.allows_tool("scan"));
1485 assert!(mode.allows_tool("web"));
1486 assert!(mode.allows_tool("git"));
1487 assert!(mode.allows_tool("recall"));
1488 assert!(mode.allows_tool("mana"));
1489 assert!(mode.allows_tool("ask"));
1490 assert!(mode.allows_tool("spawn"));
1491 }
1492
1493 #[test]
1494 fn agent_mode_orchestrator_blocks_write() {
1495 let mode = AgentMode::Orchestrator;
1496 assert!(!mode.allows_tool("write"));
1497 assert!(!mode.allows_tool("edit"));
1498 assert!(!mode.allows_tool("shell"));
1499 }
1500
1501 #[test]
1502 fn non_orchestrator_modes_block_spawn() {
1503 for mode in [
1504 AgentMode::Worker,
1505 AgentMode::Planner,
1506 AgentMode::Reviewer,
1507 AgentMode::Auditor,
1508 ] {
1509 assert!(
1510 !mode.allows_tool("spawn"),
1511 "mode {mode:?} should block spawn"
1512 );
1513 }
1514 }
1515
1516 #[test]
1517 fn agent_mode_planner_allows_mana_create() {
1518 let mode = AgentMode::Planner;
1519 assert!(mode.allows_mana_action("create"));
1520 assert!(mode.allows_mana_action("status"));
1521 assert!(mode.allows_mana_action("list"));
1522 assert!(mode.allows_mana_action("show"));
1523 assert!(mode.allows_tool("git"));
1524 }
1525
1526 #[test]
1527 fn agent_mode_planner_blocks_mana_close_and_run() {
1528 let mode = AgentMode::Planner;
1529 assert!(!mode.allows_mana_action("close"));
1530 assert!(!mode.allows_mana_action("run"));
1531 assert!(mode.allows_mana_action("update"));
1532 assert!(mode.allows_tool("git"));
1533 }
1534
1535 #[test]
1536 fn agent_mode_worker_blocks_mana_create() {
1537 let mode = AgentMode::Worker;
1538 assert!(!mode.allows_mana_action("create"));
1539 assert!(!mode.allows_mana_action("run"));
1540 assert!(!mode.allows_mana_action("close"));
1541 assert!(mode.allows_tool("git"));
1542 }
1543
1544 #[test]
1545 fn agent_mode_worker_allows_mana_update() {
1546 let mode = AgentMode::Worker;
1547 assert!(mode.allows_mana_action("update"));
1548 assert!(mode.allows_mana_action("show"));
1549 assert!(mode.allows_mana_action("status"));
1550 assert!(mode.allows_mana_action("list"));
1551 }
1552
1553 #[test]
1554 fn agent_mode_reviewer_no_mana() {
1555 let mode = AgentMode::Reviewer;
1556 assert!(!mode.allows_mana_action("status"));
1557 assert!(!mode.allows_mana_action("list"));
1558 assert!(!mode.allows_mana_action("show"));
1559 assert!(!mode.allows_mana_action("create"));
1560 assert!(!mode.allows_mana_action("run"));
1561 assert!(!mode.allows_tool("mana"));
1563 assert!(mode.allows_tool("git"));
1564 }
1565
1566 #[test]
1567 fn agent_mode_auditor_mana_readonly() {
1568 let mode = AgentMode::Auditor;
1569 assert!(mode.allows_mana_action("status"));
1570 assert!(mode.allows_mana_action("list"));
1571 assert!(mode.allows_mana_action("show"));
1572 assert!(!mode.allows_mana_action("create"));
1573 assert!(!mode.allows_mana_action("close"));
1574 assert!(!mode.allows_mana_action("run"));
1575 assert!(!mode.allows_mana_action("update"));
1576 assert!(mode.allows_tool("git"));
1577 }
1578
1579 #[test]
1580 fn agent_mode_config_deserialize() {
1581 let dir = TempDir::new().unwrap();
1582 let config_path = dir.path().join("config.toml");
1583 fs::write(&config_path, r#"mode = "orchestrator""#).unwrap();
1584 let config = Config::load(&config_path).unwrap();
1585 assert_eq!(config.mode, AgentMode::Orchestrator);
1586 }
1587
1588 #[test]
1589 fn agent_mode_instructions() {
1590 assert!(AgentMode::Full.instructions().is_none());
1591 assert!(AgentMode::Worker.instructions().is_some());
1592 assert!(AgentMode::Orchestrator.instructions().is_some());
1593 assert!(AgentMode::Planner.instructions().is_some());
1594 assert!(AgentMode::Reviewer.instructions().is_some());
1595 assert!(AgentMode::Auditor.instructions().is_some());
1596
1597 let worker = AgentMode::Worker.instructions().unwrap();
1599 assert!(worker.contains("worker"));
1600 assert!(worker.contains("implement the assigned unit as specified"));
1601 assert!(
1602 worker.contains("final verification and closure belong to the orchestrator workflow")
1603 );
1604
1605 let orchestrator = AgentMode::Orchestrator.instructions().unwrap();
1606 assert!(orchestrator.contains("orchestrator agent"));
1607 assert!(orchestrator.contains("primary execution substrate"));
1608 assert!(orchestrator.contains("final verification, retry, and closure workflow"));
1609
1610 let reviewer = AgentMode::Reviewer.instructions().unwrap();
1611 assert!(reviewer.contains("reviewer") || reviewer.contains("read"));
1612 }
1613}