ralph_workflow/cli/handlers/
diagnose.rs1use crate::agents::{global_agents_config_path, AgentRegistry, AgentRole, ConfigSource};
7use crate::checkpoint::load_checkpoint;
8use crate::config::Config;
9use crate::diagnostics::run_diagnostics;
10use crate::executor::ProcessExecutor;
11use crate::guidelines::{CheckSeverity, ReviewGuidelines};
12use crate::language_detector;
13use crate::logger::Colors;
14use std::fs;
15use std::path::Path;
16
17pub fn handle_diagnose(
37 colors: Colors,
38 config: &Config,
39 registry: &AgentRegistry,
40 config_path: &Path,
41 config_sources: &[ConfigSource],
42 executor: &dyn ProcessExecutor,
43) {
44 let report = run_diagnostics(registry);
46
47 println!(
48 "{}=== Ralph Diagnostic Report ==={}",
49 colors.bold(),
50 colors.reset()
51 );
52 println!();
53
54 print_system_info(colors);
55 print_git_info(colors, executor);
56 print_config_info(colors, config, config_path, config_sources);
57 print_agent_chain_info(colors, registry);
58 print_agent_availability(colors, registry);
59 print_prompt_status(colors);
60 print_checkpoint_status(colors);
61 print_project_stack(colors);
62 print_recent_logs(colors);
63
64 let _ = report.agents.total_agents;
66 let _ = report.agents.available_agents;
67 let _ = report.agents.unavailable_agents;
68 for status in &report.agents.agent_status {
69 let _ = (
70 &status.name,
71 &status.display_name,
72 status.available,
73 &status.json_parser,
74 &status.command,
75 );
76 }
77 let _ = (
78 &report.system.os,
79 &report.system.arch,
80 &report.system.working_directory,
81 &report.system.shell,
82 &report.system.git_version,
83 report.system.git_repo,
84 &report.system.git_branch,
85 &report.system.uncommitted_changes,
86 );
87
88 println!();
89 println!(
90 "{}Copy this output for bug reports: https://github.com/anthropics/ralph/issues{}",
91 colors.dim(),
92 colors.reset()
93 );
94}
95
96fn print_system_info(colors: Colors) {
98 println!("{}System:{}", colors.bold(), colors.reset());
99 println!(" OS: {} {}", std::env::consts::OS, std::env::consts::ARCH);
100 if let Ok(cwd) = std::env::current_dir() {
101 println!(" Working directory: {}", cwd.display());
102 }
103 if let Ok(shell) = std::env::var("SHELL") {
104 println!(" Shell: {shell}");
105 }
106 println!();
107}
108
109fn print_git_info(colors: Colors, executor: &dyn ProcessExecutor) {
111 println!("{}Git:{}", colors.bold(), colors.reset());
112 if let Ok(output) = executor.execute("git", &["--version"], &[], None) {
113 println!(" Version: {}", output.stdout.trim());
114 }
115 let is_repo = executor
116 .execute("git", &["rev-parse", "--git-dir"], &[], None)
117 .map(|o| o.status.success())
118 .unwrap_or(false);
119 println!(" In git repo: {}", if is_repo { "yes" } else { "no" });
120 if is_repo {
121 if let Ok(output) = executor.execute("git", &["branch", "--show-current"], &[], None) {
122 println!(" Current branch: {}", output.stdout.trim());
123 }
124 if let Ok(output) = executor.execute("git", &["status", "--porcelain"], &[], None) {
126 let changes = output.stdout.lines().count();
127 println!(" Uncommitted changes: {changes}");
128 }
129 }
130 println!();
131}
132
133fn print_config_info(
135 colors: Colors,
136 config: &Config,
137 config_path: &Path,
138 config_sources: &[ConfigSource],
139) {
140 println!("{}Configuration:{}", colors.bold(), colors.reset());
141 println!(" Unified config: {}", config_path.display());
142 println!(" Config exists: {}", config_path.exists());
143 println!(
144 " Review depth: {:?} ({})",
145 config.review_depth,
146 config.review_depth.description()
147 );
148 if let Some(global_path) = global_agents_config_path() {
149 println!(" Legacy global agents.toml: {}", global_path.display());
150 println!(" Legacy global exists: {}", global_path.exists());
151 }
152 if !config_sources.is_empty() {
153 println!(" Loaded sources:");
154 for src in config_sources {
155 println!(
156 " - {} ({} agents)",
157 src.path.display(),
158 src.agents_loaded
159 );
160 }
161 }
162 println!();
163}
164
165fn print_agent_chain_info(colors: Colors, registry: &AgentRegistry) {
167 println!("{}Agent Chain:{}", colors.bold(), colors.reset());
168 let fallback = registry.fallback_config();
169 let dev_chain = fallback.get_fallbacks(AgentRole::Developer);
170 let rev_chain = fallback.get_fallbacks(AgentRole::Reviewer);
171 println!(" Developer chain: {dev_chain:?}");
172 println!(" Reviewer chain: {rev_chain:?}");
173 println!(" Max retries: {}", fallback.max_retries);
174 println!(" Retry delay: {}ms", fallback.retry_delay_ms);
175 println!();
176}
177
178fn print_agent_availability(colors: Colors, registry: &AgentRegistry) {
180 println!("{}Agent Availability:{}", colors.bold(), colors.reset());
181 let all_agents = registry.list();
182 let mut sorted_agents: Vec<_> = all_agents.into_iter().collect();
183 sorted_agents.sort_by(|(a, _), (b, _)| a.cmp(b));
184 for (name, cfg) in sorted_agents {
185 let available = registry.is_agent_available(name);
186 let status_color = if available {
187 colors.green()
188 } else {
189 colors.red()
190 };
191 let status_icon = if available { "✓" } else { "✗" };
192 let display_name = registry.display_name(name);
193 println!(
194 " {}{}{} {} (parser: {}, cmd: {})",
195 status_color,
196 status_icon,
197 colors.reset(),
198 display_name,
199 cfg.json_parser,
200 cfg.cmd.split_whitespace().next().unwrap_or(&cfg.cmd)
201 );
202 }
203 println!();
204}
205
206fn print_prompt_status(colors: Colors) {
208 println!("{}PROMPT.md:{}", colors.bold(), colors.reset());
209 let prompt_path = Path::new("PROMPT.md");
210 if prompt_path.exists() {
211 if let Ok(content) = fs::read_to_string(prompt_path) {
212 println!(" Exists: yes");
213 println!(" Size: {} bytes", content.len());
214 println!(" Lines: {}", content.lines().count());
215 let has_goal = content.contains("## Goal") || content.contains("# Goal");
216 let has_acceptance =
217 content.contains("## Acceptance") || content.contains("Acceptance Criteria");
218 println!(
219 " Has Goal section: {}",
220 if has_goal { "yes" } else { "no" }
221 );
222 println!(
223 " Has Acceptance section: {}",
224 if has_acceptance { "yes" } else { "no" }
225 );
226 }
227 } else {
228 println!(" Exists: no");
229 }
230 println!();
231}
232
233fn print_checkpoint_status(colors: Colors) {
235 println!("{}Checkpoint:{}", colors.bold(), colors.reset());
236 let checkpoint_path = Path::new(".agent/checkpoint.json");
237 if checkpoint_path.exists() {
238 println!(" Exists: yes");
239 if let Ok(Some(cp)) = load_checkpoint() {
240 println!(" Phase: {:?}", cp.phase);
241 println!(" Developer agent: {}", cp.developer_agent);
242 println!(" Reviewer agent: {}", cp.reviewer_agent);
243 println!(
244 " Iterations: {}/{} dev, {}/{} review",
245 cp.iteration, cp.total_iterations, cp.reviewer_pass, cp.total_reviewer_passes
246 );
247 }
248 } else {
249 println!(" Exists: no (no interrupted run to resume)");
250 }
251 println!();
252}
253
254fn print_project_stack(colors: Colors) {
256 println!("{}Project Stack:{}", colors.bold(), colors.reset());
257 if let Ok(cwd) = std::env::current_dir() {
258 match language_detector::detect_stack(&cwd) {
259 Ok(stack) => {
260 println!(" Primary language: {}", stack.primary_language);
261 if !stack.secondary_languages.is_empty() {
262 println!(" Secondary languages: {:?}", stack.secondary_languages);
263 }
264 if !stack.frameworks.is_empty() {
265 println!(" Frameworks: {:?}", stack.frameworks);
266 }
267 if let Some(pm) = &stack.package_manager {
268 println!(" Package manager: {pm}");
269 }
270 if let Some(tf) = &stack.test_framework {
271 println!(" Test framework: {tf}");
272 }
273
274 let language_types: Vec<&str> = [
276 if stack.is_rust() { Some("Rust") } else { None },
277 if stack.is_python() {
278 Some("Python")
279 } else {
280 None
281 },
282 if stack.is_javascript_or_typescript() {
283 Some("JS/TS")
284 } else {
285 None
286 },
287 if stack.is_go() { Some("Go") } else { None },
288 ]
289 .into_iter()
290 .flatten()
291 .collect();
292 if !language_types.is_empty() {
293 println!(" Language flags: {}", language_types.join(", "));
294 }
295
296 let guidelines = ReviewGuidelines::for_stack(&stack);
298 println!(" Review checks: {} total", guidelines.total_checks());
299
300 let all_checks = guidelines.get_all_checks();
302 let critical_count = all_checks
303 .iter()
304 .filter(|c| matches!(c.severity, CheckSeverity::Critical))
305 .count();
306 let high_count = all_checks
307 .iter()
308 .filter(|c| matches!(c.severity, CheckSeverity::High))
309 .count();
310 if critical_count > 0 || high_count > 0 {
311 println!(" Check severities: {critical_count} critical, {high_count} high");
312 }
313
314 let critical_checks: Vec<_> = all_checks
316 .iter()
317 .filter(|c| matches!(c.severity, CheckSeverity::Critical))
318 .take(3)
319 .collect();
320 if !critical_checks.is_empty() {
321 println!(" Critical checks (sample):");
322 for check in critical_checks {
323 println!(" - {}", check.check);
324 }
325 }
326 }
327 Err(e) => {
328 println!(" Detection failed: {e}");
329 }
330 }
331 }
332 println!();
333}
334
335fn print_recent_logs(colors: Colors) {
337 let log_path = Path::new(".agent/logs/pipeline.log");
338 if log_path.exists() {
339 println!(
340 "{}Recent Log Entries (last 10):{}",
341 colors.bold(),
342 colors.reset()
343 );
344 if let Ok(content) = fs::read_to_string(log_path) {
345 let lines: Vec<&str> = content.lines().collect();
346 let start = lines.len().saturating_sub(10);
347 for line in &lines[start..] {
348 println!(" {line}");
349 }
350 }
351 }
352}