1const SHORT_OID_LENGTH: usize = 8;
8
9use crate::agents::AgentRegistry;
10use crate::checkpoint::file_state::{FileSystemState, ValidationError};
11use crate::checkpoint::{
12 checkpoint_exists, load_checkpoint, validate_checkpoint, PipelineCheckpoint, PipelinePhase,
13};
14use crate::config::Config;
15use crate::git_helpers::rebase_in_progress;
16use crate::logger::Logger;
17use std::fs;
18use std::io::{self, IsTerminal};
19
20pub struct ResumeResult {
22 pub checkpoint: PipelineCheckpoint,
24}
25
26pub fn offer_resume_if_checkpoint_exists(
47 args: &crate::cli::Args,
48 config: &Config,
49 registry: &AgentRegistry,
50 logger: &Logger,
51 developer_agent: &str,
52 reviewer_agent: &str,
53) -> Option<ResumeResult> {
54 if args.recovery.resume {
56 return None;
57 }
58
59 if args.recovery.no_resume {
61 return None;
62 }
63
64 if std::env::var("RALPH_NO_RESUME_PROMPT").is_ok() {
66 return None;
67 }
68
69 if !can_prompt_user() {
71 return None;
72 }
73
74 if !checkpoint_exists() {
76 return None;
77 }
78
79 let checkpoint = match load_checkpoint() {
81 Ok(Some(cp)) => cp,
82 Ok(None) => return None,
83 Err(e) => {
84 logger.warn(&format!("Checkpoint exists but failed to load: {e}"));
85 return None;
86 }
87 };
88
89 logger.header("FOUND PREVIOUS RUN", crate::logger::Colors::cyan);
91 display_user_friendly_checkpoint_summary(&checkpoint, logger);
92
93 if !prompt_user_to_resume(logger) {
95 logger.info("Deleting checkpoint and starting fresh...");
97 let _ = crate::checkpoint::clear_checkpoint();
98 return None;
99 }
100
101 logger.header("RESUME: Loading Checkpoint", crate::logger::Colors::yellow);
103
104 let validation = validate_checkpoint(&checkpoint, config, registry);
105
106 for warning in &validation.warnings {
107 logger.warn(warning);
108 }
109 for error in &validation.errors {
110 logger.error(error);
111 }
112
113 if !validation.is_valid {
114 logger.error("Checkpoint validation failed. Cannot resume.");
115 logger.info("Delete .agent/checkpoint.json and start fresh, or fix the issues above.");
116 return None;
117 }
118
119 if checkpoint.developer_agent != developer_agent {
120 logger.warn(&format!(
121 "Developer agent changed: {} -> {}",
122 checkpoint.developer_agent, developer_agent
123 ));
124 }
125 if checkpoint.reviewer_agent != reviewer_agent {
126 logger.warn(&format!(
127 "Reviewer agent changed: {} -> {}",
128 checkpoint.reviewer_agent, reviewer_agent
129 ));
130 }
131
132 check_rebase_state_on_resume(&checkpoint, logger);
133
134 let validation_outcome = if let Some(ref file_system_state) = checkpoint.file_system_state {
135 validate_file_system_state(
136 file_system_state,
137 logger,
138 args.recovery.recovery_strategy.into(),
139 )
140 } else {
141 ValidationOutcome::Passed
142 };
143
144 if matches!(validation_outcome, ValidationOutcome::Failed(_)) {
145 return None;
146 }
147
148 Some(ResumeResult { checkpoint })
149}
150
151fn can_prompt_user() -> bool {
153 io::stdin().is_terminal() && (io::stdout().is_terminal() || io::stderr().is_terminal())
154}
155
156fn display_user_friendly_checkpoint_summary(checkpoint: &PipelineCheckpoint, logger: &Logger) {
158 use chrono::{DateTime, Local, NaiveDateTime};
159
160 let phase_emoji = get_phase_emoji(checkpoint.phase);
162 logger.info(&format!("{} {}", phase_emoji, checkpoint.description()));
163
164 let checkpoint_time =
167 match NaiveDateTime::parse_from_str(&checkpoint.timestamp, "%Y-%m-%d %H:%M:%S") {
168 Ok(dt) => {
169 DateTime::<Local>::from_naive_utc_and_offset(dt, Local::now().offset().to_owned())
170 }
171 Err(_) => {
172 logger.info(&format!(
174 "Session was interrupted at: {}",
175 checkpoint.timestamp
176 ));
177 return;
178 }
179 };
180 let now = Local::now();
181 let duration = now.signed_duration_since(checkpoint_time);
182
183 let time_str = if duration.num_days() > 0 {
184 format!("{} day(s) ago", duration.num_days())
185 } else if duration.num_hours() > 0 {
186 format!("{} hour(s) ago", duration.num_hours())
187 } else if duration.num_minutes() > 0 {
188 format!("{} minute(s) ago", duration.num_minutes())
189 } else {
190 "just now".to_string()
191 };
192
193 logger.info(&format!("Session was interrupted: {}", time_str));
194
195 if matches!(
197 checkpoint.rebase_state,
198 crate::checkpoint::RebaseState::HasConflicts { .. }
199 ) {
200 if let crate::checkpoint::RebaseState::HasConflicts { files } = &checkpoint.rebase_state {
201 logger.warn(&format!(
202 "Rebase conflicts detected in {} file(s)",
203 files.len()
204 ));
205 let display_files: Vec<_> = files.iter().take(5).cloned().collect();
207 for file in display_files {
208 logger.info(&format!(" - {}", file));
209 }
210 if files.len() > 5 {
211 logger.info(&format!(" ... and {} more", files.len() - 5));
212 }
213 }
214 }
215
216 if checkpoint.total_iterations > 0 {
218 let progress_bar = create_progress_bar(
219 checkpoint.actual_developer_runs,
220 checkpoint.total_iterations,
221 );
222 logger.info(&format!(
223 "Development: {} {}/{} completed",
224 progress_bar, checkpoint.actual_developer_runs, checkpoint.total_iterations
225 ));
226 }
227 if checkpoint.total_reviewer_passes > 0 {
228 let progress_bar = create_progress_bar(
229 checkpoint.actual_reviewer_runs,
230 checkpoint.total_reviewer_passes,
231 );
232 logger.info(&format!(
233 "Review: {} {}/{} completed",
234 progress_bar, checkpoint.actual_reviewer_runs, checkpoint.total_reviewer_passes
235 ));
236 }
237
238 if checkpoint.resume_count > 0 {
240 logger.info(&format!(
241 "This session has been resumed {} time(s) before",
242 checkpoint.resume_count
243 ));
244 }
245
246 if let Some(reconstructed_command) = reconstruct_command(checkpoint) {
248 logger.info(&format!("Original command: {}", reconstructed_command));
249 }
250
251 logger.info(&format!("Developer agent: {}", checkpoint.developer_agent));
253 logger.info(&format!("Reviewer agent: {}", checkpoint.reviewer_agent));
254
255 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
257 logger.info(&format!("Developer model: {}", model));
258 }
259 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
260 logger.info(&format!("Reviewer model: {}", model));
261 }
262
263 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
265 logger.info(&format!("Developer provider: {}", provider));
266 }
267 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
268 logger.info(&format!("Reviewer provider: {}", provider));
269 }
270
271 if let Some(ref history) = checkpoint.execution_history {
273 if !history.steps.is_empty() {
274 logger.info(&format!(
275 "Execution history: {} step(s) recorded",
276 history.steps.len()
277 ));
278
279 let recent_steps: Vec<_> = history
281 .steps
282 .iter()
283 .rev()
284 .take(5)
285 .collect::<Vec<_>>()
286 .into_iter()
287 .rev()
288 .collect();
289
290 logger.info("");
291 logger.info("Recent Activity:");
292
293 for step in &recent_steps {
294 let outcome_emoji = match step.outcome {
295 crate::checkpoint::execution_history::StepOutcome::Success { .. } => "✓",
296 crate::checkpoint::execution_history::StepOutcome::Failure { .. } => "✗",
297 crate::checkpoint::execution_history::StepOutcome::Partial { .. } => "◐",
298 crate::checkpoint::execution_history::StepOutcome::Skipped { .. } => "○",
299 };
300
301 logger.info(&format!(
302 " {} {} ({})",
303 outcome_emoji, step.step_type, step.phase
304 ));
305
306 if let Some(ref detail) = step.modified_files_detail {
308 let total_files =
309 detail.added.len() + detail.modified.len() + detail.deleted.len();
310 if total_files > 0 {
311 let mut file_summary = String::from(" Files: ");
312 let mut parts = Vec::new();
313 if !detail.added.is_empty() {
314 parts.push(format!("{} added", detail.added.len()));
315 }
316 if !detail.modified.is_empty() {
317 parts.push(format!("{} modified", detail.modified.len()));
318 }
319 if !detail.deleted.is_empty() {
320 parts.push(format!("{} deleted", detail.deleted.len()));
321 }
322 file_summary.push_str(&parts.join(", "));
323 logger.info(&file_summary);
324 }
325 }
326
327 if let Some(ref issues) = step.issues_summary {
329 if issues.found > 0 || issues.fixed > 0 {
330 logger.info(&format!(
331 " Issues: {} found, {} fixed",
332 issues.found, issues.fixed
333 ));
334 }
335 }
336
337 if let Some(ref oid) = step.git_commit_oid {
339 let short_oid = if oid.len() > SHORT_OID_LENGTH {
340 &oid[..SHORT_OID_LENGTH]
341 } else {
342 oid
343 };
344 logger.info(&format!(" Commit: {}", short_oid));
345 }
346 }
347 }
348 }
349
350 if let Some(next_step) = suggest_next_step(checkpoint) {
352 logger.info("");
353 logger.info(&format!("Next: {}", next_step));
354 }
355
356 logger.info("");
358 logger.info("To inspect the current state, you can run:");
359 logger.info(" git status - See current changes");
360 logger.info(" git log --oneline -5 - See recent commits");
361}
362
363fn reconstruct_command(checkpoint: &PipelineCheckpoint) -> Option<String> {
368 let cli = &checkpoint.cli_args;
369 let mut parts = vec!["ralph".to_string()];
370
371 if cli.developer_iters > 0 {
373 parts.push(format!("-D {}", cli.developer_iters));
374 }
375
376 if cli.reviewer_reviews > 0 {
378 parts.push(format!("-R {}", cli.reviewer_reviews));
379 }
380
381 if let Some(ref depth) = cli.review_depth {
383 parts.push(format!("--review-depth {}", depth));
384 }
385
386 if cli.skip_rebase {
388 parts.push("--skip-rebase".to_string());
389 }
390
391 if !cli.isolation_mode {
393 parts.push("--no-isolation".to_string());
394 }
395
396 match cli.verbosity {
398 0 => parts.push("--quiet".to_string()),
399 1 => {} 2 => parts.push("--verbose".to_string()),
401 3 => parts.push("--full".to_string()),
402 4 => parts.push("--debug".to_string()),
403 _ => {}
404 }
405
406 if cli.show_streaming_metrics {
408 parts.push("--show-streaming-metrics".to_string());
409 }
410
411 if let Some(ref parser) = cli.reviewer_json_parser {
413 parts.push(format!("--reviewer-json-parser {}", parser));
414 }
415
416 parts.push(format!("--agent {}", checkpoint.developer_agent));
419 parts.push(format!("--reviewer-agent {}", checkpoint.reviewer_agent));
420
421 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
423 parts.push(format!("--model \"{}\"", model));
424 }
425 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
426 parts.push(format!("--reviewer-model \"{}\"", model));
427 }
428
429 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
431 parts.push(format!("--provider \"{}\"", provider));
432 }
433 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
434 parts.push(format!("--reviewer-provider \"{}\"", provider));
435 }
436
437 if parts.len() > 1 {
438 Some(parts.join(" "))
439 } else {
440 None
441 }
442}
443
444fn suggest_next_step(checkpoint: &PipelineCheckpoint) -> Option<String> {
449 match checkpoint.phase {
450 PipelinePhase::Planning => {
451 Some("continue creating implementation plan from PROMPT.md".to_string())
452 }
453 PipelinePhase::PreRebase => Some("complete rebase before starting development".to_string()),
454 PipelinePhase::PreRebaseConflict => {
455 Some("resolve rebase conflicts then continue to development".to_string())
456 }
457 PipelinePhase::Development => {
458 if checkpoint.iteration < checkpoint.total_iterations {
459 Some(format!(
460 "continue development iteration {} of {} (will use same prompts as before)",
461 checkpoint.iteration + 1,
462 checkpoint.total_iterations
463 ))
464 } else {
465 Some("move to review phase".to_string())
466 }
467 }
468 PipelinePhase::Review => {
469 if checkpoint.reviewer_pass < checkpoint.total_reviewer_passes {
470 Some(format!(
471 "continue review pass {} of {} (will review recent changes)",
472 checkpoint.reviewer_pass + 1,
473 checkpoint.total_reviewer_passes
474 ))
475 } else {
476 Some("complete review cycle".to_string())
477 }
478 }
479 PipelinePhase::Fix => Some("address issues from code review".to_string()),
480 PipelinePhase::ReviewAgain => Some("complete verification review".to_string()),
481 PipelinePhase::PostRebase => Some("complete post-development rebase".to_string()),
482 PipelinePhase::PostRebaseConflict => Some("resolve post-rebase conflicts".to_string()),
483 PipelinePhase::CommitMessage => Some("finalize commit message".to_string()),
484 PipelinePhase::FinalValidation => Some("complete final validation".to_string()),
485 PipelinePhase::Complete => Some("pipeline complete!".to_string()),
486 PipelinePhase::Rebase => Some("complete rebase operation".to_string()),
487 PipelinePhase::Interrupted => {
488 let mut context = vec!["resume from interrupted state".to_string()];
492
493 if checkpoint.iteration > 0 {
495 context.push(format!(
496 "(development iteration {}/{})",
497 checkpoint.iteration, checkpoint.total_iterations
498 ));
499 }
500 if checkpoint.reviewer_pass > 0 {
501 context.push(format!(
502 "(review pass {}/{})",
503 checkpoint.reviewer_pass, checkpoint.total_reviewer_passes
504 ));
505 }
506
507 context.push("full pipeline will run from interrupted point".to_string());
509
510 Some(context.join(" - "))
511 }
512 }
513}
514
515fn prompt_user_to_resume(logger: &Logger) -> bool {
519 use std::io::Write;
520
521 println!();
522 logger.info("Would you like to resume from where you left off?");
523
524 let prompt = "Resume? [y/N] ";
525 let colors = crate::logger::Colors::new();
526
527 let mut input = String::new();
528 print!("{}", colors.yellow());
530 let _ = io::stdout().write_all(prompt.as_bytes());
531 let _ = io::stdout().flush();
532 print!("{}", colors.reset());
533
534 match io::stdin().read_line(&mut input) {
535 Ok(0) => {
536 println!();
538 false
539 }
540 Ok(_) => {
541 let response = input.trim().to_lowercase();
542 println!();
543
544 matches!(response.as_str(), "y" | "yes" | "Y" | "YES")
545 }
546 Err(_) => false,
547 }
548}
549
550#[derive(Debug, Clone, PartialEq, Eq)]
552pub enum ValidationOutcome {
553 Passed,
555 Failed(String),
557}
558
559pub fn handle_resume_with_validation(
578 args: &crate::cli::Args,
579 config: &Config,
580 registry: &AgentRegistry,
581 logger: &Logger,
582 developer_agent: &str,
583 reviewer_agent: &str,
584) -> Option<ResumeResult> {
585 if args.recovery.inspect_checkpoint {
587 match load_checkpoint() {
588 Ok(Some(checkpoint)) => {
589 logger.header("CHECKPOINT INSPECTION", crate::logger::Colors::cyan);
590 display_detailed_checkpoint_info(&checkpoint, logger);
591 std::process::exit(0);
592 }
593 Ok(None) => {
594 logger.error("No checkpoint found to inspect.");
595 std::process::exit(1);
596 }
597 Err(e) => {
598 logger.error(&format!("Failed to load checkpoint: {}", e));
599 std::process::exit(1);
600 }
601 }
602 }
603
604 if !args.recovery.resume {
605 return None;
606 }
607
608 match load_checkpoint() {
609 Ok(Some(checkpoint)) => {
610 logger.header("RESUME: Loading Checkpoint", crate::logger::Colors::yellow);
611 display_checkpoint_summary(&checkpoint, logger);
612
613 let validation = validate_checkpoint(&checkpoint, config, registry);
615
616 for warning in &validation.warnings {
618 logger.warn(warning);
619 }
620 for error in &validation.errors {
621 logger.error(error);
622 }
623
624 if !validation.is_valid {
625 logger.error("Checkpoint validation failed. Cannot resume.");
626 logger.info(
627 "Delete .agent/checkpoint.json and start fresh, or fix the issues above.",
628 );
629 return None;
630 }
631
632 if checkpoint.developer_agent != developer_agent {
634 logger.warn(&format!(
635 "Developer agent changed: {} -> {}",
636 checkpoint.developer_agent, developer_agent
637 ));
638 }
639 if checkpoint.reviewer_agent != reviewer_agent {
640 logger.warn(&format!(
641 "Reviewer agent changed: {} -> {}",
642 checkpoint.reviewer_agent, reviewer_agent
643 ));
644 }
645
646 check_rebase_state_on_resume(&checkpoint, logger);
648
649 let validation_outcome = if let Some(file_system_state) = &checkpoint.file_system_state
651 {
652 validate_file_system_state(
653 file_system_state,
654 logger,
655 args.recovery.recovery_strategy.into(),
656 )
657 } else {
658 ValidationOutcome::Passed
659 };
660
661 if matches!(validation_outcome, ValidationOutcome::Failed(_)) {
662 return None;
663 }
664
665 Some(ResumeResult { checkpoint })
666 }
667 Ok(None) => {
668 logger.warn("No checkpoint found. Starting fresh pipeline...");
669 None
670 }
671 Err(e) => {
672 logger.warn(&format!("Failed to load checkpoint (starting fresh): {e}"));
673 None
674 }
675 }
676}
677
678fn validate_file_system_state(
687 file_system_state: &FileSystemState,
688 logger: &Logger,
689 strategy: crate::checkpoint::recovery::RecoveryStrategy,
690) -> ValidationOutcome {
691 let errors = file_system_state.validate_with_executor_impl(None);
692
693 if errors.is_empty() {
694 logger.info("File system state validation passed.");
695 return ValidationOutcome::Passed;
696 }
697
698 logger.warn("File system state validation detected changes:");
699
700 for error in &errors {
701 let (problem, commands) = error.recovery_commands();
702 logger.warn(&format!(" - {}", error));
703 logger.info(&format!(" What's wrong: {}", problem));
704 logger.info(" How to fix:");
705 for cmd in commands {
706 logger.info(&format!(" {}", cmd));
707 }
708 }
709
710 match strategy {
712 crate::checkpoint::recovery::RecoveryStrategy::Fail => {
713 logger.error("File system state validation failed (strategy: fail).");
714 logger.info("Use --recovery-strategy=auto to attempt automatic recovery.");
715 logger.info("Use --recovery-strategy=force to proceed anyway (not recommended).");
716 ValidationOutcome::Failed(
717 "File system state changed - see errors above or use --recovery-strategy=force to proceed anyway".to_string()
718 )
719 }
720 crate::checkpoint::recovery::RecoveryStrategy::Force => {
721 logger.warn("Proceeding with resume despite file changes (strategy: force).");
722 logger.info("Note: Pipeline behavior may be unpredictable.");
723 ValidationOutcome::Passed
724 }
725 crate::checkpoint::recovery::RecoveryStrategy::Auto => {
726 let (_recovered, remaining) = attempt_auto_recovery(file_system_state, &errors, logger);
728
729 if remaining.is_empty() {
730 logger.success("Automatic recovery completed successfully.");
731 ValidationOutcome::Passed
732 } else {
733 logger.warn("Some issues could not be automatically recovered:");
734 for error in &remaining {
735 logger.warn(&format!(" - {}", error));
736 }
737 logger.warn("Proceeding with resume despite unrecovered issues (strategy: auto).");
738 logger.info("Note: Pipeline behavior may be unpredictable.");
739 ValidationOutcome::Passed
740 }
741 }
742 }
743}
744
745fn attempt_auto_recovery(
761 file_system_state: &FileSystemState,
762 errors: &[ValidationError],
763 logger: &Logger,
764) -> (usize, Vec<ValidationError>) {
765 let mut recovered = 0;
766 let mut remaining = Vec::new();
767
768 for error in errors {
769 match attempt_recovery_for_error(file_system_state, error, logger) {
770 Ok(()) => {
771 recovered += 1;
772 logger.success(&format!("Recovered: {}", error));
773 }
774 Err(e) => {
775 remaining.push(error.clone());
776 logger.warn(&format!("Could not recover: {} - {}", error, e));
777 }
778 }
779 }
780
781 (recovered, remaining)
782}
783
784fn attempt_recovery_for_error(
790 file_system_state: &FileSystemState,
791 error: &ValidationError,
792 logger: &Logger,
793) -> Result<(), String> {
794 match error {
795 ValidationError::FileContentChanged { path } => {
796 if let Some(snapshot) = file_system_state.files.get(path) {
798 if let Some(content) = snapshot.get_content() {
799 fs::write(path, content).map_err(|e| format!("Failed to write file: {}", e))?;
800 logger.info(&format!("Restored {} from checkpoint content.", path));
801 return Ok(());
802 }
803 }
804 Err("No content available in snapshot".to_string())
805 }
806 ValidationError::GitHeadChanged { .. } => {
807 Err("Git HEAD changes require manual intervention".to_string())
810 }
811 ValidationError::GitStateInvalid { .. } => {
812 Err("Git state validation requires manual intervention".to_string())
813 }
814 ValidationError::GitWorkingTreeChanged { .. } => {
815 Err("Git working tree changes require manual intervention".to_string())
817 }
818 ValidationError::FileMissing { path } => {
819 if let Some(snapshot) = file_system_state.files.get(path) {
821 if let Some(content) = snapshot.get_content() {
822 fs::write(path, content).map_err(|e| format!("Failed to write file: {}", e))?;
823 logger.info(&format!("Restored missing {} from checkpoint.", path));
824 return Ok(());
825 }
826 }
827 Err(format!("Cannot recover missing file {}", path))
828 }
829 ValidationError::FileUnexpectedlyExists { path } => {
830 Err(format!(
832 "File {} should not exist - requires manual removal",
833 path
834 ))
835 }
836 }
837}
838
839fn check_rebase_state_on_resume(checkpoint: &PipelineCheckpoint, logger: &Logger) {
844 let is_rebase_phase = matches!(
846 checkpoint.phase,
847 PipelinePhase::PreRebase
848 | PipelinePhase::PreRebaseConflict
849 | PipelinePhase::PostRebase
850 | PipelinePhase::PostRebaseConflict
851 );
852
853 if !is_rebase_phase {
854 return;
855 }
856
857 match rebase_in_progress() {
858 Ok(true) => {
859 logger.warn("A git rebase is currently in progress.");
860 logger.info("The checkpoint indicates you were in a rebase phase.");
861 logger.info("Options:");
862 logger.info(" - Continue: Let ralph complete the rebase process");
863 logger.info(" - Abort manually: Run 'git rebase --abort' then use --resume");
864 }
865 Ok(false) => {
866 logger.info("No git rebase is currently in progress.");
869 }
870 Err(e) => {
871 logger.warn(&format!("Could not check rebase state: {e}"));
872 }
873 }
874}
875
876fn display_checkpoint_summary(checkpoint: &PipelineCheckpoint, logger: &Logger) {
878 logger.info(&format!("Resuming from: {}", checkpoint.description()));
879 logger.info(&format!("Checkpoint saved at: {}", checkpoint.timestamp));
880 logger.info(&format!("Checkpoint version: {}", checkpoint.version));
881
882 logger.info(&format!("Run ID: {}", checkpoint.run_id));
884 if checkpoint.resume_count > 0 {
885 logger.info(&format!(
886 "Resume count: {} (this is resume #{} of this session)",
887 checkpoint.resume_count,
888 checkpoint.resume_count + 1
889 ));
890 }
891 if let Some(ref parent_id) = checkpoint.parent_run_id {
892 logger.info(&format!("Parent run ID: {}", parent_id));
893 }
894
895 logger.info(&format!(
897 "Development: {} iteration(s) configured, {} completed",
898 checkpoint.total_iterations, checkpoint.actual_developer_runs
899 ));
900 logger.info(&format!(
901 "Review: {} pass(es) configured, {} completed",
902 checkpoint.total_reviewer_passes, checkpoint.actual_reviewer_runs
903 ));
904
905 if checkpoint.total_iterations > 0 {
907 logger.info(&format!(
908 "Current position: iteration {}/{}",
909 checkpoint.iteration, checkpoint.total_iterations
910 ));
911 }
912 if checkpoint.total_reviewer_passes > 0 {
913 logger.info(&format!(
914 "Current position: pass {}/{}",
915 checkpoint.reviewer_pass, checkpoint.total_reviewer_passes
916 ));
917 }
918
919 let cli = &checkpoint.cli_args;
921 if cli.developer_iters > 0 || cli.reviewer_reviews > 0 {
922 logger.info(&format!(
923 "Original config: -D {} -R {}",
924 cli.developer_iters, cli.reviewer_reviews
925 ));
926 }
927
928 logger.info(&format!("Developer agent: {}", checkpoint.developer_agent));
930 logger.info(&format!("Reviewer agent: {}", checkpoint.reviewer_agent));
931
932 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
934 logger.info(&format!("Developer model override: {}", model));
935 }
936 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
937 logger.info(&format!("Reviewer model override: {}", model));
938 }
939
940 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
942 logger.info(&format!("Developer provider: {}", provider));
943 }
944 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
945 logger.info(&format!("Reviewer provider: {}", provider));
946 }
947
948 match &checkpoint.rebase_state {
950 crate::checkpoint::RebaseState::PreRebaseInProgress { upstream_branch } => {
951 logger.warn(&format!("Pre-rebase in progress to: {}", upstream_branch));
952 }
953 crate::checkpoint::RebaseState::HasConflicts { files } => {
954 logger.warn(&format!("Rebase has conflicts in {} files", files.len()));
955 for file in files.iter().take(3) {
956 logger.warn(&format!(" - {}", file));
957 }
958 if files.len() > 3 {
959 logger.warn(&format!(" ... and {} more", files.len() - 3));
960 }
961 }
962 _ => {}
963 }
964}
965
966fn create_progress_bar(current: u32, total: u32) -> String {
969 if total == 0 {
970 return "[----]".to_string();
971 }
972
973 let width = 20; let filled = ((current as f64 / total as f64) * width as f64).round() as usize;
975 let filled = filled.min(width);
976
977 let mut bar = String::from("[");
978 for i in 0..width {
979 if i < filled {
980 bar.push('=');
981 } else {
982 bar.push('-');
983 }
984 }
985 bar.push(']');
986
987 let percentage = ((current as f64 / total as f64) * 100.0).round() as u32;
988 format!("{} {}%", bar, percentage)
989}
990
991fn display_detailed_checkpoint_info(checkpoint: &PipelineCheckpoint, logger: &Logger) {
996 use chrono::{DateTime, Local, NaiveDateTime};
997
998 logger.info(&format!("Phase: {}", checkpoint.phase));
999 logger.info(&format!("Timestamp: {}", checkpoint.timestamp));
1000
1001 if let Ok(dt) = NaiveDateTime::parse_from_str(&checkpoint.timestamp, "%Y-%m-%d %H:%M:%S") {
1003 let checkpoint_time =
1004 DateTime::<Local>::from_naive_utc_and_offset(dt, Local::now().offset().to_owned());
1005 let now = Local::now();
1006 let duration = now.signed_duration_since(checkpoint_time);
1007
1008 let time_str = if duration.num_days() > 0 {
1009 format!("{} day(s) ago", duration.num_days())
1010 } else if duration.num_hours() > 0 {
1011 format!("{} hour(s) ago", duration.num_hours())
1012 } else if duration.num_minutes() > 0 {
1013 format!("{} minute(s) ago", duration.num_minutes())
1014 } else {
1015 "just now".to_string()
1016 };
1017 logger.info(&format!("Time elapsed: {}", time_str));
1018 }
1019
1020 logger.info("");
1021 logger.info("Configuration:");
1022
1023 if checkpoint.total_iterations > 0 {
1025 let progress_bar = create_progress_bar(
1026 checkpoint.actual_developer_runs,
1027 checkpoint.total_iterations,
1028 );
1029 logger.info(&format!(
1030 " Development: {} {}/{}",
1031 progress_bar, checkpoint.actual_developer_runs, checkpoint.total_iterations
1032 ));
1033 }
1034 if checkpoint.total_reviewer_passes > 0 {
1035 let progress_bar = create_progress_bar(
1036 checkpoint.actual_reviewer_runs,
1037 checkpoint.total_reviewer_passes,
1038 );
1039 logger.info(&format!(
1040 " Review: {} {}/{}",
1041 progress_bar, checkpoint.actual_reviewer_runs, checkpoint.total_reviewer_passes
1042 ));
1043 }
1044
1045 logger.info("");
1046 logger.info("Agents:");
1047 logger.info(&format!(" Developer: {}", checkpoint.developer_agent));
1048 logger.info(&format!(" Reviewer: {}", checkpoint.reviewer_agent));
1049
1050 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
1052 logger.info(&format!(" Developer model: {}", model));
1053 }
1054 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
1055 logger.info(&format!(" Reviewer model: {}", model));
1056 }
1057 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
1058 logger.info(&format!(" Developer provider: {}", provider));
1059 }
1060 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
1061 logger.info(&format!(" Reviewer provider: {}", provider));
1062 }
1063
1064 if let Some(ref cmd) = reconstruct_command(checkpoint) {
1066 logger.info("");
1067 logger.info(&format!("Command: {}", cmd));
1068 }
1069
1070 if checkpoint.resume_count > 0 {
1072 logger.info("");
1073 logger.info(&format!(
1074 "Resumed {} time(s) before",
1075 checkpoint.resume_count
1076 ));
1077 }
1078
1079 logger.info("");
1081 logger.info(&format!("Run ID: {}", checkpoint.run_id));
1082 if let Some(ref parent_id) = checkpoint.parent_run_id {
1083 logger.info(&format!("Parent Run ID: {}", parent_id));
1084 }
1085
1086 if matches!(
1088 checkpoint.rebase_state,
1089 crate::checkpoint::RebaseState::HasConflicts { .. }
1090 ) {
1091 logger.info("");
1092 logger.warn("Rebase conflicts detected:");
1093 if let crate::checkpoint::RebaseState::HasConflicts { files } = &checkpoint.rebase_state {
1094 for file in files.iter().take(10) {
1095 logger.info(&format!(" - {}", file));
1096 }
1097 if files.len() > 10 {
1098 logger.info(&format!(" ... and {} more", files.len() - 10));
1099 }
1100 }
1101 }
1102
1103 if let Some(ref history) = checkpoint.execution_history {
1105 if !history.steps.is_empty() {
1106 logger.info("");
1107 logger.info(&format!(
1108 "Execution History: {} step(s)",
1109 history.steps.len()
1110 ));
1111 for (i, step) in history.steps.iter().take(10).enumerate() {
1112 let outcome_str = match &step.outcome {
1113 crate::checkpoint::execution_history::StepOutcome::Success { .. } => "✓",
1114 crate::checkpoint::execution_history::StepOutcome::Failure { .. } => "✗",
1115 crate::checkpoint::execution_history::StepOutcome::Partial { .. } => "◐",
1116 crate::checkpoint::execution_history::StepOutcome::Skipped { .. } => "○",
1117 };
1118 logger.info(&format!(
1119 " {}. {} {} ({})",
1120 i + 1,
1121 outcome_str,
1122 step.step_type,
1123 step.phase
1124 ));
1125 }
1126 if history.steps.len() > 10 {
1127 logger.info(&format!(
1128 " ... and {} more steps",
1129 history.steps.len() - 10
1130 ));
1131 }
1132 }
1133 }
1134
1135 if let Some(ref fs_state) = checkpoint.file_system_state {
1137 logger.info("");
1138 logger.info(&format!(
1139 "File System State: {} file(s) tracked",
1140 fs_state.files.len()
1141 ));
1142
1143 if let Some(ref branch) = fs_state.git_branch {
1145 logger.info(&format!(" Git branch: {}", branch));
1146 }
1147 if let Some(ref head) = fs_state.git_head_oid {
1148 logger.info(&format!(" Git HEAD: {}", head));
1149 }
1150 if let Some(ref status) = fs_state.git_status {
1151 if !status.is_empty() {
1152 logger.warn(" Git working tree has changes:");
1153 for line in status.lines().take(5) {
1154 logger.info(&format!(" {}", line));
1155 }
1156 }
1157 }
1158 }
1159
1160 if let Some(ref env_snap) = checkpoint.env_snapshot {
1162 if !env_snap.ralph_vars.is_empty() {
1163 logger.info("");
1164 logger.info(&format!(
1165 "Environment Variables: {} RALPH_* var(s)",
1166 env_snap.ralph_vars.len()
1167 ));
1168 for (key, value) in env_snap.ralph_vars.iter().take(10) {
1169 logger.info(&format!(" {}={}", key, value));
1170 }
1171 if env_snap.ralph_vars.len() > 10 {
1172 logger.info(&format!(
1173 " ... and {} more",
1174 env_snap.ralph_vars.len() - 10
1175 ));
1176 }
1177 }
1178 }
1179
1180 logger.info("");
1182 logger.info(&format!("Working directory: {}", checkpoint.working_dir));
1183}
1184
1185fn get_phase_emoji(phase: PipelinePhase) -> &'static str {
1187 match phase {
1188 PipelinePhase::Rebase => "🔄",
1189 PipelinePhase::Planning => "📋",
1190 PipelinePhase::Development => "🔨",
1191 PipelinePhase::Review => "👀",
1192 PipelinePhase::Fix => "🔧",
1193 PipelinePhase::ReviewAgain => "🔍",
1194 PipelinePhase::CommitMessage => "📝",
1195 PipelinePhase::FinalValidation => "✅",
1196 PipelinePhase::Complete => "🎉",
1197 PipelinePhase::PreRebase => "⏪",
1198 PipelinePhase::PreRebaseConflict => "⚠️",
1199 PipelinePhase::PostRebase => "⏩",
1200 PipelinePhase::PostRebaseConflict => "⚠️",
1201 PipelinePhase::Interrupted => "⏸️",
1202 }
1203}