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