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