ralph_workflow/app/
validation.rs1use crate::agents::AgentRegistry;
10use crate::config::Config;
11use crate::logger::Colors;
12use std::path::Path;
13
14pub struct ValidatedAgents {
16 pub developer_agent: String,
18 pub reviewer_agent: String,
20}
21
22pub 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
54pub 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 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 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
118pub 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 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
189pub 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 let err = validate_can_commit(
235 &config,
236 ®istry,
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 ®istry,
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}