Skip to main content

ralph_workflow/app/
validation.rs

1//! Agent validation and chain validation.
2//!
3//! This module handles validation of agents and agent chains:
4//! - Resolving required agent names from config
5//! - Validating that agent commands exist in the registry
6//! - Enforcing workflow-capable agents (`can_commit=true`)
7//! - Validating agent chain configuration
8
9use crate::agents::AgentRegistry;
10use crate::config::Config;
11use crate::logger::Colors;
12use std::path::Path;
13
14/// Result of agent validation containing the resolved agent names.
15pub struct ValidatedAgents {
16    /// The validated developer agent name.
17    pub developer_agent: String,
18    /// The validated reviewer agent name.
19    pub reviewer_agent: String,
20}
21
22/// Resolves and validates the required agent names from configuration.
23///
24/// Both developer and reviewer agents must be configured at this point,
25/// either via CLI args, environment variables, or `agent_chain` defaults.
26///
27/// # Arguments
28///
29/// * `config` - The pipeline configuration
30///
31/// # Returns
32///
33/// Returns the validated agent names or an error if agents are not configured.
34pub fn resolve_required_agents(config: &Config) -> anyhow::Result<ValidatedAgents> {
35    let developer_agent = config.developer_agent.clone().ok_or_else(|| {
36        anyhow::anyhow!(
37            "No developer agent configured.\n\
38            Set via --developer-agent, RALPH_DEVELOPER_AGENT env, or [agent_chain] in ~/.config/ralph-workflow.toml."
39        )
40    })?;
41    let reviewer_agent = config.reviewer_agent.clone().ok_or_else(|| {
42        anyhow::anyhow!(
43            "No reviewer agent configured.\n\
44            Set via --reviewer-agent, RALPH_REVIEWER_AGENT env, or [agent_chain] in ~/.config/ralph-workflow.toml."
45        )
46    })?;
47
48    Ok(ValidatedAgents {
49        developer_agent,
50        reviewer_agent,
51    })
52}
53
54/// Validates that agent commands exist in the registry.
55///
56/// Checks that both developer and reviewer agents have valid commands
57/// defined either in the config or the registry.
58///
59/// # Arguments
60///
61/// * `config` - The pipeline configuration
62/// * `registry` - The agent registry
63/// * `developer_agent` - Name of the developer agent
64/// * `reviewer_agent` - Name of the reviewer agent
65/// * `config_path` - Path to the unified config file for error messages
66///
67/// # Returns
68///
69/// Returns `Ok(())` if validation passes, or an error with details.
70pub fn validate_agent_commands(
71    config: &Config,
72    registry: &AgentRegistry,
73    developer_agent: &str,
74    reviewer_agent: &str,
75    config_path: &Path,
76) -> anyhow::Result<()> {
77    // Validate developer command exists
78    if config.developer_cmd.is_none() {
79        let resolved_developer = registry.resolve_fuzzy(developer_agent);
80        let dev_agent_ref = resolved_developer.as_deref().unwrap_or(developer_agent);
81        registry.developer_cmd(dev_agent_ref).ok_or_else(|| {
82            let suggestion = resolved_developer
83                .as_ref()
84                .filter(|n| n != &developer_agent)
85                .map(|correct| format!(" Did you mean '{correct}'?"))
86                .unwrap_or_default();
87            anyhow::anyhow!(
88                "Unknown developer agent '{}'.{}. Use --list-agents or define it in {} under [agents].",
89                developer_agent,
90                suggestion,
91                config_path.display()
92            )
93        })?;
94    }
95
96    // Validate reviewer command exists
97    if config.reviewer_cmd.is_none() {
98        let resolved_reviewer = registry.resolve_fuzzy(reviewer_agent);
99        let rev_agent_ref = resolved_reviewer.as_deref().unwrap_or(reviewer_agent);
100        registry.reviewer_cmd(rev_agent_ref).ok_or_else(|| {
101            let suggestion = resolved_reviewer
102                .as_ref()
103                .filter(|n| n != &reviewer_agent)
104                .map(|correct| format!(" Did you mean '{correct}'?"))
105                .unwrap_or_default();
106            anyhow::anyhow!(
107                "Unknown reviewer agent '{}'.{}. Use --list-agents or define it in {} under [agents].",
108                reviewer_agent,
109                suggestion,
110                config_path.display()
111            )
112        })?;
113    }
114
115    Ok(())
116}
117
118/// Validates that agents are workflow-capable (`can_commit=true`).
119///
120/// Agents with `can_commit=false` are chat-only / non-tool agents and will
121/// stall Ralph's workflow. This validation is skipped if a custom command
122/// override is provided.
123///
124/// # Arguments
125///
126/// * `config` - The pipeline configuration
127/// * `registry` - The agent registry
128/// * `developer_agent` - Name of the developer agent
129/// * `reviewer_agent` - Name of the reviewer agent
130/// * `config_path` - Path to the unified config file for error messages
131///
132/// # Returns
133///
134/// Returns `Ok(())` if validation passes, or an error with details.
135pub fn validate_can_commit(
136    config: &Config,
137    registry: &AgentRegistry,
138    developer_agent: &str,
139    reviewer_agent: &str,
140    config_path: &Path,
141) -> anyhow::Result<()> {
142    // Enforce workflow-capable agents unless custom command override provided
143    if config.developer_cmd.is_none() {
144        let resolved = registry
145            .resolve_fuzzy(developer_agent)
146            .unwrap_or_else(|| developer_agent.to_string());
147        if let Some(cfg) = registry.resolve_config(&resolved) {
148            if !cfg.can_commit {
149                let resolved_note = if resolved == developer_agent {
150                    String::new()
151                } else {
152                    format!(" (resolved to '{resolved}')")
153                };
154                anyhow::bail!(
155                    "Developer agent '{}'{} has can_commit=false and cannot run Ralph's workflow.\n\
156                    Fix: choose a different agent (see --list-agents) or set can_commit=true in {} under [agents].",
157                    developer_agent,
158                    resolved_note,
159                    config_path.display()
160                );
161            }
162        }
163    }
164    if config.reviewer_cmd.is_none() {
165        let resolved = registry
166            .resolve_fuzzy(reviewer_agent)
167            .unwrap_or_else(|| reviewer_agent.to_string());
168        if let Some(cfg) = registry.resolve_config(&resolved) {
169            if !cfg.can_commit {
170                let resolved_note = if resolved == reviewer_agent {
171                    String::new()
172                } else {
173                    format!(" (resolved to '{resolved}')")
174                };
175                anyhow::bail!(
176                    "Reviewer agent '{}'{} has can_commit=false and cannot run Ralph's workflow.\n\
177                    Fix: choose a different agent (see --list-agents) or set can_commit=true in {} under [agents].",
178                    reviewer_agent,
179                    resolved_note,
180                    config_path.display()
181                );
182            }
183        }
184    }
185
186    Ok(())
187}
188
189/// Validates that agent chains are properly configured.
190///
191/// Displays an error and exits if the agent chains are not configured.
192///
193/// # Arguments
194///
195/// * `registry` - The agent registry
196/// * `colors` - Color configuration for output
197pub fn validate_agent_chains(registry: &AgentRegistry, colors: Colors) {
198    if let Err(msg) = registry.validate_agent_chains() {
199        eprintln!();
200        eprintln!(
201            "{}{}Error:{} {}",
202            colors.bold(),
203            colors.red(),
204            colors.reset(),
205            msg
206        );
207        eprintln!();
208        eprintln!(
209            "{}Hint:{} Run 'ralph --init-global' to create ~/.config/ralph-workflow.toml.",
210            colors.yellow(),
211            colors.reset()
212        );
213        eprintln!();
214        std::process::exit(1);
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::config::CcsConfig;
222    use std::collections::HashMap;
223
224    #[test]
225    fn validate_can_commit_uses_fuzzy_resolution() {
226        let registry = AgentRegistry::new().unwrap();
227        let config = Config {
228            developer_cmd: None,
229            reviewer_cmd: None,
230            ..Config::default()
231        };
232
233        // "AiChat" resolves to "aichat" (can_commit=false). This must be rejected.
234        let err = validate_can_commit(
235            &config,
236            &registry,
237            "AiChat",
238            "claude",
239            Path::new("ralph-workflow.toml"),
240        )
241        .unwrap_err();
242        let msg = err.to_string();
243        assert!(msg.contains("can_commit=false"));
244        assert!(msg.contains("AiChat"));
245        assert!(msg.contains("resolved to 'aichat'"));
246    }
247
248    #[test]
249    fn validate_can_commit_uses_resolve_config_for_ccs_refs() {
250        let mut registry = AgentRegistry::new().unwrap();
251        let defaults = CcsConfig {
252            can_commit: false,
253            ..CcsConfig::default()
254        };
255        registry.set_ccs_aliases(&HashMap::new(), defaults);
256
257        let config = Config {
258            developer_cmd: None,
259            reviewer_cmd: None,
260            ..Config::default()
261        };
262
263        let err = validate_can_commit(
264            &config,
265            &registry,
266            "ccs/random",
267            "claude",
268            Path::new("ralph-workflow.toml"),
269        )
270        .unwrap_err();
271        assert!(err.to_string().contains("can_commit=false"));
272    }
273}