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