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_generate_completion, handle_init_global, handle_init_legacy,
12    handle_init_prompt, handle_list_templates, handle_smart_init, 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 --generate-completion flag: generate shell completion script and exit
83    if let Some(shell) = args.completion.generate_completion {
84        if handle_generate_completion(shell) {
85            return Ok(None);
86        }
87    }
88
89    // Handle --list-templates flag: display available templates and exit
90    if args.template_list.list_templates && handle_list_templates(colors) {
91        return Ok(None);
92    }
93
94    // Handle --init-prompt flag: create PROMPT.md from template and exit
95    if let Some(ref template_name) = args.init_prompt {
96        if handle_init_prompt(template_name, colors)? {
97            return Ok(None);
98        }
99    }
100
101    // Handle smart --init flag: intelligently determine what to initialize
102    if args.unified_init.init.is_some()
103        && handle_smart_init(args.unified_init.init.as_deref(), colors)?
104    {
105        return Ok(None);
106    }
107
108    // Handle --init-config flag: explicit config creation and exit
109    if args.unified_init.init_config && handle_init_global(colors)? {
110        return Ok(None);
111    }
112
113    // Handle --init-global flag: create unified config if it doesn't exist and exit
114    if args.unified_init.init_global && handle_init_global(colors)? {
115        return Ok(None);
116    }
117
118    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
119    if args.legacy_init.init_legacy {
120        let repo_root = get_repo_root().ok();
121        let legacy_path = repo_root.map_or_else(
122            || PathBuf::from(".agent/agents.toml"),
123            |root| root.join(".agent/agents.toml"),
124        );
125        if handle_init_legacy(colors, &legacy_path)? {
126            return Ok(None);
127        }
128    }
129
130    // Initialize agent registry with built-in defaults + unified config.
131    let (registry, config_sources) = load_agent_registry(unified.as_ref(), config_path.as_path())?;
132
133    // Apply default agents from fallback chains
134    apply_default_agents(&mut config, &registry);
135
136    Ok(Some(ConfigInitResult {
137        config,
138        registry,
139        config_path,
140        config_sources,
141    }))
142}
143
144fn load_agent_registry(
145    unified: Option<&UnifiedConfig>,
146    config_path: &std::path::Path,
147) -> anyhow::Result<(AgentRegistry, Vec<ConfigSource>)> {
148    let mut registry = AgentRegistry::new().map_err(|e| {
149        anyhow::anyhow!("Failed to load built-in default agents config (examples/agents.toml): {e}")
150    })?;
151
152    let mut sources = Vec::new();
153
154    // Backwards compatibility: load legacy agent config files only when unified config
155    // isn't present (this matches the deprecation warning behavior in config loader).
156    if unified.is_none() {
157        if let Some(global_path) = global_agents_config_path() {
158            if global_path.exists() {
159                let loaded = registry.load_from_file(&global_path).map_err(|e| {
160                    anyhow::anyhow!(
161                        "Failed to load legacy global agent config {}: {}",
162                        global_path.display(),
163                        e
164                    )
165                })?;
166                sources.push(ConfigSource {
167                    path: global_path,
168                    agents_loaded: loaded,
169                });
170            }
171        }
172
173        let repo_root = get_repo_root().ok();
174        let project_path = repo_root.map_or_else(
175            || PathBuf::from(".agent/agents.toml"),
176            |root| root.join(".agent/agents.toml"),
177        );
178        if project_path.exists() {
179            let loaded = registry.load_from_file(&project_path).map_err(|e| {
180                anyhow::anyhow!(
181                    "Failed to load legacy per-repo agent config {}: {}",
182                    project_path.display(),
183                    e
184                )
185            })?;
186            sources.push(ConfigSource {
187                path: project_path,
188                agents_loaded: loaded,
189            });
190        }
191    }
192
193    if let Some(unified_cfg) = unified {
194        let loaded = registry.apply_unified_config(unified_cfg);
195        if loaded > 0 || unified_cfg.agent_chain.is_some() {
196            sources.push(ConfigSource {
197                path: config_path.to_path_buf(),
198                agents_loaded: loaded,
199            });
200        }
201    }
202
203    Ok((registry, sources))
204}
205
206/// Applies default agent selection from fallback chains.
207///
208/// If no agent was explicitly selected via CLI/env/preset, uses the first entry
209/// from the `agent_chain` configuration.
210fn apply_default_agents(config: &mut Config, registry: &AgentRegistry) {
211    if config.developer_agent.is_none() {
212        config.developer_agent = registry
213            .fallback_config()
214            .get_fallbacks(AgentRole::Developer)
215            .first()
216            .cloned();
217    }
218    if config.reviewer_agent.is_none() {
219        config.reviewer_agent = registry
220            .fallback_config()
221            .get_fallbacks(AgentRole::Reviewer)
222            .first()
223            .cloned();
224    }
225}