Skip to main content

ralph_workflow/app/
resume.rs

1//! Resume functionality for pipeline checkpoints.
2//!
3//! This module handles the --resume flag and checkpoint loading logic,
4//! including validation and state restoration.
5
6/// Length for shortened git OID display (e.g., "a1b2c3d4").
7const 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
20/// Result of handling resume, containing the checkpoint.
21pub struct ResumeResult {
22    /// The loaded checkpoint.
23    pub checkpoint: PipelineCheckpoint,
24}
25
26/// Offer interactive resume prompt when checkpoint exists without --resume flag.
27///
28/// This function checks if a checkpoint exists when the user did NOT specify
29/// the --resume flag, and if so, offers to resume via an interactive prompt.
30/// This provides a better user experience by detecting incomplete runs and
31/// offering to continue them.
32///
33/// # Arguments
34///
35/// * `args` - CLI arguments (to check if --resume was already specified)
36/// * `config` - Current configuration (for validation comparison)
37/// * `registry` - Agent registry (for agent validation)
38/// * `logger` - Logger for output
39/// * `developer_agent` - Current developer agent name
40/// * `reviewer_agent` - Current reviewer agent name
41///
42/// # Returns
43///
44/// `Some(ResumeResult)` if user chose to resume from a valid checkpoint,
45/// `None` if no checkpoint exists, not in a TTY, user declined, or validation failed.
46pub 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    // Skip if --resume flag was already specified (handled by handle_resume_with_validation)
55    if args.recovery.resume {
56        return None;
57    }
58
59    // Skip if --no-resume flag is specified
60    if args.recovery.no_resume {
61        return None;
62    }
63
64    // Skip if RALPH_NO_RESUME_PROMPT env var is set (for CI/automation)
65    if std::env::var("RALPH_NO_RESUME_PROMPT").is_ok() {
66        return None;
67    }
68
69    // Skip if not in a TTY (can't prompt user)
70    if !can_prompt_user() {
71        return None;
72    }
73
74    // Check if checkpoint exists
75    if !checkpoint_exists() {
76        return None;
77    }
78
79    // Load checkpoint to display summary
80    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    // Display user-friendly checkpoint summary with time elapsed
90    logger.header("FOUND PREVIOUS RUN", crate::logger::Colors::cyan);
91    display_user_friendly_checkpoint_summary(&checkpoint, logger);
92
93    // Prompt user to resume
94    if !prompt_user_to_resume(logger) {
95        // User declined - delete checkpoint and start fresh
96        logger.info("Deleting checkpoint and starting fresh...");
97        let _ = crate::checkpoint::clear_checkpoint();
98        return None;
99    }
100
101    // User chose to resume - validate and proceed
102    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
151/// Check if we can prompt the user (stdin/stdout is a TTY).
152fn can_prompt_user() -> bool {
153    io::stdin().is_terminal() && (io::stdout().is_terminal() || io::stderr().is_terminal())
154}
155
156/// Display a user-friendly checkpoint summary with time elapsed.
157fn display_user_friendly_checkpoint_summary(checkpoint: &PipelineCheckpoint, logger: &Logger) {
158    use chrono::{DateTime, Local, NaiveDateTime};
159
160    // Display phase with emoji indicator
161    let phase_emoji = get_phase_emoji(checkpoint.phase);
162    logger.info(&format!("{} {}", phase_emoji, checkpoint.description()));
163
164    // Calculate and display time elapsed
165    // Parse the timestamp string which is in "YYYY-MM-DD HH:MM:SS" format
166    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                // If parsing fails, just show the timestamp string
173                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    // Show rebase conflict information if applicable
196    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            // Show up to 5 conflicted files
206            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    // Show progress with visual bar
217    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    // Show resume count if this is a resumed session
239    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    // Show the reconstructed command that was used
247    if let Some(reconstructed_command) = reconstruct_command(checkpoint) {
248        logger.info(&format!("Original command: {}", reconstructed_command));
249    }
250
251    // Show agent configuration details
252    logger.info(&format!("Developer agent: {}", checkpoint.developer_agent));
253    logger.info(&format!("Reviewer agent: {}", checkpoint.reviewer_agent));
254
255    // Show model overrides if present
256    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    // Show provider overrides if present
264    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    // Show execution history info if available
272    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            // Show recent activity (last 5 steps) with user-friendly details
280            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                // Add files modified count if available
307                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                // Add issues summary if available
328                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                // Add git commit if available (shortened)
338                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    // Show helpful next step based on current phase
351    if let Some(next_step) = suggest_next_step(checkpoint) {
352        logger.info("");
353        logger.info(&format!("Next: {}", next_step));
354    }
355
356    // Show example commands for inspecting state
357    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
363/// Reconstruct the original command from checkpoint data.
364///
365/// This function attempts to reconstruct the exact command that was used
366/// to create the checkpoint, including all relevant flags and options.
367fn reconstruct_command(checkpoint: &PipelineCheckpoint) -> Option<String> {
368    let cli = &checkpoint.cli_args;
369    let mut parts = vec!["ralph".to_string()];
370
371    // Add -D flag
372    if cli.developer_iters > 0 {
373        parts.push(format!("-D {}", cli.developer_iters));
374    }
375
376    // Add -R flag
377    if cli.reviewer_reviews > 0 {
378        parts.push(format!("-R {}", cli.reviewer_reviews));
379    }
380
381    // Add --review-depth if specified
382    if let Some(ref depth) = cli.review_depth {
383        parts.push(format!("--review-depth {}", depth));
384    }
385
386    // Add --skip-rebase if true
387    if cli.skip_rebase {
388        parts.push("--skip-rebase".to_string());
389    }
390
391    // Add --no-isolation if false (isolation_mode defaults to true)
392    if !cli.isolation_mode {
393        parts.push("--no-isolation".to_string());
394    }
395
396    // Add verbosity flags
397    match cli.verbosity {
398        0 => parts.push("--quiet".to_string()),
399        1 => {} // Normal is default
400        2 => parts.push("--verbose".to_string()),
401        3 => parts.push("--full".to_string()),
402        4 => parts.push("--debug".to_string()),
403        _ => {}
404    }
405
406    // Add --show-streaming-metrics if true
407    if cli.show_streaming_metrics {
408        parts.push("--show-streaming-metrics".to_string());
409    }
410
411    // Add --reviewer-json-parser if specified
412    if let Some(ref parser) = cli.reviewer_json_parser {
413        parts.push(format!("--reviewer-json-parser {}", parser));
414    }
415
416    // Add --agent flags if agents differ from defaults
417    // Note: We can't determine defaults here, so we always show them
418    parts.push(format!("--agent {}", checkpoint.developer_agent));
419    parts.push(format!("--reviewer-agent {}", checkpoint.reviewer_agent));
420
421    // Add model overrides if present
422    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    // Add provider overrides if present
430    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
444/// Suggest the next step based on the current checkpoint phase.
445///
446/// Returns a detailed, actionable description of what will happen next
447/// when the user resumes from this checkpoint.
448fn 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            // Provide more detailed information for interrupted state
489            // The interrupted phase can occur at any point, so we need to describe
490            // what the user was doing when interrupted
491            let mut context = vec!["resume from interrupted state".to_string()];
492
493            // Add context about what was being worked on
494            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            // Explain what will happen on resume
508            context.push("full pipeline will run from interrupted point".to_string());
509
510            Some(context.join(" - "))
511        }
512    }
513}
514
515/// Prompt user to decide whether to resume or start fresh.
516///
517/// Returns `true` if user wants to resume, `false` if they want to start fresh.
518fn 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 prompt directly to stdout for better UX
529    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            // EOF
537            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/// Result of file system validation.
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub enum ValidationOutcome {
553    /// Validation passed, safe to resume
554    Passed,
555    /// Validation failed, cannot resume
556    Failed(String),
557}
558
559/// Handles the --resume flag and loads checkpoint if applicable.
560///
561/// This function loads and validates the checkpoint, providing detailed
562/// feedback about what state is being restored and any configuration changes.
563///
564/// # Arguments
565///
566/// * `args` - CLI arguments
567/// * `config` - Current configuration (for validation comparison)
568/// * `registry` - Agent registry (for agent validation)
569/// * `logger` - Logger for output
570/// * `developer_agent` - Current developer agent name
571/// * `reviewer_agent` - Current reviewer agent name
572///
573/// # Returns
574///
575/// `Some(ResumeResult)` if a valid checkpoint was found and loaded,
576/// `None` if no checkpoint exists or --resume was not specified.
577pub 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    // Handle --inspect-checkpoint flag
586    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            // Validate checkpoint
614            let validation = validate_checkpoint(&checkpoint, config, registry);
615
616            // Display validation results
617            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            // Verify agents match (additional agent-specific warnings)
633            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 for in-progress git rebase
647            check_rebase_state_on_resume(&checkpoint, logger);
648
649            // Perform file system state validation
650            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
678/// Validate file system state when resuming.
679///
680/// This function validates that the current file system state matches
681/// the state captured in the checkpoint. This is part of the hardened
682/// resume feature that ensures idempotent recovery.
683///
684/// Returns a `ValidationOutcome` indicating whether validation passed
685/// or failed with a reason.
686fn 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    // Handle based on the recovery strategy
711    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            // Attempt automatic recovery for recoverable errors
727            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
745/// Attempt automatic recovery from file system state changes.
746///
747/// This function attempts to automatically fix recoverable issues:
748/// - Restores small files from content stored in snapshot
749/// - Warns about unrecoverable issues (large files, git changes)
750///
751/// # Arguments
752///
753/// * `file_system_state` - The file system state from checkpoint
754/// * `errors` - Validation errors that were detected
755/// * `logger` - Logger for output
756///
757/// # Returns
758///
759/// A tuple of (number of issues recovered, remaining errors)
760fn 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
784/// Attempt to recover from a single validation error.
785///
786/// # Returns
787///
788/// `Ok(())` if recovery succeeded, `Err(reason)` if it failed.
789fn 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            // Try to restore from snapshot if content is available
797            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            // Git state changes are not automatically recoverable
808            // They require user intervention to reset or accept the new state
809            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            // Working tree changes are not automatically recoverable
816            Err("Git working tree changes require manual intervention".to_string())
817        }
818        ValidationError::FileMissing { path } => {
819            // Can't recover a missing file unless we have content
820            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            // Unexpected files should be removed by user
831            Err(format!(
832                "File {} should not exist - requires manual removal",
833                path
834            ))
835        }
836    }
837}
838
839/// Check for in-progress git rebase when resuming.
840///
841/// This function detects if a git rebase is in progress and provides
842/// appropriate guidance to the user.
843fn check_rebase_state_on_resume(checkpoint: &PipelineCheckpoint, logger: &Logger) {
844    // Only check for rebase if we're resuming from a rebase-related phase
845    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            // No rebase in progress - this is expected if rebase completed
867            // but checkpoint wasn't cleared (e.g., pipeline was interrupted)
868            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
876/// Display a summary of the checkpoint being loaded.
877fn 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    // Show run ID and resume count
883    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    // Show actual execution counts vs configured counts
896    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    // Show iteration progress
906    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    // Show CLI args if available
920    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    // Show agent configs
929    logger.info(&format!("Developer agent: {}", checkpoint.developer_agent));
930    logger.info(&format!("Reviewer agent: {}", checkpoint.reviewer_agent));
931
932    // Show model overrides if present
933    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    // Show provider overrides if present
941    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    // Show rebase state if applicable
949    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
966/// Helper to get phase rank for resume logic.
967/// Create a visual progress bar for checkpoint summary display.
968fn create_progress_bar(current: u32, total: u32) -> String {
969    if total == 0 {
970        return "[----]".to_string();
971    }
972
973    let width = 20; // Total width of progress bar
974    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
991/// Display detailed checkpoint information for inspection.
992///
993/// This function shows comprehensive checkpoint details when the user
994/// runs with the --inspect-checkpoint flag.
995fn 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    // Calculate and display time elapsed
1002    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    // Show iterations and reviews
1024    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    // Show model overrides
1051    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    // Show CLI args
1065    if let Some(ref cmd) = reconstruct_command(checkpoint) {
1066        logger.info("");
1067        logger.info(&format!("Command: {}", cmd));
1068    }
1069
1070    // Show resume count
1071    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    // Show run ID
1080    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    // Show rebase state if applicable
1087    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    // Show execution history if available
1104    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    // Show file system state if available
1136    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        // Show git state
1144        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    // Show environment snapshot if available
1161    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    // Show working directory
1181    logger.info("");
1182    logger.info(&format!("Working directory: {}", checkpoint.working_dir));
1183}
1184
1185/// Get an emoji indicator for a pipeline phase.
1186fn 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}