Skip to main content

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