Skip to main content

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 effect;
23pub mod effect_handler;
24pub mod effectful;
25pub mod event_loop;
26pub mod finalization;
27#[cfg(any(test, feature = "test-utils"))]
28pub mod mock_effect_handler;
29pub mod plumbing;
30pub mod resume;
31pub mod validation;
32
33use crate::agents::AgentRegistry;
34use crate::app::finalization::finalize_pipeline;
35use crate::banner::print_welcome_banner;
36use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
37
38use crate::checkpoint::{
39    save_checkpoint_with_workspace, CheckpointBuilder, PipelineCheckpoint, PipelinePhase,
40    RebaseState,
41};
42use crate::cli::{
43    create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
44    handle_list_available_agents, handle_list_providers, handle_show_baseline,
45    handle_template_commands, prompt_template_selection, Args,
46};
47
48use crate::executor::ProcessExecutor;
49use crate::files::protection::monitoring::PromptMonitor;
50use crate::files::{
51    create_prompt_backup_with_workspace, make_prompt_read_only_with_workspace,
52    update_status_with_workspace, validate_prompt_md_with_workspace,
53};
54use crate::git_helpers::{
55    abort_rebase, continue_rebase, get_conflicted_files, get_default_branch,
56    is_main_or_master_branch, rebase_onto, reset_start_commit, RebaseResult,
57};
58#[cfg(not(feature = "test-utils"))]
59use crate::git_helpers::{
60    cleanup_orphaned_marker, get_start_commit_summary, save_start_commit, start_agent_phase,
61};
62use crate::logger::Colors;
63use crate::logger::Logger;
64use crate::phases::PhaseContext;
65use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
66use crate::prompts::{get_stored_or_generate_prompt, template_context::TemplateContext};
67
68use config_init::initialize_config;
69use context::PipelineContext;
70use detection::detect_project_stack;
71use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
72use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
73use validation::{
74    resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
75};
76
77/// Main application entry point.
78///
79/// Orchestrates the entire Ralph pipeline:
80/// 1. Configuration initialization
81/// 2. Agent validation
82/// 3. Plumbing commands (if requested)
83/// 4. Development phase
84/// 5. Review & fix phase
85/// 6. Final validation
86/// 7. Commit phase
87///
88/// # Arguments
89///
90/// * `args` - The parsed CLI arguments
91/// * `executor` - Process executor for external process execution
92///
93/// # Returns
94///
95/// Returns `Ok(())` on success or an error if any phase fails.
96pub fn run(args: Args, executor: std::sync::Arc<dyn ProcessExecutor>) -> anyhow::Result<()> {
97    let colors = Colors::new();
98    let logger = Logger::new(colors);
99
100    // Set working directory first if override is provided
101    // This ensures all subsequent operations (including config init) use the correct directory
102    if let Some(ref override_dir) = args.working_dir_override {
103        std::env::set_current_dir(override_dir)?;
104    }
105
106    // Initialize configuration and agent registry
107    let Some(init_result) = initialize_config(&args, colors, &logger)? else {
108        return Ok(()); // Early exit (--init/--init-global/--init-legacy)
109    };
110
111    let config_init::ConfigInitResult {
112        config,
113        registry,
114        config_path,
115        config_sources,
116    } = init_result;
117
118    // Resolve required agent names
119    let validated = resolve_required_agents(&config)?;
120    let developer_agent = validated.developer_agent;
121    let reviewer_agent = validated.reviewer_agent;
122
123    // Handle listing commands (these can run without git repo)
124    if handle_listing_commands(&args, &registry, colors) {
125        return Ok(());
126    }
127
128    // Handle --diagnose
129    if args.recovery.diagnose {
130        handle_diagnose(
131            colors,
132            &config,
133            &registry,
134            &config_path,
135            &config_sources,
136            &*executor,
137        );
138        return Ok(());
139    }
140
141    // Validate agent chains
142    validate_agent_chains(&registry, colors);
143
144    // Handle plumbing commands
145    // In production mode, no workspace is available for plumbing commands
146    // (they use real filesystem operations)
147    let mut handler = effect_handler::RealAppEffectHandler::new();
148    if handle_plumbing_commands(&args, &logger, colors, &mut handler, None)? {
149        return Ok(());
150    }
151
152    // Validate agents and set up git repo and PROMPT.md
153    let Some(repo_root) = validate_and_setup_agents(
154        AgentSetupParams {
155            config: &config,
156            registry: &registry,
157            developer_agent: &developer_agent,
158            reviewer_agent: &reviewer_agent,
159            config_path: &config_path,
160            colors,
161            logger: &logger,
162            working_dir_override: args.working_dir_override.as_deref(),
163        },
164        &mut handler,
165    )?
166    else {
167        return Ok(());
168    };
169
170    // Create workspace for explicit path resolution
171    let workspace: std::sync::Arc<dyn crate::workspace::Workspace> =
172        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()));
173
174    // Prepare pipeline context or exit early
175    (prepare_pipeline_or_exit(PipelinePreparationParams {
176        args,
177        config,
178        registry,
179        developer_agent,
180        reviewer_agent,
181        repo_root,
182        logger,
183        colors,
184        executor,
185        handler: &mut handler,
186        workspace,
187    })?)
188    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
189}
190
191/// Test-only entry point that accepts a pre-built Config.
192///
193/// This function is for integration testing only. It bypasses environment variable
194/// loading and uses the provided Config directly, enabling deterministic tests
195/// that don't rely on process-global state.
196///
197/// This function handles ALL commands including early-exit commands (--init, --diagnose,
198/// --reset-start-commit, etc.) so that tests can use a single entry point.
199///
200/// # Arguments
201///
202/// * `args` - The parsed CLI arguments
203/// * `executor` - Process executor for external process execution  
204/// * `config` - Pre-built configuration (bypasses env var loading)
205/// * `registry` - Pre-built agent registry
206///
207/// # Returns
208///
209/// Returns `Ok(())` on success or an error if any phase fails.
210#[cfg(feature = "test-utils")]
211pub fn run_with_config(
212    args: Args,
213    executor: std::sync::Arc<dyn ProcessExecutor>,
214    config: crate::config::Config,
215    registry: AgentRegistry,
216) -> anyhow::Result<()> {
217    // Use real path resolver and effect handler by default for backward compatibility
218    let mut handler = effect_handler::RealAppEffectHandler::new();
219    run_with_config_and_resolver(
220        args,
221        executor,
222        config,
223        registry,
224        &crate::config::RealConfigEnvironment,
225        &mut handler,
226        None, // Use default WorkspaceFs
227    )
228}
229
230/// Test-only entry point that accepts a pre-built Config and a custom path resolver.
231///
232/// This function is for integration testing only. It bypasses environment variable
233/// loading and uses the provided Config and path resolver directly, enabling
234/// deterministic tests that don't rely on process-global state or env vars.
235///
236/// This function handles ALL commands including early-exit commands (--init, --diagnose,
237/// --reset-start-commit, etc.) so that tests can use a single entry point.
238///
239/// # Arguments
240///
241/// * `args` - The parsed CLI arguments
242/// * `executor` - Process executor for external process execution
243/// * `config` - Pre-built configuration (bypasses env var loading)
244/// * `registry` - Pre-built agent registry
245/// * `path_resolver` - Custom path resolver for init commands
246/// * `handler` - Effect handler for git/filesystem operations
247/// * `workspace` - Optional workspace for file operations (if `None`, uses `WorkspaceFs`)
248///
249/// # Returns
250///
251/// Returns `Ok(())` on success or an error if any phase fails.
252#[cfg(feature = "test-utils")]
253pub fn run_with_config_and_resolver<
254    P: crate::config::ConfigEnvironment,
255    H: effect::AppEffectHandler,
256>(
257    args: Args,
258    executor: std::sync::Arc<dyn ProcessExecutor>,
259    config: crate::config::Config,
260    registry: AgentRegistry,
261    path_resolver: &P,
262    handler: &mut H,
263    workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
264) -> anyhow::Result<()> {
265    use crate::cli::{
266        handle_extended_help, handle_init_global_with, handle_init_prompt_with,
267        handle_list_work_guides, handle_smart_init_with,
268    };
269
270    let colors = Colors::new();
271    let logger = Logger::new(colors);
272
273    // Set working directory first if override is provided
274    if let Some(ref override_dir) = args.working_dir_override {
275        std::env::set_current_dir(override_dir)?;
276    }
277
278    // Handle --extended-help / --man flag: display extended help and exit.
279    if args.recovery.extended_help {
280        handle_extended_help();
281        if args.work_guide_list.list_work_guides {
282            println!();
283            handle_list_work_guides(colors);
284        }
285        return Ok(());
286    }
287
288    // Handle --list-work-guides / --list-templates flag
289    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
290        return Ok(());
291    }
292
293    // Handle --init-prompt flag: create PROMPT.md from template and exit
294    if let Some(ref template_name) = args.init_prompt {
295        if handle_init_prompt_with(
296            template_name,
297            args.unified_init.force_init,
298            colors,
299            path_resolver,
300        )? {
301            return Ok(());
302        }
303    }
304
305    // Handle smart --init flag: intelligently determine what to initialize
306    if args.unified_init.init.is_some()
307        && handle_smart_init_with(
308            args.unified_init.init.as_deref(),
309            args.unified_init.force_init,
310            colors,
311            path_resolver,
312        )?
313    {
314        return Ok(());
315    }
316
317    // Handle --init-config flag: explicit config creation and exit
318    if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
319        return Ok(());
320    }
321
322    // Handle --init-global flag: create unified config if it doesn't exist and exit
323    if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
324        return Ok(());
325    }
326
327    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
328    if args.legacy_init.init_legacy {
329        let repo_root = match handler.execute(effect::AppEffect::GitGetRepoRoot) {
330            effect::AppEffectResult::Path(p) => Some(p),
331            _ => None,
332        };
333        let legacy_path = repo_root.map_or_else(
334            || std::path::PathBuf::from(".agent/agents.toml"),
335            |root| root.join(".agent/agents.toml"),
336        );
337        if crate::cli::handle_init_legacy(colors, &legacy_path)? {
338            return Ok(());
339        }
340    }
341
342    // Use provided config directly (no env var loading)
343    let config_path = std::path::PathBuf::from("test-config");
344
345    // Resolve required agent names
346    let validated = resolve_required_agents(&config)?;
347    let developer_agent = validated.developer_agent;
348    let reviewer_agent = validated.reviewer_agent;
349
350    // Handle listing commands (these can run without git repo)
351    if handle_listing_commands(&args, &registry, colors) {
352        return Ok(());
353    }
354
355    // Handle --diagnose
356    if args.recovery.diagnose {
357        handle_diagnose(colors, &config, &registry, &config_path, &[], &*executor);
358        return Ok(());
359    }
360
361    // Handle plumbing commands (--reset-start-commit, --show-commit-msg, etc.)
362    // Pass workspace reference for testability with MemoryWorkspace
363    if handle_plumbing_commands(
364        &args,
365        &logger,
366        colors,
367        handler,
368        workspace.as_ref().map(|w| w.as_ref()),
369    )? {
370        return Ok(());
371    }
372
373    // Validate agents and set up git repo and PROMPT.md
374    let Some(repo_root) = validate_and_setup_agents(
375        AgentSetupParams {
376            config: &config,
377            registry: &registry,
378            developer_agent: &developer_agent,
379            reviewer_agent: &reviewer_agent,
380            config_path: &config_path,
381            colors,
382            logger: &logger,
383            working_dir_override: args.working_dir_override.as_deref(),
384        },
385        handler,
386    )?
387    else {
388        return Ok(());
389    };
390
391    // Create workspace for explicit path resolution, or use injected workspace
392    let workspace = workspace.unwrap_or_else(|| {
393        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
394    });
395
396    // Prepare pipeline context or exit early
397    (prepare_pipeline_or_exit(PipelinePreparationParams {
398        args,
399        config,
400        registry,
401        developer_agent,
402        reviewer_agent,
403        repo_root,
404        logger,
405        colors,
406        executor,
407        handler,
408        workspace,
409    })?)
410    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
411}
412
413/// Parameters for `run_with_config_and_handlers`.
414///
415/// Groups related parameters to reduce function argument count.
416#[cfg(feature = "test-utils")]
417pub struct RunWithHandlersParams<'a, 'ctx, P, A, E>
418where
419    P: crate::config::ConfigEnvironment,
420    A: effect::AppEffectHandler,
421    E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
422{
423    pub args: Args,
424    pub executor: std::sync::Arc<dyn ProcessExecutor>,
425    pub config: crate::config::Config,
426    pub registry: AgentRegistry,
427    pub path_resolver: &'a P,
428    pub app_handler: &'a mut A,
429    pub effect_handler: &'a mut E,
430    pub workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
431    /// Phantom data to bind the `'ctx` lifetime from `EffectHandler<'ctx>`.
432    pub _marker: std::marker::PhantomData<&'ctx ()>,
433}
434
435/// Run with both AppEffectHandler AND EffectHandler for full isolation.
436///
437/// This function is the ultimate test entry point that allows injecting BOTH:
438/// - `AppEffectHandler` for CLI-layer operations (git require repo, set cwd, etc.)
439/// - `EffectHandler` for reducer-layer operations (create commit, run rebase, etc.)
440///
441/// Using both handlers ensures tests make ZERO real git calls at any layer.
442///
443/// # Example
444///
445/// ```ignore
446/// use ralph_workflow::app::mock_effect_handler::MockAppEffectHandler;
447/// use ralph_workflow::reducer::mock_effect_handler::MockEffectHandler;
448///
449/// let mut app_handler = MockAppEffectHandler::new().with_head_oid("abc123");
450/// let mut effect_handler = MockEffectHandler::new(PipelineState::initial(1, 0));
451///
452/// run_with_config_and_handlers(RunWithHandlersParams {
453///     args, executor, config, registry, path_resolver: &env,
454///     app_handler: &mut app_handler, effect_handler: &mut effect_handler,
455///     workspace: None,
456/// })?;
457///
458/// // Verify no real git operations at either layer
459/// assert!(app_handler.captured().iter().any(|e| matches!(e, AppEffect::GitRequireRepo)));
460/// assert!(effect_handler.captured_effects().iter().any(|e| matches!(e, Effect::CreateCommit { .. })));
461/// ```
462#[cfg(feature = "test-utils")]
463pub fn run_with_config_and_handlers<'a, 'ctx, P, A, E>(
464    params: RunWithHandlersParams<'a, 'ctx, P, A, E>,
465) -> anyhow::Result<()>
466where
467    P: crate::config::ConfigEnvironment,
468    A: effect::AppEffectHandler,
469    E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
470{
471    let RunWithHandlersParams {
472        args,
473        executor,
474        config,
475        registry,
476        path_resolver,
477        app_handler,
478        effect_handler,
479        workspace,
480        ..
481    } = params;
482    use crate::cli::{
483        handle_extended_help, handle_init_global_with, handle_init_prompt_with,
484        handle_list_work_guides, handle_smart_init_with,
485    };
486
487    let colors = Colors::new();
488    let logger = Logger::new(colors);
489
490    // Set working directory first if override is provided
491    if let Some(ref override_dir) = args.working_dir_override {
492        std::env::set_current_dir(override_dir)?;
493    }
494
495    // Handle --extended-help / --man flag
496    if args.recovery.extended_help {
497        handle_extended_help();
498        if args.work_guide_list.list_work_guides {
499            println!();
500            handle_list_work_guides(colors);
501        }
502        return Ok(());
503    }
504
505    // Handle --list-work-guides / --list-templates flag
506    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
507        return Ok(());
508    }
509
510    // Handle --init-prompt flag
511    if let Some(ref template_name) = args.init_prompt {
512        if handle_init_prompt_with(
513            template_name,
514            args.unified_init.force_init,
515            colors,
516            path_resolver,
517        )? {
518            return Ok(());
519        }
520    }
521
522    // Handle smart --init flag
523    if args.unified_init.init.is_some()
524        && handle_smart_init_with(
525            args.unified_init.init.as_deref(),
526            args.unified_init.force_init,
527            colors,
528            path_resolver,
529        )?
530    {
531        return Ok(());
532    }
533
534    // Handle --init-config flag
535    if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
536        return Ok(());
537    }
538
539    // Handle --init-global flag
540    if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
541        return Ok(());
542    }
543
544    // Handle --init-legacy flag
545    if args.legacy_init.init_legacy {
546        let repo_root = match app_handler.execute(effect::AppEffect::GitGetRepoRoot) {
547            effect::AppEffectResult::Path(p) => Some(p),
548            _ => None,
549        };
550        let legacy_path = repo_root.map_or_else(
551            || std::path::PathBuf::from(".agent/agents.toml"),
552            |root| root.join(".agent/agents.toml"),
553        );
554        if crate::cli::handle_init_legacy(colors, &legacy_path)? {
555            return Ok(());
556        }
557    }
558
559    // Use provided config directly
560    let config_path = std::path::PathBuf::from("test-config");
561
562    // Resolve required agent names
563    let validated = resolve_required_agents(&config)?;
564    let developer_agent = validated.developer_agent;
565    let reviewer_agent = validated.reviewer_agent;
566
567    // Handle listing commands
568    if handle_listing_commands(&args, &registry, colors) {
569        return Ok(());
570    }
571
572    // Handle --diagnose
573    if args.recovery.diagnose {
574        handle_diagnose(colors, &config, &registry, &config_path, &[], &*executor);
575        return Ok(());
576    }
577
578    // Handle plumbing commands with app_handler
579    // Pass workspace reference for testability with MemoryWorkspace
580    if handle_plumbing_commands(
581        &args,
582        &logger,
583        colors,
584        app_handler,
585        workspace.as_ref().map(|w| w.as_ref()),
586    )? {
587        return Ok(());
588    }
589
590    // Validate agents and set up git repo with app_handler
591    let Some(repo_root) = validate_and_setup_agents(
592        AgentSetupParams {
593            config: &config,
594            registry: &registry,
595            developer_agent: &developer_agent,
596            reviewer_agent: &reviewer_agent,
597            config_path: &config_path,
598            colors,
599            logger: &logger,
600            working_dir_override: args.working_dir_override.as_deref(),
601        },
602        app_handler,
603    )?
604    else {
605        return Ok(());
606    };
607
608    // Create workspace for explicit path resolution, or use injected workspace
609    let workspace = workspace.unwrap_or_else(|| {
610        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
611    });
612
613    // Prepare pipeline context or exit early
614    let ctx = prepare_pipeline_or_exit(PipelinePreparationParams {
615        args,
616        config,
617        registry,
618        developer_agent,
619        reviewer_agent,
620        repo_root,
621        logger,
622        colors,
623        executor,
624        handler: app_handler,
625        workspace,
626    })?;
627
628    // Run pipeline with the injected effect_handler
629    match ctx {
630        Some(ctx) => run_pipeline_with_effect_handler(&ctx, effect_handler),
631        None => Ok(()),
632    }
633}
634
635/// Handles listing commands that don't require the full pipeline.
636///
637/// Returns `true` if a listing command was handled and we should exit.
638fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
639    if args.agent_list.list_agents {
640        handle_list_agents(registry);
641        return true;
642    }
643    if args.agent_list.list_available_agents {
644        handle_list_available_agents(registry);
645        return true;
646    }
647    if args.provider_list.list_providers {
648        handle_list_providers(colors);
649        return true;
650    }
651
652    // Handle template commands
653    let template_cmds = &args.template_commands;
654    if template_cmds.init_templates_enabled()
655        || template_cmds.validate
656        || template_cmds.show.is_some()
657        || template_cmds.list
658        || template_cmds.list_all
659        || template_cmds.variables.is_some()
660        || template_cmds.render.is_some()
661    {
662        let _ = handle_template_commands(template_cmds, colors);
663        return true;
664    }
665
666    false
667}
668
669/// Handles plumbing commands that require git repo but not full validation.
670///
671/// Returns `Ok(true)` if a plumbing command was handled and we should exit.
672/// Returns `Ok(false)` if we should continue to the main pipeline.
673///
674/// # Workspace Support
675///
676/// When `workspace` is `Some`, the workspace-aware versions of plumbing commands
677/// are used, enabling testing with `MemoryWorkspace`. When `None`, the direct
678/// filesystem versions are used (production behavior).
679fn handle_plumbing_commands<H: effect::AppEffectHandler>(
680    args: &Args,
681    logger: &Logger,
682    colors: Colors,
683    handler: &mut H,
684    workspace: Option<&dyn crate::workspace::Workspace>,
685) -> anyhow::Result<bool> {
686    use plumbing::{handle_apply_commit_with_handler, handle_show_commit_msg_with_workspace};
687
688    // Helper to set up working directory for plumbing commands using the effect handler
689    fn setup_working_dir_via_handler<H: effect::AppEffectHandler>(
690        override_dir: Option<&std::path::Path>,
691        handler: &mut H,
692    ) -> anyhow::Result<()> {
693        use effect::{AppEffect, AppEffectResult};
694
695        if let Some(dir) = override_dir {
696            match handler.execute(AppEffect::SetCurrentDir {
697                path: dir.to_path_buf(),
698            }) {
699                AppEffectResult::Ok => Ok(()),
700                AppEffectResult::Error(e) => anyhow::bail!(e),
701                other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
702            }
703        } else {
704            // Require git repo
705            match handler.execute(AppEffect::GitRequireRepo) {
706                AppEffectResult::Ok => {}
707                AppEffectResult::Error(e) => anyhow::bail!(e),
708                other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
709            }
710            // Get repo root
711            let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
712                AppEffectResult::Path(p) => p,
713                AppEffectResult::Error(e) => anyhow::bail!(e),
714                other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
715            };
716            // Set current dir to repo root
717            match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
718                AppEffectResult::Ok => Ok(()),
719                AppEffectResult::Error(e) => anyhow::bail!(e),
720                other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
721            }
722        }
723    }
724
725    // Show commit message
726    if args.commit_display.show_commit_msg {
727        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
728        if let Some(ws) = workspace {
729            return handle_show_commit_msg_with_workspace(ws).map(|()| true);
730        }
731        return handle_show_commit_msg().map(|()| true);
732    }
733
734    // Apply commit
735    if args.commit_plumbing.apply_commit {
736        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
737        if let Some(ws) = workspace {
738            return handle_apply_commit_with_handler(ws, handler, logger, colors).map(|()| true);
739        }
740        return handle_apply_commit(logger, colors).map(|()| true);
741    }
742
743    // Reset start commit
744    if args.commit_display.reset_start_commit {
745        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
746
747        // Use the effect handler for reset_start_commit
748        return match handler.execute(effect::AppEffect::GitResetStartCommit) {
749            effect::AppEffectResult::String(oid) => {
750                // Simple case - just got the OID back
751                let short_oid = &oid[..8.min(oid.len())];
752                logger.success(&format!("Starting commit reference reset ({})", short_oid));
753                logger.info(".agent/start_commit has been updated");
754                Ok(true)
755            }
756            effect::AppEffectResult::Error(e) => {
757                logger.error(&format!("Failed to reset starting commit: {e}"));
758                anyhow::bail!("Failed to reset starting commit");
759            }
760            other => {
761                // Fallback to old implementation for other result types
762                // This allows gradual migration
763                drop(other);
764                match reset_start_commit() {
765                    Ok(result) => {
766                        let short_oid = &result.oid[..8.min(result.oid.len())];
767                        if result.fell_back_to_head {
768                            logger.success(&format!(
769                                "Starting commit reference reset to current HEAD ({})",
770                                short_oid
771                            ));
772                            logger.info("On main/master branch - using HEAD as baseline");
773                        } else if let Some(ref branch) = result.default_branch {
774                            logger.success(&format!(
775                                "Starting commit reference reset to merge-base with '{}' ({})",
776                                branch, short_oid
777                            ));
778                            logger.info("Baseline set to common ancestor with default branch");
779                        } else {
780                            logger.success(&format!(
781                                "Starting commit reference reset ({})",
782                                short_oid
783                            ));
784                        }
785                        logger.info(".agent/start_commit has been updated");
786                        Ok(true)
787                    }
788                    Err(e) => {
789                        logger.error(&format!("Failed to reset starting commit: {e}"));
790                        anyhow::bail!("Failed to reset starting commit");
791                    }
792                }
793            }
794        };
795    }
796
797    // Show baseline state
798    if args.commit_display.show_baseline {
799        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
800
801        return match handle_show_baseline() {
802            Ok(()) => Ok(true),
803            Err(e) => {
804                logger.error(&format!("Failed to show baseline: {e}"));
805                anyhow::bail!("Failed to show baseline");
806            }
807        };
808    }
809
810    Ok(false)
811}
812
813/// Parameters for preparing the pipeline context.
814///
815/// Groups related parameters to avoid too many function arguments.
816struct PipelinePreparationParams<'a, H: effect::AppEffectHandler> {
817    args: Args,
818    config: crate::config::Config,
819    registry: AgentRegistry,
820    developer_agent: String,
821    reviewer_agent: String,
822    repo_root: std::path::PathBuf,
823    logger: Logger,
824    colors: Colors,
825    executor: std::sync::Arc<dyn ProcessExecutor>,
826    handler: &'a mut H,
827    /// Workspace for explicit path resolution.
828    ///
829    /// Production code passes `Arc::new(WorkspaceFs::new(...))`.
830    /// Tests can pass `Arc::new(MemoryWorkspace::new(...))`.
831    workspace: std::sync::Arc<dyn crate::workspace::Workspace>,
832}
833
834/// Prepares the pipeline context after agent validation.
835///
836/// Returns `Some(ctx)` if pipeline should run, or `None` if we should exit early.
837fn prepare_pipeline_or_exit<H: effect::AppEffectHandler>(
838    params: PipelinePreparationParams<'_, H>,
839) -> anyhow::Result<Option<PipelineContext>> {
840    let PipelinePreparationParams {
841        args,
842        config,
843        registry,
844        developer_agent,
845        reviewer_agent,
846        repo_root,
847        mut logger,
848        colors,
849        executor,
850        handler,
851        workspace,
852    } = params;
853
854    // Ensure required files and directories exist via effects
855    effectful::ensure_files_effectful(handler, config.isolation_mode)
856        .map_err(|e| anyhow::anyhow!("{}", e))?;
857
858    // Reset context for isolation mode via effects
859    if config.isolation_mode {
860        effectful::reset_context_for_isolation_effectful(handler)
861            .map_err(|e| anyhow::anyhow!("{}", e))?;
862    }
863
864    logger = logger.with_log_file(".agent/logs/pipeline.log");
865
866    // Handle --dry-run
867    if args.recovery.dry_run {
868        let developer_display = registry.display_name(&developer_agent);
869        let reviewer_display = registry.display_name(&reviewer_agent);
870        handle_dry_run(
871            &logger,
872            colors,
873            &config,
874            &developer_display,
875            &reviewer_display,
876            &repo_root,
877        )?;
878        return Ok(None);
879    }
880
881    // Create template context for user template overrides
882    let template_context =
883        TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
884
885    // Handle --rebase-only
886    if args.rebase_flags.rebase_only {
887        handle_rebase_only(
888            &args,
889            &config,
890            &template_context,
891            &logger,
892            colors,
893            std::sync::Arc::clone(&executor),
894            &repo_root,
895        )?;
896        return Ok(None);
897    }
898
899    // Handle --generate-commit-msg
900    if args.commit_plumbing.generate_commit_msg {
901        handle_generate_commit_msg(plumbing::CommitGenerationConfig {
902            config: &config,
903            template_context: &template_context,
904            workspace: &*workspace,
905            registry: &registry,
906            logger: &logger,
907            colors,
908            developer_agent: &developer_agent,
909            _reviewer_agent: &reviewer_agent,
910            executor: std::sync::Arc::clone(&executor),
911        })?;
912        return Ok(None);
913    }
914
915    // Get display names before moving registry
916    let developer_display = registry.display_name(&developer_agent);
917    let reviewer_display = registry.display_name(&reviewer_agent);
918
919    // Build pipeline context (workspace was injected via params)
920    let ctx = PipelineContext {
921        args,
922        config,
923        registry,
924        developer_agent,
925        reviewer_agent,
926        developer_display,
927        reviewer_display,
928        repo_root,
929        workspace,
930        logger,
931        colors,
932        template_context,
933        executor,
934    };
935    Ok(Some(ctx))
936}
937
938/// Parameters for agent validation and setup.
939struct AgentSetupParams<'a> {
940    config: &'a crate::config::Config,
941    registry: &'a AgentRegistry,
942    developer_agent: &'a str,
943    reviewer_agent: &'a str,
944    config_path: &'a std::path::Path,
945    colors: Colors,
946    logger: &'a Logger,
947    /// If Some, use this path as the working directory without discovering the repo root
948    /// or changing the global CWD. This enables test parallelism.
949    working_dir_override: Option<&'a std::path::Path>,
950}
951
952/// Validates agent commands and workflow capability, then sets up git repo and PROMPT.md.
953///
954/// Returns `Some(repo_root)` if setup succeeded and should continue.
955/// Returns `None` if the user declined PROMPT.md creation (to exit early).
956fn validate_and_setup_agents<H: effect::AppEffectHandler>(
957    params: AgentSetupParams<'_>,
958    handler: &mut H,
959) -> anyhow::Result<Option<std::path::PathBuf>> {
960    let AgentSetupParams {
961        config,
962        registry,
963        developer_agent,
964        reviewer_agent,
965        config_path,
966        colors,
967        logger,
968        working_dir_override,
969    } = params;
970    // Validate agent commands exist
971    validate_agent_commands(
972        config,
973        registry,
974        developer_agent,
975        reviewer_agent,
976        config_path,
977    )?;
978
979    // Validate agents are workflow-capable
980    validate_can_commit(
981        config,
982        registry,
983        developer_agent,
984        reviewer_agent,
985        config_path,
986    )?;
987
988    // Determine repo root - use override if provided (for testing), otherwise discover
989    let repo_root = if let Some(override_dir) = working_dir_override {
990        // Testing mode: use provided directory and change CWD to it via handler
991        handler.execute(effect::AppEffect::SetCurrentDir {
992            path: override_dir.to_path_buf(),
993        });
994        override_dir.to_path_buf()
995    } else {
996        // Production mode: discover repo root and change CWD via handler
997        let require_result = handler.execute(effect::AppEffect::GitRequireRepo);
998        if let effect::AppEffectResult::Error(e) = require_result {
999            anyhow::bail!("Not in a git repository: {}", e);
1000        }
1001
1002        let root_result = handler.execute(effect::AppEffect::GitGetRepoRoot);
1003        let root = match root_result {
1004            effect::AppEffectResult::Path(p) => p,
1005            effect::AppEffectResult::Error(e) => {
1006                anyhow::bail!("Failed to get repo root: {}", e);
1007            }
1008            _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
1009        };
1010
1011        handler.execute(effect::AppEffect::SetCurrentDir { path: root.clone() });
1012        root
1013    };
1014
1015    // Set up PROMPT.md if needed (may return None to exit early)
1016    let should_continue = setup_git_and_prompt_file(config, colors, logger, handler)?;
1017    if should_continue.is_none() {
1018        return Ok(None);
1019    }
1020
1021    Ok(Some(repo_root))
1022}
1023
1024/// In interactive mode, prompts to create PROMPT.md from a template before `ensure_files()`.
1025///
1026/// Returns `Ok(Some(()))` if setup succeeded and should continue.
1027/// Returns `Ok(None)` if the user declined PROMPT.md creation (to exit early).
1028fn setup_git_and_prompt_file<H: effect::AppEffectHandler>(
1029    config: &crate::config::Config,
1030    colors: Colors,
1031    logger: &Logger,
1032    handler: &mut H,
1033) -> anyhow::Result<Option<()>> {
1034    let prompt_exists =
1035        effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{}", e))?;
1036
1037    // In interactive mode, prompt to create PROMPT.md from a template BEFORE ensure_files().
1038    // If the user declines (or we can't prompt), exit without creating a placeholder PROMPT.md.
1039    if config.behavior.interactive && !prompt_exists {
1040        if let Some(template_name) = prompt_template_selection(colors) {
1041            create_prompt_from_template(&template_name, colors)?;
1042            println!();
1043            logger.info(
1044                "PROMPT.md created. Please edit it with your task details, then run ralph again.",
1045            );
1046            logger.info(&format!(
1047                "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
1048                config.commit_msg
1049            ));
1050            return Ok(None);
1051        }
1052        println!();
1053        logger.error("PROMPT.md not found in current directory.");
1054        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1055        println!();
1056        logger.info("To get started:");
1057        logger.info("  ralph --init                    # Smart setup wizard");
1058        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1059        logger.info("  ralph --list-work-guides          # See all Work Guides");
1060        println!();
1061        return Ok(None);
1062    }
1063
1064    // Non-interactive mode: show helpful error if PROMPT.md doesn't exist
1065    if !prompt_exists {
1066        logger.error("PROMPT.md not found in current directory.");
1067        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1068        println!();
1069        logger.info("Quick start:");
1070        logger.info("  ralph --init                    # Smart setup wizard");
1071        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1072        logger.info("  ralph --list-work-guides          # See all Work Guides");
1073        println!();
1074        logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1075        println!();
1076        return Ok(None);
1077    }
1078
1079    Ok(Some(()))
1080}
1081
1082/// Runs the full development/review/commit pipeline using reducer-based event loop.
1083fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1084    // Use MainEffectHandler for production
1085    run_pipeline_with_default_handler(ctx)
1086}
1087
1088/// Runs the pipeline with the default MainEffectHandler.
1089///
1090/// This is the production entry point - it creates a MainEffectHandler internally.
1091fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1092    use crate::app::event_loop::EventLoopConfig;
1093    #[cfg(not(feature = "test-utils"))]
1094    use crate::reducer::MainEffectHandler;
1095    use crate::reducer::PipelineState;
1096
1097    // First, offer interactive resume if checkpoint exists without --resume flag
1098    let resume_result = offer_resume_if_checkpoint_exists(
1099        &ctx.args,
1100        &ctx.config,
1101        &ctx.registry,
1102        &ctx.logger,
1103        &ctx.developer_agent,
1104        &ctx.reviewer_agent,
1105    );
1106
1107    // If interactive resume didn't happen, check for --resume flag
1108    let resume_result = match resume_result {
1109        Some(result) => Some(result),
1110        None => handle_resume_with_validation(
1111            &ctx.args,
1112            &ctx.config,
1113            &ctx.registry,
1114            &ctx.logger,
1115            &ctx.developer_display,
1116            &ctx.reviewer_display,
1117        ),
1118    };
1119
1120    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1121
1122    // Create run context - either new or from checkpoint
1123    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1124        use crate::checkpoint::RunContext;
1125        RunContext::from_checkpoint(checkpoint)
1126    } else {
1127        use crate::checkpoint::RunContext;
1128        RunContext::new()
1129    };
1130
1131    // Apply checkpoint configuration restoration if resuming
1132    let config = if let Some(ref checkpoint) = resume_checkpoint {
1133        use crate::checkpoint::apply_checkpoint_to_config;
1134        let mut restored_config = ctx.config.clone();
1135        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1136        ctx.logger.info("Restored configuration from checkpoint:");
1137        if checkpoint.cli_args.developer_iters > 0 {
1138            ctx.logger.info(&format!(
1139                "  Developer iterations: {} (from checkpoint)",
1140                checkpoint.cli_args.developer_iters
1141            ));
1142        }
1143        if checkpoint.cli_args.reviewer_reviews > 0 {
1144            ctx.logger.info(&format!(
1145                "  Reviewer passes: {} (from checkpoint)",
1146                checkpoint.cli_args.reviewer_reviews
1147            ));
1148        }
1149        restored_config
1150    } else {
1151        ctx.config.clone()
1152    };
1153
1154    // Restore environment variables from checkpoint if resuming
1155    if let Some(ref checkpoint) = resume_checkpoint {
1156        use crate::checkpoint::restore::restore_environment_from_checkpoint;
1157        let restored_count = restore_environment_from_checkpoint(checkpoint);
1158        if restored_count > 0 {
1159            ctx.logger.info(&format!(
1160                "  Restored {} environment variable(s) from checkpoint",
1161                restored_count
1162            ));
1163        }
1164    }
1165
1166    // Set up git helpers and agent phase
1167    // Use workspace-aware versions when test-utils feature is enabled
1168    // to avoid real git operations that would cause test failures.
1169    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1170
1171    #[cfg(feature = "test-utils")]
1172    {
1173        use crate::git_helpers::{
1174            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1175        };
1176        // Use workspace-based operations that don't require real git
1177        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1178        create_marker_with_workspace(&*ctx.workspace)?;
1179        // Skip hook installation and git wrapper in test mode
1180    }
1181    #[cfg(not(feature = "test-utils"))]
1182    {
1183        cleanup_orphaned_marker(&ctx.logger)?;
1184        start_agent_phase(&mut git_helpers)?;
1185    }
1186    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1187
1188    // Print welcome banner and validate PROMPT.md
1189    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1190    print_pipeline_info_with_config(ctx, &config);
1191    validate_prompt_and_setup_backup(ctx)?;
1192
1193    // Set up PROMPT.md monitoring
1194    let mut prompt_monitor = setup_prompt_monitor(ctx);
1195
1196    // Detect project stack and review guidelines
1197    let (_project_stack, review_guidelines) =
1198        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1199
1200    print_review_guidelines(ctx, review_guidelines.as_ref());
1201    println!();
1202
1203    // Create phase context and save starting commit
1204    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1205    let mut phase_ctx = create_phase_context_with_config(
1206        ctx,
1207        &config,
1208        &mut timer,
1209        &mut stats,
1210        review_guidelines.as_ref(),
1211        &run_context,
1212        resume_checkpoint.as_ref(),
1213    );
1214    save_start_commit_or_warn(ctx);
1215
1216    // Set up interrupt context for checkpoint saving on Ctrl+C
1217    // This must be done after phase_ctx is created
1218    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1219        checkpoint.phase
1220    } else {
1221        PipelinePhase::Planning
1222    };
1223    setup_interrupt_context_for_pipeline(
1224        initial_phase,
1225        config.developer_iters,
1226        config.reviewer_reviews,
1227        &phase_ctx.execution_history,
1228        &phase_ctx.prompt_history,
1229        &run_context,
1230    );
1231
1232    // Ensure interrupt context is cleared on completion
1233    let _interrupt_guard = defer_clear_interrupt_context();
1234
1235    // Determine if we should run rebase based on checkpoint or current args
1236    let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1237        // Use checkpoint's skip_rebase value if it has meaningful cli_args
1238        if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1239            !checkpoint.cli_args.skip_rebase
1240        } else {
1241            // Fallback to current args
1242            ctx.args.rebase_flags.with_rebase
1243        }
1244    } else {
1245        ctx.args.rebase_flags.with_rebase
1246    };
1247
1248    // Run pre-development rebase (only if explicitly requested via --with-rebase)
1249    if should_run_rebase {
1250        run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1251        // Update interrupt context after rebase
1252        update_interrupt_context_from_phase(
1253            &phase_ctx,
1254            PipelinePhase::Planning,
1255            config.developer_iters,
1256            config.reviewer_reviews,
1257            &run_context,
1258        );
1259    } else {
1260        // Save initial checkpoint when rebase is disabled
1261        if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1262            let builder = CheckpointBuilder::new()
1263                .phase(PipelinePhase::Planning, 0, config.developer_iters)
1264                .reviewer_pass(0, config.reviewer_reviews)
1265                .skip_rebase(true) // Rebase is disabled
1266                .capture_from_context(
1267                    &config,
1268                    &ctx.registry,
1269                    &ctx.developer_agent,
1270                    &ctx.reviewer_agent,
1271                    &ctx.logger,
1272                    &run_context,
1273                )
1274                .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1275                .with_execution_history(phase_ctx.execution_history.clone())
1276                .with_prompt_history(phase_ctx.clone_prompt_history());
1277
1278            if let Some(checkpoint) = builder.build() {
1279                let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1280            }
1281        }
1282        // Update interrupt context after initial checkpoint
1283        update_interrupt_context_from_phase(
1284            &phase_ctx,
1285            PipelinePhase::Planning,
1286            config.developer_iters,
1287            config.reviewer_reviews,
1288            &run_context,
1289        );
1290    }
1291
1292    // ============================================
1293    // RUN PIPELINE PHASES VIA REDUCER EVENT LOOP
1294    // ============================================
1295
1296    // Initialize pipeline state
1297    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1298        // Migrate from old checkpoint format to new reducer state
1299        PipelineState::from(checkpoint.clone())
1300    } else {
1301        // Create new initial state
1302        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1303    };
1304
1305    // Configure event loop
1306    let event_loop_config = EventLoopConfig {
1307        max_iterations: 1000,
1308        enable_checkpointing: config.features.checkpoint_enabled,
1309    };
1310
1311    // Clone execution_history and prompt_history BEFORE running event loop (to avoid borrow issues)
1312    let execution_history_before = phase_ctx.execution_history.clone();
1313    let prompt_history_before = phase_ctx.clone_prompt_history();
1314
1315    // Create effect handler and run event loop.
1316    // Under test-utils feature, use MockEffectHandler to avoid real git operations.
1317    #[cfg(feature = "test-utils")]
1318    let loop_result = {
1319        use crate::app::event_loop::run_event_loop_with_handler;
1320        use crate::reducer::mock_effect_handler::MockEffectHandler;
1321        let mut handler = MockEffectHandler::new(initial_state.clone());
1322        let phase_ctx_ref = &mut phase_ctx;
1323        run_event_loop_with_handler(
1324            phase_ctx_ref,
1325            Some(initial_state),
1326            event_loop_config,
1327            &mut handler,
1328        )
1329    };
1330    #[cfg(not(feature = "test-utils"))]
1331    let loop_result = {
1332        use crate::app::event_loop::run_event_loop_with_handler;
1333        let mut handler = MainEffectHandler::new(initial_state.clone());
1334        let phase_ctx_ref = &mut phase_ctx;
1335        run_event_loop_with_handler(
1336            phase_ctx_ref,
1337            Some(initial_state),
1338            event_loop_config,
1339            &mut handler,
1340        )
1341    };
1342
1343    // Handle event loop result
1344    let loop_result = loop_result?;
1345    if loop_result.completed {
1346        ctx.logger
1347            .success("Pipeline completed successfully via reducer event loop");
1348        ctx.logger.info(&format!(
1349            "Total events processed: {}",
1350            loop_result.events_processed
1351        ));
1352    } else {
1353        ctx.logger.warn("Pipeline exited without completion marker");
1354    }
1355
1356    // Save Complete checkpoint before clearing (for idempotent resume)
1357    if config.features.checkpoint_enabled {
1358        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1359        let builder = CheckpointBuilder::new()
1360            .phase(
1361                PipelinePhase::Complete,
1362                config.developer_iters,
1363                config.developer_iters,
1364            )
1365            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1366            .skip_rebase(skip_rebase)
1367            .capture_from_context(
1368                &config,
1369                &ctx.registry,
1370                &ctx.developer_agent,
1371                &ctx.reviewer_agent,
1372                &ctx.logger,
1373                &run_context,
1374            )
1375            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1376
1377        let builder = builder
1378            .with_execution_history(execution_history_before)
1379            .with_prompt_history(prompt_history_before);
1380
1381        if let Some(checkpoint) = builder.build() {
1382            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1383        }
1384    }
1385
1386    // Post-pipeline operations
1387    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1388    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1389
1390    // Commit phase
1391    finalize_pipeline(
1392        &mut agent_phase_guard,
1393        &ctx.logger,
1394        ctx.colors,
1395        &config,
1396        finalization::RuntimeStats {
1397            timer: &timer,
1398            stats: &stats,
1399        },
1400        prompt_monitor,
1401        Some(&*ctx.workspace),
1402    );
1403    Ok(())
1404}
1405
1406/// Runs the pipeline with a custom effect handler for testing.
1407///
1408/// This function is only available with the `test-utils` feature and allows
1409/// injecting a `MockEffectHandler` to prevent real git operations during tests.
1410///
1411/// # Arguments
1412///
1413/// * `ctx` - Pipeline context
1414/// * `effect_handler` - Custom effect handler (e.g., `MockEffectHandler`)
1415///
1416/// # Type Parameters
1417///
1418/// * `H` - Effect handler type that implements `EffectHandler` and `StatefulHandler`
1419#[cfg(feature = "test-utils")]
1420pub fn run_pipeline_with_effect_handler<'ctx, H>(
1421    ctx: &PipelineContext,
1422    effect_handler: &mut H,
1423) -> anyhow::Result<()>
1424where
1425    H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1426{
1427    use crate::app::event_loop::EventLoopConfig;
1428    use crate::reducer::PipelineState;
1429
1430    // First, offer interactive resume if checkpoint exists without --resume flag
1431    let resume_result = offer_resume_if_checkpoint_exists(
1432        &ctx.args,
1433        &ctx.config,
1434        &ctx.registry,
1435        &ctx.logger,
1436        &ctx.developer_agent,
1437        &ctx.reviewer_agent,
1438    );
1439
1440    // If interactive resume didn't happen, check for --resume flag
1441    let resume_result = match resume_result {
1442        Some(result) => Some(result),
1443        None => handle_resume_with_validation(
1444            &ctx.args,
1445            &ctx.config,
1446            &ctx.registry,
1447            &ctx.logger,
1448            &ctx.developer_display,
1449            &ctx.reviewer_display,
1450        ),
1451    };
1452
1453    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1454
1455    // Create run context - either new or from checkpoint
1456    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1457        use crate::checkpoint::RunContext;
1458        RunContext::from_checkpoint(checkpoint)
1459    } else {
1460        use crate::checkpoint::RunContext;
1461        RunContext::new()
1462    };
1463
1464    // Apply checkpoint configuration restoration if resuming
1465    let config = if let Some(ref checkpoint) = resume_checkpoint {
1466        use crate::checkpoint::apply_checkpoint_to_config;
1467        let mut restored_config = ctx.config.clone();
1468        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1469        restored_config
1470    } else {
1471        ctx.config.clone()
1472    };
1473
1474    // Set up git helpers and agent phase
1475    // Use workspace-aware versions when test-utils feature is enabled
1476    // to avoid real git operations that would cause test failures.
1477    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1478
1479    #[cfg(feature = "test-utils")]
1480    {
1481        use crate::git_helpers::{
1482            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1483        };
1484        // Use workspace-based operations that don't require real git
1485        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1486        create_marker_with_workspace(&*ctx.workspace)?;
1487        // Skip hook installation and git wrapper in test mode
1488    }
1489    #[cfg(not(feature = "test-utils"))]
1490    {
1491        cleanup_orphaned_marker(&ctx.logger)?;
1492        start_agent_phase(&mut git_helpers)?;
1493    }
1494    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1495
1496    // Print welcome banner and validate PROMPT.md
1497    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1498    print_pipeline_info_with_config(ctx, &config);
1499    validate_prompt_and_setup_backup(ctx)?;
1500
1501    // Set up PROMPT.md monitoring
1502    let mut prompt_monitor = setup_prompt_monitor(ctx);
1503
1504    // Detect project stack and review guidelines
1505    let (_project_stack, review_guidelines) =
1506        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1507
1508    print_review_guidelines(ctx, review_guidelines.as_ref());
1509    println!();
1510
1511    // Create phase context and save starting commit
1512    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1513    let mut phase_ctx = create_phase_context_with_config(
1514        ctx,
1515        &config,
1516        &mut timer,
1517        &mut stats,
1518        review_guidelines.as_ref(),
1519        &run_context,
1520        resume_checkpoint.as_ref(),
1521    );
1522    save_start_commit_or_warn(ctx);
1523
1524    // Set up interrupt context for checkpoint saving on Ctrl+C
1525    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1526        checkpoint.phase
1527    } else {
1528        PipelinePhase::Planning
1529    };
1530    setup_interrupt_context_for_pipeline(
1531        initial_phase,
1532        config.developer_iters,
1533        config.reviewer_reviews,
1534        &phase_ctx.execution_history,
1535        &phase_ctx.prompt_history,
1536        &run_context,
1537    );
1538
1539    // Ensure interrupt context is cleared on completion
1540    let _interrupt_guard = defer_clear_interrupt_context();
1541
1542    // Initialize pipeline state
1543    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1544        PipelineState::from(checkpoint.clone())
1545    } else {
1546        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1547    };
1548
1549    // Configure event loop
1550    let event_loop_config = EventLoopConfig {
1551        max_iterations: 1000,
1552        enable_checkpointing: config.features.checkpoint_enabled,
1553    };
1554
1555    // Clone execution_history and prompt_history BEFORE running event loop
1556    let execution_history_before = phase_ctx.execution_history.clone();
1557    let prompt_history_before = phase_ctx.clone_prompt_history();
1558
1559    // Run event loop with the provided handler
1560    effect_handler.update_state(initial_state.clone());
1561    let loop_result = {
1562        use crate::app::event_loop::run_event_loop_with_handler;
1563        let phase_ctx_ref = &mut phase_ctx;
1564        run_event_loop_with_handler(
1565            phase_ctx_ref,
1566            Some(initial_state),
1567            event_loop_config,
1568            effect_handler,
1569        )
1570    };
1571
1572    // Handle event loop result
1573    let loop_result = loop_result?;
1574    if loop_result.completed {
1575        ctx.logger
1576            .success("Pipeline completed successfully via reducer event loop");
1577        ctx.logger.info(&format!(
1578            "Total events processed: {}",
1579            loop_result.events_processed
1580        ));
1581    } else {
1582        ctx.logger.warn("Pipeline exited without completion marker");
1583    }
1584
1585    // Save Complete checkpoint before clearing (for idempotent resume)
1586    if config.features.checkpoint_enabled {
1587        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1588        let builder = CheckpointBuilder::new()
1589            .phase(
1590                PipelinePhase::Complete,
1591                config.developer_iters,
1592                config.developer_iters,
1593            )
1594            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1595            .skip_rebase(skip_rebase)
1596            .capture_from_context(
1597                &config,
1598                &ctx.registry,
1599                &ctx.developer_agent,
1600                &ctx.reviewer_agent,
1601                &ctx.logger,
1602                &run_context,
1603            )
1604            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1605
1606        let builder = builder
1607            .with_execution_history(execution_history_before)
1608            .with_prompt_history(prompt_history_before);
1609
1610        if let Some(checkpoint) = builder.build() {
1611            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1612        }
1613    }
1614
1615    // Post-pipeline operations
1616    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1617    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1618
1619    // Commit phase
1620    finalize_pipeline(
1621        &mut agent_phase_guard,
1622        &ctx.logger,
1623        ctx.colors,
1624        &config,
1625        finalization::RuntimeStats {
1626            timer: &timer,
1627            stats: &stats,
1628        },
1629        prompt_monitor,
1630        Some(&*ctx.workspace),
1631    );
1632    Ok(())
1633}
1634
1635/// Set up the interrupt context with initial pipeline state.
1636///
1637/// This function initializes the global interrupt context so that if
1638/// the user presses Ctrl+C, the interrupt handler can save a checkpoint.
1639fn setup_interrupt_context_for_pipeline(
1640    phase: PipelinePhase,
1641    total_iterations: u32,
1642    total_reviewer_passes: u32,
1643    execution_history: &crate::checkpoint::ExecutionHistory,
1644    prompt_history: &std::collections::HashMap<String, String>,
1645    run_context: &crate::checkpoint::RunContext,
1646) {
1647    use crate::interrupt::{set_interrupt_context, InterruptContext};
1648
1649    // Determine initial iteration based on phase
1650    let (iteration, reviewer_pass) = match phase {
1651        PipelinePhase::Development => (1, 0),
1652        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1653            (total_iterations, 1)
1654        }
1655        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1656            (total_iterations, total_reviewer_passes)
1657        }
1658        _ => (0, 0),
1659    };
1660
1661    let context = InterruptContext {
1662        phase,
1663        iteration,
1664        total_iterations,
1665        reviewer_pass,
1666        total_reviewer_passes,
1667        run_context: run_context.clone(),
1668        execution_history: execution_history.clone(),
1669        prompt_history: prompt_history.clone(),
1670    };
1671
1672    set_interrupt_context(context);
1673}
1674
1675/// Update the interrupt context from the current phase context.
1676///
1677/// This function should be called after each major phase to keep the
1678/// interrupt context up-to-date with the latest execution history.
1679fn update_interrupt_context_from_phase(
1680    phase_ctx: &crate::phases::PhaseContext,
1681    phase: PipelinePhase,
1682    total_iterations: u32,
1683    total_reviewer_passes: u32,
1684    run_context: &crate::checkpoint::RunContext,
1685) {
1686    use crate::interrupt::{set_interrupt_context, InterruptContext};
1687
1688    // Determine current iteration based on phase
1689    let (iteration, reviewer_pass) = match phase {
1690        PipelinePhase::Development => {
1691            // Estimate iteration from actual runs
1692            let iter = run_context.actual_developer_runs.max(1);
1693            (iter, 0)
1694        }
1695        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1696            (total_iterations, run_context.actual_reviewer_runs.max(1))
1697        }
1698        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1699            (total_iterations, total_reviewer_passes)
1700        }
1701        _ => (0, 0),
1702    };
1703
1704    let context = InterruptContext {
1705        phase,
1706        iteration,
1707        total_iterations,
1708        reviewer_pass,
1709        total_reviewer_passes,
1710        run_context: run_context.clone(),
1711        execution_history: phase_ctx.execution_history.clone(),
1712        prompt_history: phase_ctx.clone_prompt_history(),
1713    };
1714
1715    set_interrupt_context(context);
1716}
1717
1718/// Helper to defer clearing interrupt context until function exit.
1719///
1720/// Uses a scope guard pattern to ensure the interrupt context is cleared
1721/// when the pipeline completes successfully, preventing an "interrupted"
1722/// checkpoint from being saved after normal completion.
1723fn defer_clear_interrupt_context() -> InterruptContextGuard {
1724    InterruptContextGuard
1725}
1726
1727/// RAII guard for clearing interrupt context on drop.
1728///
1729/// Ensures the interrupt context is cleared when the guard is dropped,
1730/// preventing an "interrupted" checkpoint from being saved after normal
1731/// pipeline completion.
1732struct InterruptContextGuard;
1733
1734impl Drop for InterruptContextGuard {
1735    fn drop(&mut self) {
1736        crate::interrupt::clear_interrupt_context();
1737    }
1738}
1739
1740/// Validate PROMPT.md and set up backup/protection.
1741fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1742    let prompt_validation = validate_prompt_md_with_workspace(
1743        &*ctx.workspace,
1744        ctx.config.behavior.strict_validation,
1745        ctx.args.interactive,
1746    );
1747    for err in &prompt_validation.errors {
1748        ctx.logger.error(err);
1749    }
1750    for warn in &prompt_validation.warnings {
1751        ctx.logger.warn(warn);
1752    }
1753    if !prompt_validation.is_valid() {
1754        anyhow::bail!("PROMPT.md validation errors");
1755    }
1756
1757    // Create a backup of PROMPT.md to protect against accidental deletion.
1758    match create_prompt_backup_with_workspace(&*ctx.workspace) {
1759        Ok(None) => {}
1760        Ok(Some(warning)) => {
1761            ctx.logger.warn(&format!(
1762                "PROMPT.md backup created but: {warning}. Continuing anyway."
1763            ));
1764        }
1765        Err(e) => {
1766            ctx.logger.warn(&format!(
1767                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1768            ));
1769        }
1770    }
1771
1772    // Make PROMPT.md read-only to protect against accidental deletion.
1773    match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1774        None => {}
1775        Some(warning) => {
1776            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1777        }
1778    }
1779
1780    Ok(())
1781}
1782
1783/// Set up PROMPT.md monitoring for deletion detection.
1784fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1785    match PromptMonitor::new() {
1786        Ok(mut monitor) => {
1787            if let Err(e) = monitor.start() {
1788                ctx.logger.warn(&format!(
1789                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1790                ));
1791                None
1792            } else {
1793                if ctx.config.verbosity.is_debug() {
1794                    ctx.logger.info("Started real-time PROMPT.md monitoring");
1795                }
1796                Some(monitor)
1797            }
1798        }
1799        Err(e) => {
1800            ctx.logger.warn(&format!(
1801                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1802            ));
1803            None
1804        }
1805    }
1806}
1807
1808/// Print review guidelines if detected.
1809fn print_review_guidelines(
1810    ctx: &PipelineContext,
1811    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1812) {
1813    if let Some(guidelines) = review_guidelines {
1814        ctx.logger.info(&format!(
1815            "Review guidelines: {}{}{}",
1816            ctx.colors.dim(),
1817            guidelines.summary(),
1818            ctx.colors.reset()
1819        ));
1820    }
1821}
1822
1823/// Create the phase context with a modified config (for resume restoration).
1824fn create_phase_context_with_config<'ctx>(
1825    ctx: &'ctx PipelineContext,
1826    config: &'ctx crate::config::Config,
1827    timer: &'ctx mut Timer,
1828    stats: &'ctx mut Stats,
1829    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1830    run_context: &'ctx crate::checkpoint::RunContext,
1831    resume_checkpoint: Option<&PipelineCheckpoint>,
1832) -> PhaseContext<'ctx> {
1833    // Restore execution history and prompt history from checkpoint if available
1834    let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1835        let exec_history = checkpoint
1836            .execution_history
1837            .clone()
1838            .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1839        let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1840        (exec_history, prompt_hist)
1841    } else {
1842        (
1843            crate::checkpoint::execution_history::ExecutionHistory::new(),
1844            std::collections::HashMap::new(),
1845        )
1846    };
1847
1848    PhaseContext {
1849        config,
1850        registry: &ctx.registry,
1851        logger: &ctx.logger,
1852        colors: &ctx.colors,
1853        timer,
1854        stats,
1855        developer_agent: &ctx.developer_agent,
1856        reviewer_agent: &ctx.reviewer_agent,
1857        review_guidelines,
1858        template_context: &ctx.template_context,
1859        run_context: run_context.clone(),
1860        execution_history,
1861        prompt_history,
1862        executor: &*ctx.executor,
1863        executor_arc: std::sync::Arc::clone(&ctx.executor),
1864        repo_root: &ctx.repo_root,
1865        workspace: &*ctx.workspace,
1866    }
1867}
1868
1869/// Print pipeline info with a specific config.
1870fn print_pipeline_info_with_config(ctx: &PipelineContext, config: &crate::config::Config) {
1871    ctx.logger.info(&format!(
1872        "Working directory: {}{}{}",
1873        ctx.colors.cyan(),
1874        ctx.repo_root.display(),
1875        ctx.colors.reset()
1876    ));
1877    ctx.logger.info(&format!(
1878        "Commit message: {}{}{}",
1879        ctx.colors.cyan(),
1880        config.commit_msg,
1881        ctx.colors.reset()
1882    ));
1883}
1884
1885/// Save starting commit or warn if it fails.
1886///
1887/// Under `test-utils` feature, this function uses mock data to avoid real git operations.
1888fn save_start_commit_or_warn(ctx: &PipelineContext) {
1889    // Skip real git operations when test-utils feature is enabled.
1890    // These functions call git2::Repository::discover which requires a real git repo.
1891    #[cfg(feature = "test-utils")]
1892    {
1893        // In tests, just log a mock message
1894        if ctx.config.verbosity.is_debug() {
1895            ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1896        }
1897        ctx.logger
1898            .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1899    }
1900
1901    #[cfg(not(feature = "test-utils"))]
1902    {
1903        match save_start_commit() {
1904            Ok(()) => {
1905                if ctx.config.verbosity.is_debug() {
1906                    ctx.logger
1907                        .info("Saved starting commit for incremental diff generation");
1908                }
1909            }
1910            Err(e) => {
1911                ctx.logger.warn(&format!(
1912                    "Failed to save starting commit: {e}. \
1913                     Incremental diffs may be unavailable as a result."
1914                ));
1915                ctx.logger.info(
1916                    "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
1917                );
1918            }
1919        }
1920
1921        // Display start commit information to user
1922        match get_start_commit_summary() {
1923            Ok(summary) => {
1924                if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
1925                {
1926                    ctx.logger.info(&summary.format_compact());
1927                    if summary.is_stale {
1928                        ctx.logger.warn(
1929                            "Start commit is stale. Consider running: ralph --reset-start-commit",
1930                        );
1931                    } else if summary.commits_since > 5 {
1932                        ctx.logger
1933                            .info("Tip: Run 'ralph --show-baseline' for more details");
1934                    }
1935                }
1936            }
1937            Err(e) => {
1938                // Only show error in debug mode since this is informational
1939                if ctx.config.verbosity.is_debug() {
1940                    ctx.logger
1941                        .warn(&format!("Failed to get start commit summary: {e}"));
1942                }
1943            }
1944        }
1945    }
1946}
1947
1948/// Check for PROMPT.md restoration after a phase.
1949fn check_prompt_restoration(
1950    ctx: &PipelineContext,
1951    prompt_monitor: &mut Option<PromptMonitor>,
1952    phase: &str,
1953) {
1954    if let Some(ref mut monitor) = prompt_monitor {
1955        if monitor.check_and_restore() {
1956            ctx.logger.warn(&format!(
1957                "PROMPT.md was deleted and restored during {phase} phase"
1958            ));
1959        }
1960    }
1961}
1962
1963/// Handle --rebase-only flag.
1964///
1965/// This function performs a rebase to the default branch with AI conflict resolution and exits,
1966/// without running the full pipeline.
1967pub fn handle_rebase_only(
1968    _args: &Args,
1969    config: &crate::config::Config,
1970    template_context: &TemplateContext,
1971    logger: &Logger,
1972    colors: Colors,
1973    executor: std::sync::Arc<dyn ProcessExecutor>,
1974    repo_root: &std::path::Path,
1975) -> anyhow::Result<()> {
1976    // Check if we're on main/master branch
1977    if is_main_or_master_branch()? {
1978        logger.warn("Already on main/master branch - rebasing on main is not recommended");
1979        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1980        logger.info("  git worktree add ../feature-branch feature-branch");
1981        logger.info("This allows multiple AI agents to work on different features simultaneously.");
1982        logger.info("Proceeding with rebase anyway as requested...");
1983    }
1984
1985    logger.header("Rebase to default branch", Colors::cyan);
1986
1987    match run_rebase_to_default(logger, colors, &*executor) {
1988        Ok(RebaseResult::Success) => {
1989            logger.success("Rebase completed successfully");
1990            Ok(())
1991        }
1992        Ok(RebaseResult::NoOp { reason }) => {
1993            logger.info(&format!("No rebase needed: {reason}"));
1994            Ok(())
1995        }
1996        Ok(RebaseResult::Failed(err)) => {
1997            logger.error(&format!("Rebase failed: {err}"));
1998            anyhow::bail!("Rebase failed: {err}")
1999        }
2000        Ok(RebaseResult::Conflicts(_conflicts)) => {
2001            // Get the actual conflicted files
2002            let conflicted_files = get_conflicted_files()?;
2003            if conflicted_files.is_empty() {
2004                logger.warn("Rebase reported conflicts but no conflicted files found");
2005                let _ = abort_rebase(&*executor);
2006                return Ok(());
2007            }
2008
2009            logger.warn(&format!(
2010                "Rebase resulted in {} conflict(s), attempting AI resolution",
2011                conflicted_files.len()
2012            ));
2013
2014            // For --rebase-only, we don't have a full PhaseContext, so we use a wrapper
2015            match try_resolve_conflicts_without_phase_ctx(
2016                &conflicted_files,
2017                config,
2018                template_context,
2019                logger,
2020                colors,
2021                std::sync::Arc::clone(&executor),
2022                repo_root,
2023            ) {
2024                Ok(true) => {
2025                    // Conflicts resolved, continue the rebase
2026                    logger.info("Continuing rebase after conflict resolution");
2027                    match continue_rebase(&*executor) {
2028                        Ok(()) => {
2029                            logger.success("Rebase completed successfully after AI resolution");
2030                            Ok(())
2031                        }
2032                        Err(e) => {
2033                            logger.error(&format!("Failed to continue rebase: {e}"));
2034                            let _ = abort_rebase(&*executor);
2035                            anyhow::bail!("Rebase failed after conflict resolution")
2036                        }
2037                    }
2038                }
2039                Ok(false) => {
2040                    // AI resolution failed
2041                    logger.error("AI conflict resolution failed, aborting rebase");
2042                    let _ = abort_rebase(&*executor);
2043                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
2044                }
2045                Err(e) => {
2046                    logger.error(&format!("Conflict resolution error: {e}"));
2047                    let _ = abort_rebase(&*executor);
2048                    anyhow::bail!("Rebase conflict resolution failed: {e}")
2049                }
2050            }
2051        }
2052        Err(e) => {
2053            logger.error(&format!("Rebase failed: {e}"));
2054            anyhow::bail!("Rebase failed: {e}")
2055        }
2056    }
2057}
2058
2059/// Run rebase to the default branch.
2060///
2061/// This function performs a rebase from the current branch to the
2062/// default branch (main/master). It handles all edge cases including:
2063/// - Already on main/master (proceeds with rebase attempt)
2064/// - Empty repository (returns `NoOp`)
2065/// - Upstream branch not found (error)
2066/// - Conflicts during rebase (returns `Conflicts` result)
2067///
2068/// # Returns
2069///
2070/// Returns `RebaseResult` indicating the outcome.
2071fn run_rebase_to_default(
2072    logger: &Logger,
2073    colors: Colors,
2074    executor: &dyn ProcessExecutor,
2075) -> std::io::Result<RebaseResult> {
2076    // Get the default branch
2077    let default_branch = get_default_branch()?;
2078    logger.info(&format!(
2079        "Rebasing onto {}{}{}",
2080        colors.cyan(),
2081        default_branch,
2082        colors.reset()
2083    ));
2084
2085    // Perform the rebase
2086    rebase_onto(&default_branch, executor)
2087}
2088
2089/// Run initial rebase before development phase.
2090///
2091/// This function is called before the development phase starts to ensure
2092/// the feature branch is up-to-date with the default branch.
2093///
2094/// Uses a state machine for fault tolerance and automatic recovery from
2095/// interruptions or failures.
2096///
2097/// # Rebase Control
2098///
2099/// Rebase is only performed when both conditions are met:
2100/// - `--with-rebase` CLI flag is set (caller already checked this)
2101/// - `auto_rebase` config is enabled (checked here)
2102fn run_initial_rebase(
2103    ctx: &PipelineContext,
2104    phase_ctx: &mut PhaseContext<'_>,
2105    run_context: &crate::checkpoint::RunContext,
2106    executor: &dyn ProcessExecutor,
2107) -> anyhow::Result<()> {
2108    ctx.logger.header("Pre-development rebase", Colors::cyan);
2109
2110    // Record execution step: pre-rebase started
2111    let step = ExecutionStep::new(
2112        "PreRebase",
2113        0,
2114        "pre_rebase_start",
2115        StepOutcome::success(None, vec![]),
2116    );
2117    phase_ctx.execution_history.add_step(step);
2118
2119    // Save checkpoint at start of pre-rebase phase
2120    if ctx.config.features.checkpoint_enabled {
2121        let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
2122        let mut builder = CheckpointBuilder::new()
2123            .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
2124            .reviewer_pass(0, ctx.config.reviewer_reviews)
2125            .capture_from_context(
2126                &ctx.config,
2127                &ctx.registry,
2128                &ctx.developer_agent,
2129                &ctx.reviewer_agent,
2130                &ctx.logger,
2131                run_context,
2132            )
2133            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2134
2135        // Include prompt history and execution history for hardened resume
2136        builder = builder
2137            .with_execution_history(phase_ctx.execution_history.clone())
2138            .with_prompt_history(phase_ctx.clone_prompt_history());
2139
2140        if let Some(mut checkpoint) = builder.build() {
2141            checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
2142                upstream_branch: default_branch,
2143            };
2144            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2145        }
2146    }
2147
2148    match run_rebase_to_default(&ctx.logger, ctx.colors, &*ctx.executor) {
2149        Ok(RebaseResult::Success) => {
2150            ctx.logger.success("Rebase completed successfully");
2151            // Record execution step: pre-rebase completed successfully
2152            let step = ExecutionStep::new(
2153                "PreRebase",
2154                0,
2155                "pre_rebase_complete",
2156                StepOutcome::success(None, vec![]),
2157            );
2158            phase_ctx.execution_history.add_step(step);
2159
2160            // Save checkpoint after pre-rebase completes successfully
2161            if ctx.config.features.checkpoint_enabled {
2162                let builder = CheckpointBuilder::new()
2163                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2164                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2165                    .skip_rebase(true) // Pre-rebase is done
2166                    .capture_from_context(
2167                        &ctx.config,
2168                        &ctx.registry,
2169                        &ctx.developer_agent,
2170                        &ctx.reviewer_agent,
2171                        &ctx.logger,
2172                        run_context,
2173                    )
2174                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2175                    .with_execution_history(phase_ctx.execution_history.clone())
2176                    .with_prompt_history(phase_ctx.clone_prompt_history());
2177
2178                if let Some(checkpoint) = builder.build() {
2179                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2180                }
2181            }
2182
2183            Ok(())
2184        }
2185        Ok(RebaseResult::NoOp { reason }) => {
2186            ctx.logger.info(&format!("No rebase needed: {reason}"));
2187            // Record execution step: pre-rebase skipped
2188            let step = ExecutionStep::new(
2189                "PreRebase",
2190                0,
2191                "pre_rebase_skipped",
2192                StepOutcome::skipped(reason.clone()),
2193            );
2194            phase_ctx.execution_history.add_step(step);
2195
2196            // Save checkpoint after pre-rebase no-op
2197            if ctx.config.features.checkpoint_enabled {
2198                let builder = CheckpointBuilder::new()
2199                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2200                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2201                    .skip_rebase(true) // Pre-rebase is done
2202                    .capture_from_context(
2203                        &ctx.config,
2204                        &ctx.registry,
2205                        &ctx.developer_agent,
2206                        &ctx.reviewer_agent,
2207                        &ctx.logger,
2208                        run_context,
2209                    )
2210                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2211                    .with_execution_history(phase_ctx.execution_history.clone())
2212                    .with_prompt_history(phase_ctx.clone_prompt_history());
2213
2214                if let Some(checkpoint) = builder.build() {
2215                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2216                }
2217            }
2218
2219            Ok(())
2220        }
2221        Ok(RebaseResult::Conflicts(_conflicts)) => {
2222            // Get the actual conflicted files
2223            let conflicted_files = get_conflicted_files()?;
2224            if conflicted_files.is_empty() {
2225                ctx.logger
2226                    .warn("Rebase reported conflicts but no conflicted files found");
2227                let _ = abort_rebase(executor);
2228                return Ok(());
2229            }
2230
2231            // Record execution step: pre-rebase conflicts detected
2232            let step = ExecutionStep::new(
2233                "PreRebase",
2234                0,
2235                "pre_rebase_conflict",
2236                StepOutcome::partial(
2237                    "Rebase started".to_string(),
2238                    format!("{} conflicts detected", conflicted_files.len()),
2239                ),
2240            );
2241            phase_ctx.execution_history.add_step(step);
2242
2243            // Save checkpoint for conflict state
2244            if ctx.config.features.checkpoint_enabled {
2245                let mut builder = CheckpointBuilder::new()
2246                    .phase(
2247                        PipelinePhase::PreRebaseConflict,
2248                        0,
2249                        ctx.config.developer_iters,
2250                    )
2251                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2252                    .capture_from_context(
2253                        &ctx.config,
2254                        &ctx.registry,
2255                        &ctx.developer_agent,
2256                        &ctx.reviewer_agent,
2257                        &ctx.logger,
2258                        run_context,
2259                    )
2260                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2261
2262                // Include prompt history and execution history for hardened resume
2263                builder = builder
2264                    .with_execution_history(phase_ctx.execution_history.clone())
2265                    .with_prompt_history(phase_ctx.clone_prompt_history());
2266
2267                if let Some(mut checkpoint) = builder.build() {
2268                    checkpoint.rebase_state = RebaseState::HasConflicts {
2269                        files: conflicted_files.clone(),
2270                    };
2271                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2272                }
2273            }
2274
2275            ctx.logger.warn(&format!(
2276                "Rebase resulted in {} conflict(s), attempting AI resolution",
2277                conflicted_files.len()
2278            ));
2279
2280            // Attempt to resolve conflicts with AI
2281            let resolution_ctx = ConflictResolutionContext {
2282                config: &ctx.config,
2283                template_context: &ctx.template_context,
2284                logger: &ctx.logger,
2285                colors: ctx.colors,
2286                executor_arc: std::sync::Arc::clone(&ctx.executor),
2287                workspace: &*ctx.workspace,
2288            };
2289            match try_resolve_conflicts_with_fallback(
2290                &conflicted_files,
2291                resolution_ctx,
2292                phase_ctx,
2293                "PreRebase",
2294                &*ctx.executor,
2295            ) {
2296                Ok(true) => {
2297                    // Conflicts resolved, continue the rebase
2298                    ctx.logger
2299                        .info("Continuing rebase after conflict resolution");
2300                    match continue_rebase(executor) {
2301                        Ok(()) => {
2302                            ctx.logger
2303                                .success("Rebase completed successfully after AI resolution");
2304                            // Record execution step: conflicts resolved successfully
2305                            let step = ExecutionStep::new(
2306                                "PreRebase",
2307                                0,
2308                                "pre_rebase_resolution",
2309                                StepOutcome::success(None, vec![]),
2310                            );
2311                            phase_ctx.execution_history.add_step(step);
2312
2313                            // Save checkpoint after pre-rebase conflict resolution completes
2314                            if ctx.config.features.checkpoint_enabled {
2315                                let builder = CheckpointBuilder::new()
2316                                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2317                                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2318                                    .skip_rebase(true) // Pre-rebase is done
2319                                    .capture_from_context(
2320                                        &ctx.config,
2321                                        &ctx.registry,
2322                                        &ctx.developer_agent,
2323                                        &ctx.reviewer_agent,
2324                                        &ctx.logger,
2325                                        run_context,
2326                                    )
2327                                    .with_executor_from_context(std::sync::Arc::clone(
2328                                        &ctx.executor,
2329                                    ))
2330                                    .with_execution_history(phase_ctx.execution_history.clone())
2331                                    .with_prompt_history(phase_ctx.clone_prompt_history());
2332
2333                                if let Some(checkpoint) = builder.build() {
2334                                    let _ = save_checkpoint_with_workspace(
2335                                        &*ctx.workspace,
2336                                        &checkpoint,
2337                                    );
2338                                }
2339                            }
2340
2341                            Ok(())
2342                        }
2343                        Err(e) => {
2344                            ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
2345                            let _ = abort_rebase(executor);
2346                            // Record execution step: resolution succeeded but continue failed
2347                            let step = ExecutionStep::new(
2348                                "PreRebase",
2349                                0,
2350                                "pre_rebase_resolution",
2351                                StepOutcome::partial(
2352                                    "Conflicts resolved by AI".to_string(),
2353                                    format!("Failed to continue rebase: {e}"),
2354                                ),
2355                            );
2356                            phase_ctx.execution_history.add_step(step);
2357                            Ok(()) // Continue anyway - conflicts were resolved
2358                        }
2359                    }
2360                }
2361                Ok(false) => {
2362                    // AI resolution failed
2363                    ctx.logger
2364                        .warn("AI conflict resolution failed, aborting rebase");
2365                    let _ = abort_rebase(executor);
2366                    // Record execution step: resolution failed
2367                    let step = ExecutionStep::new(
2368                        "PreRebase",
2369                        0,
2370                        "pre_rebase_resolution",
2371                        StepOutcome::failure("AI conflict resolution failed".to_string(), true),
2372                    );
2373                    phase_ctx.execution_history.add_step(step);
2374                    Ok(()) // Continue pipeline - don't block on rebase failure
2375                }
2376                Err(e) => {
2377                    ctx.logger.error(&format!("Conflict resolution error: {e}"));
2378                    let _ = abort_rebase(executor);
2379                    // Record execution step: resolution error
2380                    let step = ExecutionStep::new(
2381                        "PreRebase",
2382                        0,
2383                        "pre_rebase_resolution",
2384                        StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
2385                    );
2386                    phase_ctx.execution_history.add_step(step);
2387                    Ok(()) // Continue pipeline
2388                }
2389            }
2390        }
2391        Ok(RebaseResult::Failed(err)) => {
2392            ctx.logger.error(&format!("Rebase failed: {err}"));
2393            let _ = abort_rebase(&*ctx.executor);
2394            // Record execution step: rebase failed
2395            let step = ExecutionStep::new(
2396                "PreRebase",
2397                0,
2398                "pre_rebase_failed",
2399                StepOutcome::failure(format!("Rebase failed: {err}"), true),
2400            );
2401            phase_ctx.execution_history.add_step(step);
2402            Ok(()) // Continue pipeline despite failure
2403        }
2404        Err(e) => {
2405            ctx.logger
2406                .warn(&format!("Rebase failed, continuing without rebase: {e}"));
2407            // Record execution step: rebase error
2408            let step = ExecutionStep::new(
2409                "PreRebase",
2410                0,
2411                "pre_rebase_error",
2412                StepOutcome::failure(format!("Rebase error: {e}"), true),
2413            );
2414            phase_ctx.execution_history.add_step(step);
2415            Ok(())
2416        }
2417    }
2418}
2419
2420/// Result type for conflict resolution attempts.
2421///
2422/// Represents the different ways conflict resolution can succeed or fail.
2423enum ConflictResolutionResult {
2424    /// Agent provided JSON output with resolved file contents
2425    WithJson(String),
2426    /// Agent resolved conflicts by editing files directly (no JSON output)
2427    FileEditsOnly,
2428    /// Resolution failed completely
2429    Failed,
2430}
2431
2432/// Context for conflict resolution operations.
2433///
2434/// Groups together the configuration and runtime state needed for
2435/// AI-assisted conflict resolution during rebase operations.
2436struct ConflictResolutionContext<'a> {
2437    config: &'a crate::config::Config,
2438    template_context: &'a TemplateContext,
2439    logger: &'a Logger,
2440    colors: Colors,
2441    executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2442    workspace: &'a dyn crate::workspace::Workspace,
2443}
2444
2445/// Attempt to resolve rebase conflicts with AI fallback.
2446///
2447/// This function accepts `PhaseContext` to capture prompts and track
2448/// execution history for hardened resume functionality.
2449fn try_resolve_conflicts_with_fallback(
2450    conflicted_files: &[String],
2451    ctx: ConflictResolutionContext<'_>,
2452    phase_ctx: &mut PhaseContext<'_>,
2453    phase: &str,
2454    executor: &dyn ProcessExecutor,
2455) -> anyhow::Result<bool> {
2456    if conflicted_files.is_empty() {
2457        return Ok(false);
2458    }
2459
2460    ctx.logger.info(&format!(
2461        "Attempting AI conflict resolution for {} file(s)",
2462        conflicted_files.len()
2463    ));
2464
2465    let conflicts = collect_conflict_info_or_error(conflicted_files, ctx.logger)?;
2466
2467    // Use stored_or_generate pattern for hardened resume
2468    // On resume, use the exact same prompt that was used before
2469    let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2470    let (resolution_prompt, was_replayed) =
2471        get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2472            build_resolution_prompt(&conflicts, ctx.template_context)
2473        });
2474
2475    // Capture the resolution prompt for deterministic resume (only if newly generated)
2476    if !was_replayed {
2477        phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2478    } else {
2479        ctx.logger.info(&format!(
2480            "Using stored prompt from checkpoint for determinism: {}",
2481            prompt_key
2482        ));
2483    }
2484
2485    match run_ai_conflict_resolution(
2486        &resolution_prompt,
2487        ctx.config,
2488        ctx.logger,
2489        ctx.colors,
2490        std::sync::Arc::clone(&ctx.executor_arc),
2491        ctx.workspace,
2492    ) {
2493        Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2494            // Agent provided JSON output - attempt to parse and write files
2495            // JSON is optional for verification - LibGit2 state is authoritative
2496            match parse_and_validate_resolved_files(&resolved_content, ctx.logger) {
2497                Ok(resolved_files) => {
2498                    write_resolved_files(&resolved_files, ctx.logger)?;
2499                }
2500                Err(_) => {
2501                    // JSON parsing failed - this is expected and normal
2502                    // We verify conflicts via LibGit2 state, not JSON parsing
2503                    // Continue to LibGit2 verification below
2504                }
2505            }
2506
2507            // Verify all conflicts are resolved via LibGit2 (authoritative source)
2508            let remaining_conflicts = get_conflicted_files()?;
2509            if remaining_conflicts.is_empty() {
2510                Ok(true)
2511            } else {
2512                ctx.logger.warn(&format!(
2513                    "{} conflicts remain after AI resolution",
2514                    remaining_conflicts.len()
2515                ));
2516                Ok(false)
2517            }
2518        }
2519        Ok(ConflictResolutionResult::FileEditsOnly) => {
2520            // Agent resolved conflicts by editing files directly
2521            ctx.logger
2522                .info("Agent resolved conflicts via file edits (no JSON output)");
2523
2524            // Verify all conflicts are resolved
2525            let remaining_conflicts = get_conflicted_files()?;
2526            if remaining_conflicts.is_empty() {
2527                ctx.logger.success("All conflicts resolved via file edits");
2528                Ok(true)
2529            } else {
2530                ctx.logger.warn(&format!(
2531                    "{} conflicts remain after AI resolution",
2532                    remaining_conflicts.len()
2533                ));
2534                Ok(false)
2535            }
2536        }
2537        Ok(ConflictResolutionResult::Failed) => {
2538            ctx.logger.warn("AI conflict resolution failed");
2539            ctx.logger.info("Attempting to continue rebase anyway...");
2540
2541            // Try to continue rebase - user may have manually resolved conflicts
2542            match crate::git_helpers::continue_rebase(executor) {
2543                Ok(()) => {
2544                    ctx.logger.info("Successfully continued rebase");
2545                    Ok(true)
2546                }
2547                Err(rebase_err) => {
2548                    ctx.logger
2549                        .warn(&format!("Failed to continue rebase: {rebase_err}"));
2550                    Ok(false) // Conflicts remain
2551                }
2552            }
2553        }
2554        Err(e) => {
2555            ctx.logger
2556                .warn(&format!("AI conflict resolution failed: {e}"));
2557            ctx.logger.info("Attempting to continue rebase anyway...");
2558
2559            // Try to continue rebase - user may have manually resolved conflicts
2560            match crate::git_helpers::continue_rebase(executor) {
2561                Ok(()) => {
2562                    ctx.logger.info("Successfully continued rebase");
2563                    Ok(true)
2564                }
2565                Err(rebase_err) => {
2566                    ctx.logger
2567                        .warn(&format!("Failed to continue rebase: {rebase_err}"));
2568                    Ok(false) // Conflicts remain
2569                }
2570            }
2571        }
2572    }
2573}
2574
2575/// Wrapper for conflict resolution without PhaseContext.
2576///
2577/// This is used for --rebase-only mode where we don't have a full pipeline context.
2578/// It creates a minimal PhaseContext for the conflict resolution call.
2579fn try_resolve_conflicts_without_phase_ctx(
2580    conflicted_files: &[String],
2581    config: &crate::config::Config,
2582    template_context: &TemplateContext,
2583    logger: &Logger,
2584    colors: Colors,
2585    executor: std::sync::Arc<dyn ProcessExecutor>,
2586    repo_root: &std::path::Path,
2587) -> anyhow::Result<bool> {
2588    use crate::agents::AgentRegistry;
2589    use crate::checkpoint::execution_history::ExecutionHistory;
2590    use crate::checkpoint::RunContext;
2591    use crate::pipeline::{Stats, Timer};
2592
2593    // Create minimal PhaseContext for conflict resolution
2594    let registry = AgentRegistry::new()?;
2595    let mut timer = Timer::new();
2596    let mut stats = Stats::default();
2597    let workspace = crate::workspace::WorkspaceFs::new(repo_root.to_path_buf());
2598
2599    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2600    let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2601
2602    // Clone the executor Arc for use in PhaseContext and ConflictResolutionContext
2603    let executor_arc = std::sync::Arc::clone(&executor);
2604
2605    let mut phase_ctx = PhaseContext {
2606        config,
2607        registry: &registry,
2608        logger,
2609        colors: &colors,
2610        timer: &mut timer,
2611        stats: &mut stats,
2612        developer_agent,
2613        reviewer_agent,
2614        review_guidelines: None,
2615        template_context,
2616        run_context: RunContext::new(),
2617        execution_history: ExecutionHistory::new(),
2618        prompt_history: std::collections::HashMap::new(),
2619        executor: &*executor,
2620        executor_arc: std::sync::Arc::clone(&executor_arc),
2621        repo_root,
2622        workspace: &workspace,
2623    };
2624
2625    let ctx = ConflictResolutionContext {
2626        config,
2627        template_context,
2628        logger,
2629        colors,
2630        executor_arc,
2631        workspace: &workspace,
2632    };
2633
2634    try_resolve_conflicts_with_fallback(
2635        conflicted_files,
2636        ctx,
2637        &mut phase_ctx,
2638        "RebaseOnly",
2639        &*executor,
2640    )
2641}
2642
2643/// Collect conflict information from conflicted files.
2644fn collect_conflict_info_or_error(
2645    conflicted_files: &[String],
2646    logger: &Logger,
2647) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2648    use crate::prompts::collect_conflict_info;
2649
2650    let conflicts = match collect_conflict_info(conflicted_files) {
2651        Ok(c) => c,
2652        Err(e) => {
2653            logger.error(&format!("Failed to collect conflict info: {e}"));
2654            anyhow::bail!("Failed to collect conflict info");
2655        }
2656    };
2657    Ok(conflicts)
2658}
2659
2660/// Build the conflict resolution prompt from context files.
2661fn build_resolution_prompt(
2662    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2663    template_context: &TemplateContext,
2664) -> String {
2665    build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2666        .unwrap_or_else(|_| String::new())
2667}
2668
2669/// Build the conflict resolution prompt.
2670///
2671/// This function uses the standard conflict resolution prompt.
2672fn build_enhanced_resolution_prompt(
2673    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2674    _branch_info: Option<()>, // Kept for API compatibility, currently unused
2675    template_context: &TemplateContext,
2676) -> anyhow::Result<String> {
2677    use std::fs;
2678
2679    let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2680    let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2681
2682    // Use standard prompt
2683    Ok(
2684        crate::prompts::build_conflict_resolution_prompt_with_context(
2685            template_context,
2686            conflicts,
2687            prompt_md_content.as_deref(),
2688            plan_content.as_deref(),
2689        ),
2690    )
2691}
2692
2693/// Run AI agent to resolve conflicts with fallback mechanism.
2694///
2695/// Returns `ConflictResolutionResult` indicating whether the agent provided
2696/// JSON output, resolved conflicts via file edits, or failed completely.
2697fn run_ai_conflict_resolution(
2698    resolution_prompt: &str,
2699    config: &crate::config::Config,
2700    logger: &Logger,
2701    colors: Colors,
2702    executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2703    workspace: &dyn crate::workspace::Workspace,
2704) -> anyhow::Result<ConflictResolutionResult> {
2705    use crate::agents::AgentRegistry;
2706    use crate::files::result_extraction::extract_last_result;
2707    use crate::pipeline::{
2708        run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2709    };
2710    use std::io;
2711    use std::path::Path;
2712
2713    // Note: log_dir is used as a prefix for log file names, not as a directory.
2714    // The actual log files will be created in .agent/logs/ with names like:
2715    // .agent/logs/rebase_conflict_resolution_ccs-glm_0.log
2716    let log_dir = ".agent/logs/rebase_conflict_resolution";
2717
2718    let registry = AgentRegistry::new()?;
2719    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2720
2721    // Use the injected executor for the runtime
2722    let executor_ref: &dyn crate::executor::ProcessExecutor = &*executor_arc;
2723    let mut runtime = PipelineRuntime {
2724        timer: &mut crate::pipeline::Timer::new(),
2725        logger,
2726        colors: &colors,
2727        config,
2728        executor: executor_ref,
2729        executor_arc: std::sync::Arc::clone(&executor_arc),
2730        workspace,
2731    };
2732
2733    // Output validator: checks if agent produced valid output OR resolved conflicts
2734    // Agents may edit files without returning JSON, so we verify conflicts are resolved.
2735    let validate_output: OutputValidator = |ws: &dyn crate::workspace::Workspace,
2736                                            log_dir_path: &Path,
2737                                            validation_logger: &crate::logger::Logger|
2738     -> io::Result<bool> {
2739        match extract_last_result(ws, log_dir_path) {
2740            Ok(Some(_)) => {
2741                // Valid JSON output exists
2742                Ok(true)
2743            }
2744            Ok(None) => {
2745                // No JSON output - check if conflicts were resolved anyway
2746                // (agent may have edited files without returning JSON)
2747                match crate::git_helpers::get_conflicted_files() {
2748                    Ok(conflicts) if conflicts.is_empty() => {
2749                        validation_logger
2750                            .info("Agent resolved conflicts without JSON output (file edits only)");
2751                        Ok(true) // Conflicts resolved, consider success
2752                    }
2753                    Ok(conflicts) => {
2754                        validation_logger.warn(&format!(
2755                            "{} conflict(s) remain unresolved",
2756                            conflicts.len()
2757                        ));
2758                        Ok(false) // Conflicts remain
2759                    }
2760                    Err(e) => {
2761                        validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2762                        Ok(false) // Error checking conflicts
2763                    }
2764                }
2765            }
2766            Err(e) => {
2767                validation_logger.warn(&format!("Output validation check failed: {e}"));
2768                Ok(false) // Treat validation errors as missing output
2769            }
2770        }
2771    };
2772
2773    let mut fallback_config = FallbackConfig {
2774        role: crate::agents::AgentRole::Reviewer,
2775        base_label: "conflict resolution",
2776        prompt: resolution_prompt,
2777        logfile_prefix: log_dir,
2778        runtime: &mut runtime,
2779        registry: &registry,
2780        primary_agent: reviewer_agent,
2781        output_validator: Some(validate_output),
2782        workspace,
2783    };
2784
2785    let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2786
2787    if exit_code != 0 {
2788        return Ok(ConflictResolutionResult::Failed);
2789    }
2790
2791    // Check if conflicts are resolved after agent run
2792    // The validator already checked this, but we verify again to determine the result type
2793    let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2794
2795    if remaining_conflicts.is_empty() {
2796        // Conflicts are resolved - check if agent provided JSON output
2797        match extract_last_result(workspace, Path::new(log_dir)) {
2798            Ok(Some(content)) => {
2799                logger.info("Agent provided JSON output with resolved files");
2800                Ok(ConflictResolutionResult::WithJson(content))
2801            }
2802            Ok(None) => {
2803                logger.info("Agent resolved conflicts via file edits (no JSON output)");
2804                Ok(ConflictResolutionResult::FileEditsOnly)
2805            }
2806            Err(e) => {
2807                // Extraction failed but conflicts are resolved - treat as file edits only
2808                logger.warn(&format!(
2809                    "Failed to extract JSON output but conflicts are resolved: {e}"
2810                ));
2811                Ok(ConflictResolutionResult::FileEditsOnly)
2812            }
2813        }
2814    } else {
2815        logger.warn(&format!(
2816            "{} conflict(s) remain after agent attempted resolution",
2817            remaining_conflicts.len()
2818        ));
2819        Ok(ConflictResolutionResult::Failed)
2820    }
2821}
2822
2823/// Parse and validate the resolved files from AI output.
2824///
2825/// JSON parsing failures are expected and handled gracefully - LibGit2 state
2826/// is used for verification, not JSON output. This function only parses the
2827/// JSON to write resolved files if available.
2828fn parse_and_validate_resolved_files(
2829    resolved_content: &str,
2830    logger: &Logger,
2831) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2832    let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2833        // Agent did not provide JSON output - fall back to LibGit2 verification
2834        // This is expected and normal, not an error condition
2835        anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2836    })?;
2837
2838    let resolved_files = match json.get("resolved_files") {
2839        Some(v) if v.is_object() => v.as_object().unwrap(),
2840        _ => {
2841            logger.info("Agent output missing 'resolved_files' object");
2842            anyhow::bail!("Agent output missing 'resolved_files' object");
2843        }
2844    };
2845
2846    if resolved_files.is_empty() {
2847        logger.info("No resolved files in JSON output");
2848        anyhow::bail!("No files were resolved by the agent");
2849    }
2850
2851    Ok(resolved_files.clone())
2852}
2853
2854/// Write resolved files to disk and stage them.
2855fn write_resolved_files(
2856    resolved_files: &serde_json::Map<String, serde_json::Value>,
2857    logger: &Logger,
2858) -> anyhow::Result<usize> {
2859    use std::fs;
2860
2861    let mut files_written = 0;
2862    for (path, content) in resolved_files {
2863        if let Some(content_str) = content.as_str() {
2864            fs::write(path, content_str).map_err(|e| {
2865                logger.error(&format!("Failed to write {path}: {e}"));
2866                anyhow::anyhow!("Failed to write {path}: {e}")
2867            })?;
2868            logger.info(&format!("Resolved and wrote: {path}"));
2869            files_written += 1;
2870            // Stage the resolved file
2871            if let Err(e) = crate::git_helpers::git_add_all() {
2872                logger.warn(&format!("Failed to stage {path}: {e}"));
2873            }
2874        }
2875    }
2876
2877    logger.success(&format!("Successfully resolved {files_written} file(s)"));
2878    Ok(files_written)
2879}