Skip to main content

ralph_workflow/cli/handlers/
diagnose.rs

1//! Diagnostic command handler.
2//!
3//! This module provides comprehensive diagnostic output for troubleshooting
4//! Ralph configuration and environment issues.
5
6use 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
17/// Handle --diagnose command.
18///
19/// Outputs comprehensive diagnostic information including:
20/// - System information (OS, architecture, working directory)
21/// - Git status and configuration
22/// - Agent configuration and availability
23/// - PROMPT.md validation
24/// - Checkpoint status
25/// - Project stack detection
26///
27/// This output is designed to be copy-pasted into bug reports.
28///
29/// # Arguments
30///
31/// * `colors` - Color configuration for output formatting
32/// * `config` - The current Ralph configuration
33/// * `registry` - The agent registry
34/// * `config_path` - Path to the unified config file
35/// * `config_sources` - List of configuration sources that were loaded
36pub 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    // Gather diagnostics using the diagnostics module
45    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    // Use diagnostic data to suppress dead code warnings
65    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
96/// Print system information section.
97fn 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
109/// Print git information section.
110fn 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        // Check for uncommitted changes
125        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
133/// Print configuration information section.
134fn 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
165/// Print agent chain configuration section.
166fn 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
178/// Print agent availability section.
179fn 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
206/// Print PROMPT.md status section.
207fn 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
233/// Print checkpoint status section.
234fn 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
254/// Print project stack detection section.
255fn 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                // Show language type indicators
275                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                // Show review guidelines summary
297                let guidelines = ReviewGuidelines::for_stack(&stack);
298                println!("  Review checks: {} total", guidelines.total_checks());
299
300                // Show severity breakdown
301                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                // Show first few critical checks as examples
315                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
335/// Print recent log entries section.
336fn 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}