1use super::parser::parse_env_bool;
17use super::path_resolver::ConfigEnvironment;
18use super::types::{Config, ReviewDepth, Verbosity};
19use super::unified::UnifiedConfig;
20use std::env;
21use std::path::PathBuf;
22
23pub fn load_config() -> (Config, Option<UnifiedConfig>, Vec<String>) {
33 load_config_from_path(None)
34}
35
36pub fn load_config_from_path(
50 config_path: Option<&std::path::Path>,
51) -> (Config, Option<UnifiedConfig>, Vec<String>) {
52 load_config_from_path_with_env(config_path, &super::path_resolver::RealConfigEnvironment)
53}
54
55pub fn load_config_from_path_with_env(
70 config_path: Option<&std::path::Path>,
71 env: &dyn ConfigEnvironment,
72) -> (Config, Option<UnifiedConfig>, Vec<String>) {
73 let mut warnings = Vec::new();
74
75 let unified = config_path.map_or_else(
77 || UnifiedConfig::load_with_env(env),
78 |path| {
79 if env.file_exists(path) {
80 match UnifiedConfig::load_from_path_with_env(path, env) {
81 Ok(cfg) => Some(cfg),
82 Err(e) => {
83 warnings.push(format!(
84 "Failed to load config from {}: {}",
85 path.display(),
86 e
87 ));
88 None
89 }
90 }
91 } else {
92 warnings.push(format!("Config file not found: {}", path.display()));
93 None
94 }
95 },
96 );
97
98 let config = if let Some(ref unified_cfg) = unified {
100 config_from_unified(unified_cfg, &mut warnings)
101 } else {
102 default_config()
104 };
105
106 let config = apply_env_overrides(config, &mut warnings);
108
109 (config, unified, warnings)
110}
111
112fn config_from_unified(unified: &UnifiedConfig, warnings: &mut Vec<String>) -> Config {
114 use super::types::{BehavioralFlags, FeatureFlags};
115
116 let general = &unified.general;
117 let max_dev_continuations = if general.max_dev_continuations >= 1 {
118 general.max_dev_continuations
119 } else {
120 warnings.push(
121 "Invalid max_dev_continuations in config; must be a positive integer (>= 1). Falling back to default."
122 .to_string(),
123 );
124 2
125 };
126 let max_xsd_retries = general.max_xsd_retries;
129 let max_same_agent_retries = general.max_same_agent_retries;
132
133 let review_depth = ReviewDepth::from_str(&general.review_depth).unwrap_or_else(|| {
134 warnings.push(format!(
135 "Invalid review_depth '{}' in config; falling back to 'standard'.",
136 general.review_depth
137 ));
138 ReviewDepth::default()
139 });
140
141 Config {
142 developer_agent: None, reviewer_agent: None, developer_cmd: None,
145 reviewer_cmd: None,
146 commit_cmd: None,
147 developer_model: None,
148 reviewer_model: None,
149 developer_provider: None,
150 reviewer_provider: None,
151 reviewer_json_parser: None, features: FeatureFlags {
153 checkpoint_enabled: general.workflow.checkpoint_enabled,
154 force_universal_prompt: general.execution.force_universal_prompt,
155 },
156 developer_iters: general.developer_iters,
157 reviewer_reviews: general.reviewer_reviews,
158 fast_check_cmd: None,
159 full_check_cmd: None,
160 behavior: BehavioralFlags {
161 interactive: general.behavior.interactive,
162 auto_detect_stack: general.behavior.auto_detect_stack,
163 strict_validation: general.behavior.strict_validation,
164 },
165 prompt_path: general
166 .prompt_path
167 .as_ref()
168 .map_or_else(|| PathBuf::from(".agent/last_prompt.txt"), PathBuf::from),
169 user_templates_dir: general.templates_dir.as_ref().map(PathBuf::from),
170 developer_context: general.developer_context,
171 reviewer_context: general.reviewer_context,
172 verbosity: Verbosity::from(general.verbosity),
173 review_depth,
174 isolation_mode: general.execution.isolation_mode,
175 git_user_name: general.git_user_name.clone(),
176 git_user_email: general.git_user_email.clone(),
177 show_streaming_metrics: false, review_format_retries: 5, max_dev_continuations: Some(max_dev_continuations),
180 max_xsd_retries: Some(max_xsd_retries),
181 max_same_agent_retries: Some(max_same_agent_retries),
182 }
183}
184
185fn default_config() -> Config {
187 use super::types::{BehavioralFlags, FeatureFlags};
188
189 Config {
190 developer_agent: None,
191 reviewer_agent: None,
192 developer_cmd: None,
193 reviewer_cmd: None,
194 commit_cmd: None,
195 developer_model: None,
196 reviewer_model: None,
197 developer_provider: None,
198 reviewer_provider: None,
199 reviewer_json_parser: None,
200 features: FeatureFlags {
201 checkpoint_enabled: true,
202 force_universal_prompt: false,
203 },
204 developer_iters: 5,
205 reviewer_reviews: 2,
206 fast_check_cmd: None,
207 full_check_cmd: None,
208 behavior: BehavioralFlags {
209 interactive: true,
210 auto_detect_stack: true,
211 strict_validation: false,
212 },
213 prompt_path: PathBuf::from(".agent/last_prompt.txt"),
214 user_templates_dir: None,
215 developer_context: 1,
216 reviewer_context: 0,
217 verbosity: Verbosity::Verbose,
218 review_depth: ReviewDepth::default(),
219 isolation_mode: true,
220 git_user_name: None,
221 git_user_email: None,
222 show_streaming_metrics: false,
223 review_format_retries: 5,
224 max_dev_continuations: Some(2), max_xsd_retries: Some(10), max_same_agent_retries: Some(2), }
228}
229
230fn apply_env_overrides(mut config: Config, warnings: &mut Vec<String>) -> Config {
232 const MAX_ITERS: u32 = 50;
233 const MAX_REVIEWS: u32 = 10;
234 const MAX_CONTEXT: u8 = 2;
235 const MAX_FORMAT_RETRIES: u32 = 20;
236
237 apply_agent_selection_env(&mut config, warnings);
239 apply_command_env(&mut config, warnings);
240 apply_model_provider_env(&mut config);
241 apply_iteration_counts_env(&mut config, warnings, MAX_ITERS, MAX_REVIEWS);
242 apply_review_config_env(&mut config, warnings, MAX_FORMAT_RETRIES);
243 apply_boolean_flags_env(&mut config);
244 apply_verbosity_env(&mut config, warnings);
245 apply_review_depth_env(&mut config, warnings);
246 apply_paths_env(&mut config);
247 apply_context_levels_env(&mut config, warnings, MAX_CONTEXT);
248 apply_git_identity_env(&mut config);
249
250 config
251}
252
253fn apply_agent_selection_env(config: &mut Config, warnings: &mut Vec<String>) {
255 if let Ok(val) = env::var("RALPH_DEVELOPER_AGENT") {
256 let trimmed = val.trim();
257 if trimmed.is_empty() {
258 warnings.push("Env var RALPH_DEVELOPER_AGENT is empty; ignoring.".to_string());
259 } else {
260 config.developer_agent = Some(trimmed.to_string());
261 }
262 }
263
264 if let Ok(val) = env::var("RALPH_REVIEWER_AGENT") {
265 let trimmed = val.trim();
266 if trimmed.is_empty() {
267 warnings.push("Env var RALPH_REVIEWER_AGENT is empty; ignoring.".to_string());
268 } else {
269 config.reviewer_agent = Some(trimmed.to_string());
270 }
271 }
272}
273
274fn apply_command_env(config: &mut Config, warnings: &mut Vec<String>) {
276 for (env_var, field) in [
277 ("RALPH_DEVELOPER_CMD", &mut config.developer_cmd),
278 ("RALPH_REVIEWER_CMD", &mut config.reviewer_cmd),
279 ("RALPH_COMMIT_CMD", &mut config.commit_cmd),
280 ] {
281 if let Ok(val) = env::var(env_var) {
282 let trimmed = val.trim();
283 if trimmed.is_empty() {
284 warnings.push(format!("Env var {env_var} is empty; ignoring."));
285 } else {
286 *field = Some(trimmed.to_string());
287 }
288 }
289 }
290
291 for (env_var, field) in [
292 ("FAST_CHECK_CMD", &mut config.fast_check_cmd),
293 ("FULL_CHECK_CMD", &mut config.full_check_cmd),
294 ] {
295 if let Ok(val) = env::var(env_var) {
296 if !val.is_empty() {
297 *field = Some(val);
298 }
299 }
300 }
301}
302
303fn apply_model_provider_env(config: &mut Config) {
305 for (env_var, field) in [
306 ("RALPH_DEVELOPER_MODEL", &mut config.developer_model),
307 ("RALPH_REVIEWER_MODEL", &mut config.reviewer_model),
308 ("RALPH_DEVELOPER_PROVIDER", &mut config.developer_provider),
309 ("RALPH_REVIEWER_PROVIDER", &mut config.reviewer_provider),
310 ] {
311 if let Ok(val) = env::var(env_var) {
312 *field = Some(val);
313 }
314 }
315
316 if let Ok(val) = env::var("RALPH_REVIEWER_JSON_PARSER") {
318 let trimmed = val.trim();
319 if !trimmed.is_empty() {
320 config.reviewer_json_parser = Some(trimmed.to_string());
321 }
322 }
323
324 if let Ok(val) = env::var("RALPH_REVIEWER_UNIVERSAL_PROMPT") {
326 if let Some(b) = parse_env_bool(&val) {
327 config.features.force_universal_prompt = b;
328 }
329 }
330}
331
332fn apply_iteration_counts_env(
334 config: &mut Config,
335 warnings: &mut Vec<String>,
336 max_iters: u32,
337 max_reviews: u32,
338) {
339 if let Some(n) = parse_env_u32("RALPH_DEVELOPER_ITERS", warnings, max_iters) {
340 config.developer_iters = n;
341 }
342 if let Some(n) = parse_env_u32("RALPH_REVIEWER_REVIEWS", warnings, max_reviews) {
343 config.reviewer_reviews = n;
344 }
345}
346
347fn apply_review_config_env(config: &mut Config, warnings: &mut Vec<String>, max_retries: u32) {
349 if let Some(n) = parse_env_u32("RALPH_REVIEW_FORMAT_RETRIES", warnings, max_retries) {
350 config.review_format_retries = n;
351 }
352}
353
354fn apply_boolean_flags_env(config: &mut Config) {
356 let vars: std::collections::HashMap<&str, bool> = [
358 "RALPH_INTERACTIVE",
359 "RALPH_AUTO_DETECT_STACK",
360 "RALPH_CHECKPOINT_ENABLED",
361 "RALPH_STRICT_VALIDATION",
362 "RALPH_ISOLATION_MODE",
363 ]
364 .iter()
365 .filter_map(|&name| env::var(name).ok().map(|v| (name, v)))
366 .filter_map(|(name, val)| parse_env_bool(&val).map(|b| (name, b)))
367 .collect();
368
369 for (name, value) in vars {
371 match name {
372 "RALPH_INTERACTIVE" => config.behavior.interactive = value,
373 "RALPH_AUTO_DETECT_STACK" => config.behavior.auto_detect_stack = value,
374 "RALPH_CHECKPOINT_ENABLED" => config.features.checkpoint_enabled = value,
375 "RALPH_STRICT_VALIDATION" => config.behavior.strict_validation = value,
376 "RALPH_ISOLATION_MODE" => config.isolation_mode = value,
377 _ => {}
378 }
379 }
380}
381
382fn apply_verbosity_env(config: &mut Config, warnings: &mut Vec<String>) {
384 if let Ok(val) = env::var("RALPH_VERBOSITY") {
385 let trimmed = val.trim();
386 if trimmed.is_empty() {
387 return;
388 }
389 match trimmed.parse::<u8>() {
390 Ok(n) => {
391 if n > 4 {
392 warnings.push(format!(
393 "Env var RALPH_VERBOSITY={n} is out of range; clamping to 4 (debug)."
394 ));
395 }
396 config.verbosity = Verbosity::from(n.min(4));
397 }
398 Err(_) => {
399 warnings.push(format!(
400 "Env var RALPH_VERBOSITY='{trimmed}' is not a valid number; ignoring."
401 ));
402 }
403 }
404 }
405}
406
407fn apply_review_depth_env(config: &mut Config, warnings: &mut Vec<String>) {
409 if let Ok(val) = env::var("RALPH_REVIEW_DEPTH") {
410 if let Some(depth) = ReviewDepth::from_str(&val) {
411 config.review_depth = depth;
412 } else if !val.trim().is_empty() {
413 warnings.push(format!(
414 "Env var RALPH_REVIEW_DEPTH='{}' is invalid; ignoring.",
415 val.trim()
416 ));
417 }
418 }
419}
420
421fn apply_paths_env(config: &mut Config) {
423 if let Ok(val) = env::var("RALPH_PROMPT_PATH") {
424 config.prompt_path = PathBuf::from(val);
425 }
426 if let Ok(val) = env::var("RALPH_TEMPLATES_DIR") {
427 let trimmed = val.trim();
428 if !trimmed.is_empty() {
429 config.user_templates_dir = Some(PathBuf::from(trimmed));
430 }
431 }
432}
433
434fn apply_context_levels_env(config: &mut Config, warnings: &mut Vec<String>, max_context: u8) {
436 if let Some(n) = parse_env_u8("RALPH_DEVELOPER_CONTEXT", warnings, max_context) {
437 config.developer_context = n;
438 }
439 if let Some(n) = parse_env_u8("RALPH_REVIEWER_CONTEXT", warnings, max_context) {
440 config.reviewer_context = n;
441 }
442}
443
444fn apply_git_identity_env(config: &mut Config) {
446 if let Ok(val) = env::var("RALPH_GIT_USER_NAME") {
447 let trimmed = val.trim();
448 if !trimmed.is_empty() {
449 config.git_user_name = Some(trimmed.to_string());
450 }
451 }
452 if let Ok(val) = env::var("RALPH_GIT_USER_EMAIL") {
453 let trimmed = val.trim();
454 if !trimmed.is_empty() {
455 config.git_user_email = Some(trimmed.to_string());
456 }
457 }
458}
459
460mod env_parsing;
461use env_parsing::{parse_env_u32, parse_env_u8};
462
463mod unified_config_exists;
464
465pub use unified_config_exists::{unified_config_exists, unified_config_exists_with_env};
466
467#[cfg(test)]
468mod tests;