ralph_workflow/phases/review/
validation.rs1use crate::agents::{contains_glm_model, is_glm_like_agent};
8use crate::review_metrics::ReviewMetrics;
9use crate::workspace::Workspace;
10use std::path::Path;
11
12#[derive(Debug)]
14pub enum PreflightResult {
15 Ok,
17 Warning(String),
19 Error(String),
21}
22
23#[derive(Debug)]
25pub enum PostflightResult {
26 Valid,
28 Missing(String),
30 Malformed(String),
32}
33
34pub fn pre_flight_review_check(
42 workspace: &dyn Workspace,
43 logger: &crate::logger::Logger,
44 cycle: u32,
45 reviewer_agent: &str,
46 reviewer_model: Option<&str>,
47) -> PreflightResult {
48 let agent_dir = Path::new(".agent");
49 let issues_path = Path::new(".agent/ISSUES.md");
50
51 let is_problematic_reviewer = is_problematic_prompt_target(reviewer_agent, reviewer_model);
53
54 if is_problematic_reviewer {
55 logger.warn(&format!(
56 "Note: Reviewer may have compatibility issues with review tasks. (agent='{}', model={})",
57 reviewer_agent,
58 reviewer_model.unwrap_or("none")
59 ));
60 logger.info("If review fails, consider these workarounds:");
61 logger.info(" 1. Use Claude/Codex as reviewer: ralph --reviewer-agent codex");
62 logger.info(" 2. Try generic parser: ralph --reviewer-json-parser generic");
63 logger.info(" 3. Skip review: RALPH_REVIEWER_REVIEWS=0 ralph");
64 }
66
67 if is_glm_like_agent(reviewer_agent) {
69 logger.info(&format!(
71 "GLM agent detected: '{reviewer_agent}'. Command will include '-p' flag for non-interactive mode."
72 ));
73 logger.info("Tip: Use --verbosity debug to see the full command being executed");
74 }
75
76 if workspace.exists(issues_path) {
78 match workspace.read(issues_path) {
80 Ok(content) if !content.is_empty() => {
81 logger.warn(&format!(
82 "ISSUES.md already exists from a previous run (size: {} bytes).",
83 content.len()
84 ));
85 logger
86 .info("The review agent will overwrite this file. If the previous run failed,");
87 logger.info("consider checking the old ISSUES.md for clues about what went wrong.");
88 }
89 Ok(_) => {
90 logger.warn("Found empty ISSUES.md from previous run. Will be overwritten.");
92 }
93 Err(e) => {
94 logger.warn(&format!("Cannot read ISSUES.md: {e}"));
95 }
96 }
97 }
98
99 if !workspace.is_dir(agent_dir) {
101 if let Err(e) = workspace.create_dir_all(agent_dir) {
103 return PreflightResult::Error(format!(
104 "Cannot create .agent directory: {e}. Check directory permissions."
105 ));
106 }
107 }
108
109 let test_file = agent_dir.join(format!(".write_test_{cycle}"));
111 match workspace.write(&test_file, "test") {
112 Ok(()) => {
113 let _ = workspace.remove(&test_file);
114 }
115 Err(e) => {
116 return PreflightResult::Error(format!(
117 ".agent directory is not writable: {e}. Check file permissions."
118 ));
119 }
120 }
121
122 if let Ok(entries) = workspace.read_dir(agent_dir) {
125 let entry_count = entries.len();
126 if entry_count > 1000 {
127 logger.warn(&format!(
128 ".agent directory has {entry_count} files. Consider cleaning up old logs."
129 ));
130 return PreflightResult::Warning(
131 "Large .agent directory detected. Review may be slow.".to_string(),
132 );
133 }
134 }
135
136 PreflightResult::Ok
137}
138
139pub fn post_flight_review_check(
146 workspace: &dyn Workspace,
147 logger: &crate::logger::Logger,
148 cycle: u32,
149) -> PostflightResult {
150 let issues_path = Path::new(".agent/ISSUES.md");
151
152 if !workspace.exists(issues_path) {
154 logger.warn(&format!(
155 "Review cycle {cycle} completed but ISSUES.md was not created. \
156 The agent may have failed or used a different output format."
157 ));
158 logger.info("Possible causes:");
159 logger.info(" - Agent failed to write the file (permission/execution error)");
160 logger.info(" - Agent used a different output filename or format");
161 logger.info(" - Agent was interrupted during execution");
162 return PostflightResult::Missing(
163 "ISSUES.md not found after review. Agent may have failed.".to_string(),
164 );
165 }
166
167 let file_size = match workspace.read(issues_path) {
169 Ok(content) if content.is_empty() => {
170 logger.warn(&format!("Review cycle {cycle} created an empty ISSUES.md."));
171 logger.info("Possible causes:");
172 logger.info(" - Agent reviewed but found no issues (should write 'No issues found.')");
173 logger.info(" - Agent failed during file write");
174 logger.info(" - Agent doesn't understand the expected output format");
175 return PostflightResult::Missing("ISSUES.md is empty".to_string());
176 }
177 Ok(content) => {
178 let size = content.len() as u64;
180 logger.info(&format!("ISSUES.md created ({} bytes)", size));
181 size
182 }
183 Err(e) => {
184 logger.warn(&format!("Cannot read ISSUES.md: {e}"));
185 return PostflightResult::Missing(format!("Cannot read ISSUES.md: {e}"));
186 }
187 };
188
189 match ReviewMetrics::from_issues_file_with_workspace(workspace) {
191 Ok(metrics) => {
192 if metrics.total_issues == 0 && !metrics.no_issues_declared {
194 logger.warn(&format!(
196 "Review cycle {cycle} produced ISSUES.md ({file_size} bytes) but no parseable issues detected."
197 ));
198 logger.info("Content may be in unexpected format. The fix pass may still work.");
199 logger.info(
200 "Consider checking .agent/ISSUES.md manually to see what the agent wrote.",
201 );
202 return PostflightResult::Malformed(
203 "ISSUES.md exists but no issues detected. Check format.".to_string(),
204 );
205 }
206
207 if metrics.total_issues > 0 {
209 logger.info(&format!(
210 "Review found {} issues ({} critical, {} high, {} medium, {} low)",
211 metrics.total_issues,
212 metrics.critical_issues,
213 metrics.high_issues,
214 metrics.medium_issues,
215 metrics.low_issues
216 ));
217 } else if metrics.no_issues_declared {
218 logger.info("Review declared no issues found.");
219 }
220
221 PostflightResult::Valid
222 }
223 Err(e) => {
224 logger.warn(&format!("Failed to parse ISSUES.md: {e}"));
226 logger.info(&format!(
227 "ISSUES.md has {file_size} bytes but failed to parse."
228 ));
229 logger.info("The file may be malformed or in an unexpected format.");
230 logger.info(
231 "Attempting partial recovery: fix pass will proceed but may have limited success.",
232 );
233
234 if let Ok(content) = workspace.read(issues_path) {
236 let preview: String = content.lines().take(5).collect::<Vec<_>>().join("\n");
237 if !preview.is_empty() {
238 logger.info("ISSUES.md preview (first 5 lines):");
239 for line in preview.lines() {
240 logger.info(&format!(" {line}"));
241 }
242 }
243 }
244
245 PostflightResult::Malformed(format!("Failed to parse ISSUES.md: {e}"))
246 }
247 }
248}
249
250fn is_problematic_prompt_target(agent: &str, model_flag: Option<&str>) -> bool {
255 contains_glm_model(agent) || model_flag.is_some_and(contains_glm_model)
256}