ralph_workflow/app/
config_init.rs

1//! Configuration loading and agent registry initialization.
2//!
3//! This module handles:
4//! - Loading configuration from the unified config file (~/.config/ralph-workflow.toml)
5//! - Applying environment variable and CLI overrides
6//! - Selecting default agents from fallback chains
7//! - Loading agent registry data from unified config
8
9use crate::agents::{global_agents_config_path, AgentRegistry, AgentRole, ConfigSource};
10use crate::cli::{
11    apply_args_to_config, handle_init_global, handle_init_legacy, handle_init_prompt,
12    handle_list_templates, Args,
13};
14use crate::config::{loader, unified_config_path, Config, UnifiedConfig};
15use crate::git_helpers::get_repo_root;
16use crate::logger::Colors;
17use crate::logger::Logger;
18use std::path::PathBuf;
19
20/// Result of configuration initialization.
21pub struct ConfigInitResult {
22    /// The loaded configuration with CLI args applied.
23    pub config: Config,
24    /// The agent registry with merged configs.
25    pub registry: AgentRegistry,
26    /// The resolved path to the unified config file (for diagnostics/errors).
27    pub config_path: PathBuf,
28    /// Sources from which agent configs were loaded.
29    pub config_sources: Vec<ConfigSource>,
30}
31
32/// Initializes configuration and agent registry.
33///
34/// This function performs the following steps:
35/// 1. Loads config from unified config file (~/.config/ralph-workflow.toml)
36/// 2. Applies environment variable overrides
37/// 3. Applies CLI arguments to config
38/// 4. Handles --list-templates, --init-prompt, --init/--init-global (unified), and --init-legacy if set
39/// 5. Loads agent registry from built-ins + unified config
40/// 6. Selects default agents from fallback chains
41///
42/// # Arguments
43///
44/// * `args` - The parsed CLI arguments
45/// * `colors` - Color configuration for output
46/// * `logger` - Logger for info/warning messages
47///
48/// # Returns
49///
50/// Returns `Ok(Some(result))` on success, `Ok(None)` if an early exit was triggered
51/// (e.g., --init, --init-prompt, --list-templates), or an error if initialization fails.
52pub fn initialize_config(
53    args: &Args,
54    colors: Colors,
55    logger: &Logger,
56) -> anyhow::Result<Option<ConfigInitResult>> {
57    // Load configuration from unified config file (with env overrides)
58    let (mut config, unified, warnings) = args
59        .config
60        .as_ref()
61        .map_or_else(loader::load_config, |config_path| {
62            loader::load_config_from_path(Some(config_path.as_path()))
63        });
64
65    // Display any deprecation warnings from config loading
66    for warning in warnings {
67        logger.warn(&warning);
68    }
69
70    let config_path = args
71        .config
72        .clone()
73        .or_else(unified_config_path)
74        .unwrap_or_else(|| PathBuf::from("~/.config/ralph-workflow.toml"));
75
76    // Set commit message from CLI
77    config = config.with_commit_msg(args.commit_msg.clone());
78
79    // Apply CLI arguments to config
80    apply_args_to_config(args, &mut config, colors);
81
82    // Handle --list-templates flag: display available templates and exit
83    if args.template_list.list_templates && handle_list_templates(colors) {
84        return Ok(None);
85    }
86
87    // Handle --init-prompt flag: create PROMPT.md from template and exit
88    if let Some(ref template_name) = args.init_prompt {
89        if handle_init_prompt(template_name, colors)? {
90            return Ok(None);
91        }
92    }
93
94    // Handle unified init flags: create unified config if it doesn't exist and exit
95    if (args.unified_init.init_global || args.unified_init.init) && handle_init_global(colors)? {
96        return Ok(None);
97    }
98
99    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
100    if args.legacy_init.init_legacy {
101        let repo_root = get_repo_root().ok();
102        let legacy_path = repo_root.map_or_else(
103            || PathBuf::from(".agent/agents.toml"),
104            |root| root.join(".agent/agents.toml"),
105        );
106        if handle_init_legacy(colors, &legacy_path)? {
107            return Ok(None);
108        }
109    }
110
111    // Initialize agent registry with built-in defaults + unified config.
112    let (registry, config_sources) = load_agent_registry(unified.as_ref(), config_path.as_path())?;
113
114    // Apply default agents from fallback chains
115    apply_default_agents(&mut config, &registry);
116
117    Ok(Some(ConfigInitResult {
118        config,
119        registry,
120        config_path,
121        config_sources,
122    }))
123}
124
125fn load_agent_registry(
126    unified: Option<&UnifiedConfig>,
127    config_path: &std::path::Path,
128) -> anyhow::Result<(AgentRegistry, Vec<ConfigSource>)> {
129    let mut registry = AgentRegistry::new().map_err(|e| {
130        anyhow::anyhow!("Failed to load built-in default agents config (examples/agents.toml): {e}")
131    })?;
132
133    let mut sources = Vec::new();
134
135    // Backwards compatibility: load legacy agent config files only when unified config
136    // isn't present (this matches the deprecation warning behavior in config loader).
137    if unified.is_none() {
138        if let Some(global_path) = global_agents_config_path() {
139            if global_path.exists() {
140                let loaded = registry.load_from_file(&global_path).map_err(|e| {
141                    anyhow::anyhow!(
142                        "Failed to load legacy global agent config {}: {}",
143                        global_path.display(),
144                        e
145                    )
146                })?;
147                sources.push(ConfigSource {
148                    path: global_path,
149                    agents_loaded: loaded,
150                });
151            }
152        }
153
154        let repo_root = get_repo_root().ok();
155        let project_path = repo_root.map_or_else(
156            || PathBuf::from(".agent/agents.toml"),
157            |root| root.join(".agent/agents.toml"),
158        );
159        if project_path.exists() {
160            let loaded = registry.load_from_file(&project_path).map_err(|e| {
161                anyhow::anyhow!(
162                    "Failed to load legacy per-repo agent config {}: {}",
163                    project_path.display(),
164                    e
165                )
166            })?;
167            sources.push(ConfigSource {
168                path: project_path,
169                agents_loaded: loaded,
170            });
171        }
172    }
173
174    if let Some(unified_cfg) = unified {
175        let loaded = registry.apply_unified_config(unified_cfg);
176        if loaded > 0 || unified_cfg.agent_chain.is_some() {
177            sources.push(ConfigSource {
178                path: config_path.to_path_buf(),
179                agents_loaded: loaded,
180            });
181        }
182    }
183
184    Ok((registry, sources))
185}
186
187/// Applies default agent selection from fallback chains.
188///
189/// If no agent was explicitly selected via CLI/env/preset, uses the first entry
190/// from the `agent_chain` configuration.
191fn apply_default_agents(config: &mut Config, registry: &AgentRegistry) {
192    if config.developer_agent.is_none() {
193        config.developer_agent = registry
194            .fallback_config()
195            .get_fallbacks(AgentRole::Developer)
196            .first()
197            .cloned();
198    }
199    if config.reviewer_agent.is_none() {
200        config.reviewer_agent = registry
201            .fallback_config()
202            .get_fallbacks(AgentRole::Reviewer)
203            .first()
204            .cloned();
205    }
206}