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    let prompt_exists = std::path::Path::new("PROMPT.md").exists();
377
378    // In interactive mode, prompt to create PROMPT.md from a template BEFORE ensure_files().
379    // If the user declines (or we can't prompt), exit without creating a placeholder PROMPT.md.
380    if config.behavior.interactive && !prompt_exists {
381        if let Some(template_name) = prompt_template_selection(colors) {
382            create_prompt_from_template(&template_name, colors)?;
383            println!();
384            logger.info(
385                "PROMPT.md created. Please edit it with your task details, then run ralph again.",
386            );
387            logger.info(&format!(
388                "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
389                config.commit_msg
390            ));
391            return Ok(None);
392        }
393        println!();
394        logger.error("PROMPT.md not found in current directory.");
395        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
396        println!();
397        logger.info("To get started:");
398        logger.info("  ralph --init                    # Smart setup wizard");
399        logger.info("  ralph --init bug-fix             # Create from template");
400        logger.info("  ralph --list-templates            # See all templates");
401        println!();
402        return Ok(None);
403    }
404
405    // Non-interactive mode: show helpful error if PROMPT.md doesn't exist
406    if !prompt_exists {
407        logger.error("PROMPT.md not found in current directory.");
408        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
409        println!();
410        logger.info("Quick start:");
411        logger.info("  ralph --init                    # Smart setup wizard");
412        logger.info("  ralph --init bug-fix             # Create from template");
413        logger.info("  ralph --list-templates            # See all templates");
414        println!();
415        logger.info("Use -i flag for interactive mode to be prompted for template selection.");
416        println!();
417        return Ok(None);
418    }
419
420    Ok(Some(()))
421}
422
423/// Runs the full development/review/commit pipeline.
424fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
425    // Handle --resume
426    let resume_checkpoint = handle_resume(
427        &ctx.args,
428        &ctx.logger,
429        &ctx.developer_display,
430        &ctx.reviewer_display,
431    );
432
433    // Set up git helpers and agent phase
434    let mut git_helpers = crate::git_helpers::GitHelpers::new();
435    cleanup_orphaned_marker(&ctx.logger)?;
436    start_agent_phase(&mut git_helpers)?;
437    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
438
439    // Print welcome banner and validate PROMPT.md
440    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
441    print_pipeline_info(ctx);
442    validate_prompt_and_setup_backup(ctx)?;
443
444    // Set up PROMPT.md monitoring
445    let mut prompt_monitor = setup_prompt_monitor(ctx);
446
447    // Detect project stack and review guidelines
448    let (_project_stack, review_guidelines) =
449        detect_project_stack(&ctx.config, &ctx.repo_root, &ctx.logger, ctx.colors);
450
451    print_review_guidelines(ctx, review_guidelines.as_ref());
452    println!();
453
454    // Create phase context and save starting commit
455    let (mut timer, mut stats) = (Timer::new(), Stats::new());
456    let mut phase_ctx =
457        create_phase_context(ctx, &mut timer, &mut stats, review_guidelines.as_ref());
458    save_start_commit_or_warn(ctx);
459
460    // Run pre-development rebase
461    run_initial_rebase(
462        &ctx.args,
463        &ctx.config,
464        &ctx.template_context,
465        &ctx.logger,
466        ctx.colors,
467    )?;
468
469    // Run pipeline phases
470    run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
471    check_prompt_restoration(ctx, &mut prompt_monitor, "development");
472    update_status("In progress.", ctx.config.isolation_mode)?;
473
474    run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
475    check_prompt_restoration(ctx, &mut prompt_monitor, "review");
476
477    // Run post-review rebase
478    run_post_review_rebase(
479        &ctx.args,
480        &ctx.config,
481        &ctx.template_context,
482        &ctx.logger,
483        ctx.colors,
484    )?;
485
486    update_status("In progress.", ctx.config.isolation_mode)?;
487
488    run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
489
490    // Commit phase
491    finalize_pipeline(
492        &mut agent_phase_guard,
493        &ctx.logger,
494        ctx.colors,
495        &ctx.config,
496        &timer,
497        &stats,
498        prompt_monitor,
499    );
500    Ok(())
501}
502
503/// Print pipeline information (working directory and commit message).
504fn print_pipeline_info(ctx: &PipelineContext) {
505    ctx.logger.info(&format!(
506        "Working directory: {}{}{}",
507        ctx.colors.cyan(),
508        ctx.repo_root.display(),
509        ctx.colors.reset()
510    ));
511    ctx.logger.info(&format!(
512        "Commit message: {}{}{}",
513        ctx.colors.cyan(),
514        ctx.config.commit_msg,
515        ctx.colors.reset()
516    ));
517}
518
519/// Validate PROMPT.md and set up backup/protection.
520fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
521    let prompt_validation =
522        validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
523    for err in &prompt_validation.errors {
524        ctx.logger.error(err);
525    }
526    for warn in &prompt_validation.warnings {
527        ctx.logger.warn(warn);
528    }
529    if !prompt_validation.is_valid() {
530        anyhow::bail!("PROMPT.md validation errors");
531    }
532
533    // Create a backup of PROMPT.md to protect against accidental deletion.
534    match create_prompt_backup() {
535        Ok(None) => {}
536        Ok(Some(warning)) => {
537            ctx.logger.warn(&format!(
538                "PROMPT.md backup created but: {warning}. Continuing anyway."
539            ));
540        }
541        Err(e) => {
542            ctx.logger.warn(&format!(
543                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
544            ));
545        }
546    }
547
548    // Make PROMPT.md read-only to protect against accidental deletion.
549    match make_prompt_read_only() {
550        None => {}
551        Some(warning) => {
552            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
553        }
554    }
555
556    Ok(())
557}
558
559/// Set up PROMPT.md monitoring for deletion detection.
560fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
561    match PromptMonitor::new() {
562        Ok(mut monitor) => {
563            if let Err(e) = monitor.start() {
564                ctx.logger.warn(&format!(
565                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
566                ));
567                None
568            } else {
569                if ctx.config.verbosity.is_debug() {
570                    ctx.logger.info("Started real-time PROMPT.md monitoring");
571                }
572                Some(monitor)
573            }
574        }
575        Err(e) => {
576            ctx.logger.warn(&format!(
577                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
578            ));
579            None
580        }
581    }
582}
583
584/// Print review guidelines if detected.
585fn print_review_guidelines(
586    ctx: &PipelineContext,
587    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
588) {
589    if let Some(guidelines) = review_guidelines {
590        ctx.logger.info(&format!(
591            "Review guidelines: {}{}{}",
592            ctx.colors.dim(),
593            guidelines.summary(),
594            ctx.colors.reset()
595        ));
596    }
597}
598
599/// Create the phase context for running pipeline phases.
600fn create_phase_context<'ctx>(
601    ctx: &'ctx PipelineContext,
602    timer: &'ctx mut Timer,
603    stats: &'ctx mut Stats,
604    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
605) -> PhaseContext<'ctx> {
606    PhaseContext {
607        config: &ctx.config,
608        registry: &ctx.registry,
609        logger: &ctx.logger,
610        colors: &ctx.colors,
611        timer,
612        stats,
613        developer_agent: &ctx.developer_agent,
614        reviewer_agent: &ctx.reviewer_agent,
615        review_guidelines,
616        template_context: &ctx.template_context,
617    }
618}
619
620/// Save starting commit or warn if it fails.
621fn save_start_commit_or_warn(ctx: &PipelineContext) {
622    match save_start_commit() {
623        Ok(()) => {
624            if ctx.config.verbosity.is_debug() {
625                ctx.logger
626                    .info("Saved starting commit for incremental diff generation");
627            }
628        }
629        Err(e) => {
630            ctx.logger.warn(&format!(
631                "Failed to save starting commit: {e}. \
632                 Incremental diffs may be unavailable as a result."
633            ));
634            ctx.logger.info(
635                "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
636            );
637        }
638    }
639}
640
641/// Check for PROMPT.md restoration after a phase.
642fn check_prompt_restoration(
643    ctx: &PipelineContext,
644    prompt_monitor: &mut Option<PromptMonitor>,
645    phase: &str,
646) {
647    if let Some(ref mut monitor) = prompt_monitor {
648        if monitor.check_and_restore() {
649            ctx.logger.warn(&format!(
650                "PROMPT.md was deleted and restored during {phase} phase"
651            ));
652        }
653    }
654}
655
656/// Runs the development phase.
657fn run_development(
658    ctx: &mut PhaseContext,
659    args: &Args,
660    resume_checkpoint: Option<&PipelineCheckpoint>,
661) -> anyhow::Result<()> {
662    ctx.logger
663        .header("PHASE 1: Development", crate::logger::Colors::blue);
664
665    let resume_phase = resume_checkpoint.map(|c| c.phase);
666    let resume_rank = resume_phase.map(phase_rank);
667
668    if resume_rank.is_some_and(|rank| rank >= phase_rank(PipelinePhase::Review)) {
669        ctx.logger
670            .info("Skipping development phase (checkpoint indicates it already completed)");
671        return Ok(());
672    }
673
674    if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
675        ctx.logger
676            .info("Skipping development phase (resuming from a later checkpoint phase)");
677        return Ok(());
678    }
679
680    let start_iter = match resume_phase {
681        Some(PipelinePhase::Planning | PipelinePhase::Development) => resume_checkpoint
682            .map_or(1, |c| c.iteration)
683            .clamp(1, ctx.config.developer_iters),
684        _ => 1,
685    };
686
687    let resuming_from_development =
688        args.recovery.resume && resume_phase == Some(PipelinePhase::Development);
689    let development_result = run_development_phase(ctx, start_iter, resuming_from_development)?;
690
691    if development_result.had_errors {
692        ctx.logger
693            .warn("Development phase completed with non-fatal errors");
694    }
695
696    Ok(())
697}
698
699/// Runs the review and fix phase.
700fn run_review_and_fix(
701    ctx: &mut PhaseContext,
702    _args: &Args,
703    resume_checkpoint: Option<&PipelineCheckpoint>,
704) -> anyhow::Result<()> {
705    ctx.logger
706        .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
707
708    let resume_phase = resume_checkpoint.map(|c| c.phase);
709
710    // Check if we should run any reviewer phase
711    let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
712        || should_run_from(PipelinePhase::Fix, resume_checkpoint)
713        || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
714        || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
715
716    let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
717        || resume_phase == Some(PipelinePhase::Fix)
718        || resume_phase == Some(PipelinePhase::ReviewAgain);
719
720    if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
721        let start_pass = match resume_phase {
722            Some(PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain) => {
723                resume_checkpoint
724                    .map_or(1, |c| c.reviewer_pass)
725                    .clamp(1, ctx.config.reviewer_reviews.max(1))
726            }
727            _ => 1,
728        };
729
730        let review_result = run_review_phase(ctx, start_pass)?;
731        if review_result.completed_early {
732            ctx.logger
733                .success("Review phase completed early (no issues found)");
734        }
735    } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
736        ctx.logger
737            .info("Skipping review phase (reviewer_reviews=0)");
738    } else if run_any_reviewer_phase {
739        ctx.logger
740            .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
741    }
742
743    // Note: The old dedicated commit phase has been removed.
744    // Commits now happen automatically per-iteration during development and per-cycle during review.
745
746    Ok(())
747}
748
749/// Runs final validation if configured.
750fn run_final_validation(
751    ctx: &PhaseContext,
752    resume_checkpoint: Option<&PipelineCheckpoint>,
753) -> anyhow::Result<()> {
754    let Some(ref full_cmd) = ctx.config.full_check_cmd else {
755        return Ok(());
756    };
757
758    if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
759        ctx.logger
760            .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
761        ctx.logger
762            .info("Skipping final validation (resuming from a later checkpoint phase)");
763        return Ok(());
764    }
765
766    let argv = utils::split_command(full_cmd)
767        .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
768    if argv.is_empty() {
769        ctx.logger
770            .warn("FULL_CHECK_CMD is empty; skipping final validation");
771        return Ok(());
772    }
773
774    if ctx.config.features.checkpoint_enabled {
775        let _ = save_checkpoint(&PipelineCheckpoint::new(
776            PipelinePhase::FinalValidation,
777            ctx.config.developer_iters,
778            ctx.config.developer_iters,
779            ctx.config.reviewer_reviews,
780            ctx.config.reviewer_reviews,
781            ctx.developer_agent,
782            ctx.reviewer_agent,
783        ));
784    }
785
786    ctx.logger
787        .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
788    let display_cmd = utils::format_argv_for_log(&argv);
789    ctx.logger.info(&format!(
790        "Running full check: {}{}{}",
791        ctx.colors.dim(),
792        display_cmd,
793        ctx.colors.reset()
794    ));
795
796    let Some((program, arguments)) = argv.split_first() else {
797        ctx.logger
798            .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
799        return Ok(());
800    };
801    let status = Command::new(program).args(arguments).status()?;
802
803    if status.success() {
804        ctx.logger.success("Full check passed");
805    } else {
806        ctx.logger.error("Full check failed");
807        anyhow::bail!("Full check failed");
808    }
809
810    Ok(())
811}
812
813/// Handle --rebase-only flag.
814///
815/// This function performs a rebase to the default branch with AI conflict resolution and exits,
816/// without running the full pipeline.
817fn handle_rebase_only(
818    _args: &Args,
819    config: &crate::config::Config,
820    template_context: &TemplateContext,
821    logger: &Logger,
822    colors: Colors,
823) -> anyhow::Result<()> {
824    // Check if we're on main/master branch
825    if is_main_or_master_branch()? {
826        logger.warn("Already on main/master branch - rebasing on main is not recommended");
827        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
828        logger.info("  git worktree add ../feature-branch feature-branch");
829        logger.info("This allows multiple AI agents to work on different features simultaneously.");
830        logger.info("Proceeding with rebase anyway as requested...");
831    }
832
833    logger.header("Rebase to default branch", Colors::cyan);
834
835    match run_rebase_to_default(logger, colors) {
836        Ok(RebaseResult::Success) => {
837            logger.success("Rebase completed successfully");
838            Ok(())
839        }
840        Ok(RebaseResult::NoOp) => {
841            logger.info("No rebase needed (already up-to-date)");
842            Ok(())
843        }
844        Ok(RebaseResult::Conflicts(_conflicts)) => {
845            // Get the actual conflicted files
846            let conflicted_files = get_conflicted_files()?;
847            if conflicted_files.is_empty() {
848                logger.warn("Rebase reported conflicts but no conflicted files found");
849                let _ = abort_rebase();
850                return Ok(());
851            }
852
853            logger.warn(&format!(
854                "Rebase resulted in {} conflict(s), attempting AI resolution",
855                conflicted_files.len()
856            ));
857
858            // Attempt to resolve conflicts with AI
859            match try_resolve_conflicts_with_fallback(
860                &conflicted_files,
861                config,
862                template_context,
863                logger,
864                colors,
865            ) {
866                Ok(true) => {
867                    // Conflicts resolved, continue the rebase
868                    logger.info("Continuing rebase after conflict resolution");
869                    match continue_rebase() {
870                        Ok(()) => {
871                            logger.success("Rebase completed successfully after AI resolution");
872                            Ok(())
873                        }
874                        Err(e) => {
875                            logger.error(&format!("Failed to continue rebase: {e}"));
876                            let _ = abort_rebase();
877                            anyhow::bail!("Rebase failed after conflict resolution")
878                        }
879                    }
880                }
881                Ok(false) => {
882                    // AI resolution failed
883                    logger.error("AI conflict resolution failed, aborting rebase");
884                    let _ = abort_rebase();
885                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
886                }
887                Err(e) => {
888                    logger.error(&format!("Conflict resolution error: {e}"));
889                    let _ = abort_rebase();
890                    anyhow::bail!("Rebase conflict resolution failed: {e}")
891                }
892            }
893        }
894        Err(e) => {
895            logger.error(&format!("Rebase failed: {e}"));
896            anyhow::bail!("Rebase failed: {e}")
897        }
898    }
899}
900
901/// Run rebase to the default branch.
902///
903/// This function performs a rebase from the current branch to the
904/// default branch (main/master). It handles all edge cases including:
905/// - Already on main/master (proceeds with rebase attempt)
906/// - Empty repository (returns `NoOp`)
907/// - Upstream branch not found (error)
908/// - Conflicts during rebase (returns `Conflicts` result)
909///
910/// # Returns
911///
912/// Returns `RebaseResult` indicating the outcome.
913fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
914    // Get the default branch
915    let default_branch = get_default_branch()?;
916    logger.info(&format!(
917        "Rebasing onto {}{}{}",
918        colors.cyan(),
919        default_branch,
920        colors.reset()
921    ));
922
923    // Perform the rebase
924    rebase_onto(&default_branch)
925}
926
927/// Run initial rebase before development phase.
928///
929/// This function is called before the development phase starts to ensure
930/// the feature branch is up-to-date with the default branch.
931fn run_initial_rebase(
932    _args: &Args,
933    config: &crate::config::Config,
934    template_context: &TemplateContext,
935    logger: &Logger,
936    colors: Colors,
937) -> anyhow::Result<()> {
938    logger.header("Pre-development rebase", Colors::cyan);
939
940    match run_rebase_to_default(logger, colors) {
941        Ok(RebaseResult::Success) => {
942            logger.success("Rebase completed successfully");
943            Ok(())
944        }
945        Ok(RebaseResult::NoOp) => {
946            logger.info("No rebase needed (already up-to-date or on main branch)");
947            Ok(())
948        }
949        Ok(RebaseResult::Conflicts(_conflicts)) => {
950            // Get the actual conflicted files
951            let conflicted_files = get_conflicted_files()?;
952            if conflicted_files.is_empty() {
953                logger.warn("Rebase reported conflicts but no conflicted files found");
954                let _ = abort_rebase();
955                return Ok(());
956            }
957
958            logger.warn(&format!(
959                "Rebase resulted in {} conflict(s), attempting AI resolution",
960                conflicted_files.len()
961            ));
962
963            // Attempt to resolve conflicts with AI
964            match try_resolve_conflicts_with_fallback(
965                &conflicted_files,
966                config,
967                template_context,
968                logger,
969                colors,
970            ) {
971                Ok(true) => {
972                    // Conflicts resolved, continue the rebase
973                    logger.info("Continuing rebase after conflict resolution");
974                    match continue_rebase() {
975                        Ok(()) => {
976                            logger.success("Rebase completed successfully after AI resolution");
977                            Ok(())
978                        }
979                        Err(e) => {
980                            logger.warn(&format!("Failed to continue rebase: {e}"));
981                            let _ = abort_rebase();
982                            Ok(()) // Continue anyway - conflicts were resolved
983                        }
984                    }
985                }
986                Ok(false) => {
987                    // AI resolution failed
988                    logger.warn("AI conflict resolution failed, aborting rebase");
989                    let _ = abort_rebase();
990                    Ok(()) // Continue pipeline - don't block on rebase failure
991                }
992                Err(e) => {
993                    logger.error(&format!("Conflict resolution error: {e}"));
994                    let _ = abort_rebase();
995                    Ok(()) // Continue pipeline
996                }
997            }
998        }
999        Err(e) => {
1000            logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1001            Ok(())
1002        }
1003    }
1004}
1005
1006/// Run post-review rebase after review phase.
1007///
1008/// This function is called after the review phase completes to ensure
1009/// the feature branch is still up-to-date with the default branch.
1010fn run_post_review_rebase(
1011    _args: &Args,
1012    config: &crate::config::Config,
1013    template_context: &TemplateContext,
1014    logger: &Logger,
1015    colors: Colors,
1016) -> anyhow::Result<()> {
1017    logger.header("Post-review rebase", Colors::cyan);
1018
1019    match run_rebase_to_default(logger, colors) {
1020        Ok(RebaseResult::Success) => {
1021            logger.success("Rebase completed successfully");
1022            Ok(())
1023        }
1024        Ok(RebaseResult::NoOp) => {
1025            logger.info("No rebase needed (already up-to-date or on main branch)");
1026            Ok(())
1027        }
1028        Ok(RebaseResult::Conflicts(_conflicts)) => {
1029            // Get the actual conflicted files
1030            let conflicted_files = get_conflicted_files()?;
1031            if conflicted_files.is_empty() {
1032                logger.warn("Rebase reported conflicts but no conflicted files found");
1033                let _ = abort_rebase();
1034                return Ok(());
1035            }
1036
1037            logger.warn(&format!(
1038                "Rebase resulted in {} conflict(s), attempting AI resolution",
1039                conflicted_files.len()
1040            ));
1041
1042            // Attempt to resolve conflicts with AI
1043            match try_resolve_conflicts_with_fallback(
1044                &conflicted_files,
1045                config,
1046                template_context,
1047                logger,
1048                colors,
1049            ) {
1050                Ok(true) => {
1051                    // Conflicts resolved, continue the rebase
1052                    logger.info("Continuing rebase after conflict resolution");
1053                    match continue_rebase() {
1054                        Ok(()) => {
1055                            logger.success("Rebase completed successfully after AI resolution");
1056                            Ok(())
1057                        }
1058                        Err(e) => {
1059                            logger.warn(&format!("Failed to continue rebase: {e}"));
1060                            let _ = abort_rebase();
1061                            Ok(()) // Continue anyway - conflicts were resolved
1062                        }
1063                    }
1064                }
1065                Ok(false) => {
1066                    // AI resolution failed
1067                    logger.warn("AI conflict resolution failed, aborting rebase");
1068                    let _ = abort_rebase();
1069                    Ok(()) // Continue pipeline - don't block on rebase failure
1070                }
1071                Err(e) => {
1072                    logger.error(&format!("Conflict resolution error: {e}"));
1073                    let _ = abort_rebase();
1074                    Ok(()) // Continue pipeline
1075                }
1076            }
1077        }
1078        Err(e) => {
1079            logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1080            Ok(())
1081        }
1082    }
1083}
1084
1085/// Result type for conflict resolution attempts.
1086///
1087/// Represents the different ways conflict resolution can succeed or fail.
1088enum ConflictResolutionResult {
1089    /// Agent provided JSON output with resolved file contents
1090    WithJson(String),
1091    /// Agent resolved conflicts by editing files directly (no JSON output)
1092    FileEditsOnly,
1093    /// Resolution failed completely
1094    Failed,
1095}
1096
1097/// Attempt to resolve rebase conflicts with AI fallback.
1098///
1099/// This is a helper function that creates a minimal `PhaseContext`
1100/// for conflict resolution without requiring full pipeline state.
1101fn try_resolve_conflicts_with_fallback(
1102    conflicted_files: &[String],
1103    config: &crate::config::Config,
1104    template_context: &TemplateContext,
1105    logger: &Logger,
1106    colors: Colors,
1107) -> anyhow::Result<bool> {
1108    if conflicted_files.is_empty() {
1109        return Ok(false);
1110    }
1111
1112    logger.info(&format!(
1113        "Attempting AI conflict resolution for {} file(s)",
1114        conflicted_files.len()
1115    ));
1116
1117    let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
1118    let resolution_prompt = build_resolution_prompt(&conflicts, template_context);
1119
1120    match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1121        Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1122            // Agent provided JSON output - parse and write files
1123            let resolved_files = parse_and_validate_resolved_files(&resolved_content, logger)?;
1124            write_resolved_files(&resolved_files, logger)?;
1125
1126            // Verify all conflicts are resolved
1127            let remaining_conflicts = get_conflicted_files()?;
1128            if remaining_conflicts.is_empty() {
1129                Ok(true)
1130            } else {
1131                logger.warn(&format!(
1132                    "{} conflicts remain after AI resolution",
1133                    remaining_conflicts.len()
1134                ));
1135                Ok(false)
1136            }
1137        }
1138        Ok(ConflictResolutionResult::FileEditsOnly) => {
1139            // Agent resolved conflicts by editing files directly
1140            logger.info("Agent resolved conflicts via file edits (no JSON output)");
1141
1142            // Verify all conflicts are resolved
1143            let remaining_conflicts = get_conflicted_files()?;
1144            if remaining_conflicts.is_empty() {
1145                logger.success("All conflicts resolved via file edits");
1146                Ok(true)
1147            } else {
1148                logger.warn(&format!(
1149                    "{} conflicts remain after AI resolution",
1150                    remaining_conflicts.len()
1151                ));
1152                Ok(false)
1153            }
1154        }
1155        Ok(ConflictResolutionResult::Failed) => {
1156            logger.warn("AI conflict resolution failed");
1157            logger.info("Attempting to continue rebase anyway...");
1158
1159            // Try to continue rebase - user may have manually resolved conflicts
1160            match crate::git_helpers::continue_rebase() {
1161                Ok(()) => {
1162                    logger.info("Successfully continued rebase");
1163                    Ok(true)
1164                }
1165                Err(rebase_err) => {
1166                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1167                    Ok(false) // Conflicts remain
1168                }
1169            }
1170        }
1171        Err(e) => {
1172            logger.warn(&format!("AI conflict resolution failed: {e}"));
1173            logger.info("Attempting to continue rebase anyway...");
1174
1175            // Try to continue rebase - user may have manually resolved conflicts
1176            match crate::git_helpers::continue_rebase() {
1177                Ok(()) => {
1178                    logger.info("Successfully continued rebase");
1179                    Ok(true)
1180                }
1181                Err(rebase_err) => {
1182                    logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1183                    Ok(false) // Conflicts remain
1184                }
1185            }
1186        }
1187    }
1188}
1189
1190/// Collect conflict information from conflicted files.
1191fn collect_conflict_info_or_error(
1192    conflicted_files: &[String],
1193    logger: &Logger,
1194) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
1195    use crate::prompts::collect_conflict_info;
1196
1197    let conflicts = match collect_conflict_info(conflicted_files) {
1198        Ok(c) => c,
1199        Err(e) => {
1200            logger.error(&format!("Failed to collect conflict info: {e}"));
1201            anyhow::bail!("Failed to collect conflict info");
1202        }
1203    };
1204    Ok(conflicts)
1205}
1206
1207/// Build the conflict resolution prompt from context files.
1208fn build_resolution_prompt(
1209    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1210    template_context: &TemplateContext,
1211) -> String {
1212    use crate::prompts::build_conflict_resolution_prompt_with_context;
1213    use std::fs;
1214
1215    let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
1216    let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
1217
1218    build_conflict_resolution_prompt_with_context(
1219        template_context,
1220        conflicts,
1221        prompt_md_content.as_deref(),
1222        plan_content.as_deref(),
1223    )
1224}
1225
1226/// Run AI agent to resolve conflicts with fallback mechanism.
1227///
1228/// Returns `ConflictResolutionResult` indicating whether the agent provided
1229/// JSON output, resolved conflicts via file edits, or failed completely.
1230fn run_ai_conflict_resolution(
1231    resolution_prompt: &str,
1232    config: &crate::config::Config,
1233    logger: &Logger,
1234    colors: Colors,
1235) -> anyhow::Result<ConflictResolutionResult> {
1236    use crate::agents::AgentRegistry;
1237    use crate::files::result_extraction::extract_last_result;
1238    use crate::pipeline::{
1239        run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
1240    };
1241    use std::io;
1242    use std::path::Path;
1243
1244    // Note: log_dir is used as a prefix for log file names, not as a directory.
1245    // The actual log files will be created in .agent/logs/ with names like:
1246    // .agent/logs/rebase_conflict_resolution_ccs-glm_0.log
1247    let log_dir = ".agent/logs/rebase_conflict_resolution";
1248
1249    let registry = AgentRegistry::new()?;
1250    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
1251
1252    let mut runtime = PipelineRuntime {
1253        timer: &mut crate::pipeline::Timer::new(),
1254        logger,
1255        colors: &colors,
1256        config,
1257    };
1258
1259    // Output validator: checks if agent produced valid output OR resolved conflicts
1260    // Agents may edit files without returning JSON, so we verify conflicts are resolved.
1261    let validate_output: OutputValidator = |log_dir_path: &Path,
1262                                            validation_logger: &crate::logger::Logger|
1263     -> io::Result<bool> {
1264        match extract_last_result(log_dir_path) {
1265            Ok(Some(_)) => {
1266                // Valid JSON output exists
1267                Ok(true)
1268            }
1269            Ok(None) => {
1270                // No JSON output - check if conflicts were resolved anyway
1271                // (agent may have edited files without returning JSON)
1272                match crate::git_helpers::get_conflicted_files() {
1273                    Ok(conflicts) if conflicts.is_empty() => {
1274                        validation_logger
1275                            .info("Agent resolved conflicts without JSON output (file edits only)");
1276                        Ok(true) // Conflicts resolved, consider success
1277                    }
1278                    Ok(conflicts) => {
1279                        validation_logger.warn(&format!(
1280                            "{} conflict(s) remain unresolved",
1281                            conflicts.len()
1282                        ));
1283                        Ok(false) // Conflicts remain
1284                    }
1285                    Err(e) => {
1286                        validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
1287                        Ok(false) // Error checking conflicts
1288                    }
1289                }
1290            }
1291            Err(e) => {
1292                validation_logger.warn(&format!("Output validation check failed: {e}"));
1293                Ok(false) // Treat validation errors as missing output
1294            }
1295        }
1296    };
1297
1298    let mut fallback_config = FallbackConfig {
1299        role: crate::agents::AgentRole::Reviewer,
1300        base_label: "conflict resolution",
1301        prompt: resolution_prompt,
1302        logfile_prefix: log_dir,
1303        runtime: &mut runtime,
1304        registry: &registry,
1305        primary_agent: reviewer_agent,
1306        output_validator: Some(validate_output),
1307    };
1308
1309    let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
1310
1311    if exit_code != 0 {
1312        return Ok(ConflictResolutionResult::Failed);
1313    }
1314
1315    // Check if conflicts are resolved after agent run
1316    // The validator already checked this, but we verify again to determine the result type
1317    let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
1318
1319    if remaining_conflicts.is_empty() {
1320        // Conflicts are resolved - check if agent provided JSON output
1321        match extract_last_result(Path::new(log_dir)) {
1322            Ok(Some(content)) => {
1323                logger.info("Agent provided JSON output with resolved files");
1324                Ok(ConflictResolutionResult::WithJson(content))
1325            }
1326            Ok(None) => {
1327                logger.info("Agent resolved conflicts via file edits (no JSON output)");
1328                Ok(ConflictResolutionResult::FileEditsOnly)
1329            }
1330            Err(e) => {
1331                // Extraction failed but conflicts are resolved - treat as file edits only
1332                logger.warn(&format!(
1333                    "Failed to extract JSON output but conflicts are resolved: {e}"
1334                ));
1335                Ok(ConflictResolutionResult::FileEditsOnly)
1336            }
1337        }
1338    } else {
1339        logger.warn(&format!(
1340            "{} conflict(s) remain after agent attempted resolution",
1341            remaining_conflicts.len()
1342        ));
1343        Ok(ConflictResolutionResult::Failed)
1344    }
1345}
1346
1347/// Parse and validate the resolved files from AI output.
1348fn parse_and_validate_resolved_files(
1349    resolved_content: &str,
1350    logger: &Logger,
1351) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
1352    let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|e| {
1353        logger.error(&format!("Failed to parse agent output as JSON: {e}"));
1354        anyhow::anyhow!("Failed to parse agent output as JSON")
1355    })?;
1356
1357    let resolved_files = match json.get("resolved_files") {
1358        Some(v) if v.is_object() => v.as_object().unwrap(),
1359        _ => {
1360            logger.error("Agent output missing 'resolved_files' object");
1361            anyhow::bail!("Agent output missing 'resolved_files' object");
1362        }
1363    };
1364
1365    if resolved_files.is_empty() {
1366        logger.error("No files were resolved by the agent");
1367        anyhow::bail!("No files were resolved by the agent");
1368    }
1369
1370    Ok(resolved_files.clone())
1371}
1372
1373/// Write resolved files to disk and stage them.
1374fn write_resolved_files(
1375    resolved_files: &serde_json::Map<String, serde_json::Value>,
1376    logger: &Logger,
1377) -> anyhow::Result<usize> {
1378    use std::fs;
1379
1380    let mut files_written = 0;
1381    for (path, content) in resolved_files {
1382        if let Some(content_str) = content.as_str() {
1383            fs::write(path, content_str).map_err(|e| {
1384                logger.error(&format!("Failed to write {path}: {e}"));
1385                anyhow::anyhow!("Failed to write {path}: {e}")
1386            })?;
1387            logger.info(&format!("Resolved and wrote: {path}"));
1388            files_written += 1;
1389            // Stage the resolved file
1390            if let Err(e) = crate::git_helpers::git_add_all() {
1391                logger.warn(&format!("Failed to stage {path}: {e}"));
1392            }
1393        }
1394    }
1395
1396    logger.success(&format!("Successfully resolved {files_written} file(s)"));
1397    Ok(files_written)
1398}