use super::super::types::{ParseResult, ReviewPassResult};
use super::super::xml_processing::extract_and_validate_review_output_xml;
use super::helpers::{handle_postflight_validation, stderr_contains_auth_error};
use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
use crate::files::delete_issues_file_for_isolation_with_workspace;
use crate::files::llm_output_extraction::file_based_extraction::paths as xml_paths;
use crate::phases::context::PhaseContext;
use crate::phases::timing::{capture_time, elapsed_seconds};
use crate::pipeline::{run_with_prompt, PipelineRuntime, PromptCommand};
use crate::prompts::{prompt_review_xml_with_references_and_log, PromptContentBuilder};
use anyhow::Context as _;
use std::path::Path;
pub fn run_review_pass(
ctx: &mut PhaseContext<'_>,
j: u32,
review_label: &str,
_review_prompt: &str, agent: Option<&str>,
) -> anyhow::Result<ReviewPassResult> {
let active_agent = agent.unwrap_or(ctx.reviewer_agent);
let issues_path = Path::new(".agent/ISSUES.md");
let plan_content = ctx
.workspace
.read(Path::new(".agent/PLAN.md"))
.unwrap_or_default();
let (changes_content, baseline_oid_for_prompts) =
match crate::git_helpers::get_git_diff_for_review_with_workspace(ctx.workspace) {
Ok((diff, baseline_oid)) => (diff, baseline_oid),
Err(e) => {
ctx.logger
.warn(&format!("Failed to get baseline diff for review: {e}"));
(String::new(), String::new())
}
};
let refs = PromptContentBuilder::new(ctx.workspace)
.with_plan(plan_content)
.with_diff(changes_content, &baseline_oid_for_prompts)
.build();
let rendered = prompt_review_xml_with_references_and_log(
ctx.template_context,
&refs,
ctx.workspace,
"review_xml",
);
let review_prompt_xml = rendered.content;
if !rendered.log.is_complete() {
return Err(anyhow::anyhow!(
"Review prompt has unresolved placeholders: {:?}",
rendered.log.unsubstituted
));
}
if ctx.config.verbosity.is_debug() {
ctx.logger.info(&format!(
"Review prompt length: {} characters",
review_prompt_xml.len()
));
}
let base_log_path = ctx.run_log_context.agent_log("reviewer", j, None);
let attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
&base_log_path,
ctx.workspace,
);
let logfile = if attempt == 0 {
base_log_path
.to_str()
.expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
.to_string()
} else {
ctx.run_log_context
.agent_log("reviewer", j, Some(attempt))
.to_str()
.expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
.to_string()
};
let log_header = format!(
"# Ralph Agent Invocation Log\n\
# Role: Reviewer\n\
# Agent: {}\n\
# Model Index: 0\n\
# Attempt: {}\n\
# Phase: Review\n\
# Timestamp: {}\n\n",
active_agent,
attempt,
chrono::Utc::now().to_rfc3339()
);
ctx.workspace
.append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
.context("Failed to write agent log header - log would be incomplete without metadata")?;
let log_prefix = format!("reviewer_{j}"); let model_index = 0usize;
let agent_config = ctx
.registry
.resolve_config(active_agent)
.ok_or_else(|| anyhow::anyhow!("Agent not found: {active_agent}"))?;
let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
let prompt_cmd = PromptCommand {
label: review_label,
display_name: active_agent,
cmd_str: &cmd_str,
prompt: &review_prompt_xml,
log_prefix: &log_prefix,
model_index: Some(model_index),
attempt: Some(attempt),
logfile: &logfile,
parser_type: agent_config.json_parser,
env_vars: &agent_config.env_vars,
completion_output_path: Some(Path::new(xml_paths::ISSUES_XML)),
};
let attempt_start = capture_time();
let result = run_with_prompt(
&prompt_cmd,
&mut PipelineRuntime {
timer: ctx.timer,
logger: ctx.logger,
colors: ctx.colors,
config: ctx.config,
executor: ctx.executor,
executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
workspace: ctx.workspace,
workspace_arc: std::sync::Arc::clone(&ctx.workspace_arc),
},
)?;
if result.exit_code != 0 {
let auth_failure = stderr_contains_auth_error(&result.stderr);
if auth_failure {
return Ok(ReviewPassResult::agent_failed(true));
}
if !crate::files::llm_output_extraction::has_valid_xml_output(
ctx.workspace,
Path::new(xml_paths::ISSUES_XML),
) {
return Ok(ReviewPassResult::agent_failed(false));
}
}
let parse_result = extract_and_validate_review_output_xml(ctx, &log_prefix, issues_path)?;
match parse_result {
ParseResult::IssuesFound {
issues,
xml_content,
} => {
handle_postflight_validation(ctx, j);
ctx.logger
.success(&format!("Issues extracted: {} total", issues.len()));
let step = ExecutionStep::new(
"Review",
j,
"review",
StepOutcome::success(
Some(format!("{} issues found", issues.len())),
vec![".agent/ISSUES.md".to_string()],
),
)
.with_agent(active_agent)
.with_duration(elapsed_seconds(attempt_start));
let _ = ctx
.execution_history
.add_step_bounded(step, ctx.config.execution_history_limit);
Ok(ReviewPassResult::issues_found(xml_content))
}
ParseResult::NoIssuesExplicit { xml_content } => {
ctx.logger
.success(&format!("No issues found after cycle {j} - stopping early"));
if ctx.config.isolation_mode {
delete_issues_file_for_isolation_with_workspace(ctx.workspace, ctx.logger)?;
}
let step = ExecutionStep::new(
"Review",
j,
"review",
StepOutcome::success(Some("No issues found".to_string()), vec![]),
)
.with_agent(active_agent)
.with_duration(elapsed_seconds(attempt_start));
let _ = ctx
.execution_history
.add_step_bounded(step, ctx.config.execution_history_limit);
Ok(ReviewPassResult::no_issues(xml_content))
}
ParseResult::ParseFailed(reason) => {
ctx.logger
.warn(&format!("Review output validation failed: {reason}"));
Ok(ReviewPassResult::output_invalid())
}
}
}