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