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//! - Fetching and caching OpenCode API catalog for dynamic provider/model resolution
9
10use crate::agents::opencode_api::{CatalogLoader, RealCatalogLoader};
11use crate::agents::{
12    global_agents_config_path, validation as agent_validation, AgentRegistry, AgentRole,
13    ConfigSource,
14};
15use crate::cli::{
16    apply_args_to_config, handle_extended_help, handle_generate_completion, handle_init_global,
17    handle_init_legacy, handle_init_prompt, handle_list_work_guides, handle_smart_init, Args,
18};
19use crate::config::{loader, unified_config_path, Config, UnifiedConfig};
20use crate::git_helpers::get_repo_root;
21use crate::logger::Colors;
22use crate::logger::Logger;
23use std::path::PathBuf;
24
25/// Result of configuration initialization.
26pub struct ConfigInitResult {
27    /// The loaded configuration with CLI args applied.
28    pub config: Config,
29    /// The agent registry with merged configs.
30    pub registry: AgentRegistry,
31    /// The resolved path to the unified config file (for diagnostics/errors).
32    pub config_path: PathBuf,
33    /// Sources from which agent configs were loaded.
34    pub config_sources: Vec<ConfigSource>,
35}
36
37/// Initializes configuration and agent registry.
38///
39/// This function performs the following steps:
40/// 1. Loads config from unified config file (~/.config/ralph-workflow.toml)
41/// 2. Applies environment variable overrides
42/// 3. Applies CLI arguments to config
43/// 4. Handles --list-work-guides, --init-prompt, --init/--init-global (unified), and --init-legacy if set
44/// 5. Loads agent registry from built-ins + unified config
45/// 6. Selects default agents from fallback chains
46///
47/// # Arguments
48///
49/// * `args` - The parsed CLI arguments
50/// * `colors` - Color configuration for output
51/// * `logger` - Logger for info/warning messages
52///
53/// # Returns
54///
55/// Returns `Ok(Some(result))` on success, `Ok(None)` if an early exit was triggered
56/// (e.g., --init, --init-prompt, --list-templates), or an error if initialization fails.
57pub fn initialize_config(
58    args: &Args,
59    colors: Colors,
60    logger: &Logger,
61) -> anyhow::Result<Option<ConfigInitResult>> {
62    initialize_config_with_loader(args, colors, logger, &RealCatalogLoader)
63}
64
65/// Initializes configuration and agent registry with a custom catalog loader.
66///
67/// This is the same as [`initialize_config`] but accepts a custom [`CatalogLoader`]
68/// for dependency injection. This is primarily useful for testing.
69pub fn initialize_config_with_loader<L: CatalogLoader>(
70    args: &Args,
71    colors: Colors,
72    logger: &Logger,
73    catalog_loader: &L,
74) -> anyhow::Result<Option<ConfigInitResult>> {
75    // Load configuration from unified config file (with env overrides)
76    let (mut config, unified, warnings) = args
77        .config
78        .as_ref()
79        .map_or_else(loader::load_config, |config_path| {
80            loader::load_config_from_path(Some(config_path.as_path()))
81        });
82
83    // Display any deprecation warnings from config loading
84    for warning in warnings {
85        logger.warn(&warning);
86    }
87
88    let config_path = args
89        .config
90        .clone()
91        .or_else(unified_config_path)
92        .unwrap_or_else(|| PathBuf::from("~/.config/ralph-workflow.toml"));
93
94    // Set commit message from CLI
95    config = config.with_commit_msg(args.commit_msg.clone());
96
97    // Apply CLI arguments to config
98    apply_args_to_config(args, &mut config, colors);
99
100    // Handle --generate-completion flag: generate shell completion script and exit
101    if let Some(shell) = args.completion.generate_completion {
102        if handle_generate_completion(shell) {
103            return Ok(None);
104        }
105    }
106
107    // Handle --extended-help / --man flag: display extended help and exit.
108    // If combined with --list-work-guides, show both to reduce surprises.
109    if args.recovery.extended_help {
110        handle_extended_help();
111        if args.work_guide_list.list_work_guides {
112            println!();
113            handle_list_work_guides(colors);
114        }
115        return Ok(None);
116    }
117
118    // Handle --list-work-guides / --list-templates flag: display available Work Guides and exit
119    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
120        return Ok(None);
121    }
122
123    // Handle --init-prompt flag: create PROMPT.md from template and exit
124    if let Some(ref template_name) = args.init_prompt {
125        if handle_init_prompt(template_name, args.unified_init.force_init, colors)? {
126            return Ok(None);
127        }
128    }
129
130    // Handle smart --init flag: intelligently determine what to initialize
131    if args.unified_init.init.is_some()
132        && handle_smart_init(
133            args.unified_init.init.as_deref(),
134            args.unified_init.force_init,
135            colors,
136        )?
137    {
138        return Ok(None);
139    }
140
141    // Handle --init-config flag: explicit config creation and exit
142    if args.unified_init.init_config && handle_init_global(colors)? {
143        return Ok(None);
144    }
145
146    // Handle --init-global flag: create unified config if it doesn't exist and exit
147    if args.unified_init.init_global && handle_init_global(colors)? {
148        return Ok(None);
149    }
150
151    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
152    if args.legacy_init.init_legacy {
153        let repo_root = get_repo_root().ok();
154        let legacy_path = repo_root.map_or_else(
155            || PathBuf::from(".agent/agents.toml"),
156            |root| root.join(".agent/agents.toml"),
157        );
158        if handle_init_legacy(colors, &legacy_path)? {
159            return Ok(None);
160        }
161    }
162
163    // Initialize agent registry with built-in defaults + unified config.
164    let (registry, config_sources) =
165        load_agent_registry(unified.as_ref(), config_path.as_path(), catalog_loader)?;
166
167    // Apply default agents from fallback chains
168    apply_default_agents(&mut config, &registry);
169
170    Ok(Some(ConfigInitResult {
171        config,
172        registry,
173        config_path,
174        config_sources,
175    }))
176}
177
178fn load_agent_registry<L: CatalogLoader>(
179    unified: Option<&UnifiedConfig>,
180    config_path: &std::path::Path,
181    catalog_loader: &L,
182) -> anyhow::Result<(AgentRegistry, Vec<ConfigSource>)> {
183    let mut registry = AgentRegistry::new().map_err(|e| {
184        anyhow::anyhow!("Failed to load built-in default agents config (examples/agents.toml): {e}")
185    })?;
186
187    let mut sources = Vec::new();
188
189    // Backwards compatibility: load legacy agent config files only when unified config
190    // isn't present (this matches the deprecation warning behavior in config loader).
191    if unified.is_none() {
192        if let Some(global_path) = global_agents_config_path() {
193            if global_path.exists() {
194                let loaded = registry.load_from_file(&global_path).map_err(|e| {
195                    anyhow::anyhow!(
196                        "Failed to load legacy global agent config {}: {}",
197                        global_path.display(),
198                        e
199                    )
200                })?;
201                sources.push(ConfigSource {
202                    path: global_path,
203                    agents_loaded: loaded,
204                });
205            }
206        }
207
208        let repo_root = get_repo_root().ok();
209        let project_path = repo_root.map_or_else(
210            || PathBuf::from(".agent/agents.toml"),
211            |root| root.join(".agent/agents.toml"),
212        );
213        if project_path.exists() {
214            let loaded = registry.load_from_file(&project_path).map_err(|e| {
215                anyhow::anyhow!(
216                    "Failed to load legacy per-repo agent config {}: {}",
217                    project_path.display(),
218                    e
219                )
220            })?;
221            sources.push(ConfigSource {
222                path: project_path,
223                agents_loaded: loaded,
224            });
225        }
226    }
227
228    if let Some(unified_cfg) = unified {
229        let loaded = registry.apply_unified_config(unified_cfg);
230        if loaded > 0 || unified_cfg.agent_chain.is_some() {
231            sources.push(ConfigSource {
232                path: config_path.to_path_buf(),
233                agents_loaded: loaded,
234            });
235        }
236    }
237
238    // Load OpenCode API catalog if there are any opencode/* references
239    setup_opencode_catalog(&mut registry, unified, catalog_loader)?;
240
241    Ok((registry, sources))
242}
243
244/// Setup OpenCode API catalog for dynamic provider/model resolution.
245///
246/// This function:
247/// 1. Checks if there are any `opencode/*` references in the configured agent chains
248/// 2. If yes, fetches/loads the cached OpenCode API catalog
249/// 3. Sets the catalog on the registry for dynamic agent resolution
250/// 4. Validates all opencode/* references and reports errors with suggestions
251fn setup_opencode_catalog<L: CatalogLoader>(
252    registry: &mut AgentRegistry,
253    unified: Option<&UnifiedConfig>,
254    catalog_loader: &L,
255) -> anyhow::Result<()> {
256    // Collect fallback config from unified config or registry defaults
257    let fallback = unified
258        .and_then(|u| u.agent_chain.as_ref())
259        .cloned()
260        .unwrap_or_else(|| registry.fallback_config().clone());
261
262    // Check if there are any opencode/* references
263    let opencode_refs = agent_validation::get_opencode_refs(&fallback);
264    if opencode_refs.is_empty() {
265        // No opencode references, skip catalog loading
266        return Ok(());
267    }
268
269    // Load the API catalog using the injected loader
270    let catalog = catalog_loader.load().map_err(|e| {
271        anyhow::anyhow!(
272            "Failed to load OpenCode API catalog. \
273            This is required for the following agent references: {opencode_refs:?}. \
274            Error: {e}"
275        )
276    })?;
277
278    // Set the catalog on the registry for dynamic resolution
279    registry.set_opencode_catalog(catalog.clone());
280
281    // Validate all opencode/* references
282    agent_validation::validate_opencode_agents(&fallback, &catalog)
283        .map_err(|e| anyhow::anyhow!("{e}"))?;
284
285    Ok(())
286}
287
288/// Applies default agent selection from fallback chains.
289///
290/// If no agent was explicitly selected via CLI/env/preset, uses the first entry
291/// from the `agent_chain` configuration.
292fn apply_default_agents(config: &mut Config, registry: &AgentRegistry) {
293    if config.developer_agent.is_none() {
294        config.developer_agent = registry
295            .fallback_config()
296            .get_fallbacks(AgentRole::Developer)
297            .first()
298            .cloned();
299    }
300    if config.reviewer_agent.is_none() {
301        config.reviewer_agent = registry
302            .fallback_config()
303            .get_fallbacks(AgentRole::Reviewer)
304            .first()
305            .cloned();
306    }
307}