use crate::banner::{print_final_summary, PipelineSummary};
use crate::checkpoint::clear_checkpoint_with_workspace;
use crate::config::Config;
use crate::files::protection::monitoring::PromptMonitor;
use crate::logger::Colors;
use crate::logger::Logger;
use crate::pipeline::AgentPhaseGuard;
use crate::pipeline::Timer;
use crate::reducer::state::PipelineState;
use crate::workspace::Workspace;
#[derive(Copy, Clone)]
pub struct FinalizeContext<'a> {
pub logger: &'a Logger,
pub colors: Colors,
pub config: &'a Config,
pub timer: &'a Timer,
pub workspace: &'a dyn Workspace,
}
#[must_use]
pub const fn build_pipeline_summary(
total_time: String,
config: &Config,
final_state: &PipelineState,
) -> PipelineSummary {
let connectivity_interruptions = if final_state.metrics.connectivity_interruptions_total > 0 {
Some(final_state.metrics.connectivity_interruptions_total as usize)
} else {
None
};
PipelineSummary {
total_time,
dev_runs_completed: final_state.metrics.dev_iterations_completed as usize,
dev_runs_total: final_state.metrics.max_dev_iterations as usize,
review_passes_completed: final_state.metrics.review_passes_completed as usize,
review_passes_total: final_state.metrics.max_review_passes as usize,
review_runs: final_state.metrics.review_runs_total as usize,
changes_detected: final_state.metrics.commits_created_total as usize,
isolation_mode: config.isolation_mode,
verbose: config.verbosity.is_verbose(),
review_summary: None,
connectivity_interruptions,
}
}
pub fn finalize_pipeline(
agent_phase_guard: &mut AgentPhaseGuard<'_>,
ctx: FinalizeContext<'_>,
final_state: &PipelineState,
prompt_monitor: Option<PromptMonitor>,
) {
if let Some(monitor) = prompt_monitor {
monitor.stop().iter().for_each(|warning| {
ctx.logger.warn(warning);
});
}
let repo_root = ctx.workspace.root();
crate::git_helpers::end_agent_phase_in_repo(repo_root);
crate::git_helpers::disable_git_wrapper(agent_phase_guard.git_helpers);
let uninstall_result = crate::git_helpers::uninstall_hooks_in_repo(repo_root, ctx.logger);
let hook_uninstall_ok = match uninstall_result {
Ok(_) => true,
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {
ctx.logger.warn(&format!(
"Skipping hook uninstall (repo not present on filesystem): {err}"
));
true
} else {
ctx.logger
.warn(&format!("Failed to uninstall Ralph hooks: {err}"));
false
}
}
};
let wrapper_remaining = crate::git_helpers::verify_wrapper_cleaned(repo_root);
let wrapper_ok = if wrapper_remaining.is_empty() {
true
} else {
ctx.logger.warn(&format!(
"Wrapper artifacts still present after cleanup: {}",
wrapper_remaining.join(", ")
));
false
};
let hooks_result = crate::git_helpers::verify_hooks_removed(repo_root);
let hooks_ok = match hooks_result {
Ok(remaining) => {
if remaining.is_empty() {
true
} else {
ctx.logger.warn(&format!(
"Ralph hooks still present after cleanup: {}",
remaining.join(", ")
));
false
}
}
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {
ctx.logger.warn(&format!(
"Skipping hook cleanup verification (repo not present on filesystem): {err}"
));
true
} else {
ctx.logger
.warn(&format!("Failed to verify hook cleanup: {err}"));
false
}
}
};
let cleanup_ok_initial = hook_uninstall_ok && wrapper_ok && hooks_ok;
let summary = build_pipeline_summary(ctx.timer.elapsed_formatted(), ctx.config, final_state);
print_final_summary(ctx.colors, &summary, ctx.logger);
if ctx.config.features.checkpoint_enabled {
if let Err(err) = clear_checkpoint_with_workspace(ctx.workspace) {
ctx.logger
.warn(&format!("Failed to clear checkpoint: {err}"));
}
}
crate::files::cleanup_generated_files_with_workspace(ctx.workspace);
let cleanup_ok = if !crate::git_helpers::try_remove_ralph_dir(repo_root) {
let remaining = crate::git_helpers::verify_ralph_dir_removed(repo_root);
ctx.logger.warn(&format!(
"Ralph git dir still present after cleanup: {}",
remaining.join(", ")
));
false
} else {
cleanup_ok_initial
};
if cleanup_ok {
crate::git_helpers::clear_agent_phase_global_state();
agent_phase_guard.disarm();
} else {
ctx.logger.warn(
"Agent phase cleanup incomplete; leaving AgentPhaseGuard armed for Drop best-effort",
);
}
}
#[cfg(test)]
mod tests {
use crate::reducer::state::{ContinuationState, PipelineState, RunMetrics};
#[test]
fn test_summary_derives_from_reducer_metrics() {
let state = PipelineState {
metrics: RunMetrics {
dev_iterations_completed: 3,
review_runs_total: 4,
commits_created_total: 3,
..RunMetrics::new(5, 2, &ContinuationState::new())
},
..PipelineState::initial(5, 2)
};
let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
let dev_runs_total = state.metrics.max_dev_iterations as usize;
let review_runs = state.metrics.review_runs_total as usize;
let changes_detected = state.metrics.commits_created_total as usize;
assert_eq!(dev_runs_completed, 3);
assert_eq!(dev_runs_total, 5);
assert_eq!(review_runs, 4);
assert_eq!(changes_detected, 3);
}
#[test]
fn test_metrics_reflect_actual_progress_not_config() {
let state = PipelineState {
metrics: RunMetrics {
dev_iterations_completed: 2,
review_runs_total: 0,
..RunMetrics::new(10, 5, &ContinuationState::new())
},
..PipelineState::initial(10, 5)
};
assert_eq!(state.metrics.dev_iterations_completed, 2);
assert_eq!(state.metrics.max_dev_iterations, 10);
}
#[test]
fn test_summary_no_drift_from_runtime_counters() {
let state = PipelineState {
metrics: RunMetrics {
dev_iterations_completed: 7,
review_runs_total: 3,
commits_created_total: 8,
..RunMetrics::new(10, 5, &ContinuationState::new())
},
..PipelineState::initial(10, 5)
};
let runtime_dev_completed = 5; let runtime_review_runs = 2;
let dev_runs = state.metrics.dev_iterations_completed as usize;
let review_runs = state.metrics.review_runs_total as usize;
let commits = state.metrics.commits_created_total as usize;
assert_eq!(dev_runs, 7); assert_eq!(review_runs, 3); assert_eq!(commits, 8);
assert_ne!(dev_runs, runtime_dev_completed);
assert_ne!(review_runs, runtime_review_runs);
}
#[test]
fn test_summary_uses_all_reducer_metrics() {
let state = PipelineState {
metrics: RunMetrics {
dev_iterations_started: 5,
dev_iterations_completed: 5,
dev_attempts_total: 7,
analysis_attempts_total: 5,
review_passes_started: 3,
review_passes_completed: 3,
review_runs_total: 3,
fix_runs_total: 2,
commits_created_total: 6,
xsd_retry_attempts_total: 2,
same_agent_retry_attempts_total: 1,
..RunMetrics::new(5, 3, &ContinuationState::new())
},
..PipelineState::initial(5, 3)
};
let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
let dev_runs_total = state.metrics.max_dev_iterations as usize;
let review_passes_completed = state.metrics.review_passes_completed as usize;
let review_passes_total = state.metrics.max_review_passes as usize;
let review_runs_total = state.metrics.review_runs_total as usize;
let changes_detected = state.metrics.commits_created_total as usize;
assert_eq!(dev_runs_completed, 5);
assert_eq!(dev_runs_total, 5);
assert_eq!(review_passes_completed, 3);
assert_eq!(review_passes_total, 3);
assert_eq!(review_runs_total, 3);
assert_eq!(changes_detected, 6);
}
#[test]
fn test_partial_run_shows_actual_not_configured() {
let state = PipelineState {
metrics: RunMetrics {
dev_iterations_completed: 3,
review_passes_completed: 1,
commits_created_total: 3,
..RunMetrics::new(10, 5, &ContinuationState::new())
},
..PipelineState::initial(10, 5)
};
assert_eq!(state.metrics.dev_iterations_completed, 3);
assert_eq!(state.metrics.max_dev_iterations, 10);
assert_eq!(state.metrics.review_passes_completed, 1);
assert_eq!(state.metrics.max_review_passes, 5);
}
#[test]
fn test_generated_files_includes_all_artifacts() {
use crate::files::agent_files::GENERATED_FILES;
assert!(
GENERATED_FILES.contains(&".agent/PLAN.md"),
"GENERATED_FILES must include .agent/PLAN.md"
);
assert!(
GENERATED_FILES.contains(&".agent/commit-message.txt"),
"GENERATED_FILES must include .agent/commit-message.txt"
);
assert!(
GENERATED_FILES.contains(&".agent/checkpoint.json.tmp"),
"GENERATED_FILES must include .agent/checkpoint.json.tmp"
);
assert!(
GENERATED_FILES.contains(&".git/ralph/no_agent_commit"),
"GENERATED_FILES must include .git/ralph/no_agent_commit"
);
}
}