ralph_workflow/app/
mod.rs

1//! Application entrypoint and pipeline orchestration.
2//!
3//! This module exists to keep `src/main.rs` small and focused while preserving
4//! the CLI surface and overall runtime behavior. It wires together:
5//! - CLI/config parsing and plumbing commands
6//! - Agent registry loading
7//! - Repo setup and resume support
8//! - Phase execution via `crate::phases`
9//!
10//! # Module Structure
11//!
12//! - [`config_init`]: Configuration loading and agent registry initialization
13//! - [`plumbing`]: Low-level git operations (show/apply commit messages)
14//! - [`validation`]: Agent validation and chain validation
15//! - [`resume`]: Checkpoint resume functionality
16//! - [`detection`]: Project stack detection
17//! - [`finalization`]: Pipeline cleanup and finalization
18
19pub mod config_init;
20pub mod context;
21pub mod detection;
22pub mod finalization;
23pub mod plumbing;
24pub mod resume;
25pub mod validation;
26
27use crate::agents::AgentRegistry;
28use crate::app::finalization::finalize_pipeline;
29use crate::app::resume::should_run_from;
30use crate::banner::print_welcome_banner;
31use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
32use crate::checkpoint::restore::{
33    calculate_start_iteration, calculate_start_reviewer_pass, should_skip_phase,
34};
35use crate::checkpoint::{
36    save_checkpoint, CheckpointBuilder, PipelineCheckpoint, PipelinePhase, RebaseState,
37};
38use crate::cli::{
39    create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
40    handle_list_available_agents, handle_list_providers, handle_show_baseline,
41    handle_template_commands, prompt_template_selection, Args,
42};
43use crate::common::utils;
44use crate::files::protection::monitoring::PromptMonitor;
45use crate::files::{
46    create_prompt_backup, ensure_files, make_prompt_read_only, reset_context_for_isolation,
47    update_status, validate_prompt_md,
48};
49use crate::git_helpers::{
50    abort_rebase, cleanup_orphaned_marker, continue_rebase, get_conflicted_files,
51    get_default_branch, get_repo_root, get_start_commit_summary, is_main_or_master_branch,
52    rebase_onto, require_git_repo, reset_start_commit, save_start_commit, start_agent_phase,
53    RebaseResult,
54};
55use crate::logger::Colors;
56use crate::logger::Logger;
57use crate::phases::{run_development_phase, run_review_phase, PhaseContext};
58use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
59use crate::prompts::{get_stored_or_generate_prompt, template_context::TemplateContext};
60use std::env;
61use std::process::Command;
62
63use config_init::initialize_config;
64use context::PipelineContext;
65use detection::detect_project_stack;
66use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
67use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
68use validation::{
69    resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
70};
71
72/// Main application entry point.
73///
74/// Orchestrates the entire Ralph pipeline:
75/// 1. Configuration initialization
76/// 2. Agent validation
77/// 3. Plumbing commands (if requested)
78/// 4. Development phase
79/// 5. Review & fix phase
80/// 6. Final validation
81/// 7. Commit phase
82///
83/// # Arguments
84///
85/// * `args` - The parsed CLI arguments
86///
87/// # Returns
88///
89/// Returns `Ok(())` on success or an error if any phase fails.
90pub fn run(args: Args) -> anyhow::Result<()> {
91    let colors = Colors::new();
92    let logger = Logger::new(colors);
93
94    // Initialize configuration and agent registry
95    let Some(init_result) = initialize_config(&args, colors, &logger)? else {
96        return Ok(()); // Early exit (--init/--init-global/--init-legacy)
97    };
98
99    let config_init::ConfigInitResult {
100        config,
101        registry,
102        config_path,
103        config_sources,
104    } = init_result;
105
106    // Resolve required agent names
107    let validated = resolve_required_agents(&config)?;
108    let developer_agent = validated.developer_agent;
109    let reviewer_agent = validated.reviewer_agent;
110
111    // Handle listing commands (these can run without git repo)
112    if handle_listing_commands(&args, &registry, colors) {
113        return Ok(());
114    }
115
116    // Handle --diagnose
117    if args.recovery.diagnose {
118        handle_diagnose(colors, &config, &registry, &config_path, &config_sources);
119        return Ok(());
120    }
121
122    // Validate agent chains
123    validate_agent_chains(&registry, colors);
124
125    // Handle plumbing commands
126    if handle_plumbing_commands(&args, &logger, colors)? {
127        return Ok(());
128    }
129
130    // Validate agents and set up git repo and PROMPT.md
131    let Some(repo_root) = validate_and_setup_agents(
132        &config,
133        &registry,
134        &developer_agent,
135        &reviewer_agent,
136        &config_path,
137        colors,
138        &logger,
139    )?
140    else {
141        return Ok(());
142    };
143
144    // Handle --rebase-only
145    if args.rebase_flags.rebase_only {
146        let template_context =
147            TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
148        return handle_rebase_only(&args, &config, &template_context, &logger, colors);
149    }
150
151    // Prepare pipeline context or exit early
152    (prepare_pipeline_or_exit(PipelinePreparationParams {
153        args,
154        config,
155        registry,
156        developer_agent,
157        reviewer_agent,
158        repo_root,
159        logger,
160        colors,
161    })?)
162    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
163}
164
165/// Handles listing commands that don't require the full pipeline.
166///
167/// Returns `true` if a listing command was handled and we should exit.
168fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
169    if args.agent_list.list_agents {
170        handle_list_agents(registry);
171        return true;
172    }
173    if args.agent_list.list_available_agents {
174        handle_list_available_agents(registry);
175        return true;
176    }
177    if args.provider_list.list_providers {
178        handle_list_providers(colors);
179        return true;
180    }
181
182    // Handle template commands
183    let template_cmds = &args.template_commands;
184    if template_cmds.init_templates_enabled()
185        || template_cmds.validate
186        || template_cmds.show.is_some()
187        || template_cmds.list
188        || template_cmds.list_all
189        || template_cmds.variables.is_some()
190        || template_cmds.render.is_some()
191    {
192        let _ = handle_template_commands(template_cmds, colors);
193        return true;
194    }
195
196    false
197}
198
199/// Handles plumbing commands that require git repo but not full validation.
200///
201/// Returns `Ok(true)` if a plumbing command was handled and we should exit.
202/// Returns `Ok(false)` if we should continue to the main pipeline.
203fn handle_plumbing_commands(args: &Args, logger: &Logger, colors: Colors) -> anyhow::Result<bool> {
204    // Show commit message
205    if args.commit_display.show_commit_msg {
206        return handle_show_commit_msg().map(|()| true);
207    }
208
209    // Apply commit
210    if args.commit_plumbing.apply_commit {
211        return handle_apply_commit(logger, colors).map(|()| true);
212    }
213
214    // Reset start commit
215    if args.commit_display.reset_start_commit {
216        require_git_repo()?;
217        let repo_root = get_repo_root()?;
218        env::set_current_dir(&repo_root)?;
219
220        return match reset_start_commit() {
221            Ok(result) => {
222                let short_oid = &result.oid[..8.min(result.oid.len())];
223                if result.fell_back_to_head {
224                    logger.success(&format!(
225                        "Starting commit reference reset to current HEAD ({})",
226                        short_oid
227                    ));
228                    logger.info("On main/master branch - using HEAD as baseline");
229                } else if let Some(ref branch) = result.default_branch {
230                    logger.success(&format!(
231                        "Starting commit reference reset to merge-base with '{}' ({})",
232                        branch, short_oid
233                    ));
234                    logger.info("Baseline set to common ancestor with default branch");
235                } else {
236                    logger.success(&format!("Starting commit reference reset ({})", short_oid));
237                }
238                logger.info(".agent/start_commit has been updated");
239                Ok(true)
240            }
241            Err(e) => {
242                logger.error(&format!("Failed to reset starting commit: {e}"));
243                anyhow::bail!("Failed to reset starting commit");
244            }
245        };
246    }
247
248    // Show baseline state
249    if args.commit_display.show_baseline {
250        require_git_repo()?;
251        let repo_root = get_repo_root()?;
252        env::set_current_dir(&repo_root)?;
253
254        return match handle_show_baseline() {
255            Ok(()) => Ok(true),
256            Err(e) => {
257                logger.error(&format!("Failed to show baseline: {e}"));
258                anyhow::bail!("Failed to show baseline");
259            }
260        };
261    }
262
263    Ok(false)
264}
265
266/// Parameters for preparing the pipeline context.
267///
268/// Groups related parameters to avoid too many function arguments.
269struct PipelinePreparationParams {
270    args: Args,
271    config: crate::config::Config,
272    registry: AgentRegistry,
273    developer_agent: String,
274    reviewer_agent: String,
275    repo_root: std::path::PathBuf,
276    logger: Logger,
277    colors: Colors,
278}
279
280/// Prepares the pipeline context after agent validation.
281///
282/// Returns `Some(ctx)` if pipeline should run, or `None` if we should exit early.
283fn prepare_pipeline_or_exit(
284    params: PipelinePreparationParams,
285) -> anyhow::Result<Option<PipelineContext>> {
286    let PipelinePreparationParams {
287        args,
288        config,
289        registry,
290        developer_agent,
291        reviewer_agent,
292        repo_root,
293        mut logger,
294        colors,
295    } = params;
296
297    ensure_files(config.isolation_mode)?;
298
299    // Reset context for isolation mode
300    if config.isolation_mode {
301        reset_context_for_isolation(&logger)?;
302    }
303
304    logger = logger.with_log_file(".agent/logs/pipeline.log");
305
306    // Handle --dry-run
307    if args.recovery.dry_run {
308        let developer_display = registry.display_name(&developer_agent);
309        let reviewer_display = registry.display_name(&reviewer_agent);
310        handle_dry_run(
311            &logger,
312            colors,
313            &config,
314            &developer_display,
315            &reviewer_display,
316            &repo_root,
317        )?;
318        return Ok(None);
319    }
320
321    // Create template context for user template overrides
322    let template_context =
323        TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
324
325    // Handle --generate-commit-msg
326    if args.commit_plumbing.generate_commit_msg {
327        handle_generate_commit_msg(
328            &config,
329            &template_context,
330            &registry,
331            &logger,
332            colors,
333            &developer_agent,
334            &reviewer_agent,
335        )?;
336        return Ok(None);
337    }
338
339    // Get display names before moving registry
340    let developer_display = registry.display_name(&developer_agent);
341    let reviewer_display = registry.display_name(&reviewer_agent);
342
343    // Build pipeline context
344    let ctx = PipelineContext {
345        args,
346        config,
347        registry,
348        developer_agent,
349        reviewer_agent,
350        developer_display,
351        reviewer_display,
352        repo_root,
353        logger,
354        colors,
355        template_context,
356    };
357    Ok(Some(ctx))
358}
359
360/// Validates agent commands and workflow capability, then sets up git repo and PROMPT.md.
361///
362/// Returns `Some(repo_root)` if setup succeeded and should continue.
363/// Returns `None` if the user declined PROMPT.md creation (to exit early).
364fn validate_and_setup_agents(
365    config: &crate::config::Config,
366    registry: &AgentRegistry,
367    developer_agent: &str,
368    reviewer_agent: &str,
369    config_path: &std::path::Path,
370    colors: Colors,
371    logger: &Logger,
372) -> anyhow::Result<Option<std::path::PathBuf>> {
373    // Validate agent commands exist
374    validate_agent_commands(
375        config,
376        registry,
377        developer_agent,
378        reviewer_agent,
379        config_path,
380    )?;
381
382    // Validate agents are workflow-capable
383    validate_can_commit(
384        config,
385        registry,
386        developer_agent,
387        reviewer_agent,
388        config_path,
389    )?;
390
391    // Set up git repo and working directory
392    require_git_repo()?;
393    let repo_root = get_repo_root()?;
394    env::set_current_dir(&repo_root)?;
395
396    // Set up PROMPT.md if needed (may return None to exit early)
397    let should_continue = setup_git_and_prompt_file(config, colors, logger)?;
398    if should_continue.is_none() {
399        return Ok(None);
400    }
401
402    Ok(Some(repo_root))
403}
404
405/// In interactive mode, prompts to create PROMPT.md from a template before `ensure_files()`.
406///
407/// Returns `Ok(Some(()))` if setup succeeded and should continue.
408/// Returns `Ok(None)` if the user declined PROMPT.md creation (to exit early).
409fn setup_git_and_prompt_file(
410    config: &crate::config::Config,
411    colors: Colors,
412    logger: &Logger,
413) -> anyhow::Result<Option<()>> {
414    let prompt_exists = std::path::Path::new("PROMPT.md").exists();
415
416    // In interactive mode, prompt to create PROMPT.md from a template BEFORE ensure_files().
417    // If the user declines (or we can't prompt), exit without creating a placeholder PROMPT.md.
418    if config.behavior.interactive && !prompt_exists {
419        if let Some(template_name) = prompt_template_selection(colors) {
420            create_prompt_from_template(&template_name, colors)?;
421            println!();
422            logger.info(
423                "PROMPT.md created. Please edit it with your task details, then run ralph again.",
424            );
425            logger.info(&format!(
426                "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
427                config.commit_msg
428            ));
429            return Ok(None);
430        }
431        println!();
432        logger.error("PROMPT.md not found in current directory.");
433        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
434        println!();
435        logger.info("To get started:");
436        logger.info("  ralph --init                    # Smart setup wizard");
437        logger.info("  ralph --init bug-fix             # Create from Work Guide");
438        logger.info("  ralph --list-work-guides          # See all Work Guides");
439        println!();
440        return Ok(None);
441    }
442
443    // Non-interactive mode: show helpful error if PROMPT.md doesn't exist
444    if !prompt_exists {
445        logger.error("PROMPT.md not found in current directory.");
446        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
447        println!();
448        logger.info("Quick start:");
449        logger.info("  ralph --init                    # Smart setup wizard");
450        logger.info("  ralph --init bug-fix             # Create from Work Guide");
451        logger.info("  ralph --list-work-guides          # See all Work Guides");
452        println!();
453        logger.info("Use -i flag for interactive mode to be prompted for template selection.");
454        println!();
455        return Ok(None);
456    }
457
458    Ok(Some(()))
459}
460
461/// Runs the full development/review/commit pipeline.
462fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
463    // First, offer interactive resume if checkpoint exists without --resume flag
464    let resume_result = offer_resume_if_checkpoint_exists(
465        &ctx.args,
466        &ctx.config,
467        &ctx.registry,
468        &ctx.logger,
469        &ctx.developer_agent,
470        &ctx.reviewer_agent,
471    );
472
473    // If interactive resume didn't happen, check for --resume flag
474    let resume_result = match resume_result {
475        Some(result) => Some(result),
476        None => handle_resume_with_validation(
477            &ctx.args,
478            &ctx.config,
479            &ctx.registry,
480            &ctx.logger,
481            &ctx.developer_display,
482            &ctx.reviewer_display,
483        ),
484    };
485
486    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
487
488    // Create run context - either new or from checkpoint
489    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
490        use crate::checkpoint::RunContext;
491        RunContext::from_checkpoint(checkpoint)
492    } else {
493        use crate::checkpoint::RunContext;
494        RunContext::new()
495    };
496
497    // Apply checkpoint configuration restoration if resuming
498    let config = if let Some(ref checkpoint) = resume_checkpoint {
499        use crate::checkpoint::apply_checkpoint_to_config;
500        let mut restored_config = ctx.config.clone();
501        apply_checkpoint_to_config(&mut restored_config, checkpoint);
502        ctx.logger.info("Restored configuration from checkpoint:");
503        if checkpoint.cli_args.developer_iters > 0 {
504            ctx.logger.info(&format!(
505                "  Developer iterations: {} (from checkpoint)",
506                checkpoint.cli_args.developer_iters
507            ));
508        }
509        if checkpoint.cli_args.reviewer_reviews > 0 {
510            ctx.logger.info(&format!(
511                "  Reviewer passes: {} (from checkpoint)",
512                checkpoint.cli_args.reviewer_reviews
513            ));
514        }
515        restored_config
516    } else {
517        ctx.config.clone()
518    };
519
520    // Restore environment variables from checkpoint if resuming
521    if let Some(ref checkpoint) = resume_checkpoint {
522        use crate::checkpoint::restore::restore_environment_from_checkpoint;
523        let restored_count = restore_environment_from_checkpoint(checkpoint);
524        if restored_count > 0 {
525            ctx.logger.info(&format!(
526                "  Restored {} environment variable(s) from checkpoint",
527                restored_count
528            ));
529        }
530    }
531
532    // Set up git helpers and agent phase
533    let mut git_helpers = crate::git_helpers::GitHelpers::new();
534    cleanup_orphaned_marker(&ctx.logger)?;
535    start_agent_phase(&mut git_helpers)?;
536    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
537
538    // Print welcome banner and validate PROMPT.md
539    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
540    print_pipeline_info_with_config(ctx, &config);
541    validate_prompt_and_setup_backup(ctx)?;
542
543    // Set up PROMPT.md monitoring
544    let mut prompt_monitor = setup_prompt_monitor(ctx);
545
546    // Detect project stack and review guidelines
547    let (_project_stack, review_guidelines) =
548        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
549
550    print_review_guidelines(ctx, review_guidelines.as_ref());
551    println!();
552
553    // Create phase context and save starting commit
554    let (mut timer, mut stats) = (Timer::new(), Stats::new());
555    let mut phase_ctx = create_phase_context_with_config(
556        ctx,
557        &config,
558        &mut timer,
559        &mut stats,
560        review_guidelines.as_ref(),
561        &run_context,
562        resume_checkpoint.as_ref(),
563    );
564    save_start_commit_or_warn(ctx);
565
566    // Set up interrupt context for checkpoint saving on Ctrl+C
567    // This must be done after phase_ctx is created
568    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
569        checkpoint.phase
570    } else {
571        PipelinePhase::Planning
572    };
573    setup_interrupt_context_for_pipeline(
574        initial_phase,
575        config.developer_iters,
576        config.reviewer_reviews,
577        &phase_ctx.execution_history,
578        &phase_ctx.prompt_history,
579        &run_context,
580    );
581
582    // Ensure interrupt context is cleared on completion
583    let _interrupt_guard = defer_clear_interrupt_context();
584
585    // Determine if we should run rebase based on checkpoint or current args
586    let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
587        // Use checkpoint's skip_rebase value if it has meaningful cli_args
588        if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
589            !checkpoint.cli_args.skip_rebase
590        } else {
591            // Fallback to current args
592            ctx.args.rebase_flags.with_rebase
593        }
594    } else {
595        ctx.args.rebase_flags.with_rebase
596    };
597
598    // Run pre-development rebase (only if explicitly requested via --with-rebase)
599    if should_run_rebase {
600        run_initial_rebase(ctx, &mut phase_ctx, &run_context)?;
601        // Update interrupt context after rebase
602        update_interrupt_context_from_phase(
603            &phase_ctx,
604            PipelinePhase::Planning,
605            config.developer_iters,
606            config.reviewer_reviews,
607            &run_context,
608        );
609    } else {
610        // Save initial checkpoint when rebase is disabled
611        if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
612            let builder = CheckpointBuilder::new()
613                .phase(PipelinePhase::Planning, 0, config.developer_iters)
614                .reviewer_pass(0, config.reviewer_reviews)
615                .skip_rebase(true) // Rebase is disabled
616                .capture_from_context(
617                    &config,
618                    &ctx.registry,
619                    &ctx.developer_agent,
620                    &ctx.reviewer_agent,
621                    &ctx.logger,
622                    &run_context,
623                )
624                .with_execution_history(phase_ctx.execution_history.clone())
625                .with_prompt_history(phase_ctx.clone_prompt_history());
626
627            if let Some(checkpoint) = builder.build() {
628                let _ = save_checkpoint(&checkpoint);
629            }
630        }
631        // Update interrupt context after initial checkpoint
632        update_interrupt_context_from_phase(
633            &phase_ctx,
634            PipelinePhase::Planning,
635            config.developer_iters,
636            config.reviewer_reviews,
637            &run_context,
638        );
639    }
640
641    // Run pipeline phases
642    run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
643    // Update interrupt context after development
644    update_interrupt_context_from_phase(
645        &phase_ctx,
646        PipelinePhase::Development,
647        config.developer_iters,
648        config.reviewer_reviews,
649        &run_context,
650    );
651    check_prompt_restoration(ctx, &mut prompt_monitor, "development");
652    update_status("In progress.", config.isolation_mode)?;
653
654    run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
655    // Update interrupt context after review
656    update_interrupt_context_from_phase(
657        &phase_ctx,
658        PipelinePhase::Review,
659        config.developer_iters,
660        config.reviewer_reviews,
661        &run_context,
662    );
663    check_prompt_restoration(ctx, &mut prompt_monitor, "review");
664
665    // Run post-review rebase (only if explicitly requested via --with-rebase)
666    if should_run_rebase {
667        run_post_review_rebase(ctx, &mut phase_ctx, &run_context)?;
668        // Update interrupt context after post-review rebase
669        update_interrupt_context_from_phase(
670            &phase_ctx,
671            PipelinePhase::PostRebase,
672            config.developer_iters,
673            config.reviewer_reviews,
674            &run_context,
675        );
676    }
677
678    update_status("In progress.", config.isolation_mode)?;
679
680    run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
681
682    // Save Complete checkpoint before clearing (for idempotent resume)
683    if config.features.checkpoint_enabled {
684        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
685        let builder = CheckpointBuilder::new()
686            .phase(
687                PipelinePhase::Complete,
688                config.developer_iters,
689                config.developer_iters,
690            )
691            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
692            .skip_rebase(skip_rebase)
693            .capture_from_context(
694                &config,
695                &ctx.registry,
696                &ctx.developer_agent,
697                &ctx.reviewer_agent,
698                &ctx.logger,
699                &run_context,
700            );
701
702        let builder = builder
703            .with_execution_history(phase_ctx.execution_history.clone())
704            .with_prompt_history(phase_ctx.clone_prompt_history());
705
706        if let Some(checkpoint) = builder.build() {
707            let _ = save_checkpoint(&checkpoint);
708        }
709    }
710
711    // Commit phase
712    finalize_pipeline(
713        &mut agent_phase_guard,
714        &ctx.logger,
715        ctx.colors,
716        &config,
717        &timer,
718        &stats,
719        prompt_monitor,
720    );
721    Ok(())
722}
723
724/// Set up the interrupt context with initial pipeline state.
725///
726/// This function initializes the global interrupt context so that if
727/// the user presses Ctrl+C, the interrupt handler can save a checkpoint.
728fn setup_interrupt_context_for_pipeline(
729    phase: PipelinePhase,
730    total_iterations: u32,
731    total_reviewer_passes: u32,
732    execution_history: &crate::checkpoint::ExecutionHistory,
733    prompt_history: &std::collections::HashMap<String, String>,
734    run_context: &crate::checkpoint::RunContext,
735) {
736    use crate::interrupt::{set_interrupt_context, InterruptContext};
737
738    // Determine initial iteration based on phase
739    let (iteration, reviewer_pass) = match phase {
740        PipelinePhase::Development => (1, 0),
741        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
742            (total_iterations, 1)
743        }
744        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
745            (total_iterations, total_reviewer_passes)
746        }
747        _ => (0, 0),
748    };
749
750    let context = InterruptContext {
751        phase,
752        iteration,
753        total_iterations,
754        reviewer_pass,
755        total_reviewer_passes,
756        run_context: run_context.clone(),
757        execution_history: execution_history.clone(),
758        prompt_history: prompt_history.clone(),
759    };
760
761    set_interrupt_context(context);
762}
763
764/// Update the interrupt context from the current phase context.
765///
766/// This function should be called after each major phase to keep the
767/// interrupt context up-to-date with the latest execution history.
768fn update_interrupt_context_from_phase(
769    phase_ctx: &crate::phases::PhaseContext,
770    phase: PipelinePhase,
771    total_iterations: u32,
772    total_reviewer_passes: u32,
773    run_context: &crate::checkpoint::RunContext,
774) {
775    use crate::interrupt::{set_interrupt_context, InterruptContext};
776
777    // Determine current iteration based on phase
778    let (iteration, reviewer_pass) = match phase {
779        PipelinePhase::Development => {
780            // Estimate iteration from actual runs
781            let iter = run_context.actual_developer_runs.max(1);
782            (iter, 0)
783        }
784        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
785            (total_iterations, run_context.actual_reviewer_runs.max(1))
786        }
787        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
788            (total_iterations, total_reviewer_passes)
789        }
790        _ => (0, 0),
791    };
792
793    let context = InterruptContext {
794        phase,
795        iteration,
796        total_iterations,
797        reviewer_pass,
798        total_reviewer_passes,
799        run_context: run_context.clone(),
800        execution_history: phase_ctx.execution_history.clone(),
801        prompt_history: phase_ctx.clone_prompt_history(),
802    };
803
804    set_interrupt_context(context);
805}
806
807/// Helper to defer clearing interrupt context until function exit.
808///
809/// Uses a scope guard pattern to ensure the interrupt context is cleared
810/// when the pipeline completes successfully, preventing an "interrupted"
811/// checkpoint from being saved after normal completion.
812fn defer_clear_interrupt_context() -> InterruptContextGuard {
813    InterruptContextGuard
814}
815
816/// RAII guard for clearing interrupt context on drop.
817///
818/// Ensures the interrupt context is cleared when the guard is dropped,
819/// preventing an "interrupted" checkpoint from being saved after normal
820/// pipeline completion.
821struct InterruptContextGuard;
822
823impl Drop for InterruptContextGuard {
824    fn drop(&mut self) {
825        crate::interrupt::clear_interrupt_context();
826    }
827}
828
829/// Validate PROMPT.md and set up backup/protection.
830fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
831    let prompt_validation =
832        validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
833    for err in &prompt_validation.errors {
834        ctx.logger.error(err);
835    }
836    for warn in &prompt_validation.warnings {
837        ctx.logger.warn(warn);
838    }
839    if !prompt_validation.is_valid() {
840        anyhow::bail!("PROMPT.md validation errors");
841    }
842
843    // Create a backup of PROMPT.md to protect against accidental deletion.
844    match create_prompt_backup() {
845        Ok(None) => {}
846        Ok(Some(warning)) => {
847            ctx.logger.warn(&format!(
848                "PROMPT.md backup created but: {warning}. Continuing anyway."
849            ));
850        }
851        Err(e) => {
852            ctx.logger.warn(&format!(
853                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
854            ));
855        }
856    }
857
858    // Make PROMPT.md read-only to protect against accidental deletion.
859    match make_prompt_read_only() {
860        None => {}
861        Some(warning) => {
862            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
863        }
864    }
865
866    Ok(())
867}
868
869/// Set up PROMPT.md monitoring for deletion detection.
870fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
871    match PromptMonitor::new() {
872        Ok(mut monitor) => {
873            if let Err(e) = monitor.start() {
874                ctx.logger.warn(&format!(
875                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
876                ));
877                None
878            } else {
879                if ctx.config.verbosity.is_debug() {
880                    ctx.logger.info("Started real-time PROMPT.md monitoring");
881                }
882                Some(monitor)
883            }
884        }
885        Err(e) => {
886            ctx.logger.warn(&format!(
887                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
888            ));
889            None
890        }
891    }
892}
893
894/// Print review guidelines if detected.
895fn print_review_guidelines(
896    ctx: &PipelineContext,
897    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
898) {
899    if let Some(guidelines) = review_guidelines {
900        ctx.logger.info(&format!(
901            "Review guidelines: {}{}{}",
902            ctx.colors.dim(),
903            guidelines.summary(),
904            ctx.colors.reset()
905        ));
906    }
907}
908
909/// Create the phase context with a modified config (for resume restoration).
910fn create_phase_context_with_config<'ctx>(
911    ctx: &'ctx PipelineContext,
912    config: &'ctx crate::config::Config,
913    timer: &'ctx mut Timer,
914    stats: &'ctx mut Stats,
915    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
916    run_context: &'ctx crate::checkpoint::RunContext,
917    resume_checkpoint: Option<&PipelineCheckpoint>,
918) -> PhaseContext<'ctx> {
919    // Restore execution history and prompt history from checkpoint if available
920    let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
921        let exec_history = checkpoint
922            .execution_history
923            .clone()
924            .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
925        let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
926        (exec_history, prompt_hist)
927    } else {
928        (
929            crate::checkpoint::execution_history::ExecutionHistory::new(),
930            std::collections::HashMap::new(),
931        )
932    };
933
934    PhaseContext {
935        config,
936        registry: &ctx.registry,
937        logger: &ctx.logger,
938        colors: &ctx.colors,
939        timer,
940        stats,
941        developer_agent: &ctx.developer_agent,
942        reviewer_agent: &ctx.reviewer_agent,
943        review_guidelines,
944        template_context: &ctx.template_context,
945        run_context: run_context.clone(),
946        execution_history,
947        prompt_history,
948    }
949}
950
951/// Print pipeline info with a specific config.
952fn print_pipeline_info_with_config(ctx: &PipelineContext, config: &crate::config::Config) {
953    ctx.logger.info(&format!(
954        "Working directory: {}{}{}",
955        ctx.colors.cyan(),
956        ctx.repo_root.display(),
957        ctx.colors.reset()
958    ));
959    ctx.logger.info(&format!(
960        "Commit message: {}{}{}",
961        ctx.colors.cyan(),
962        config.commit_msg,
963        ctx.colors.reset()
964    ));
965}
966
967/// Save starting commit or warn if it fails.
968fn save_start_commit_or_warn(ctx: &PipelineContext) {
969    match save_start_commit() {
970        Ok(()) => {
971            if ctx.config.verbosity.is_debug() {
972                ctx.logger
973                    .info("Saved starting commit for incremental diff generation");
974            }
975        }
976        Err(e) => {
977            ctx.logger.warn(&format!(
978                "Failed to save starting commit: {e}. \
979                 Incremental diffs may be unavailable as a result."
980            ));
981            ctx.logger.info(
982                "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
983            );
984        }
985    }
986
987    // Display start commit information to user
988    match get_start_commit_summary() {
989        Ok(summary) => {
990            if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale {
991                ctx.logger.info(&summary.format_compact());
992                if summary.is_stale {
993                    ctx.logger.warn(
994                        "Start commit is stale. Consider running: ralph --reset-start-commit",
995                    );
996                } else if summary.commits_since > 5 {
997                    ctx.logger
998                        .info("Tip: Run 'ralph --show-baseline' for more details");
999                }
1000            }
1001        }
1002        Err(e) => {
1003            // Only show error in debug mode since this is informational
1004            if ctx.config.verbosity.is_debug() {
1005                ctx.logger
1006                    .warn(&format!("Failed to get start commit summary: {e}"));
1007            }
1008        }
1009    }
1010}
1011
1012/// Check for PROMPT.md restoration after a phase.
1013fn check_prompt_restoration(
1014    ctx: &PipelineContext,
1015    prompt_monitor: &mut Option<PromptMonitor>,
1016    phase: &str,
1017) {
1018    if let Some(ref mut monitor) = prompt_monitor {
1019        if monitor.check_and_restore() {
1020            ctx.logger.warn(&format!(
1021                "PROMPT.md was deleted and restored during {phase} phase"
1022            ));
1023        }
1024    }
1025}
1026
1027/// Runs the development phase.
1028fn run_development(
1029    ctx: &mut PhaseContext,
1030    args: &Args,
1031    resume_checkpoint: Option<&PipelineCheckpoint>,
1032) -> anyhow::Result<()> {
1033    ctx.logger
1034        .header("PHASE 1: Development", crate::logger::Colors::blue);
1035
1036    // Check if we should skip this phase based on checkpoint
1037    if let Some(checkpoint) = resume_checkpoint {
1038        if should_skip_phase(PipelinePhase::Development, checkpoint) {
1039            ctx.logger
1040                .info("Skipping development phase (checkpoint indicates it already completed)");
1041            return Ok(());
1042        }
1043    }
1044
1045    if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
1046        ctx.logger
1047            .info("Skipping development phase (resuming from a later checkpoint phase)");
1048        return Ok(());
1049    }
1050
1051    let start_iter = match resume_checkpoint {
1052        Some(checkpoint) => calculate_start_iteration(checkpoint, ctx.config.developer_iters),
1053        None => 1,
1054    };
1055
1056    let resume_context = if args.recovery.resume {
1057        resume_checkpoint.map(|c| c.resume_context())
1058    } else {
1059        None
1060    };
1061    let development_result = run_development_phase(ctx, start_iter, resume_context.as_ref())?;
1062
1063    if development_result.had_errors {
1064        ctx.logger
1065            .warn("Development phase completed with non-fatal errors");
1066    }
1067
1068    Ok(())
1069}
1070
1071/// Runs the review and fix phase.
1072fn run_review_and_fix(
1073    ctx: &mut PhaseContext,
1074    args: &Args,
1075    resume_checkpoint: Option<&PipelineCheckpoint>,
1076) -> anyhow::Result<()> {
1077    ctx.logger
1078        .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
1079
1080    // Check if we should run any reviewer phase
1081    let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
1082        || should_run_from(PipelinePhase::Fix, resume_checkpoint)
1083        || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
1084        || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
1085
1086    let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
1087        || resume_checkpoint
1088            .is_some_and(|c| matches!(c.phase, PipelinePhase::Fix | PipelinePhase::ReviewAgain));
1089
1090    if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
1091        let start_pass = match resume_checkpoint {
1092            Some(checkpoint) => {
1093                calculate_start_reviewer_pass(checkpoint, ctx.config.reviewer_reviews)
1094            }
1095            None => 1,
1096        };
1097
1098        let resume_context = if args.recovery.resume {
1099            resume_checkpoint
1100                .filter(|c| {
1101                    matches!(
1102                        c.phase,
1103                        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain
1104                    )
1105                })
1106                .map(|c| c.resume_context())
1107        } else {
1108            None
1109        };
1110
1111        let review_result = run_review_phase(ctx, start_pass, resume_context.as_ref())?;
1112        if review_result.completed_early {
1113            ctx.logger
1114                .success("Review phase completed early (no issues found)");
1115        }
1116    } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
1117        ctx.logger
1118            .info("Skipping review phase (reviewer_reviews=0)");
1119    } else if run_any_reviewer_phase {
1120        ctx.logger
1121            .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
1122    }
1123
1124    // Note: The old dedicated commit phase has been removed.
1125    // Commits now happen automatically per-iteration during development and per-cycle during review.
1126
1127    Ok(())
1128}
1129
1130/// Runs final validation if configured.
1131fn run_final_validation(
1132    ctx: &PhaseContext,
1133    resume_checkpoint: Option<&PipelineCheckpoint>,
1134) -> anyhow::Result<()> {
1135    let Some(ref full_cmd) = ctx.config.full_check_cmd else {
1136        return Ok(());
1137    };
1138
1139    if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
1140        ctx.logger
1141            .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
1142        ctx.logger
1143            .info("Skipping final validation (resuming from a later checkpoint phase)");
1144        return Ok(());
1145    }
1146
1147    let argv = utils::split_command(full_cmd)
1148        .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
1149    if argv.is_empty() {
1150        ctx.logger
1151            .warn("FULL_CHECK_CMD is empty; skipping final validation");
1152        return Ok(());
1153    }
1154
1155    if ctx.config.features.checkpoint_enabled {
1156        let builder = CheckpointBuilder::new()
1157            .phase(
1158                PipelinePhase::FinalValidation,
1159                ctx.config.developer_iters,
1160                ctx.config.developer_iters,
1161            )
1162            .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1163            .capture_from_context(
1164                ctx.config,
1165                ctx.registry,
1166                ctx.developer_agent,
1167                ctx.reviewer_agent,
1168                ctx.logger,
1169                &ctx.run_context,
1170            );
1171
1172        let builder = builder
1173            .with_execution_history(ctx.execution_history.clone())
1174            .with_prompt_history(ctx.prompt_history.clone());
1175
1176        if let Some(checkpoint) = builder.build() {
1177            let _ = save_checkpoint(&checkpoint);
1178        }
1179    }
1180
1181    ctx.logger
1182        .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
1183    let display_cmd = utils::format_argv_for_log(&argv);
1184    ctx.logger.info(&format!(
1185        "Running full check: {}{}{}",
1186        ctx.colors.dim(),
1187        display_cmd,
1188        ctx.colors.reset()
1189    ));
1190
1191    let Some((program, arguments)) = argv.split_first() else {
1192        ctx.logger
1193            .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
1194        return Ok(());
1195    };
1196    let status = Command::new(program).args(arguments).status()?;
1197
1198    if status.success() {
1199        ctx.logger.success("Full check passed");
1200    } else {
1201        ctx.logger.error("Full check failed");
1202        anyhow::bail!("Full check failed");
1203    }
1204
1205    Ok(())
1206}
1207
1208/// Handle --rebase-only flag.
1209///
1210/// This function performs a rebase to the default branch with AI conflict resolution and exits,
1211/// without running the full pipeline.
1212fn handle_rebase_only(
1213    _args: &Args,
1214    config: &crate::config::Config,
1215    template_context: &TemplateContext,
1216    logger: &Logger,
1217    colors: Colors,
1218) -> anyhow::Result<()> {
1219    // Check if we're on main/master branch
1220    if is_main_or_master_branch()? {
1221        logger.warn("Already on main/master branch - rebasing on main is not recommended");
1222        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1223        logger.info("  git worktree add ../feature-branch feature-branch");
1224        logger.info("This allows multiple AI agents to work on different features simultaneously.");
1225        logger.info("Proceeding with rebase anyway as requested...");
1226    }
1227
1228    logger.header("Rebase to default branch", Colors::cyan);
1229
1230    match run_rebase_to_default(logger, colors) {
1231        Ok(RebaseResult::Success) => {
1232            logger.success("Rebase completed successfully");
1233            Ok(())
1234        }
1235        Ok(RebaseResult::NoOp { reason }) => {
1236            logger.info(&format!("No rebase needed: {reason}"));
1237            Ok(())
1238        }
1239        Ok(RebaseResult::Failed(err)) => {
1240            logger.error(&format!("Rebase failed: {err}"));
1241            anyhow::bail!("Rebase failed: {err}")
1242        }
1243        Ok(RebaseResult::Conflicts(_conflicts)) => {
1244            // Get the actual conflicted files
1245            let conflicted_files = get_conflicted_files()?;
1246            if conflicted_files.is_empty() {
1247                logger.warn("Rebase reported conflicts but no conflicted files found");
1248                let _ = abort_rebase();
1249                return Ok(());
1250            }
1251
1252            logger.warn(&format!(
1253                "Rebase resulted in {} conflict(s), attempting AI resolution",
1254                conflicted_files.len()
1255            ));
1256
1257            // For --rebase-only, we don't have a full PhaseContext, so we use a wrapper
1258            match try_resolve_conflicts_without_phase_ctx(
1259                &conflicted_files,
1260                config,
1261                template_context,
1262                logger,
1263                colors,
1264            ) {
1265                Ok(true) => {
1266                    // Conflicts resolved, continue the rebase
1267                    logger.info("Continuing rebase after conflict resolution");
1268                    match continue_rebase() {
1269                        Ok(()) => {
1270                            logger.success("Rebase completed successfully after AI resolution");
1271                            Ok(())
1272                        }
1273                        Err(e) => {
1274                            logger.error(&format!("Failed to continue rebase: {e}"));
1275                            let _ = abort_rebase();
1276                            anyhow::bail!("Rebase failed after conflict resolution")
1277                        }
1278                    }
1279                }
1280                Ok(false) => {
1281                    // AI resolution failed
1282                    logger.error("AI conflict resolution failed, aborting rebase");
1283                    let _ = abort_rebase();
1284                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
1285                }
1286                Err(e) => {
1287                    logger.error(&format!("Conflict resolution error: {e}"));
1288                    let _ = abort_rebase();
1289                    anyhow::bail!("Rebase conflict resolution failed: {e}")
1290                }
1291            }
1292        }
1293        Err(e) => {
1294            logger.error(&format!("Rebase failed: {e}"));
1295            anyhow::bail!("Rebase failed: {e}")
1296        }
1297    }
1298}
1299
1300/// Run rebase to the default branch.
1301///
1302/// This function performs a rebase from the current branch to the
1303/// default branch (main/master). It handles all edge cases including:
1304/// - Already on main/master (proceeds with rebase attempt)
1305/// - Empty repository (returns `NoOp`)
1306/// - Upstream branch not found (error)
1307/// - Conflicts during rebase (returns `Conflicts` result)
1308///
1309/// # Returns
1310///
1311/// Returns `RebaseResult` indicating the outcome.
1312fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
1313    // Get the default branch
1314    let default_branch = get_default_branch()?;
1315    logger.info(&format!(
1316        "Rebasing onto {}{}{}",
1317        colors.cyan(),
1318        default_branch,
1319        colors.reset()
1320    ));
1321
1322    // Perform the rebase
1323    rebase_onto(&default_branch)
1324}
1325
1326/// Run initial rebase before development phase.
1327///
1328/// This function is called before the development phase starts to ensure
1329/// the feature branch is up-to-date with the default branch.
1330///
1331/// Uses a state machine for fault tolerance and automatic recovery from
1332/// interruptions or failures.
1333///
1334/// # Rebase Control
1335///
1336/// Rebase is only performed when both conditions are met:
1337/// - `--with-rebase` CLI flag is set (caller already checked this)
1338/// - `auto_rebase` config is enabled (checked here)
1339fn run_initial_rebase(
1340    ctx: &PipelineContext,
1341    phase_ctx: &mut PhaseContext,
1342    run_context: &crate::checkpoint::RunContext,
1343) -> anyhow::Result<()> {
1344    ctx.logger.header("Pre-development rebase", Colors::cyan);
1345
1346    // Record execution step: pre-rebase started
1347    let step = ExecutionStep::new(
1348        "PreRebase",
1349        0,
1350        "pre_rebase_start",
1351        StepOutcome::success(None, vec![]),
1352    );
1353    phase_ctx.execution_history.add_step(step);
1354
1355    // Save checkpoint at start of pre-rebase phase
1356    if ctx.config.features.checkpoint_enabled {
1357        let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
1358        let mut builder = CheckpointBuilder::new()
1359            .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
1360            .reviewer_pass(0, ctx.config.reviewer_reviews)
1361            .capture_from_context(
1362                &ctx.config,
1363                &ctx.registry,
1364                &ctx.developer_agent,
1365                &ctx.reviewer_agent,
1366                &ctx.logger,
1367                run_context,
1368            );
1369
1370        // Include prompt history and execution history for hardened resume
1371        builder = builder
1372            .with_execution_history(phase_ctx.execution_history.clone())
1373            .with_prompt_history(phase_ctx.clone_prompt_history());
1374
1375        if let Some(mut checkpoint) = builder.build() {
1376            checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
1377                upstream_branch: default_branch,
1378            };
1379            let _ = save_checkpoint(&checkpoint);
1380        }
1381    }
1382
1383    match run_rebase_to_default(&ctx.logger, ctx.colors) {
1384        Ok(RebaseResult::Success) => {
1385            ctx.logger.success("Rebase completed successfully");
1386            // Record execution step: pre-rebase completed successfully
1387            let step = ExecutionStep::new(
1388                "PreRebase",
1389                0,
1390                "pre_rebase_complete",
1391                StepOutcome::success(None, vec![]),
1392            );
1393            phase_ctx.execution_history.add_step(step);
1394
1395            // Save checkpoint after pre-rebase completes successfully
1396            if ctx.config.features.checkpoint_enabled {
1397                let builder = CheckpointBuilder::new()
1398                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1399                    .reviewer_pass(0, ctx.config.reviewer_reviews)
1400                    .skip_rebase(true) // Pre-rebase is done
1401                    .capture_from_context(
1402                        &ctx.config,
1403                        &ctx.registry,
1404                        &ctx.developer_agent,
1405                        &ctx.reviewer_agent,
1406                        &ctx.logger,
1407                        run_context,
1408                    )
1409                    .with_execution_history(phase_ctx.execution_history.clone())
1410                    .with_prompt_history(phase_ctx.clone_prompt_history());
1411
1412                if let Some(checkpoint) = builder.build() {
1413                    let _ = save_checkpoint(&checkpoint);
1414                }
1415            }
1416
1417            Ok(())
1418        }
1419        Ok(RebaseResult::NoOp { reason }) => {
1420            ctx.logger.info(&format!("No rebase needed: {reason}"));
1421            // Record execution step: pre-rebase skipped
1422            let step = ExecutionStep::new(
1423                "PreRebase",
1424                0,
1425                "pre_rebase_skipped",
1426                StepOutcome::skipped(reason.clone()),
1427            );
1428            phase_ctx.execution_history.add_step(step);
1429
1430            // Save checkpoint after pre-rebase no-op
1431            if ctx.config.features.checkpoint_enabled {
1432                let builder = CheckpointBuilder::new()
1433                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1434                    .reviewer_pass(0, ctx.config.reviewer_reviews)
1435                    .skip_rebase(true) // Pre-rebase is done
1436                    .capture_from_context(
1437                        &ctx.config,
1438                        &ctx.registry,
1439                        &ctx.developer_agent,
1440                        &ctx.reviewer_agent,
1441                        &ctx.logger,
1442                        run_context,
1443                    )
1444                    .with_execution_history(phase_ctx.execution_history.clone())
1445                    .with_prompt_history(phase_ctx.clone_prompt_history());
1446
1447                if let Some(checkpoint) = builder.build() {
1448                    let _ = save_checkpoint(&checkpoint);
1449                }
1450            }
1451
1452            Ok(())
1453        }
1454        Ok(RebaseResult::Conflicts(_conflicts)) => {
1455            // Get the actual conflicted files
1456            let conflicted_files = get_conflicted_files()?;
1457            if conflicted_files.is_empty() {
1458                ctx.logger
1459                    .warn("Rebase reported conflicts but no conflicted files found");
1460                let _ = abort_rebase();
1461                return Ok(());
1462            }
1463
1464            // Record execution step: pre-rebase conflicts detected
1465            let step = ExecutionStep::new(
1466                "PreRebase",
1467                0,
1468                "pre_rebase_conflict",
1469                StepOutcome::partial(
1470                    "Rebase started".to_string(),
1471                    format!("{} conflicts detected", conflicted_files.len()),
1472                ),
1473            );
1474            phase_ctx.execution_history.add_step(step);
1475
1476            // Save checkpoint for conflict state
1477            if ctx.config.features.checkpoint_enabled {
1478                let mut builder = CheckpointBuilder::new()
1479                    .phase(
1480                        PipelinePhase::PreRebaseConflict,
1481                        0,
1482                        ctx.config.developer_iters,
1483                    )
1484                    .reviewer_pass(0, ctx.config.reviewer_reviews)
1485                    .capture_from_context(
1486                        &ctx.config,
1487                        &ctx.registry,
1488                        &ctx.developer_agent,
1489                        &ctx.reviewer_agent,
1490                        &ctx.logger,
1491                        run_context,
1492                    );
1493
1494                // Include prompt history and execution history for hardened resume
1495                builder = builder
1496                    .with_execution_history(phase_ctx.execution_history.clone())
1497                    .with_prompt_history(phase_ctx.clone_prompt_history());
1498
1499                if let Some(mut checkpoint) = builder.build() {
1500                    checkpoint.rebase_state = RebaseState::HasConflicts {
1501                        files: conflicted_files.clone(),
1502                    };
1503                    let _ = save_checkpoint(&checkpoint);
1504                }
1505            }
1506
1507            ctx.logger.warn(&format!(
1508                "Rebase resulted in {} conflict(s), attempting AI resolution",
1509                conflicted_files.len()
1510            ));
1511
1512            // Attempt to resolve conflicts with AI
1513            match try_resolve_conflicts_with_fallback(
1514                &conflicted_files,
1515                &ctx.config,
1516                &ctx.template_context,
1517                &ctx.logger,
1518                ctx.colors,
1519                phase_ctx,
1520                "PreRebase",
1521            ) {
1522                Ok(true) => {
1523                    // Conflicts resolved, continue the rebase
1524                    ctx.logger
1525                        .info("Continuing rebase after conflict resolution");
1526                    match continue_rebase() {
1527                        Ok(()) => {
1528                            ctx.logger
1529                                .success("Rebase completed successfully after AI resolution");
1530                            // Record execution step: conflicts resolved successfully
1531                            let step = ExecutionStep::new(
1532                                "PreRebase",
1533                                0,
1534                                "pre_rebase_resolution",
1535                                StepOutcome::success(None, vec![]),
1536                            );
1537                            phase_ctx.execution_history.add_step(step);
1538
1539                            // Save checkpoint after pre-rebase conflict resolution completes
1540                            if ctx.config.features.checkpoint_enabled {
1541                                let builder = CheckpointBuilder::new()
1542                                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1543                                    .reviewer_pass(0, ctx.config.reviewer_reviews)
1544                                    .skip_rebase(true) // Pre-rebase is done
1545                                    .capture_from_context(
1546                                        &ctx.config,
1547                                        &ctx.registry,
1548                                        &ctx.developer_agent,
1549                                        &ctx.reviewer_agent,
1550                                        &ctx.logger,
1551                                        run_context,
1552                                    )
1553                                    .with_execution_history(phase_ctx.execution_history.clone())
1554                                    .with_prompt_history(phase_ctx.clone_prompt_history());
1555
1556                                if let Some(checkpoint) = builder.build() {
1557                                    let _ = save_checkpoint(&checkpoint);
1558                                }
1559                            }
1560
1561                            Ok(())
1562                        }
1563                        Err(e) => {
1564                            ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
1565                            let _ = abort_rebase();
1566                            // Record execution step: resolution succeeded but continue failed
1567                            let step = ExecutionStep::new(
1568                                "PreRebase",
1569                                0,
1570                                "pre_rebase_resolution",
1571                                StepOutcome::partial(
1572                                    "Conflicts resolved by AI".to_string(),
1573                                    format!("Failed to continue rebase: {e}"),
1574                                ),
1575                            );
1576                            phase_ctx.execution_history.add_step(step);
1577                            Ok(()) // Continue anyway - conflicts were resolved
1578                        }
1579                    }
1580                }
1581                Ok(false) => {
1582                    // AI resolution failed
1583                    ctx.logger
1584                        .warn("AI conflict resolution failed, aborting rebase");
1585                    let _ = abort_rebase();
1586                    // Record execution step: resolution failed
1587                    let step = ExecutionStep::new(
1588                        "PreRebase",
1589                        0,
1590                        "pre_rebase_resolution",
1591                        StepOutcome::failure("AI conflict resolution failed".to_string(), true),
1592                    );
1593                    phase_ctx.execution_history.add_step(step);
1594                    Ok(()) // Continue pipeline - don't block on rebase failure
1595                }
1596                Err(e) => {
1597                    ctx.logger.error(&format!("Conflict resolution error: {e}"));
1598                    let _ = abort_rebase();
1599                    // Record execution step: resolution error
1600                    let step = ExecutionStep::new(
1601                        "PreRebase",
1602                        0,
1603                        "pre_rebase_resolution",
1604                        StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
1605                    );
1606                    phase_ctx.execution_history.add_step(step);
1607                    Ok(()) // Continue pipeline
1608                }
1609            }
1610        }
1611        Ok(RebaseResult::Failed(err)) => {
1612            ctx.logger.error(&format!("Rebase failed: {err}"));
1613            let _ = abort_rebase();
1614            // Record execution step: rebase failed
1615            let step = ExecutionStep::new(
1616                "PreRebase",
1617                0,
1618                "pre_rebase_failed",
1619                StepOutcome::failure(format!("Rebase failed: {err}"), true),
1620            );
1621            phase_ctx.execution_history.add_step(step);
1622            Ok(()) // Continue pipeline despite failure
1623        }
1624        Err(e) => {
1625            ctx.logger
1626                .warn(&format!("Rebase failed, continuing without rebase: {e}"));
1627            // Record execution step: rebase error
1628            let step = ExecutionStep::new(
1629                "PreRebase",
1630                0,
1631                "pre_rebase_error",
1632                StepOutcome::failure(format!("Rebase error: {e}"), true),
1633            );
1634            phase_ctx.execution_history.add_step(step);
1635            Ok(())
1636        }
1637    }
1638}
1639
1640/// Run post-review rebase after review phase.
1641///
1642/// This function is called after the review phase completes to ensure
1643/// the feature branch is still up-to-date with the default branch.
1644///
1645/// Uses a state machine for fault tolerance and automatic recovery from
1646/// interruptions or failures.
1647///
1648/// # Rebase Control
1649///
1650/// Rebase is only performed when both conditions are met:
1651/// - `--with-rebase` CLI flag is set (caller already checked this)
1652/// - `auto_rebase` config is enabled (checked here)
1653fn run_post_review_rebase(
1654    ctx: &PipelineContext,
1655    phase_ctx: &mut PhaseContext,
1656    run_context: &crate::checkpoint::RunContext,
1657) -> anyhow::Result<()> {
1658    ctx.logger.header("Post-review rebase", Colors::cyan);
1659
1660    // Record execution step: post-rebase started
1661    let step = ExecutionStep::new(
1662        "PostRebase",
1663        ctx.config.developer_iters,
1664        "post_rebase_start",
1665        StepOutcome::success(None, vec![]),
1666    );
1667    phase_ctx.execution_history.add_step(step);
1668
1669    // Save checkpoint at start of post-rebase phase
1670    if ctx.config.features.checkpoint_enabled {
1671        let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
1672        let mut builder = CheckpointBuilder::new()
1673            .phase(
1674                PipelinePhase::PostRebase,
1675                ctx.config.developer_iters,
1676                ctx.config.developer_iters,
1677            )
1678            .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1679            .capture_from_context(
1680                &ctx.config,
1681                &ctx.registry,
1682                &ctx.developer_agent,
1683                &ctx.reviewer_agent,
1684                &ctx.logger,
1685                run_context,
1686            );
1687
1688        // Include prompt history and execution history for hardened resume
1689        builder = builder
1690            .with_execution_history(phase_ctx.execution_history.clone())
1691            .with_prompt_history(phase_ctx.clone_prompt_history());
1692
1693        if let Some(mut checkpoint) = builder.build() {
1694            checkpoint.rebase_state = RebaseState::PostRebaseInProgress {
1695                upstream_branch: default_branch,
1696            };
1697            let _ = save_checkpoint(&checkpoint);
1698        }
1699    }
1700
1701    match run_rebase_to_default(&ctx.logger, ctx.colors) {
1702        Ok(RebaseResult::Success) => {
1703            ctx.logger.success("Rebase completed successfully");
1704            // Record execution step: post-rebase completed successfully
1705            let step = ExecutionStep::new(
1706                "PostRebase",
1707                ctx.config.developer_iters,
1708                "post_rebase_complete",
1709                StepOutcome::success(None, vec![]),
1710            );
1711            phase_ctx.execution_history.add_step(step);
1712
1713            // Save checkpoint after post-review rebase completes successfully
1714            if ctx.config.features.checkpoint_enabled {
1715                let builder = CheckpointBuilder::new()
1716                    .phase(
1717                        PipelinePhase::CommitMessage,
1718                        ctx.config.developer_iters,
1719                        ctx.config.developer_iters,
1720                    )
1721                    .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1722                    .skip_rebase(true) // Post-rebase is done
1723                    .capture_from_context(
1724                        &ctx.config,
1725                        &ctx.registry,
1726                        &ctx.developer_agent,
1727                        &ctx.reviewer_agent,
1728                        &ctx.logger,
1729                        run_context,
1730                    )
1731                    .with_execution_history(phase_ctx.execution_history.clone())
1732                    .with_prompt_history(phase_ctx.clone_prompt_history());
1733
1734                if let Some(checkpoint) = builder.build() {
1735                    let _ = save_checkpoint(&checkpoint);
1736                }
1737            }
1738
1739            Ok(())
1740        }
1741        Ok(RebaseResult::NoOp { reason }) => {
1742            ctx.logger.info(&format!("No rebase needed: {reason}"));
1743            // Record execution step: post-rebase skipped
1744            let step = ExecutionStep::new(
1745                "PostRebase",
1746                ctx.config.developer_iters,
1747                "post_rebase_skipped",
1748                StepOutcome::skipped(reason.clone()),
1749            );
1750            phase_ctx.execution_history.add_step(step);
1751
1752            // Save checkpoint after post-review rebase no-op
1753            if ctx.config.features.checkpoint_enabled {
1754                let builder = CheckpointBuilder::new()
1755                    .phase(
1756                        PipelinePhase::CommitMessage,
1757                        ctx.config.developer_iters,
1758                        ctx.config.developer_iters,
1759                    )
1760                    .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1761                    .skip_rebase(true) // Post-rebase is done
1762                    .capture_from_context(
1763                        &ctx.config,
1764                        &ctx.registry,
1765                        &ctx.developer_agent,
1766                        &ctx.reviewer_agent,
1767                        &ctx.logger,
1768                        run_context,
1769                    )
1770                    .with_execution_history(phase_ctx.execution_history.clone())
1771                    .with_prompt_history(phase_ctx.clone_prompt_history());
1772
1773                if let Some(checkpoint) = builder.build() {
1774                    let _ = save_checkpoint(&checkpoint);
1775                }
1776            }
1777
1778            Ok(())
1779        }
1780        Ok(RebaseResult::Conflicts(_conflicts)) => {
1781            // Get the actual conflicted files
1782            let conflicted_files = get_conflicted_files()?;
1783            if conflicted_files.is_empty() {
1784                ctx.logger
1785                    .warn("Rebase reported conflicts but no conflicted files found");
1786                let _ = abort_rebase();
1787                return Ok(());
1788            }
1789
1790            // Record execution step: post-rebase conflicts detected
1791            let step = ExecutionStep::new(
1792                "PostRebase",
1793                ctx.config.developer_iters,
1794                "post_rebase_conflict",
1795                StepOutcome::partial(
1796                    "Rebase started".to_string(),
1797                    format!("{} conflicts detected", conflicted_files.len()),
1798                ),
1799            );
1800            phase_ctx.execution_history.add_step(step);
1801
1802            // Save checkpoint for conflict state
1803            if ctx.config.features.checkpoint_enabled {
1804                let mut builder = CheckpointBuilder::new()
1805                    .phase(
1806                        PipelinePhase::PostRebaseConflict,
1807                        ctx.config.developer_iters,
1808                        ctx.config.developer_iters,
1809                    )
1810                    .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1811                    .capture_from_context(
1812                        &ctx.config,
1813                        &ctx.registry,
1814                        &ctx.developer_agent,
1815                        &ctx.reviewer_agent,
1816                        &ctx.logger,
1817                        run_context,
1818                    );
1819
1820                // Include prompt history and execution history for hardened resume
1821                builder = builder
1822                    .with_execution_history(phase_ctx.execution_history.clone())
1823                    .with_prompt_history(phase_ctx.clone_prompt_history());
1824
1825                if let Some(mut checkpoint) = builder.build() {
1826                    checkpoint.rebase_state = RebaseState::HasConflicts {
1827                        files: conflicted_files.clone(),
1828                    };
1829                    let _ = save_checkpoint(&checkpoint);
1830                }
1831            }
1832
1833            ctx.logger.warn(&format!(
1834                "Rebase resulted in {} conflict(s), attempting AI resolution",
1835                conflicted_files.len()
1836            ));
1837
1838            // Attempt to resolve conflicts with AI
1839            match try_resolve_conflicts_with_fallback(
1840                &conflicted_files,
1841                &ctx.config,
1842                &ctx.template_context,
1843                &ctx.logger,
1844                ctx.colors,
1845                phase_ctx,
1846                "PostRebase",
1847            ) {
1848                Ok(true) => {
1849                    // Conflicts resolved, continue the rebase
1850                    ctx.logger
1851                        .info("Continuing rebase after conflict resolution");
1852                    match continue_rebase() {
1853                        Ok(()) => {
1854                            ctx.logger
1855                                .success("Rebase completed successfully after AI resolution");
1856                            // Record execution step: conflicts resolved successfully
1857                            let step = ExecutionStep::new(
1858                                "PostRebase",
1859                                ctx.config.developer_iters,
1860                                "post_rebase_resolution",
1861                                StepOutcome::success(None, vec![]),
1862                            );
1863                            phase_ctx.execution_history.add_step(step);
1864
1865                            // Save checkpoint after post-review rebase conflict resolution completes
1866                            if ctx.config.features.checkpoint_enabled {
1867                                let builder = CheckpointBuilder::new()
1868                                    .phase(
1869                                        PipelinePhase::CommitMessage,
1870                                        ctx.config.developer_iters,
1871                                        ctx.config.developer_iters,
1872                                    )
1873                                    .reviewer_pass(
1874                                        ctx.config.reviewer_reviews,
1875                                        ctx.config.reviewer_reviews,
1876                                    )
1877                                    .skip_rebase(true) // Post-rebase is done
1878                                    .capture_from_context(
1879                                        &ctx.config,
1880                                        &ctx.registry,
1881                                        &ctx.developer_agent,
1882                                        &ctx.reviewer_agent,
1883                                        &ctx.logger,
1884                                        run_context,
1885                                    )
1886                                    .with_execution_history(phase_ctx.execution_history.clone())
1887                                    .with_prompt_history(phase_ctx.clone_prompt_history());
1888
1889                                if let Some(checkpoint) = builder.build() {
1890                                    let _ = save_checkpoint(&checkpoint);
1891                                }
1892                            }
1893
1894                            Ok(())
1895                        }
1896                        Err(e) => {
1897                            ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
1898                            let _ = abort_rebase();
1899                            // Record execution step: resolution succeeded but continue failed
1900                            let step = ExecutionStep::new(
1901                                "PostRebase",
1902                                ctx.config.developer_iters,
1903                                "post_rebase_resolution",
1904                                StepOutcome::partial(
1905                                    "Conflicts resolved by AI".to_string(),
1906                                    format!("Failed to continue rebase: {e}"),
1907                                ),
1908                            );
1909                            phase_ctx.execution_history.add_step(step);
1910                            Ok(()) // Continue anyway - conflicts were resolved
1911                        }
1912                    }
1913                }
1914                Ok(false) => {
1915                    // AI resolution failed
1916                    ctx.logger
1917                        .warn("AI conflict resolution failed, aborting rebase");
1918                    let _ = abort_rebase();
1919                    // Record execution step: resolution failed
1920                    let step = ExecutionStep::new(
1921                        "PostRebase",
1922                        ctx.config.developer_iters,
1923                        "post_rebase_resolution",
1924                        StepOutcome::failure("AI conflict resolution failed".to_string(), true),
1925                    );
1926                    phase_ctx.execution_history.add_step(step);
1927                    Ok(()) // Continue pipeline - don't block on rebase failure
1928                }
1929                Err(e) => {
1930                    ctx.logger.error(&format!("Conflict resolution error: {e}"));
1931                    let _ = abort_rebase();
1932                    // Record execution step: resolution error
1933                    let step = ExecutionStep::new(
1934                        "PostRebase",
1935                        ctx.config.developer_iters,
1936                        "post_rebase_resolution",
1937                        StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
1938                    );
1939                    phase_ctx.execution_history.add_step(step);
1940                    Ok(()) // Continue pipeline
1941                }
1942            }
1943        }
1944        Ok(RebaseResult::Failed(err)) => {
1945            ctx.logger.error(&format!("Rebase failed: {err}"));
1946            let _ = abort_rebase();
1947            // Record execution step: rebase failed
1948            let step = ExecutionStep::new(
1949                "PostRebase",
1950                ctx.config.developer_iters,
1951                "post_rebase_failed",
1952                StepOutcome::failure(format!("Rebase failed: {err}"), true),
1953            );
1954            phase_ctx.execution_history.add_step(step);
1955            Ok(()) // Continue pipeline despite failure
1956        }
1957        Err(e) => {
1958            ctx.logger
1959                .warn(&format!("Rebase failed, continuing without rebase: {e}"));
1960            // Record execution step: rebase error
1961            let step = ExecutionStep::new(
1962                "PostRebase",
1963                ctx.config.developer_iters,
1964                "post_rebase_error",
1965                StepOutcome::failure(format!("Rebase error: {e}"), true),
1966            );
1967            phase_ctx.execution_history.add_step(step);
1968            Ok(())
1969        }
1970    }
1971}
1972
1973/// Result type for conflict resolution attempts.
1974///
1975/// Represents the different ways conflict resolution can succeed or fail.
1976enum ConflictResolutionResult {
1977    /// Agent provided JSON output with resolved file contents
1978    WithJson(String),
1979    /// Agent resolved conflicts by editing files directly (no JSON output)
1980    FileEditsOnly,
1981    /// Resolution failed completely
1982    Failed,
1983}
1984
1985/// Attempt to resolve rebase conflicts with AI fallback.
1986///
1987/// This function accepts `PhaseContext` to capture prompts and track
1988/// execution history for hardened resume functionality.
1989fn try_resolve_conflicts_with_fallback(
1990    conflicted_files: &[String],
1991    config: &crate::config::Config,
1992    template_context: &TemplateContext,
1993    logger: &Logger,
1994    colors: Colors,
1995    phase_ctx: &mut PhaseContext<'_>,
1996    phase: &str,
1997) -> anyhow::Result<bool> {
1998    if conflicted_files.is_empty() {
1999        return Ok(false);
2000    }
2001
2002    logger.info(&format!(
2003        "Attempting AI conflict resolution for {} file(s)",
2004        conflicted_files.len()
2005    ));
2006
2007    let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
2008
2009    // Use stored_or_generate pattern for hardened resume
2010    // On resume, use the exact same prompt that was used before
2011    let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2012    let (resolution_prompt, was_replayed) =
2013        get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2014            build_resolution_prompt(&conflicts, template_context)
2015        });
2016
2017    // Capture the resolution prompt for deterministic resume (only if newly generated)
2018    if !was_replayed {
2019        phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2020    } else {
2021        logger.info(&format!(
2022            "Using stored prompt from checkpoint for determinism: {}",
2023            prompt_key
2024        ));
2025    }
2026
2027    match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
2028        Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2029            // Agent provided JSON output - attempt to parse and write files
2030            // JSON is optional for verification - LibGit2 state is authoritative
2031            match parse_and_validate_resolved_files(&resolved_content, logger) {
2032                Ok(resolved_files) => {
2033                    write_resolved_files(&resolved_files, logger)?;
2034                }
2035                Err(_) => {
2036                    // JSON parsing failed - this is expected and normal
2037                    // We verify conflicts via LibGit2 state, not JSON parsing
2038                    // Continue to LibGit2 verification below
2039                }
2040            }
2041
2042            // Verify all conflicts are resolved via LibGit2 (authoritative source)
2043            let remaining_conflicts = get_conflicted_files()?;
2044            if remaining_conflicts.is_empty() {
2045                Ok(true)
2046            } else {
2047                logger.warn(&format!(
2048                    "{} conflicts remain after AI resolution",
2049                    remaining_conflicts.len()
2050                ));
2051                Ok(false)
2052            }
2053        }
2054        Ok(ConflictResolutionResult::FileEditsOnly) => {
2055            // Agent resolved conflicts by editing files directly
2056            logger.info("Agent resolved conflicts via file edits (no JSON output)");
2057
2058            // Verify all conflicts are resolved
2059            let remaining_conflicts = get_conflicted_files()?;
2060            if remaining_conflicts.is_empty() {
2061                logger.success("All conflicts resolved via file edits");
2062                Ok(true)
2063            } else {
2064                logger.warn(&format!(
2065                    "{} conflicts remain after AI resolution",
2066                    remaining_conflicts.len()
2067                ));
2068                Ok(false)
2069            }
2070        }
2071        Ok(ConflictResolutionResult::Failed) => {
2072            logger.warn("AI conflict resolution failed");
2073            logger.info("Attempting to continue rebase anyway...");
2074
2075            // Try to continue rebase - user may have manually resolved conflicts
2076            match crate::git_helpers::continue_rebase() {
2077                Ok(()) => {
2078                    logger.info("Successfully continued rebase");
2079                    Ok(true)
2080                }
2081                Err(rebase_err) => {
2082                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
2083                    Ok(false) // Conflicts remain
2084                }
2085            }
2086        }
2087        Err(e) => {
2088            logger.warn(&format!("AI conflict resolution failed: {e}"));
2089            logger.info("Attempting to continue rebase anyway...");
2090
2091            // Try to continue rebase - user may have manually resolved conflicts
2092            match crate::git_helpers::continue_rebase() {
2093                Ok(()) => {
2094                    logger.info("Successfully continued rebase");
2095                    Ok(true)
2096                }
2097                Err(rebase_err) => {
2098                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
2099                    Ok(false) // Conflicts remain
2100                }
2101            }
2102        }
2103    }
2104}
2105
2106/// Wrapper for conflict resolution without PhaseContext.
2107///
2108/// This is used for --rebase-only mode where we don't have a full pipeline context.
2109/// It creates a minimal PhaseContext for the conflict resolution call.
2110fn try_resolve_conflicts_without_phase_ctx(
2111    conflicted_files: &[String],
2112    config: &crate::config::Config,
2113    template_context: &TemplateContext,
2114    logger: &Logger,
2115    colors: Colors,
2116) -> anyhow::Result<bool> {
2117    use crate::agents::AgentRegistry;
2118    use crate::checkpoint::execution_history::ExecutionHistory;
2119    use crate::checkpoint::RunContext;
2120    use crate::pipeline::{Stats, Timer};
2121
2122    // Create minimal PhaseContext for conflict resolution
2123    let registry = AgentRegistry::new()?;
2124    let mut timer = Timer::new();
2125    let mut stats = Stats::default();
2126
2127    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2128    let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2129
2130    let mut phase_ctx = PhaseContext {
2131        config,
2132        registry: &registry,
2133        logger,
2134        colors: &colors,
2135        timer: &mut timer,
2136        stats: &mut stats,
2137        developer_agent,
2138        reviewer_agent,
2139        review_guidelines: None,
2140        template_context,
2141        run_context: RunContext::new(),
2142        execution_history: ExecutionHistory::new(),
2143        prompt_history: std::collections::HashMap::new(),
2144    };
2145
2146    try_resolve_conflicts_with_fallback(
2147        conflicted_files,
2148        config,
2149        template_context,
2150        logger,
2151        colors,
2152        &mut phase_ctx,
2153        "RebaseOnly",
2154    )
2155}
2156
2157/// Collect conflict information from conflicted files.
2158fn collect_conflict_info_or_error(
2159    conflicted_files: &[String],
2160    logger: &Logger,
2161) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2162    use crate::prompts::collect_conflict_info;
2163
2164    let conflicts = match collect_conflict_info(conflicted_files) {
2165        Ok(c) => c,
2166        Err(e) => {
2167            logger.error(&format!("Failed to collect conflict info: {e}"));
2168            anyhow::bail!("Failed to collect conflict info");
2169        }
2170    };
2171    Ok(conflicts)
2172}
2173
2174/// Build the conflict resolution prompt from context files.
2175fn build_resolution_prompt(
2176    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2177    template_context: &TemplateContext,
2178) -> String {
2179    build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2180        .unwrap_or_else(|_| String::new())
2181}
2182
2183/// Build the conflict resolution prompt.
2184///
2185/// This function uses the standard conflict resolution prompt.
2186fn build_enhanced_resolution_prompt(
2187    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2188    _branch_info: Option<()>, // Kept for API compatibility, currently unused
2189    template_context: &TemplateContext,
2190) -> anyhow::Result<String> {
2191    use std::fs;
2192
2193    let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2194    let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2195
2196    // Use standard prompt
2197    Ok(
2198        crate::prompts::build_conflict_resolution_prompt_with_context(
2199            template_context,
2200            conflicts,
2201            prompt_md_content.as_deref(),
2202            plan_content.as_deref(),
2203        ),
2204    )
2205}
2206
2207/// Run AI agent to resolve conflicts with fallback mechanism.
2208///
2209/// Returns `ConflictResolutionResult` indicating whether the agent provided
2210/// JSON output, resolved conflicts via file edits, or failed completely.
2211fn run_ai_conflict_resolution(
2212    resolution_prompt: &str,
2213    config: &crate::config::Config,
2214    logger: &Logger,
2215    colors: Colors,
2216) -> anyhow::Result<ConflictResolutionResult> {
2217    use crate::agents::AgentRegistry;
2218    use crate::files::result_extraction::extract_last_result;
2219    use crate::pipeline::{
2220        run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2221    };
2222    use std::io;
2223    use std::path::Path;
2224
2225    // Note: log_dir is used as a prefix for log file names, not as a directory.
2226    // The actual log files will be created in .agent/logs/ with names like:
2227    // .agent/logs/rebase_conflict_resolution_ccs-glm_0.log
2228    let log_dir = ".agent/logs/rebase_conflict_resolution";
2229
2230    let registry = AgentRegistry::new()?;
2231    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2232
2233    let mut runtime = PipelineRuntime {
2234        timer: &mut crate::pipeline::Timer::new(),
2235        logger,
2236        colors: &colors,
2237        config,
2238        #[cfg(any(test, feature = "test-utils"))]
2239        agent_executor: None,
2240    };
2241
2242    // Output validator: checks if agent produced valid output OR resolved conflicts
2243    // Agents may edit files without returning JSON, so we verify conflicts are resolved.
2244    let validate_output: OutputValidator = |log_dir_path: &Path,
2245                                            validation_logger: &crate::logger::Logger|
2246     -> io::Result<bool> {
2247        match extract_last_result(log_dir_path) {
2248            Ok(Some(_)) => {
2249                // Valid JSON output exists
2250                Ok(true)
2251            }
2252            Ok(None) => {
2253                // No JSON output - check if conflicts were resolved anyway
2254                // (agent may have edited files without returning JSON)
2255                match crate::git_helpers::get_conflicted_files() {
2256                    Ok(conflicts) if conflicts.is_empty() => {
2257                        validation_logger
2258                            .info("Agent resolved conflicts without JSON output (file edits only)");
2259                        Ok(true) // Conflicts resolved, consider success
2260                    }
2261                    Ok(conflicts) => {
2262                        validation_logger.warn(&format!(
2263                            "{} conflict(s) remain unresolved",
2264                            conflicts.len()
2265                        ));
2266                        Ok(false) // Conflicts remain
2267                    }
2268                    Err(e) => {
2269                        validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2270                        Ok(false) // Error checking conflicts
2271                    }
2272                }
2273            }
2274            Err(e) => {
2275                validation_logger.warn(&format!("Output validation check failed: {e}"));
2276                Ok(false) // Treat validation errors as missing output
2277            }
2278        }
2279    };
2280
2281    let mut fallback_config = FallbackConfig {
2282        role: crate::agents::AgentRole::Reviewer,
2283        base_label: "conflict resolution",
2284        prompt: resolution_prompt,
2285        logfile_prefix: log_dir,
2286        runtime: &mut runtime,
2287        registry: &registry,
2288        primary_agent: reviewer_agent,
2289        output_validator: Some(validate_output),
2290    };
2291
2292    let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2293
2294    if exit_code != 0 {
2295        return Ok(ConflictResolutionResult::Failed);
2296    }
2297
2298    // Check if conflicts are resolved after agent run
2299    // The validator already checked this, but we verify again to determine the result type
2300    let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2301
2302    if remaining_conflicts.is_empty() {
2303        // Conflicts are resolved - check if agent provided JSON output
2304        match extract_last_result(Path::new(log_dir)) {
2305            Ok(Some(content)) => {
2306                logger.info("Agent provided JSON output with resolved files");
2307                Ok(ConflictResolutionResult::WithJson(content))
2308            }
2309            Ok(None) => {
2310                logger.info("Agent resolved conflicts via file edits (no JSON output)");
2311                Ok(ConflictResolutionResult::FileEditsOnly)
2312            }
2313            Err(e) => {
2314                // Extraction failed but conflicts are resolved - treat as file edits only
2315                logger.warn(&format!(
2316                    "Failed to extract JSON output but conflicts are resolved: {e}"
2317                ));
2318                Ok(ConflictResolutionResult::FileEditsOnly)
2319            }
2320        }
2321    } else {
2322        logger.warn(&format!(
2323            "{} conflict(s) remain after agent attempted resolution",
2324            remaining_conflicts.len()
2325        ));
2326        Ok(ConflictResolutionResult::Failed)
2327    }
2328}
2329
2330/// Parse and validate the resolved files from AI output.
2331///
2332/// JSON parsing failures are expected and handled gracefully - LibGit2 state
2333/// is used for verification, not JSON output. This function only parses the
2334/// JSON to write resolved files if available.
2335fn parse_and_validate_resolved_files(
2336    resolved_content: &str,
2337    logger: &Logger,
2338) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2339    let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2340        // Agent did not provide JSON output - fall back to LibGit2 verification
2341        // This is expected and normal, not an error condition
2342        anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2343    })?;
2344
2345    let resolved_files = match json.get("resolved_files") {
2346        Some(v) if v.is_object() => v.as_object().unwrap(),
2347        _ => {
2348            logger.info("Agent output missing 'resolved_files' object");
2349            anyhow::bail!("Agent output missing 'resolved_files' object");
2350        }
2351    };
2352
2353    if resolved_files.is_empty() {
2354        logger.info("No resolved files in JSON output");
2355        anyhow::bail!("No files were resolved by the agent");
2356    }
2357
2358    Ok(resolved_files.clone())
2359}
2360
2361/// Write resolved files to disk and stage them.
2362fn write_resolved_files(
2363    resolved_files: &serde_json::Map<String, serde_json::Value>,
2364    logger: &Logger,
2365) -> anyhow::Result<usize> {
2366    use std::fs;
2367
2368    let mut files_written = 0;
2369    for (path, content) in resolved_files {
2370        if let Some(content_str) = content.as_str() {
2371            fs::write(path, content_str).map_err(|e| {
2372                logger.error(&format!("Failed to write {path}: {e}"));
2373                anyhow::anyhow!("Failed to write {path}: {e}")
2374            })?;
2375            logger.info(&format!("Resolved and wrote: {path}"));
2376            files_written += 1;
2377            // Stage the resolved file
2378            if let Err(e) = crate::git_helpers::git_add_all() {
2379                logger.warn(&format!("Failed to stage {path}: {e}"));
2380            }
2381        }
2382    }
2383
2384    logger.success(&format!("Successfully resolved {files_written} file(s)"));
2385    Ok(files_written)
2386}