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_template_commands,
35    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, is_main_or_master_branch, rebase_onto, require_git_repo,
46    reset_start_commit, save_start_commit, start_agent_phase, RebaseResult,
47};
48use crate::logger::Colors;
49use crate::logger::Logger;
50use crate::phases::{run_development_phase, run_review_phase, PhaseContext};
51use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
52use crate::prompts::template_context::TemplateContext;
53use std::env;
54use std::process::Command;
55
56use config_init::initialize_config;
57use context::PipelineContext;
58use detection::detect_project_stack;
59use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
60use resume::handle_resume;
61use validation::{
62    resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
63};
64
65/// Main application entry point.
66///
67/// Orchestrates the entire Ralph pipeline:
68/// 1. Configuration initialization
69/// 2. Agent validation
70/// 3. Plumbing commands (if requested)
71/// 4. Development phase
72/// 5. Review & fix phase
73/// 6. Final validation
74/// 7. Commit phase
75///
76/// # Arguments
77///
78/// * `args` - The parsed CLI arguments
79///
80/// # Returns
81///
82/// Returns `Ok(())` on success or an error if any phase fails.
83pub fn run(args: Args) -> anyhow::Result<()> {
84    let colors = Colors::new();
85    let logger = Logger::new(colors);
86
87    // Initialize configuration and agent registry
88    let Some(init_result) = initialize_config(&args, colors, &logger)? else {
89        return Ok(()); // Early exit (--init/--init-global/--init-legacy)
90    };
91
92    let config_init::ConfigInitResult {
93        config,
94        registry,
95        config_path,
96        config_sources,
97    } = init_result;
98
99    // Resolve required agent names
100    let validated = resolve_required_agents(&config)?;
101    let developer_agent = validated.developer_agent;
102    let reviewer_agent = validated.reviewer_agent;
103
104    // Handle listing commands (these can run without git repo)
105    if handle_listing_commands(&args, &registry, colors) {
106        return Ok(());
107    }
108
109    // Handle --diagnose
110    if args.recovery.diagnose {
111        handle_diagnose(colors, &config, &registry, &config_path, &config_sources);
112        return Ok(());
113    }
114
115    // Validate agent chains
116    validate_agent_chains(&registry, colors);
117
118    // Handle plumbing commands
119    if handle_plumbing_commands(&args, &logger, colors)? {
120        return Ok(());
121    }
122
123    // Validate agents and set up git repo and PROMPT.md
124    let Some(repo_root) = validate_and_setup_agents(
125        &config,
126        &registry,
127        &developer_agent,
128        &reviewer_agent,
129        &config_path,
130        colors,
131        &logger,
132    )?
133    else {
134        return Ok(());
135    };
136
137    // Handle --rebase-only
138    if args.rebase_flags.rebase_only {
139        let template_context =
140            TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
141        return handle_rebase_only(&args, &config, &template_context, &logger, colors);
142    }
143
144    // Prepare pipeline context or exit early
145    (prepare_pipeline_or_exit(PipelinePreparationParams {
146        args,
147        config,
148        registry,
149        developer_agent,
150        reviewer_agent,
151        repo_root,
152        logger,
153        colors,
154    })?)
155    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
156}
157
158/// Handles listing commands that don't require the full pipeline.
159///
160/// Returns `true` if a listing command was handled and we should exit.
161fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
162    if args.agent_list.list_agents {
163        handle_list_agents(registry);
164        return true;
165    }
166    if args.agent_list.list_available_agents {
167        handle_list_available_agents(registry);
168        return true;
169    }
170    if args.provider_list.list_providers {
171        handle_list_providers(colors);
172        return true;
173    }
174
175    // Handle template commands
176    let template_cmds = &args.template_commands;
177    if template_cmds.init_templates_enabled()
178        || template_cmds.validate
179        || template_cmds.show.is_some()
180        || template_cmds.list
181        || template_cmds.variables.is_some()
182        || template_cmds.render.is_some()
183    {
184        let _ = handle_template_commands(template_cmds, colors);
185        return true;
186    }
187
188    false
189}
190
191/// Handles plumbing commands that require git repo but not full validation.
192///
193/// Returns `Ok(true)` if a plumbing command was handled and we should exit.
194/// Returns `Ok(false)` if we should continue to the main pipeline.
195fn handle_plumbing_commands(args: &Args, logger: &Logger, colors: Colors) -> anyhow::Result<bool> {
196    // Show commit message
197    if args.commit_display.show_commit_msg {
198        return handle_show_commit_msg().map(|()| true);
199    }
200
201    // Apply commit
202    if args.commit_plumbing.apply_commit {
203        return handle_apply_commit(logger, colors).map(|()| true);
204    }
205
206    // Reset start commit
207    if args.commit_display.reset_start_commit {
208        require_git_repo()?;
209        let repo_root = get_repo_root()?;
210        env::set_current_dir(&repo_root)?;
211
212        return match reset_start_commit() {
213            Ok(()) => {
214                logger.success("Starting commit reference reset to current HEAD");
215                logger.info(".agent/start_commit has been updated");
216                Ok(true)
217            }
218            Err(e) => {
219                logger.error(&format!("Failed to reset starting commit: {e}"));
220                anyhow::bail!("Failed to reset starting commit");
221            }
222        };
223    }
224
225    Ok(false)
226}
227
228/// Parameters for preparing the pipeline context.
229///
230/// Groups related parameters to avoid too many function arguments.
231struct PipelinePreparationParams {
232    args: Args,
233    config: crate::config::Config,
234    registry: AgentRegistry,
235    developer_agent: String,
236    reviewer_agent: String,
237    repo_root: std::path::PathBuf,
238    logger: Logger,
239    colors: Colors,
240}
241
242/// Prepares the pipeline context after agent validation.
243///
244/// Returns `Some(ctx)` if pipeline should run, or `None` if we should exit early.
245fn prepare_pipeline_or_exit(
246    params: PipelinePreparationParams,
247) -> anyhow::Result<Option<PipelineContext>> {
248    let PipelinePreparationParams {
249        args,
250        config,
251        registry,
252        developer_agent,
253        reviewer_agent,
254        repo_root,
255        mut logger,
256        colors,
257    } = params;
258
259    ensure_files(config.isolation_mode)?;
260
261    // Reset context for isolation mode
262    if config.isolation_mode {
263        reset_context_for_isolation(&logger)?;
264    }
265
266    logger = logger.with_log_file(".agent/logs/pipeline.log");
267
268    // Handle --dry-run
269    if args.recovery.dry_run {
270        let developer_display = registry.display_name(&developer_agent);
271        let reviewer_display = registry.display_name(&reviewer_agent);
272        handle_dry_run(
273            &logger,
274            colors,
275            &config,
276            &developer_display,
277            &reviewer_display,
278            &repo_root,
279        )?;
280        return Ok(None);
281    }
282
283    // Create template context for user template overrides
284    let template_context =
285        TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
286
287    // Handle --generate-commit-msg
288    if args.commit_plumbing.generate_commit_msg {
289        handle_generate_commit_msg(
290            &config,
291            &template_context,
292            &registry,
293            &logger,
294            colors,
295            &developer_agent,
296            &reviewer_agent,
297        )?;
298        return Ok(None);
299    }
300
301    // Get display names before moving registry
302    let developer_display = registry.display_name(&developer_agent);
303    let reviewer_display = registry.display_name(&reviewer_agent);
304
305    // Build pipeline context
306    let ctx = PipelineContext {
307        args,
308        config,
309        registry,
310        developer_agent,
311        reviewer_agent,
312        developer_display,
313        reviewer_display,
314        repo_root,
315        logger,
316        colors,
317        template_context,
318    };
319    Ok(Some(ctx))
320}
321
322/// Validates agent commands and workflow capability, then sets up git repo and PROMPT.md.
323///
324/// Returns `Some(repo_root)` if setup succeeded and should continue.
325/// Returns `None` if the user declined PROMPT.md creation (to exit early).
326fn validate_and_setup_agents(
327    config: &crate::config::Config,
328    registry: &AgentRegistry,
329    developer_agent: &str,
330    reviewer_agent: &str,
331    config_path: &std::path::Path,
332    colors: Colors,
333    logger: &Logger,
334) -> anyhow::Result<Option<std::path::PathBuf>> {
335    // Validate agent commands exist
336    validate_agent_commands(
337        config,
338        registry,
339        developer_agent,
340        reviewer_agent,
341        config_path,
342    )?;
343
344    // Validate agents are workflow-capable
345    validate_can_commit(
346        config,
347        registry,
348        developer_agent,
349        reviewer_agent,
350        config_path,
351    )?;
352
353    // Set up git repo and working directory
354    require_git_repo()?;
355    let repo_root = get_repo_root()?;
356    env::set_current_dir(&repo_root)?;
357
358    // Set up PROMPT.md if needed (may return None to exit early)
359    let should_continue = setup_git_and_prompt_file(config, colors, logger)?;
360    if should_continue.is_none() {
361        return Ok(None);
362    }
363
364    Ok(Some(repo_root))
365}
366
367/// In interactive mode, prompts to create PROMPT.md from a template before `ensure_files()`.
368///
369/// Returns `Ok(Some(()))` if setup succeeded and should continue.
370/// Returns `Ok(None)` if the user declined PROMPT.md creation (to exit early).
371fn setup_git_and_prompt_file(
372    config: &crate::config::Config,
373    colors: Colors,
374    logger: &Logger,
375) -> anyhow::Result<Option<()>> {
376    // In interactive mode, prompt to create PROMPT.md from a template BEFORE ensure_files().
377    // If the user declines (or we can't prompt), exit without creating a placeholder PROMPT.md.
378    if config.behavior.interactive && !std::path::Path::new("PROMPT.md").exists() {
379        if let Some(template_name) = prompt_template_selection(colors) {
380            create_prompt_from_template(&template_name, colors)?;
381            println!();
382            logger.info(
383                "PROMPT.md created. Please edit it with your task details, then run ralph again.",
384            );
385            logger.info(&format!(
386                "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
387                config.commit_msg
388            ));
389            return Ok(None);
390        }
391        println!();
392        logger.info("PROMPT.md is required to run the pipeline.");
393        logger.info(
394            "Create one with 'ralph --init-prompt <template>' (see: 'ralph --list-templates'), then rerun.",
395        );
396        return Ok(None);
397    }
398    Ok(Some(()))
399}
400
401/// Runs the full development/review/commit pipeline.
402fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
403    // Handle --resume
404    let resume_checkpoint = handle_resume(
405        &ctx.args,
406        &ctx.logger,
407        &ctx.developer_display,
408        &ctx.reviewer_display,
409    );
410
411    // Set up git helpers and agent phase
412    let mut git_helpers = crate::git_helpers::GitHelpers::new();
413    cleanup_orphaned_marker(&ctx.logger)?;
414    start_agent_phase(&mut git_helpers)?;
415    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
416
417    // Print welcome banner and validate PROMPT.md
418    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
419    print_pipeline_info(ctx);
420    validate_prompt_and_setup_backup(ctx)?;
421
422    // Set up PROMPT.md monitoring
423    let mut prompt_monitor = setup_prompt_monitor(ctx);
424
425    // Detect project stack and review guidelines
426    let (_project_stack, review_guidelines) =
427        detect_project_stack(&ctx.config, &ctx.repo_root, &ctx.logger, ctx.colors);
428
429    print_review_guidelines(ctx, review_guidelines.as_ref());
430    println!();
431
432    // Create phase context and save starting commit
433    let (mut timer, mut stats) = (Timer::new(), Stats::new());
434    let mut phase_ctx =
435        create_phase_context(ctx, &mut timer, &mut stats, review_guidelines.as_ref());
436    save_start_commit_or_warn(ctx);
437
438    // Run pre-development rebase
439    run_initial_rebase(
440        &ctx.args,
441        &ctx.config,
442        &ctx.template_context,
443        &ctx.logger,
444        ctx.colors,
445    )?;
446
447    // Run pipeline phases
448    run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
449    check_prompt_restoration(ctx, &mut prompt_monitor, "development");
450    update_status("In progress.", ctx.config.isolation_mode)?;
451
452    run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
453    check_prompt_restoration(ctx, &mut prompt_monitor, "review");
454
455    // Run post-review rebase
456    run_post_review_rebase(
457        &ctx.args,
458        &ctx.config,
459        &ctx.template_context,
460        &ctx.logger,
461        ctx.colors,
462    )?;
463
464    update_status("In progress.", ctx.config.isolation_mode)?;
465
466    run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
467
468    // Commit phase
469    finalize_pipeline(
470        &mut agent_phase_guard,
471        &ctx.logger,
472        ctx.colors,
473        &ctx.config,
474        &timer,
475        &stats,
476        prompt_monitor,
477    );
478    Ok(())
479}
480
481/// Print pipeline information (working directory and commit message).
482fn print_pipeline_info(ctx: &PipelineContext) {
483    ctx.logger.info(&format!(
484        "Working directory: {}{}{}",
485        ctx.colors.cyan(),
486        ctx.repo_root.display(),
487        ctx.colors.reset()
488    ));
489    ctx.logger.info(&format!(
490        "Commit message: {}{}{}",
491        ctx.colors.cyan(),
492        ctx.config.commit_msg,
493        ctx.colors.reset()
494    ));
495}
496
497/// Validate PROMPT.md and set up backup/protection.
498fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
499    let prompt_validation =
500        validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
501    for err in &prompt_validation.errors {
502        ctx.logger.error(err);
503    }
504    for warn in &prompt_validation.warnings {
505        ctx.logger.warn(warn);
506    }
507    if !prompt_validation.is_valid() {
508        anyhow::bail!("PROMPT.md validation errors");
509    }
510
511    // Create a backup of PROMPT.md to protect against accidental deletion.
512    match create_prompt_backup() {
513        Ok(None) => {}
514        Ok(Some(warning)) => {
515            ctx.logger.warn(&format!(
516                "PROMPT.md backup created but: {warning}. Continuing anyway."
517            ));
518        }
519        Err(e) => {
520            ctx.logger.warn(&format!(
521                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
522            ));
523        }
524    }
525
526    // Make PROMPT.md read-only to protect against accidental deletion.
527    match make_prompt_read_only() {
528        None => {}
529        Some(warning) => {
530            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
531        }
532    }
533
534    Ok(())
535}
536
537/// Set up PROMPT.md monitoring for deletion detection.
538fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
539    match PromptMonitor::new() {
540        Ok(mut monitor) => {
541            if let Err(e) = monitor.start() {
542                ctx.logger.warn(&format!(
543                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
544                ));
545                None
546            } else {
547                if ctx.config.verbosity.is_debug() {
548                    ctx.logger.info("Started real-time PROMPT.md monitoring");
549                }
550                Some(monitor)
551            }
552        }
553        Err(e) => {
554            ctx.logger.warn(&format!(
555                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
556            ));
557            None
558        }
559    }
560}
561
562/// Print review guidelines if detected.
563fn print_review_guidelines(
564    ctx: &PipelineContext,
565    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
566) {
567    if let Some(guidelines) = review_guidelines {
568        ctx.logger.info(&format!(
569            "Review guidelines: {}{}{}",
570            ctx.colors.dim(),
571            guidelines.summary(),
572            ctx.colors.reset()
573        ));
574    }
575}
576
577/// Create the phase context for running pipeline phases.
578fn create_phase_context<'ctx>(
579    ctx: &'ctx PipelineContext,
580    timer: &'ctx mut Timer,
581    stats: &'ctx mut Stats,
582    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
583) -> PhaseContext<'ctx> {
584    PhaseContext {
585        config: &ctx.config,
586        registry: &ctx.registry,
587        logger: &ctx.logger,
588        colors: &ctx.colors,
589        timer,
590        stats,
591        developer_agent: &ctx.developer_agent,
592        reviewer_agent: &ctx.reviewer_agent,
593        review_guidelines,
594        template_context: &ctx.template_context,
595    }
596}
597
598/// Save starting commit or warn if it fails.
599fn save_start_commit_or_warn(ctx: &PipelineContext) {
600    match save_start_commit() {
601        Ok(()) => {
602            if ctx.config.verbosity.is_debug() {
603                ctx.logger
604                    .info("Saved starting commit for incremental diff generation");
605            }
606        }
607        Err(e) => {
608            ctx.logger.warn(&format!(
609                "Failed to save starting commit: {e}. \
610                 Incremental diffs may be unavailable as a result."
611            ));
612            ctx.logger.info(
613                "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
614            );
615        }
616    }
617}
618
619/// Check for PROMPT.md restoration after a phase.
620fn check_prompt_restoration(
621    ctx: &PipelineContext,
622    prompt_monitor: &mut Option<PromptMonitor>,
623    phase: &str,
624) {
625    if let Some(ref mut monitor) = prompt_monitor {
626        if monitor.check_and_restore() {
627            ctx.logger.warn(&format!(
628                "PROMPT.md was deleted and restored during {phase} phase"
629            ));
630        }
631    }
632}
633
634/// Runs the development phase.
635fn run_development(
636    ctx: &mut PhaseContext,
637    args: &Args,
638    resume_checkpoint: Option<&PipelineCheckpoint>,
639) -> anyhow::Result<()> {
640    ctx.logger
641        .header("PHASE 1: Development", crate::logger::Colors::blue);
642
643    let resume_phase = resume_checkpoint.map(|c| c.phase);
644    let resume_rank = resume_phase.map(phase_rank);
645
646    if resume_rank.is_some_and(|rank| rank >= phase_rank(PipelinePhase::Review)) {
647        ctx.logger
648            .info("Skipping development phase (checkpoint indicates it already completed)");
649        return Ok(());
650    }
651
652    if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
653        ctx.logger
654            .info("Skipping development phase (resuming from a later checkpoint phase)");
655        return Ok(());
656    }
657
658    let start_iter = match resume_phase {
659        Some(PipelinePhase::Planning | PipelinePhase::Development) => resume_checkpoint
660            .map_or(1, |c| c.iteration)
661            .clamp(1, ctx.config.developer_iters),
662        _ => 1,
663    };
664
665    let resuming_from_development =
666        args.recovery.resume && resume_phase == Some(PipelinePhase::Development);
667    let development_result = run_development_phase(ctx, start_iter, resuming_from_development)?;
668
669    if development_result.had_errors {
670        ctx.logger
671            .warn("Development phase completed with non-fatal errors");
672    }
673
674    Ok(())
675}
676
677/// Runs the review and fix phase.
678fn run_review_and_fix(
679    ctx: &mut PhaseContext,
680    _args: &Args,
681    resume_checkpoint: Option<&PipelineCheckpoint>,
682) -> anyhow::Result<()> {
683    ctx.logger
684        .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
685
686    let resume_phase = resume_checkpoint.map(|c| c.phase);
687
688    // Check if we should run any reviewer phase
689    let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
690        || should_run_from(PipelinePhase::Fix, resume_checkpoint)
691        || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
692        || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
693
694    let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
695        || resume_phase == Some(PipelinePhase::Fix)
696        || resume_phase == Some(PipelinePhase::ReviewAgain);
697
698    if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
699        let start_pass = match resume_phase {
700            Some(PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain) => {
701                resume_checkpoint
702                    .map_or(1, |c| c.reviewer_pass)
703                    .clamp(1, ctx.config.reviewer_reviews.max(1))
704            }
705            _ => 1,
706        };
707
708        let review_result = run_review_phase(ctx, start_pass)?;
709        if review_result.completed_early {
710            ctx.logger
711                .success("Review phase completed early (no issues found)");
712        }
713    } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
714        ctx.logger
715            .info("Skipping review phase (reviewer_reviews=0)");
716    } else if run_any_reviewer_phase {
717        ctx.logger
718            .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
719    }
720
721    // Note: The old dedicated commit phase has been removed.
722    // Commits now happen automatically per-iteration during development and per-cycle during review.
723
724    Ok(())
725}
726
727/// Runs final validation if configured.
728fn run_final_validation(
729    ctx: &PhaseContext,
730    resume_checkpoint: Option<&PipelineCheckpoint>,
731) -> anyhow::Result<()> {
732    let Some(ref full_cmd) = ctx.config.full_check_cmd else {
733        return Ok(());
734    };
735
736    if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
737        ctx.logger
738            .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
739        ctx.logger
740            .info("Skipping final validation (resuming from a later checkpoint phase)");
741        return Ok(());
742    }
743
744    let argv = utils::split_command(full_cmd)
745        .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
746    if argv.is_empty() {
747        ctx.logger
748            .warn("FULL_CHECK_CMD is empty; skipping final validation");
749        return Ok(());
750    }
751
752    if ctx.config.features.checkpoint_enabled {
753        let _ = save_checkpoint(&PipelineCheckpoint::new(
754            PipelinePhase::FinalValidation,
755            ctx.config.developer_iters,
756            ctx.config.developer_iters,
757            ctx.config.reviewer_reviews,
758            ctx.config.reviewer_reviews,
759            ctx.developer_agent,
760            ctx.reviewer_agent,
761        ));
762    }
763
764    ctx.logger
765        .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
766    let display_cmd = utils::format_argv_for_log(&argv);
767    ctx.logger.info(&format!(
768        "Running full check: {}{}{}",
769        ctx.colors.dim(),
770        display_cmd,
771        ctx.colors.reset()
772    ));
773
774    let Some((program, arguments)) = argv.split_first() else {
775        ctx.logger
776            .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
777        return Ok(());
778    };
779    let status = Command::new(program).args(arguments).status()?;
780
781    if status.success() {
782        ctx.logger.success("Full check passed");
783    } else {
784        ctx.logger.error("Full check failed");
785        anyhow::bail!("Full check failed");
786    }
787
788    Ok(())
789}
790
791/// Handle --rebase-only flag.
792///
793/// This function performs a rebase to the default branch with AI conflict resolution and exits,
794/// without running the full pipeline.
795fn handle_rebase_only(
796    _args: &Args,
797    config: &crate::config::Config,
798    template_context: &TemplateContext,
799    logger: &Logger,
800    colors: Colors,
801) -> anyhow::Result<()> {
802    // Check if we're on main/master branch
803    if is_main_or_master_branch()? {
804        logger.warn("Already on main/master branch - rebasing on main is not recommended");
805        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
806        logger.info("  git worktree add ../feature-branch feature-branch");
807        logger.info("This allows multiple AI agents to work on different features simultaneously.");
808        logger.info("Proceeding with rebase anyway as requested...");
809    }
810
811    logger.header("Rebase to default branch", Colors::cyan);
812
813    match run_rebase_to_default(logger, colors) {
814        Ok(RebaseResult::Success) => {
815            logger.success("Rebase completed successfully");
816            Ok(())
817        }
818        Ok(RebaseResult::NoOp) => {
819            logger.info("No rebase needed (already up-to-date)");
820            Ok(())
821        }
822        Ok(RebaseResult::Conflicts(_conflicts)) => {
823            // Get the actual conflicted files
824            let conflicted_files = get_conflicted_files()?;
825            if conflicted_files.is_empty() {
826                logger.warn("Rebase reported conflicts but no conflicted files found");
827                let _ = abort_rebase();
828                return Ok(());
829            }
830
831            logger.warn(&format!(
832                "Rebase resulted in {} conflict(s), attempting AI resolution",
833                conflicted_files.len()
834            ));
835
836            // Attempt to resolve conflicts with AI
837            match try_resolve_conflicts_with_fallback(
838                &conflicted_files,
839                config,
840                template_context,
841                logger,
842                colors,
843            ) {
844                Ok(true) => {
845                    // Conflicts resolved, continue the rebase
846                    logger.info("Continuing rebase after conflict resolution");
847                    match continue_rebase() {
848                        Ok(()) => {
849                            logger.success("Rebase completed successfully after AI resolution");
850                            Ok(())
851                        }
852                        Err(e) => {
853                            logger.error(&format!("Failed to continue rebase: {e}"));
854                            let _ = abort_rebase();
855                            anyhow::bail!("Rebase failed after conflict resolution")
856                        }
857                    }
858                }
859                Ok(false) => {
860                    // AI resolution failed
861                    logger.error("AI conflict resolution failed, aborting rebase");
862                    let _ = abort_rebase();
863                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
864                }
865                Err(e) => {
866                    logger.error(&format!("Conflict resolution error: {e}"));
867                    let _ = abort_rebase();
868                    anyhow::bail!("Rebase conflict resolution failed: {e}")
869                }
870            }
871        }
872        Err(e) => {
873            logger.error(&format!("Rebase failed: {e}"));
874            anyhow::bail!("Rebase failed: {e}")
875        }
876    }
877}
878
879/// Run rebase to the default branch.
880///
881/// This function performs a rebase from the current branch to the
882/// default branch (main/master). It handles all edge cases including:
883/// - Already on main/master (proceeds with rebase attempt)
884/// - Empty repository (returns `NoOp`)
885/// - Upstream branch not found (error)
886/// - Conflicts during rebase (returns `Conflicts` result)
887///
888/// # Returns
889///
890/// Returns `RebaseResult` indicating the outcome.
891fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
892    // Get the default branch
893    let default_branch = get_default_branch()?;
894    logger.info(&format!(
895        "Rebasing onto {}{}{}",
896        colors.cyan(),
897        default_branch,
898        colors.reset()
899    ));
900
901    // Perform the rebase
902    rebase_onto(&default_branch)
903}
904
905/// Run initial rebase before development phase.
906///
907/// This function is called before the development phase starts to ensure
908/// the feature branch is up-to-date with the default branch.
909fn run_initial_rebase(
910    _args: &Args,
911    config: &crate::config::Config,
912    template_context: &TemplateContext,
913    logger: &Logger,
914    colors: Colors,
915) -> anyhow::Result<()> {
916    logger.header("Pre-development rebase", Colors::cyan);
917
918    match run_rebase_to_default(logger, colors) {
919        Ok(RebaseResult::Success) => {
920            logger.success("Rebase completed successfully");
921            Ok(())
922        }
923        Ok(RebaseResult::NoOp) => {
924            logger.info("No rebase needed (already up-to-date or on main branch)");
925            Ok(())
926        }
927        Ok(RebaseResult::Conflicts(_conflicts)) => {
928            // Get the actual conflicted files
929            let conflicted_files = get_conflicted_files()?;
930            if conflicted_files.is_empty() {
931                logger.warn("Rebase reported conflicts but no conflicted files found");
932                let _ = abort_rebase();
933                return Ok(());
934            }
935
936            logger.warn(&format!(
937                "Rebase resulted in {} conflict(s), attempting AI resolution",
938                conflicted_files.len()
939            ));
940
941            // Attempt to resolve conflicts with AI
942            match try_resolve_conflicts_with_fallback(
943                &conflicted_files,
944                config,
945                template_context,
946                logger,
947                colors,
948            ) {
949                Ok(true) => {
950                    // Conflicts resolved, continue the rebase
951                    logger.info("Continuing rebase after conflict resolution");
952                    match continue_rebase() {
953                        Ok(()) => {
954                            logger.success("Rebase completed successfully after AI resolution");
955                            Ok(())
956                        }
957                        Err(e) => {
958                            logger.warn(&format!("Failed to continue rebase: {e}"));
959                            let _ = abort_rebase();
960                            Ok(()) // Continue anyway - conflicts were resolved
961                        }
962                    }
963                }
964                Ok(false) => {
965                    // AI resolution failed
966                    logger.warn("AI conflict resolution failed, aborting rebase");
967                    let _ = abort_rebase();
968                    Ok(()) // Continue pipeline - don't block on rebase failure
969                }
970                Err(e) => {
971                    logger.error(&format!("Conflict resolution error: {e}"));
972                    let _ = abort_rebase();
973                    Ok(()) // Continue pipeline
974                }
975            }
976        }
977        Err(e) => {
978            logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
979            Ok(())
980        }
981    }
982}
983
984/// Run post-review rebase after review phase.
985///
986/// This function is called after the review phase completes to ensure
987/// the feature branch is still up-to-date with the default branch.
988fn run_post_review_rebase(
989    _args: &Args,
990    config: &crate::config::Config,
991    template_context: &TemplateContext,
992    logger: &Logger,
993    colors: Colors,
994) -> anyhow::Result<()> {
995    logger.header("Post-review rebase", Colors::cyan);
996
997    match run_rebase_to_default(logger, colors) {
998        Ok(RebaseResult::Success) => {
999            logger.success("Rebase completed successfully");
1000            Ok(())
1001        }
1002        Ok(RebaseResult::NoOp) => {
1003            logger.info("No rebase needed (already up-to-date or on main branch)");
1004            Ok(())
1005        }
1006        Ok(RebaseResult::Conflicts(_conflicts)) => {
1007            // Get the actual conflicted files
1008            let conflicted_files = get_conflicted_files()?;
1009            if conflicted_files.is_empty() {
1010                logger.warn("Rebase reported conflicts but no conflicted files found");
1011                let _ = abort_rebase();
1012                return Ok(());
1013            }
1014
1015            logger.warn(&format!(
1016                "Rebase resulted in {} conflict(s), attempting AI resolution",
1017                conflicted_files.len()
1018            ));
1019
1020            // Attempt to resolve conflicts with AI
1021            match try_resolve_conflicts_with_fallback(
1022                &conflicted_files,
1023                config,
1024                template_context,
1025                logger,
1026                colors,
1027            ) {
1028                Ok(true) => {
1029                    // Conflicts resolved, continue the rebase
1030                    logger.info("Continuing rebase after conflict resolution");
1031                    match continue_rebase() {
1032                        Ok(()) => {
1033                            logger.success("Rebase completed successfully after AI resolution");
1034                            Ok(())
1035                        }
1036                        Err(e) => {
1037                            logger.warn(&format!("Failed to continue rebase: {e}"));
1038                            let _ = abort_rebase();
1039                            Ok(()) // Continue anyway - conflicts were resolved
1040                        }
1041                    }
1042                }
1043                Ok(false) => {
1044                    // AI resolution failed
1045                    logger.warn("AI conflict resolution failed, aborting rebase");
1046                    let _ = abort_rebase();
1047                    Ok(()) // Continue pipeline - don't block on rebase failure
1048                }
1049                Err(e) => {
1050                    logger.error(&format!("Conflict resolution error: {e}"));
1051                    let _ = abort_rebase();
1052                    Ok(()) // Continue pipeline
1053                }
1054            }
1055        }
1056        Err(e) => {
1057            logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1058            Ok(())
1059        }
1060    }
1061}
1062
1063/// Result type for conflict resolution attempts.
1064///
1065/// Represents the different ways conflict resolution can succeed or fail.
1066enum ConflictResolutionResult {
1067    /// Agent provided JSON output with resolved file contents
1068    WithJson(String),
1069    /// Agent resolved conflicts by editing files directly (no JSON output)
1070    FileEditsOnly,
1071    /// Resolution failed completely
1072    Failed,
1073}
1074
1075/// Attempt to resolve rebase conflicts with AI fallback.
1076///
1077/// This is a helper function that creates a minimal `PhaseContext`
1078/// for conflict resolution without requiring full pipeline state.
1079fn try_resolve_conflicts_with_fallback(
1080    conflicted_files: &[String],
1081    config: &crate::config::Config,
1082    template_context: &TemplateContext,
1083    logger: &Logger,
1084    colors: Colors,
1085) -> anyhow::Result<bool> {
1086    if conflicted_files.is_empty() {
1087        return Ok(false);
1088    }
1089
1090    logger.info(&format!(
1091        "Attempting AI conflict resolution for {} file(s)",
1092        conflicted_files.len()
1093    ));
1094
1095    let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
1096    let resolution_prompt = build_resolution_prompt(&conflicts, template_context);
1097
1098    match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1099        Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1100            // Agent provided JSON output - parse and write files
1101            let resolved_files = parse_and_validate_resolved_files(&resolved_content, logger)?;
1102            write_resolved_files(&resolved_files, logger)?;
1103
1104            // Verify all conflicts are resolved
1105            let remaining_conflicts = get_conflicted_files()?;
1106            if remaining_conflicts.is_empty() {
1107                Ok(true)
1108            } else {
1109                logger.warn(&format!(
1110                    "{} conflicts remain after AI resolution",
1111                    remaining_conflicts.len()
1112                ));
1113                Ok(false)
1114            }
1115        }
1116        Ok(ConflictResolutionResult::FileEditsOnly) => {
1117            // Agent resolved conflicts by editing files directly
1118            logger.info("Agent resolved conflicts via file edits (no JSON output)");
1119
1120            // Verify all conflicts are resolved
1121            let remaining_conflicts = get_conflicted_files()?;
1122            if remaining_conflicts.is_empty() {
1123                logger.success("All conflicts resolved via file edits");
1124                Ok(true)
1125            } else {
1126                logger.warn(&format!(
1127                    "{} conflicts remain after AI resolution",
1128                    remaining_conflicts.len()
1129                ));
1130                Ok(false)
1131            }
1132        }
1133        Ok(ConflictResolutionResult::Failed) => {
1134            logger.warn("AI conflict resolution failed");
1135            logger.info("Attempting to continue rebase anyway...");
1136
1137            // Try to continue rebase - user may have manually resolved conflicts
1138            match crate::git_helpers::continue_rebase() {
1139                Ok(()) => {
1140                    logger.info("Successfully continued rebase");
1141                    Ok(true)
1142                }
1143                Err(rebase_err) => {
1144                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1145                    Ok(false) // Conflicts remain
1146                }
1147            }
1148        }
1149        Err(e) => {
1150            logger.warn(&format!("AI conflict resolution failed: {e}"));
1151            logger.info("Attempting to continue rebase anyway...");
1152
1153            // Try to continue rebase - user may have manually resolved conflicts
1154            match crate::git_helpers::continue_rebase() {
1155                Ok(()) => {
1156                    logger.info("Successfully continued rebase");
1157                    Ok(true)
1158                }
1159                Err(rebase_err) => {
1160                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1161                    Ok(false) // Conflicts remain
1162                }
1163            }
1164        }
1165    }
1166}
1167
1168/// Collect conflict information from conflicted files.
1169fn collect_conflict_info_or_error(
1170    conflicted_files: &[String],
1171    logger: &Logger,
1172) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
1173    use crate::prompts::collect_conflict_info;
1174
1175    let conflicts = match collect_conflict_info(conflicted_files) {
1176        Ok(c) => c,
1177        Err(e) => {
1178            logger.error(&format!("Failed to collect conflict info: {e}"));
1179            anyhow::bail!("Failed to collect conflict info");
1180        }
1181    };
1182    Ok(conflicts)
1183}
1184
1185/// Build the conflict resolution prompt from context files.
1186fn build_resolution_prompt(
1187    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1188    template_context: &TemplateContext,
1189) -> String {
1190    use crate::prompts::build_conflict_resolution_prompt_with_context;
1191    use std::fs;
1192
1193    let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
1194    let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
1195
1196    build_conflict_resolution_prompt_with_context(
1197        template_context,
1198        conflicts,
1199        prompt_md_content.as_deref(),
1200        plan_content.as_deref(),
1201    )
1202}
1203
1204/// Run AI agent to resolve conflicts with fallback mechanism.
1205///
1206/// Returns `ConflictResolutionResult` indicating whether the agent provided
1207/// JSON output, resolved conflicts via file edits, or failed completely.
1208fn run_ai_conflict_resolution(
1209    resolution_prompt: &str,
1210    config: &crate::config::Config,
1211    logger: &Logger,
1212    colors: Colors,
1213) -> anyhow::Result<ConflictResolutionResult> {
1214    use crate::agents::AgentRegistry;
1215    use crate::files::result_extraction::extract_last_result;
1216    use crate::pipeline::{
1217        run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
1218    };
1219    use std::io;
1220    use std::path::Path;
1221
1222    // Note: log_dir is used as a prefix for log file names, not as a directory.
1223    // The actual log files will be created in .agent/logs/ with names like:
1224    // .agent/logs/rebase_conflict_resolution_ccs-glm_0.log
1225    let log_dir = ".agent/logs/rebase_conflict_resolution";
1226
1227    let registry = AgentRegistry::new()?;
1228    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
1229
1230    let mut runtime = PipelineRuntime {
1231        timer: &mut crate::pipeline::Timer::new(),
1232        logger,
1233        colors: &colors,
1234        config,
1235    };
1236
1237    // Output validator: checks if agent produced valid output OR resolved conflicts
1238    // Agents may edit files without returning JSON, so we verify conflicts are resolved.
1239    let validate_output: OutputValidator = |log_dir_path: &Path,
1240                                            validation_logger: &crate::logger::Logger|
1241     -> io::Result<bool> {
1242        match extract_last_result(log_dir_path) {
1243            Ok(Some(_)) => {
1244                // Valid JSON output exists
1245                Ok(true)
1246            }
1247            Ok(None) => {
1248                // No JSON output - check if conflicts were resolved anyway
1249                // (agent may have edited files without returning JSON)
1250                match crate::git_helpers::get_conflicted_files() {
1251                    Ok(conflicts) if conflicts.is_empty() => {
1252                        validation_logger
1253                            .info("Agent resolved conflicts without JSON output (file edits only)");
1254                        Ok(true) // Conflicts resolved, consider success
1255                    }
1256                    Ok(conflicts) => {
1257                        validation_logger.warn(&format!(
1258                            "{} conflict(s) remain unresolved",
1259                            conflicts.len()
1260                        ));
1261                        Ok(false) // Conflicts remain
1262                    }
1263                    Err(e) => {
1264                        validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
1265                        Ok(false) // Error checking conflicts
1266                    }
1267                }
1268            }
1269            Err(e) => {
1270                validation_logger.warn(&format!("Output validation check failed: {e}"));
1271                Ok(false) // Treat validation errors as missing output
1272            }
1273        }
1274    };
1275
1276    let mut fallback_config = FallbackConfig {
1277        role: crate::agents::AgentRole::Reviewer,
1278        base_label: "conflict resolution",
1279        prompt: resolution_prompt,
1280        logfile_prefix: log_dir,
1281        runtime: &mut runtime,
1282        registry: &registry,
1283        primary_agent: reviewer_agent,
1284        output_validator: Some(validate_output),
1285    };
1286
1287    let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
1288
1289    if exit_code != 0 {
1290        return Ok(ConflictResolutionResult::Failed);
1291    }
1292
1293    // Check if conflicts are resolved after agent run
1294    // The validator already checked this, but we verify again to determine the result type
1295    let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
1296
1297    if remaining_conflicts.is_empty() {
1298        // Conflicts are resolved - check if agent provided JSON output
1299        match extract_last_result(Path::new(log_dir)) {
1300            Ok(Some(content)) => {
1301                logger.info("Agent provided JSON output with resolved files");
1302                Ok(ConflictResolutionResult::WithJson(content))
1303            }
1304            Ok(None) => {
1305                logger.info("Agent resolved conflicts via file edits (no JSON output)");
1306                Ok(ConflictResolutionResult::FileEditsOnly)
1307            }
1308            Err(e) => {
1309                // Extraction failed but conflicts are resolved - treat as file edits only
1310                logger.warn(&format!(
1311                    "Failed to extract JSON output but conflicts are resolved: {e}"
1312                ));
1313                Ok(ConflictResolutionResult::FileEditsOnly)
1314            }
1315        }
1316    } else {
1317        logger.warn(&format!(
1318            "{} conflict(s) remain after agent attempted resolution",
1319            remaining_conflicts.len()
1320        ));
1321        Ok(ConflictResolutionResult::Failed)
1322    }
1323}
1324
1325/// Parse and validate the resolved files from AI output.
1326fn parse_and_validate_resolved_files(
1327    resolved_content: &str,
1328    logger: &Logger,
1329) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
1330    let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|e| {
1331        logger.error(&format!("Failed to parse agent output as JSON: {e}"));
1332        anyhow::anyhow!("Failed to parse agent output as JSON")
1333    })?;
1334
1335    let resolved_files = match json.get("resolved_files") {
1336        Some(v) if v.is_object() => v.as_object().unwrap(),
1337        _ => {
1338            logger.error("Agent output missing 'resolved_files' object");
1339            anyhow::bail!("Agent output missing 'resolved_files' object");
1340        }
1341    };
1342
1343    if resolved_files.is_empty() {
1344        logger.error("No files were resolved by the agent");
1345        anyhow::bail!("No files were resolved by the agent");
1346    }
1347
1348    Ok(resolved_files.clone())
1349}
1350
1351/// Write resolved files to disk and stage them.
1352fn write_resolved_files(
1353    resolved_files: &serde_json::Map<String, serde_json::Value>,
1354    logger: &Logger,
1355) -> anyhow::Result<usize> {
1356    use std::fs;
1357
1358    let mut files_written = 0;
1359    for (path, content) in resolved_files {
1360        if let Some(content_str) = content.as_str() {
1361            fs::write(path, content_str).map_err(|e| {
1362                logger.error(&format!("Failed to write {path}: {e}"));
1363                anyhow::anyhow!("Failed to write {path}: {e}")
1364            })?;
1365            logger.info(&format!("Resolved and wrote: {path}"));
1366            files_written += 1;
1367            // Stage the resolved file
1368            if let Err(e) = crate::git_helpers::git_add_all() {
1369                logger.warn(&format!("Failed to stage {path}: {e}"));
1370            }
1371        }
1372    }
1373
1374    logger.success(&format!("Successfully resolved {files_written} file(s)"));
1375    Ok(files_written)
1376}