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