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("Tip: Edit PROMPT.md, then run: ralph");
1047            return Ok(None);
1048        }
1049        println!();
1050        logger.error("PROMPT.md not found in current directory.");
1051        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1052        println!();
1053        logger.info("To get started:");
1054        logger.info("  ralph --init                    # Smart setup wizard");
1055        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1056        logger.info("  ralph --list-work-guides          # See all Work Guides");
1057        println!();
1058        return Ok(None);
1059    }
1060
1061    // Non-interactive mode: show helpful error if PROMPT.md doesn't exist
1062    if !prompt_exists {
1063        logger.error("PROMPT.md not found in current directory.");
1064        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1065        println!();
1066        logger.info("Quick start:");
1067        logger.info("  ralph --init                    # Smart setup wizard");
1068        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1069        logger.info("  ralph --list-work-guides          # See all Work Guides");
1070        println!();
1071        logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1072        println!();
1073        return Ok(None);
1074    }
1075
1076    Ok(Some(()))
1077}
1078
1079/// Runs the full development/review/commit pipeline using reducer-based event loop.
1080fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1081    // Use MainEffectHandler for production
1082    run_pipeline_with_default_handler(ctx)
1083}
1084
1085/// Runs the pipeline with the default MainEffectHandler.
1086///
1087/// This is the production entry point - it creates a MainEffectHandler internally.
1088fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1089    use crate::app::event_loop::EventLoopConfig;
1090    #[cfg(not(feature = "test-utils"))]
1091    use crate::reducer::MainEffectHandler;
1092    use crate::reducer::PipelineState;
1093
1094    // First, offer interactive resume if checkpoint exists without --resume flag
1095    let resume_result = offer_resume_if_checkpoint_exists(
1096        &ctx.args,
1097        &ctx.config,
1098        &ctx.registry,
1099        &ctx.logger,
1100        &ctx.developer_agent,
1101        &ctx.reviewer_agent,
1102    );
1103
1104    // If interactive resume didn't happen, check for --resume flag
1105    let resume_result = match resume_result {
1106        Some(result) => Some(result),
1107        None => handle_resume_with_validation(
1108            &ctx.args,
1109            &ctx.config,
1110            &ctx.registry,
1111            &ctx.logger,
1112            &ctx.developer_display,
1113            &ctx.reviewer_display,
1114        ),
1115    };
1116
1117    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1118
1119    // Create run context - either new or from checkpoint
1120    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1121        use crate::checkpoint::RunContext;
1122        RunContext::from_checkpoint(checkpoint)
1123    } else {
1124        use crate::checkpoint::RunContext;
1125        RunContext::new()
1126    };
1127
1128    // Apply checkpoint configuration restoration if resuming
1129    let config = if let Some(ref checkpoint) = resume_checkpoint {
1130        use crate::checkpoint::apply_checkpoint_to_config;
1131        let mut restored_config = ctx.config.clone();
1132        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1133        ctx.logger.info("Restored configuration from checkpoint:");
1134        if checkpoint.cli_args.developer_iters > 0 {
1135            ctx.logger.info(&format!(
1136                "  Developer iterations: {} (from checkpoint)",
1137                checkpoint.cli_args.developer_iters
1138            ));
1139        }
1140        if checkpoint.cli_args.reviewer_reviews > 0 {
1141            ctx.logger.info(&format!(
1142                "  Reviewer passes: {} (from checkpoint)",
1143                checkpoint.cli_args.reviewer_reviews
1144            ));
1145        }
1146        restored_config
1147    } else {
1148        ctx.config.clone()
1149    };
1150
1151    // Restore environment variables from checkpoint if resuming
1152    if let Some(ref checkpoint) = resume_checkpoint {
1153        use crate::checkpoint::restore::restore_environment_from_checkpoint;
1154        let restored_count = restore_environment_from_checkpoint(checkpoint);
1155        if restored_count > 0 {
1156            ctx.logger.info(&format!(
1157                "  Restored {} environment variable(s) from checkpoint",
1158                restored_count
1159            ));
1160        }
1161    }
1162
1163    // Set up git helpers and agent phase
1164    // Use workspace-aware versions when test-utils feature is enabled
1165    // to avoid real git operations that would cause test failures.
1166    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1167
1168    #[cfg(feature = "test-utils")]
1169    {
1170        use crate::git_helpers::{
1171            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1172        };
1173        // Use workspace-based operations that don't require real git
1174        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1175        create_marker_with_workspace(&*ctx.workspace)?;
1176        // Skip hook installation and git wrapper in test mode
1177    }
1178    #[cfg(not(feature = "test-utils"))]
1179    {
1180        cleanup_orphaned_marker(&ctx.logger)?;
1181        start_agent_phase(&mut git_helpers)?;
1182    }
1183    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1184
1185    // Print welcome banner and validate PROMPT.md
1186    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1187    print_pipeline_info_with_config(ctx, &config);
1188    validate_prompt_and_setup_backup(ctx)?;
1189
1190    // Set up PROMPT.md monitoring
1191    let mut prompt_monitor = setup_prompt_monitor(ctx);
1192
1193    // Detect project stack and review guidelines
1194    let (_project_stack, review_guidelines) =
1195        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1196
1197    print_review_guidelines(ctx, review_guidelines.as_ref());
1198    println!();
1199
1200    // Create phase context and save starting commit
1201    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1202    let mut phase_ctx = create_phase_context_with_config(
1203        ctx,
1204        &config,
1205        &mut timer,
1206        &mut stats,
1207        review_guidelines.as_ref(),
1208        &run_context,
1209        resume_checkpoint.as_ref(),
1210    );
1211    save_start_commit_or_warn(ctx);
1212
1213    // Set up interrupt context for checkpoint saving on Ctrl+C
1214    // This must be done after phase_ctx is created
1215    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1216        checkpoint.phase
1217    } else {
1218        PipelinePhase::Planning
1219    };
1220    setup_interrupt_context_for_pipeline(
1221        initial_phase,
1222        config.developer_iters,
1223        config.reviewer_reviews,
1224        &phase_ctx.execution_history,
1225        &phase_ctx.prompt_history,
1226        &run_context,
1227    );
1228
1229    // Ensure interrupt context is cleared on completion
1230    let _interrupt_guard = defer_clear_interrupt_context();
1231
1232    // Determine if we should run rebase based on checkpoint or current args
1233    let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1234        // Use checkpoint's skip_rebase value if it has meaningful cli_args
1235        if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1236            !checkpoint.cli_args.skip_rebase
1237        } else {
1238            // Fallback to current args
1239            ctx.args.rebase_flags.with_rebase
1240        }
1241    } else {
1242        ctx.args.rebase_flags.with_rebase
1243    };
1244
1245    // Run pre-development rebase (only if explicitly requested via --with-rebase)
1246    if should_run_rebase {
1247        run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1248        // Update interrupt context after rebase
1249        update_interrupt_context_from_phase(
1250            &phase_ctx,
1251            PipelinePhase::Planning,
1252            config.developer_iters,
1253            config.reviewer_reviews,
1254            &run_context,
1255        );
1256    } else {
1257        // Save initial checkpoint when rebase is disabled
1258        if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1259            let builder = CheckpointBuilder::new()
1260                .phase(PipelinePhase::Planning, 0, config.developer_iters)
1261                .reviewer_pass(0, config.reviewer_reviews)
1262                .skip_rebase(true) // Rebase is disabled
1263                .capture_from_context(
1264                    &config,
1265                    &ctx.registry,
1266                    &ctx.developer_agent,
1267                    &ctx.reviewer_agent,
1268                    &ctx.logger,
1269                    &run_context,
1270                )
1271                .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1272                .with_execution_history(phase_ctx.execution_history.clone())
1273                .with_prompt_history(phase_ctx.clone_prompt_history());
1274
1275            if let Some(checkpoint) = builder.build() {
1276                let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1277            }
1278        }
1279        // Update interrupt context after initial checkpoint
1280        update_interrupt_context_from_phase(
1281            &phase_ctx,
1282            PipelinePhase::Planning,
1283            config.developer_iters,
1284            config.reviewer_reviews,
1285            &run_context,
1286        );
1287    }
1288
1289    // ============================================
1290    // RUN PIPELINE PHASES VIA REDUCER EVENT LOOP
1291    // ============================================
1292
1293    // Initialize pipeline state
1294    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1295        // Migrate from old checkpoint format to new reducer state
1296        PipelineState::from(checkpoint.clone())
1297    } else {
1298        // Create new initial state
1299        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1300    };
1301
1302    // Configure event loop
1303    let event_loop_config = EventLoopConfig {
1304        max_iterations: 1000,
1305        enable_checkpointing: config.features.checkpoint_enabled,
1306    };
1307
1308    // Clone execution_history and prompt_history BEFORE running event loop (to avoid borrow issues)
1309    let execution_history_before = phase_ctx.execution_history.clone();
1310    let prompt_history_before = phase_ctx.clone_prompt_history();
1311
1312    // Create effect handler and run event loop.
1313    // Under test-utils feature, use MockEffectHandler to avoid real git operations.
1314    #[cfg(feature = "test-utils")]
1315    let loop_result = {
1316        use crate::app::event_loop::run_event_loop_with_handler;
1317        use crate::reducer::mock_effect_handler::MockEffectHandler;
1318        let mut handler = MockEffectHandler::new(initial_state.clone());
1319        let phase_ctx_ref = &mut phase_ctx;
1320        run_event_loop_with_handler(
1321            phase_ctx_ref,
1322            Some(initial_state),
1323            event_loop_config,
1324            &mut handler,
1325        )
1326    };
1327    #[cfg(not(feature = "test-utils"))]
1328    let loop_result = {
1329        use crate::app::event_loop::run_event_loop_with_handler;
1330        let mut handler = MainEffectHandler::new(initial_state.clone());
1331        let phase_ctx_ref = &mut phase_ctx;
1332        run_event_loop_with_handler(
1333            phase_ctx_ref,
1334            Some(initial_state),
1335            event_loop_config,
1336            &mut handler,
1337        )
1338    };
1339
1340    // Handle event loop result
1341    let loop_result = loop_result?;
1342    if loop_result.completed {
1343        ctx.logger
1344            .success("Pipeline completed successfully via reducer event loop");
1345        ctx.logger.info(&format!(
1346            "Total events processed: {}",
1347            loop_result.events_processed
1348        ));
1349    } else {
1350        ctx.logger.warn("Pipeline exited without completion marker");
1351    }
1352
1353    // Save Complete checkpoint before clearing (for idempotent resume)
1354    if config.features.checkpoint_enabled {
1355        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1356        let builder = CheckpointBuilder::new()
1357            .phase(
1358                PipelinePhase::Complete,
1359                config.developer_iters,
1360                config.developer_iters,
1361            )
1362            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1363            .skip_rebase(skip_rebase)
1364            .capture_from_context(
1365                &config,
1366                &ctx.registry,
1367                &ctx.developer_agent,
1368                &ctx.reviewer_agent,
1369                &ctx.logger,
1370                &run_context,
1371            )
1372            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1373
1374        let builder = builder
1375            .with_execution_history(execution_history_before)
1376            .with_prompt_history(prompt_history_before);
1377
1378        if let Some(checkpoint) = builder.build() {
1379            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1380        }
1381    }
1382
1383    // Post-pipeline operations
1384    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1385    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1386
1387    // Commit phase
1388    finalize_pipeline(
1389        &mut agent_phase_guard,
1390        &ctx.logger,
1391        ctx.colors,
1392        &config,
1393        finalization::RuntimeStats {
1394            timer: &timer,
1395            stats: &stats,
1396        },
1397        prompt_monitor,
1398        Some(&*ctx.workspace),
1399    );
1400    Ok(())
1401}
1402
1403/// Runs the pipeline with a custom effect handler for testing.
1404///
1405/// This function is only available with the `test-utils` feature and allows
1406/// injecting a `MockEffectHandler` to prevent real git operations during tests.
1407///
1408/// # Arguments
1409///
1410/// * `ctx` - Pipeline context
1411/// * `effect_handler` - Custom effect handler (e.g., `MockEffectHandler`)
1412///
1413/// # Type Parameters
1414///
1415/// * `H` - Effect handler type that implements `EffectHandler` and `StatefulHandler`
1416#[cfg(feature = "test-utils")]
1417pub fn run_pipeline_with_effect_handler<'ctx, H>(
1418    ctx: &PipelineContext,
1419    effect_handler: &mut H,
1420) -> anyhow::Result<()>
1421where
1422    H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1423{
1424    use crate::app::event_loop::EventLoopConfig;
1425    use crate::reducer::PipelineState;
1426
1427    // First, offer interactive resume if checkpoint exists without --resume flag
1428    let resume_result = offer_resume_if_checkpoint_exists(
1429        &ctx.args,
1430        &ctx.config,
1431        &ctx.registry,
1432        &ctx.logger,
1433        &ctx.developer_agent,
1434        &ctx.reviewer_agent,
1435    );
1436
1437    // If interactive resume didn't happen, check for --resume flag
1438    let resume_result = match resume_result {
1439        Some(result) => Some(result),
1440        None => handle_resume_with_validation(
1441            &ctx.args,
1442            &ctx.config,
1443            &ctx.registry,
1444            &ctx.logger,
1445            &ctx.developer_display,
1446            &ctx.reviewer_display,
1447        ),
1448    };
1449
1450    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1451
1452    // Create run context - either new or from checkpoint
1453    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1454        use crate::checkpoint::RunContext;
1455        RunContext::from_checkpoint(checkpoint)
1456    } else {
1457        use crate::checkpoint::RunContext;
1458        RunContext::new()
1459    };
1460
1461    // Apply checkpoint configuration restoration if resuming
1462    let config = if let Some(ref checkpoint) = resume_checkpoint {
1463        use crate::checkpoint::apply_checkpoint_to_config;
1464        let mut restored_config = ctx.config.clone();
1465        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1466        restored_config
1467    } else {
1468        ctx.config.clone()
1469    };
1470
1471    // Set up git helpers and agent phase
1472    // Use workspace-aware versions when test-utils feature is enabled
1473    // to avoid real git operations that would cause test failures.
1474    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1475
1476    #[cfg(feature = "test-utils")]
1477    {
1478        use crate::git_helpers::{
1479            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1480        };
1481        // Use workspace-based operations that don't require real git
1482        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1483        create_marker_with_workspace(&*ctx.workspace)?;
1484        // Skip hook installation and git wrapper in test mode
1485    }
1486    #[cfg(not(feature = "test-utils"))]
1487    {
1488        cleanup_orphaned_marker(&ctx.logger)?;
1489        start_agent_phase(&mut git_helpers)?;
1490    }
1491    let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1492
1493    // Print welcome banner and validate PROMPT.md
1494    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1495    print_pipeline_info_with_config(ctx, &config);
1496    validate_prompt_and_setup_backup(ctx)?;
1497
1498    // Set up PROMPT.md monitoring
1499    let mut prompt_monitor = setup_prompt_monitor(ctx);
1500
1501    // Detect project stack and review guidelines
1502    let (_project_stack, review_guidelines) =
1503        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1504
1505    print_review_guidelines(ctx, review_guidelines.as_ref());
1506    println!();
1507
1508    // Create phase context and save starting commit
1509    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1510    let mut phase_ctx = create_phase_context_with_config(
1511        ctx,
1512        &config,
1513        &mut timer,
1514        &mut stats,
1515        review_guidelines.as_ref(),
1516        &run_context,
1517        resume_checkpoint.as_ref(),
1518    );
1519    save_start_commit_or_warn(ctx);
1520
1521    // Set up interrupt context for checkpoint saving on Ctrl+C
1522    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1523        checkpoint.phase
1524    } else {
1525        PipelinePhase::Planning
1526    };
1527    setup_interrupt_context_for_pipeline(
1528        initial_phase,
1529        config.developer_iters,
1530        config.reviewer_reviews,
1531        &phase_ctx.execution_history,
1532        &phase_ctx.prompt_history,
1533        &run_context,
1534    );
1535
1536    // Ensure interrupt context is cleared on completion
1537    let _interrupt_guard = defer_clear_interrupt_context();
1538
1539    // Initialize pipeline state
1540    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1541        PipelineState::from(checkpoint.clone())
1542    } else {
1543        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1544    };
1545
1546    // Configure event loop
1547    let event_loop_config = EventLoopConfig {
1548        max_iterations: 1000,
1549        enable_checkpointing: config.features.checkpoint_enabled,
1550    };
1551
1552    // Clone execution_history and prompt_history BEFORE running event loop
1553    let execution_history_before = phase_ctx.execution_history.clone();
1554    let prompt_history_before = phase_ctx.clone_prompt_history();
1555
1556    // Run event loop with the provided handler
1557    effect_handler.update_state(initial_state.clone());
1558    let loop_result = {
1559        use crate::app::event_loop::run_event_loop_with_handler;
1560        let phase_ctx_ref = &mut phase_ctx;
1561        run_event_loop_with_handler(
1562            phase_ctx_ref,
1563            Some(initial_state),
1564            event_loop_config,
1565            effect_handler,
1566        )
1567    };
1568
1569    // Handle event loop result
1570    let loop_result = loop_result?;
1571    if loop_result.completed {
1572        ctx.logger
1573            .success("Pipeline completed successfully via reducer event loop");
1574        ctx.logger.info(&format!(
1575            "Total events processed: {}",
1576            loop_result.events_processed
1577        ));
1578    } else {
1579        ctx.logger.warn("Pipeline exited without completion marker");
1580    }
1581
1582    // Save Complete checkpoint before clearing (for idempotent resume)
1583    if config.features.checkpoint_enabled {
1584        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1585        let builder = CheckpointBuilder::new()
1586            .phase(
1587                PipelinePhase::Complete,
1588                config.developer_iters,
1589                config.developer_iters,
1590            )
1591            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1592            .skip_rebase(skip_rebase)
1593            .capture_from_context(
1594                &config,
1595                &ctx.registry,
1596                &ctx.developer_agent,
1597                &ctx.reviewer_agent,
1598                &ctx.logger,
1599                &run_context,
1600            )
1601            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1602
1603        let builder = builder
1604            .with_execution_history(execution_history_before)
1605            .with_prompt_history(prompt_history_before);
1606
1607        if let Some(checkpoint) = builder.build() {
1608            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1609        }
1610    }
1611
1612    // Post-pipeline operations
1613    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1614    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1615
1616    // Commit phase
1617    finalize_pipeline(
1618        &mut agent_phase_guard,
1619        &ctx.logger,
1620        ctx.colors,
1621        &config,
1622        finalization::RuntimeStats {
1623            timer: &timer,
1624            stats: &stats,
1625        },
1626        prompt_monitor,
1627        Some(&*ctx.workspace),
1628    );
1629    Ok(())
1630}
1631
1632/// Set up the interrupt context with initial pipeline state.
1633///
1634/// This function initializes the global interrupt context so that if
1635/// the user presses Ctrl+C, the interrupt handler can save a checkpoint.
1636fn setup_interrupt_context_for_pipeline(
1637    phase: PipelinePhase,
1638    total_iterations: u32,
1639    total_reviewer_passes: u32,
1640    execution_history: &crate::checkpoint::ExecutionHistory,
1641    prompt_history: &std::collections::HashMap<String, String>,
1642    run_context: &crate::checkpoint::RunContext,
1643) {
1644    use crate::interrupt::{set_interrupt_context, InterruptContext};
1645
1646    // Determine initial iteration based on phase
1647    let (iteration, reviewer_pass) = match phase {
1648        PipelinePhase::Development => (1, 0),
1649        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1650            (total_iterations, 1)
1651        }
1652        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1653            (total_iterations, total_reviewer_passes)
1654        }
1655        _ => (0, 0),
1656    };
1657
1658    let context = InterruptContext {
1659        phase,
1660        iteration,
1661        total_iterations,
1662        reviewer_pass,
1663        total_reviewer_passes,
1664        run_context: run_context.clone(),
1665        execution_history: execution_history.clone(),
1666        prompt_history: prompt_history.clone(),
1667    };
1668
1669    set_interrupt_context(context);
1670}
1671
1672/// Update the interrupt context from the current phase context.
1673///
1674/// This function should be called after each major phase to keep the
1675/// interrupt context up-to-date with the latest execution history.
1676fn update_interrupt_context_from_phase(
1677    phase_ctx: &crate::phases::PhaseContext,
1678    phase: PipelinePhase,
1679    total_iterations: u32,
1680    total_reviewer_passes: u32,
1681    run_context: &crate::checkpoint::RunContext,
1682) {
1683    use crate::interrupt::{set_interrupt_context, InterruptContext};
1684
1685    // Determine current iteration based on phase
1686    let (iteration, reviewer_pass) = match phase {
1687        PipelinePhase::Development => {
1688            // Estimate iteration from actual runs
1689            let iter = run_context.actual_developer_runs.max(1);
1690            (iter, 0)
1691        }
1692        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1693            (total_iterations, run_context.actual_reviewer_runs.max(1))
1694        }
1695        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1696            (total_iterations, total_reviewer_passes)
1697        }
1698        _ => (0, 0),
1699    };
1700
1701    let context = InterruptContext {
1702        phase,
1703        iteration,
1704        total_iterations,
1705        reviewer_pass,
1706        total_reviewer_passes,
1707        run_context: run_context.clone(),
1708        execution_history: phase_ctx.execution_history.clone(),
1709        prompt_history: phase_ctx.clone_prompt_history(),
1710    };
1711
1712    set_interrupt_context(context);
1713}
1714
1715/// Helper to defer clearing interrupt context until function exit.
1716///
1717/// Uses a scope guard pattern to ensure the interrupt context is cleared
1718/// when the pipeline completes successfully, preventing an "interrupted"
1719/// checkpoint from being saved after normal completion.
1720fn defer_clear_interrupt_context() -> InterruptContextGuard {
1721    InterruptContextGuard
1722}
1723
1724/// RAII guard for clearing interrupt context on drop.
1725///
1726/// Ensures the interrupt context is cleared when the guard is dropped,
1727/// preventing an "interrupted" checkpoint from being saved after normal
1728/// pipeline completion.
1729struct InterruptContextGuard;
1730
1731impl Drop for InterruptContextGuard {
1732    fn drop(&mut self) {
1733        crate::interrupt::clear_interrupt_context();
1734    }
1735}
1736
1737/// Validate PROMPT.md and set up backup/protection.
1738fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1739    let prompt_validation = validate_prompt_md_with_workspace(
1740        &*ctx.workspace,
1741        ctx.config.behavior.strict_validation,
1742        ctx.args.interactive,
1743    );
1744    for err in &prompt_validation.errors {
1745        ctx.logger.error(err);
1746    }
1747    for warn in &prompt_validation.warnings {
1748        ctx.logger.warn(warn);
1749    }
1750    if !prompt_validation.is_valid() {
1751        anyhow::bail!("PROMPT.md validation errors");
1752    }
1753
1754    // Create a backup of PROMPT.md to protect against accidental deletion.
1755    match create_prompt_backup_with_workspace(&*ctx.workspace) {
1756        Ok(None) => {}
1757        Ok(Some(warning)) => {
1758            ctx.logger.warn(&format!(
1759                "PROMPT.md backup created but: {warning}. Continuing anyway."
1760            ));
1761        }
1762        Err(e) => {
1763            ctx.logger.warn(&format!(
1764                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1765            ));
1766        }
1767    }
1768
1769    // Make PROMPT.md read-only to protect against accidental deletion.
1770    match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1771        None => {}
1772        Some(warning) => {
1773            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1774        }
1775    }
1776
1777    Ok(())
1778}
1779
1780/// Set up PROMPT.md monitoring for deletion detection.
1781fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1782    match PromptMonitor::new() {
1783        Ok(mut monitor) => {
1784            if let Err(e) = monitor.start() {
1785                ctx.logger.warn(&format!(
1786                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1787                ));
1788                None
1789            } else {
1790                if ctx.config.verbosity.is_debug() {
1791                    ctx.logger.info("Started real-time PROMPT.md monitoring");
1792                }
1793                Some(monitor)
1794            }
1795        }
1796        Err(e) => {
1797            ctx.logger.warn(&format!(
1798                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1799            ));
1800            None
1801        }
1802    }
1803}
1804
1805/// Print review guidelines if detected.
1806fn print_review_guidelines(
1807    ctx: &PipelineContext,
1808    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1809) {
1810    if let Some(guidelines) = review_guidelines {
1811        ctx.logger.info(&format!(
1812            "Review guidelines: {}{}{}",
1813            ctx.colors.dim(),
1814            guidelines.summary(),
1815            ctx.colors.reset()
1816        ));
1817    }
1818}
1819
1820/// Create the phase context with a modified config (for resume restoration).
1821fn create_phase_context_with_config<'ctx>(
1822    ctx: &'ctx PipelineContext,
1823    config: &'ctx crate::config::Config,
1824    timer: &'ctx mut Timer,
1825    stats: &'ctx mut Stats,
1826    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1827    run_context: &'ctx crate::checkpoint::RunContext,
1828    resume_checkpoint: Option<&PipelineCheckpoint>,
1829) -> PhaseContext<'ctx> {
1830    // Restore execution history and prompt history from checkpoint if available
1831    let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1832        let exec_history = checkpoint
1833            .execution_history
1834            .clone()
1835            .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1836        let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1837        (exec_history, prompt_hist)
1838    } else {
1839        (
1840            crate::checkpoint::execution_history::ExecutionHistory::new(),
1841            std::collections::HashMap::new(),
1842        )
1843    };
1844
1845    PhaseContext {
1846        config,
1847        registry: &ctx.registry,
1848        logger: &ctx.logger,
1849        colors: &ctx.colors,
1850        timer,
1851        stats,
1852        developer_agent: &ctx.developer_agent,
1853        reviewer_agent: &ctx.reviewer_agent,
1854        review_guidelines,
1855        template_context: &ctx.template_context,
1856        run_context: run_context.clone(),
1857        execution_history,
1858        prompt_history,
1859        executor: &*ctx.executor,
1860        executor_arc: std::sync::Arc::clone(&ctx.executor),
1861        repo_root: &ctx.repo_root,
1862        workspace: &*ctx.workspace,
1863    }
1864}
1865
1866/// Print pipeline info with a specific config.
1867fn print_pipeline_info_with_config(ctx: &PipelineContext, _config: &crate::config::Config) {
1868    ctx.logger.info(&format!(
1869        "Working directory: {}{}{}",
1870        ctx.colors.cyan(),
1871        ctx.repo_root.display(),
1872        ctx.colors.reset()
1873    ));
1874}
1875
1876/// Save starting commit or warn if it fails.
1877///
1878/// Under `test-utils` feature, this function uses mock data to avoid real git operations.
1879fn save_start_commit_or_warn(ctx: &PipelineContext) {
1880    // Skip real git operations when test-utils feature is enabled.
1881    // These functions call git2::Repository::discover which requires a real git repo.
1882    #[cfg(feature = "test-utils")]
1883    {
1884        // In tests, just log a mock message
1885        if ctx.config.verbosity.is_debug() {
1886            ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1887        }
1888        ctx.logger
1889            .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1890    }
1891
1892    #[cfg(not(feature = "test-utils"))]
1893    {
1894        match save_start_commit() {
1895            Ok(()) => {
1896                if ctx.config.verbosity.is_debug() {
1897                    ctx.logger
1898                        .info("Saved starting commit for incremental diff generation");
1899                }
1900            }
1901            Err(e) => {
1902                ctx.logger.warn(&format!(
1903                    "Failed to save starting commit: {e}. \
1904                     Incremental diffs may be unavailable as a result."
1905                ));
1906                ctx.logger.info(
1907                    "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
1908                );
1909            }
1910        }
1911
1912        // Display start commit information to user
1913        match get_start_commit_summary() {
1914            Ok(summary) => {
1915                if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
1916                {
1917                    ctx.logger.info(&summary.format_compact());
1918                    if summary.is_stale {
1919                        ctx.logger.warn(
1920                            "Start commit is stale. Consider running: ralph --reset-start-commit",
1921                        );
1922                    } else if summary.commits_since > 5 {
1923                        ctx.logger
1924                            .info("Tip: Run 'ralph --show-baseline' for more details");
1925                    }
1926                }
1927            }
1928            Err(e) => {
1929                // Only show error in debug mode since this is informational
1930                if ctx.config.verbosity.is_debug() {
1931                    ctx.logger
1932                        .warn(&format!("Failed to get start commit summary: {e}"));
1933                }
1934            }
1935        }
1936    }
1937}
1938
1939/// Check for PROMPT.md restoration after a phase.
1940fn check_prompt_restoration(
1941    ctx: &PipelineContext,
1942    prompt_monitor: &mut Option<PromptMonitor>,
1943    phase: &str,
1944) {
1945    if let Some(ref mut monitor) = prompt_monitor {
1946        if monitor.check_and_restore() {
1947            ctx.logger.warn(&format!(
1948                "PROMPT.md was deleted and restored during {phase} phase"
1949            ));
1950        }
1951    }
1952}
1953
1954/// Handle --rebase-only flag.
1955///
1956/// This function performs a rebase to the default branch with AI conflict resolution and exits,
1957/// without running the full pipeline.
1958pub fn handle_rebase_only(
1959    _args: &Args,
1960    config: &crate::config::Config,
1961    template_context: &TemplateContext,
1962    logger: &Logger,
1963    colors: Colors,
1964    executor: std::sync::Arc<dyn ProcessExecutor>,
1965    repo_root: &std::path::Path,
1966) -> anyhow::Result<()> {
1967    // Check if we're on main/master branch
1968    if is_main_or_master_branch()? {
1969        logger.warn("Already on main/master branch - rebasing on main is not recommended");
1970        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1971        logger.info("  git worktree add ../feature-branch feature-branch");
1972        logger.info("This allows multiple AI agents to work on different features simultaneously.");
1973        logger.info("Proceeding with rebase anyway as requested...");
1974    }
1975
1976    logger.header("Rebase to default branch", Colors::cyan);
1977
1978    match run_rebase_to_default(logger, colors, &*executor) {
1979        Ok(RebaseResult::Success) => {
1980            logger.success("Rebase completed successfully");
1981            Ok(())
1982        }
1983        Ok(RebaseResult::NoOp { reason }) => {
1984            logger.info(&format!("No rebase needed: {reason}"));
1985            Ok(())
1986        }
1987        Ok(RebaseResult::Failed(err)) => {
1988            logger.error(&format!("Rebase failed: {err}"));
1989            anyhow::bail!("Rebase failed: {err}")
1990        }
1991        Ok(RebaseResult::Conflicts(_conflicts)) => {
1992            // Get the actual conflicted files
1993            let conflicted_files = get_conflicted_files()?;
1994            if conflicted_files.is_empty() {
1995                logger.warn("Rebase reported conflicts but no conflicted files found");
1996                let _ = abort_rebase(&*executor);
1997                return Ok(());
1998            }
1999
2000            logger.warn(&format!(
2001                "Rebase resulted in {} conflict(s), attempting AI resolution",
2002                conflicted_files.len()
2003            ));
2004
2005            // For --rebase-only, we don't have a full PhaseContext, so we use a wrapper
2006            match try_resolve_conflicts_without_phase_ctx(
2007                &conflicted_files,
2008                config,
2009                template_context,
2010                logger,
2011                colors,
2012                std::sync::Arc::clone(&executor),
2013                repo_root,
2014            ) {
2015                Ok(true) => {
2016                    // Conflicts resolved, continue the rebase
2017                    logger.info("Continuing rebase after conflict resolution");
2018                    match continue_rebase(&*executor) {
2019                        Ok(()) => {
2020                            logger.success("Rebase completed successfully after AI resolution");
2021                            Ok(())
2022                        }
2023                        Err(e) => {
2024                            logger.error(&format!("Failed to continue rebase: {e}"));
2025                            let _ = abort_rebase(&*executor);
2026                            anyhow::bail!("Rebase failed after conflict resolution")
2027                        }
2028                    }
2029                }
2030                Ok(false) => {
2031                    // AI resolution failed
2032                    logger.error("AI conflict resolution failed, aborting rebase");
2033                    let _ = abort_rebase(&*executor);
2034                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
2035                }
2036                Err(e) => {
2037                    logger.error(&format!("Conflict resolution error: {e}"));
2038                    let _ = abort_rebase(&*executor);
2039                    anyhow::bail!("Rebase conflict resolution failed: {e}")
2040                }
2041            }
2042        }
2043        Err(e) => {
2044            logger.error(&format!("Rebase failed: {e}"));
2045            anyhow::bail!("Rebase failed: {e}")
2046        }
2047    }
2048}
2049
2050/// Run rebase to the default branch.
2051///
2052/// This function performs a rebase from the current branch to the
2053/// default branch (main/master). It handles all edge cases including:
2054/// - Already on main/master (proceeds with rebase attempt)
2055/// - Empty repository (returns `NoOp`)
2056/// - Upstream branch not found (error)
2057/// - Conflicts during rebase (returns `Conflicts` result)
2058///
2059/// # Returns
2060///
2061/// Returns `RebaseResult` indicating the outcome.
2062fn run_rebase_to_default(
2063    logger: &Logger,
2064    colors: Colors,
2065    executor: &dyn ProcessExecutor,
2066) -> std::io::Result<RebaseResult> {
2067    // Get the default branch
2068    let default_branch = get_default_branch()?;
2069    logger.info(&format!(
2070        "Rebasing onto {}{}{}",
2071        colors.cyan(),
2072        default_branch,
2073        colors.reset()
2074    ));
2075
2076    // Perform the rebase
2077    rebase_onto(&default_branch, executor)
2078}
2079
2080/// Run initial rebase before development phase.
2081///
2082/// This function is called before the development phase starts to ensure
2083/// the feature branch is up-to-date with the default branch.
2084///
2085/// Uses a state machine for fault tolerance and automatic recovery from
2086/// interruptions or failures.
2087///
2088/// # Rebase Control
2089///
2090/// Rebase is only performed when both conditions are met:
2091/// - `--with-rebase` CLI flag is set (caller already checked this)
2092/// - `auto_rebase` config is enabled (checked here)
2093fn run_initial_rebase(
2094    ctx: &PipelineContext,
2095    phase_ctx: &mut PhaseContext<'_>,
2096    run_context: &crate::checkpoint::RunContext,
2097    executor: &dyn ProcessExecutor,
2098) -> anyhow::Result<()> {
2099    ctx.logger.header("Pre-development rebase", Colors::cyan);
2100
2101    // Record execution step: pre-rebase started
2102    let step = ExecutionStep::new(
2103        "PreRebase",
2104        0,
2105        "pre_rebase_start",
2106        StepOutcome::success(None, vec![]),
2107    );
2108    phase_ctx.execution_history.add_step(step);
2109
2110    // Save checkpoint at start of pre-rebase phase
2111    if ctx.config.features.checkpoint_enabled {
2112        let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
2113        let mut builder = CheckpointBuilder::new()
2114            .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
2115            .reviewer_pass(0, ctx.config.reviewer_reviews)
2116            .capture_from_context(
2117                &ctx.config,
2118                &ctx.registry,
2119                &ctx.developer_agent,
2120                &ctx.reviewer_agent,
2121                &ctx.logger,
2122                run_context,
2123            )
2124            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2125
2126        // Include prompt history and execution history for hardened resume
2127        builder = builder
2128            .with_execution_history(phase_ctx.execution_history.clone())
2129            .with_prompt_history(phase_ctx.clone_prompt_history());
2130
2131        if let Some(mut checkpoint) = builder.build() {
2132            checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
2133                upstream_branch: default_branch,
2134            };
2135            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2136        }
2137    }
2138
2139    match run_rebase_to_default(&ctx.logger, ctx.colors, &*ctx.executor) {
2140        Ok(RebaseResult::Success) => {
2141            ctx.logger.success("Rebase completed successfully");
2142            // Record execution step: pre-rebase completed successfully
2143            let step = ExecutionStep::new(
2144                "PreRebase",
2145                0,
2146                "pre_rebase_complete",
2147                StepOutcome::success(None, vec![]),
2148            );
2149            phase_ctx.execution_history.add_step(step);
2150
2151            // Save checkpoint after pre-rebase completes successfully
2152            if ctx.config.features.checkpoint_enabled {
2153                let builder = CheckpointBuilder::new()
2154                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2155                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2156                    .skip_rebase(true) // Pre-rebase is done
2157                    .capture_from_context(
2158                        &ctx.config,
2159                        &ctx.registry,
2160                        &ctx.developer_agent,
2161                        &ctx.reviewer_agent,
2162                        &ctx.logger,
2163                        run_context,
2164                    )
2165                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2166                    .with_execution_history(phase_ctx.execution_history.clone())
2167                    .with_prompt_history(phase_ctx.clone_prompt_history());
2168
2169                if let Some(checkpoint) = builder.build() {
2170                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2171                }
2172            }
2173
2174            Ok(())
2175        }
2176        Ok(RebaseResult::NoOp { reason }) => {
2177            ctx.logger.info(&format!("No rebase needed: {reason}"));
2178            // Record execution step: pre-rebase skipped
2179            let step = ExecutionStep::new(
2180                "PreRebase",
2181                0,
2182                "pre_rebase_skipped",
2183                StepOutcome::skipped(reason.clone()),
2184            );
2185            phase_ctx.execution_history.add_step(step);
2186
2187            // Save checkpoint after pre-rebase no-op
2188            if ctx.config.features.checkpoint_enabled {
2189                let builder = CheckpointBuilder::new()
2190                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2191                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2192                    .skip_rebase(true) // Pre-rebase is done
2193                    .capture_from_context(
2194                        &ctx.config,
2195                        &ctx.registry,
2196                        &ctx.developer_agent,
2197                        &ctx.reviewer_agent,
2198                        &ctx.logger,
2199                        run_context,
2200                    )
2201                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2202                    .with_execution_history(phase_ctx.execution_history.clone())
2203                    .with_prompt_history(phase_ctx.clone_prompt_history());
2204
2205                if let Some(checkpoint) = builder.build() {
2206                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2207                }
2208            }
2209
2210            Ok(())
2211        }
2212        Ok(RebaseResult::Conflicts(_conflicts)) => {
2213            // Get the actual conflicted files
2214            let conflicted_files = get_conflicted_files()?;
2215            if conflicted_files.is_empty() {
2216                ctx.logger
2217                    .warn("Rebase reported conflicts but no conflicted files found");
2218                let _ = abort_rebase(executor);
2219                return Ok(());
2220            }
2221
2222            // Record execution step: pre-rebase conflicts detected
2223            let step = ExecutionStep::new(
2224                "PreRebase",
2225                0,
2226                "pre_rebase_conflict",
2227                StepOutcome::partial(
2228                    "Rebase started".to_string(),
2229                    format!("{} conflicts detected", conflicted_files.len()),
2230                ),
2231            );
2232            phase_ctx.execution_history.add_step(step);
2233
2234            // Save checkpoint for conflict state
2235            if ctx.config.features.checkpoint_enabled {
2236                let mut builder = CheckpointBuilder::new()
2237                    .phase(
2238                        PipelinePhase::PreRebaseConflict,
2239                        0,
2240                        ctx.config.developer_iters,
2241                    )
2242                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2243                    .capture_from_context(
2244                        &ctx.config,
2245                        &ctx.registry,
2246                        &ctx.developer_agent,
2247                        &ctx.reviewer_agent,
2248                        &ctx.logger,
2249                        run_context,
2250                    )
2251                    .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2252
2253                // Include prompt history and execution history for hardened resume
2254                builder = builder
2255                    .with_execution_history(phase_ctx.execution_history.clone())
2256                    .with_prompt_history(phase_ctx.clone_prompt_history());
2257
2258                if let Some(mut checkpoint) = builder.build() {
2259                    checkpoint.rebase_state = RebaseState::HasConflicts {
2260                        files: conflicted_files.clone(),
2261                    };
2262                    let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2263                }
2264            }
2265
2266            ctx.logger.warn(&format!(
2267                "Rebase resulted in {} conflict(s), attempting AI resolution",
2268                conflicted_files.len()
2269            ));
2270
2271            // Attempt to resolve conflicts with AI
2272            let resolution_ctx = ConflictResolutionContext {
2273                config: &ctx.config,
2274                template_context: &ctx.template_context,
2275                logger: &ctx.logger,
2276                colors: ctx.colors,
2277                executor_arc: std::sync::Arc::clone(&ctx.executor),
2278                workspace: &*ctx.workspace,
2279            };
2280            match try_resolve_conflicts_with_fallback(
2281                &conflicted_files,
2282                resolution_ctx,
2283                phase_ctx,
2284                "PreRebase",
2285                &*ctx.executor,
2286            ) {
2287                Ok(true) => {
2288                    // Conflicts resolved, continue the rebase
2289                    ctx.logger
2290                        .info("Continuing rebase after conflict resolution");
2291                    match continue_rebase(executor) {
2292                        Ok(()) => {
2293                            ctx.logger
2294                                .success("Rebase completed successfully after AI resolution");
2295                            // Record execution step: conflicts resolved successfully
2296                            let step = ExecutionStep::new(
2297                                "PreRebase",
2298                                0,
2299                                "pre_rebase_resolution",
2300                                StepOutcome::success(None, vec![]),
2301                            );
2302                            phase_ctx.execution_history.add_step(step);
2303
2304                            // Save checkpoint after pre-rebase conflict resolution completes
2305                            if ctx.config.features.checkpoint_enabled {
2306                                let builder = CheckpointBuilder::new()
2307                                    .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2308                                    .reviewer_pass(0, ctx.config.reviewer_reviews)
2309                                    .skip_rebase(true) // Pre-rebase is done
2310                                    .capture_from_context(
2311                                        &ctx.config,
2312                                        &ctx.registry,
2313                                        &ctx.developer_agent,
2314                                        &ctx.reviewer_agent,
2315                                        &ctx.logger,
2316                                        run_context,
2317                                    )
2318                                    .with_executor_from_context(std::sync::Arc::clone(
2319                                        &ctx.executor,
2320                                    ))
2321                                    .with_execution_history(phase_ctx.execution_history.clone())
2322                                    .with_prompt_history(phase_ctx.clone_prompt_history());
2323
2324                                if let Some(checkpoint) = builder.build() {
2325                                    let _ = save_checkpoint_with_workspace(
2326                                        &*ctx.workspace,
2327                                        &checkpoint,
2328                                    );
2329                                }
2330                            }
2331
2332                            Ok(())
2333                        }
2334                        Err(e) => {
2335                            ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
2336                            let _ = abort_rebase(executor);
2337                            // Record execution step: resolution succeeded but continue failed
2338                            let step = ExecutionStep::new(
2339                                "PreRebase",
2340                                0,
2341                                "pre_rebase_resolution",
2342                                StepOutcome::partial(
2343                                    "Conflicts resolved by AI".to_string(),
2344                                    format!("Failed to continue rebase: {e}"),
2345                                ),
2346                            );
2347                            phase_ctx.execution_history.add_step(step);
2348                            Ok(()) // Continue anyway - conflicts were resolved
2349                        }
2350                    }
2351                }
2352                Ok(false) => {
2353                    // AI resolution failed
2354                    ctx.logger
2355                        .warn("AI conflict resolution failed, aborting rebase");
2356                    let _ = abort_rebase(executor);
2357                    // Record execution step: resolution failed
2358                    let step = ExecutionStep::new(
2359                        "PreRebase",
2360                        0,
2361                        "pre_rebase_resolution",
2362                        StepOutcome::failure("AI conflict resolution failed".to_string(), true),
2363                    );
2364                    phase_ctx.execution_history.add_step(step);
2365                    Ok(()) // Continue pipeline - don't block on rebase failure
2366                }
2367                Err(e) => {
2368                    ctx.logger.error(&format!("Conflict resolution error: {e}"));
2369                    let _ = abort_rebase(executor);
2370                    // Record execution step: resolution error
2371                    let step = ExecutionStep::new(
2372                        "PreRebase",
2373                        0,
2374                        "pre_rebase_resolution",
2375                        StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
2376                    );
2377                    phase_ctx.execution_history.add_step(step);
2378                    Ok(()) // Continue pipeline
2379                }
2380            }
2381        }
2382        Ok(RebaseResult::Failed(err)) => {
2383            ctx.logger.error(&format!("Rebase failed: {err}"));
2384            let _ = abort_rebase(&*ctx.executor);
2385            // Record execution step: rebase failed
2386            let step = ExecutionStep::new(
2387                "PreRebase",
2388                0,
2389                "pre_rebase_failed",
2390                StepOutcome::failure(format!("Rebase failed: {err}"), true),
2391            );
2392            phase_ctx.execution_history.add_step(step);
2393            Ok(()) // Continue pipeline despite failure
2394        }
2395        Err(e) => {
2396            ctx.logger
2397                .warn(&format!("Rebase failed, continuing without rebase: {e}"));
2398            // Record execution step: rebase error
2399            let step = ExecutionStep::new(
2400                "PreRebase",
2401                0,
2402                "pre_rebase_error",
2403                StepOutcome::failure(format!("Rebase error: {e}"), true),
2404            );
2405            phase_ctx.execution_history.add_step(step);
2406            Ok(())
2407        }
2408    }
2409}
2410
2411/// Result type for conflict resolution attempts.
2412///
2413/// Represents the different ways conflict resolution can succeed or fail.
2414enum ConflictResolutionResult {
2415    /// Agent provided JSON output with resolved file contents
2416    WithJson(String),
2417    /// Agent resolved conflicts by editing files directly (no JSON output)
2418    FileEditsOnly,
2419    /// Resolution failed completely
2420    Failed,
2421}
2422
2423/// Context for conflict resolution operations.
2424///
2425/// Groups together the configuration and runtime state needed for
2426/// AI-assisted conflict resolution during rebase operations.
2427struct ConflictResolutionContext<'a> {
2428    config: &'a crate::config::Config,
2429    template_context: &'a TemplateContext,
2430    logger: &'a Logger,
2431    colors: Colors,
2432    executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2433    workspace: &'a dyn crate::workspace::Workspace,
2434}
2435
2436/// Attempt to resolve rebase conflicts with AI fallback.
2437///
2438/// This function accepts `PhaseContext` to capture prompts and track
2439/// execution history for hardened resume functionality.
2440fn try_resolve_conflicts_with_fallback(
2441    conflicted_files: &[String],
2442    ctx: ConflictResolutionContext<'_>,
2443    phase_ctx: &mut PhaseContext<'_>,
2444    phase: &str,
2445    executor: &dyn ProcessExecutor,
2446) -> anyhow::Result<bool> {
2447    if conflicted_files.is_empty() {
2448        return Ok(false);
2449    }
2450
2451    ctx.logger.info(&format!(
2452        "Attempting AI conflict resolution for {} file(s)",
2453        conflicted_files.len()
2454    ));
2455
2456    let conflicts = collect_conflict_info_or_error(conflicted_files, ctx.logger)?;
2457
2458    // Use stored_or_generate pattern for hardened resume
2459    // On resume, use the exact same prompt that was used before
2460    let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2461    let (resolution_prompt, was_replayed) =
2462        get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2463            build_resolution_prompt(&conflicts, ctx.template_context)
2464        });
2465
2466    // Capture the resolution prompt for deterministic resume (only if newly generated)
2467    if !was_replayed {
2468        phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2469    } else {
2470        ctx.logger.info(&format!(
2471            "Using stored prompt from checkpoint for determinism: {}",
2472            prompt_key
2473        ));
2474    }
2475
2476    match run_ai_conflict_resolution(
2477        &resolution_prompt,
2478        ctx.config,
2479        ctx.logger,
2480        ctx.colors,
2481        std::sync::Arc::clone(&ctx.executor_arc),
2482        ctx.workspace,
2483    ) {
2484        Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2485            // Agent provided JSON output - attempt to parse and write files
2486            // JSON is optional for verification - LibGit2 state is authoritative
2487            match parse_and_validate_resolved_files(&resolved_content, ctx.logger) {
2488                Ok(resolved_files) => {
2489                    write_resolved_files(&resolved_files, ctx.logger)?;
2490                }
2491                Err(_) => {
2492                    // JSON parsing failed - this is expected and normal
2493                    // We verify conflicts via LibGit2 state, not JSON parsing
2494                    // Continue to LibGit2 verification below
2495                }
2496            }
2497
2498            // Verify all conflicts are resolved via LibGit2 (authoritative source)
2499            let remaining_conflicts = get_conflicted_files()?;
2500            if remaining_conflicts.is_empty() {
2501                Ok(true)
2502            } else {
2503                ctx.logger.warn(&format!(
2504                    "{} conflicts remain after AI resolution",
2505                    remaining_conflicts.len()
2506                ));
2507                Ok(false)
2508            }
2509        }
2510        Ok(ConflictResolutionResult::FileEditsOnly) => {
2511            // Agent resolved conflicts by editing files directly
2512            ctx.logger
2513                .info("Agent resolved conflicts via file edits (no JSON output)");
2514
2515            // Verify all conflicts are resolved
2516            let remaining_conflicts = get_conflicted_files()?;
2517            if remaining_conflicts.is_empty() {
2518                ctx.logger.success("All conflicts resolved via file edits");
2519                Ok(true)
2520            } else {
2521                ctx.logger.warn(&format!(
2522                    "{} conflicts remain after AI resolution",
2523                    remaining_conflicts.len()
2524                ));
2525                Ok(false)
2526            }
2527        }
2528        Ok(ConflictResolutionResult::Failed) => {
2529            ctx.logger.warn("AI conflict resolution failed");
2530            ctx.logger.info("Attempting to continue rebase anyway...");
2531
2532            // Try to continue rebase - user may have manually resolved conflicts
2533            match crate::git_helpers::continue_rebase(executor) {
2534                Ok(()) => {
2535                    ctx.logger.info("Successfully continued rebase");
2536                    Ok(true)
2537                }
2538                Err(rebase_err) => {
2539                    ctx.logger
2540                        .warn(&format!("Failed to continue rebase: {rebase_err}"));
2541                    Ok(false) // Conflicts remain
2542                }
2543            }
2544        }
2545        Err(e) => {
2546            ctx.logger
2547                .warn(&format!("AI conflict resolution failed: {e}"));
2548            ctx.logger.info("Attempting to continue rebase anyway...");
2549
2550            // Try to continue rebase - user may have manually resolved conflicts
2551            match crate::git_helpers::continue_rebase(executor) {
2552                Ok(()) => {
2553                    ctx.logger.info("Successfully continued rebase");
2554                    Ok(true)
2555                }
2556                Err(rebase_err) => {
2557                    ctx.logger
2558                        .warn(&format!("Failed to continue rebase: {rebase_err}"));
2559                    Ok(false) // Conflicts remain
2560                }
2561            }
2562        }
2563    }
2564}
2565
2566/// Wrapper for conflict resolution without PhaseContext.
2567///
2568/// This is used for --rebase-only mode where we don't have a full pipeline context.
2569/// It creates a minimal PhaseContext for the conflict resolution call.
2570fn try_resolve_conflicts_without_phase_ctx(
2571    conflicted_files: &[String],
2572    config: &crate::config::Config,
2573    template_context: &TemplateContext,
2574    logger: &Logger,
2575    colors: Colors,
2576    executor: std::sync::Arc<dyn ProcessExecutor>,
2577    repo_root: &std::path::Path,
2578) -> anyhow::Result<bool> {
2579    use crate::agents::AgentRegistry;
2580    use crate::checkpoint::execution_history::ExecutionHistory;
2581    use crate::checkpoint::RunContext;
2582    use crate::pipeline::{Stats, Timer};
2583
2584    // Create minimal PhaseContext for conflict resolution
2585    let registry = AgentRegistry::new()?;
2586    let mut timer = Timer::new();
2587    let mut stats = Stats::default();
2588    let workspace = crate::workspace::WorkspaceFs::new(repo_root.to_path_buf());
2589
2590    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2591    let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2592
2593    // Clone the executor Arc for use in PhaseContext and ConflictResolutionContext
2594    let executor_arc = std::sync::Arc::clone(&executor);
2595
2596    let mut phase_ctx = PhaseContext {
2597        config,
2598        registry: &registry,
2599        logger,
2600        colors: &colors,
2601        timer: &mut timer,
2602        stats: &mut stats,
2603        developer_agent,
2604        reviewer_agent,
2605        review_guidelines: None,
2606        template_context,
2607        run_context: RunContext::new(),
2608        execution_history: ExecutionHistory::new(),
2609        prompt_history: std::collections::HashMap::new(),
2610        executor: &*executor,
2611        executor_arc: std::sync::Arc::clone(&executor_arc),
2612        repo_root,
2613        workspace: &workspace,
2614    };
2615
2616    let ctx = ConflictResolutionContext {
2617        config,
2618        template_context,
2619        logger,
2620        colors,
2621        executor_arc,
2622        workspace: &workspace,
2623    };
2624
2625    try_resolve_conflicts_with_fallback(
2626        conflicted_files,
2627        ctx,
2628        &mut phase_ctx,
2629        "RebaseOnly",
2630        &*executor,
2631    )
2632}
2633
2634/// Collect conflict information from conflicted files.
2635fn collect_conflict_info_or_error(
2636    conflicted_files: &[String],
2637    logger: &Logger,
2638) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2639    use crate::prompts::collect_conflict_info;
2640
2641    let conflicts = match collect_conflict_info(conflicted_files) {
2642        Ok(c) => c,
2643        Err(e) => {
2644            logger.error(&format!("Failed to collect conflict info: {e}"));
2645            anyhow::bail!("Failed to collect conflict info");
2646        }
2647    };
2648    Ok(conflicts)
2649}
2650
2651/// Build the conflict resolution prompt from context files.
2652fn build_resolution_prompt(
2653    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2654    template_context: &TemplateContext,
2655) -> String {
2656    build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2657        .unwrap_or_else(|_| String::new())
2658}
2659
2660/// Build the conflict resolution prompt.
2661///
2662/// This function uses the standard conflict resolution prompt.
2663fn build_enhanced_resolution_prompt(
2664    conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2665    _branch_info: Option<()>, // Kept for API compatibility, currently unused
2666    template_context: &TemplateContext,
2667) -> anyhow::Result<String> {
2668    use std::fs;
2669
2670    let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2671    let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2672
2673    // Use standard prompt
2674    Ok(
2675        crate::prompts::build_conflict_resolution_prompt_with_context(
2676            template_context,
2677            conflicts,
2678            prompt_md_content.as_deref(),
2679            plan_content.as_deref(),
2680        ),
2681    )
2682}
2683
2684/// Run AI agent to resolve conflicts with fallback mechanism.
2685///
2686/// Returns `ConflictResolutionResult` indicating whether the agent provided
2687/// JSON output, resolved conflicts via file edits, or failed completely.
2688fn run_ai_conflict_resolution(
2689    resolution_prompt: &str,
2690    config: &crate::config::Config,
2691    logger: &Logger,
2692    colors: Colors,
2693    executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2694    workspace: &dyn crate::workspace::Workspace,
2695) -> anyhow::Result<ConflictResolutionResult> {
2696    use crate::agents::AgentRegistry;
2697    use crate::files::result_extraction::extract_last_result;
2698    use crate::pipeline::{
2699        run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2700    };
2701    use std::io;
2702    use std::path::Path;
2703
2704    // Note: log_dir is used as a prefix for log file names, not as a directory.
2705    // The actual log files will be created in .agent/logs/ with names like:
2706    // .agent/logs/rebase_conflict_resolution_ccs-glm_0.log
2707    let log_dir = ".agent/logs/rebase_conflict_resolution";
2708
2709    let registry = AgentRegistry::new()?;
2710    let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2711
2712    // Use the injected executor for the runtime
2713    let executor_ref: &dyn crate::executor::ProcessExecutor = &*executor_arc;
2714    let mut runtime = PipelineRuntime {
2715        timer: &mut crate::pipeline::Timer::new(),
2716        logger,
2717        colors: &colors,
2718        config,
2719        executor: executor_ref,
2720        executor_arc: std::sync::Arc::clone(&executor_arc),
2721        workspace,
2722    };
2723
2724    // Output validator: checks if agent produced valid output OR resolved conflicts
2725    // Agents may edit files without returning JSON, so we verify conflicts are resolved.
2726    let validate_output: OutputValidator = |ws: &dyn crate::workspace::Workspace,
2727                                            log_dir_path: &Path,
2728                                            validation_logger: &crate::logger::Logger|
2729     -> io::Result<bool> {
2730        match extract_last_result(ws, log_dir_path) {
2731            Ok(Some(_)) => {
2732                // Valid JSON output exists
2733                Ok(true)
2734            }
2735            Ok(None) => {
2736                // No JSON output - check if conflicts were resolved anyway
2737                // (agent may have edited files without returning JSON)
2738                match crate::git_helpers::get_conflicted_files() {
2739                    Ok(conflicts) if conflicts.is_empty() => {
2740                        validation_logger
2741                            .info("Agent resolved conflicts without JSON output (file edits only)");
2742                        Ok(true) // Conflicts resolved, consider success
2743                    }
2744                    Ok(conflicts) => {
2745                        validation_logger.warn(&format!(
2746                            "{} conflict(s) remain unresolved",
2747                            conflicts.len()
2748                        ));
2749                        Ok(false) // Conflicts remain
2750                    }
2751                    Err(e) => {
2752                        validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2753                        Ok(false) // Error checking conflicts
2754                    }
2755                }
2756            }
2757            Err(e) => {
2758                validation_logger.warn(&format!("Output validation check failed: {e}"));
2759                Ok(false) // Treat validation errors as missing output
2760            }
2761        }
2762    };
2763
2764    let mut fallback_config = FallbackConfig {
2765        role: crate::agents::AgentRole::Reviewer,
2766        base_label: "conflict resolution",
2767        prompt: resolution_prompt,
2768        logfile_prefix: log_dir,
2769        runtime: &mut runtime,
2770        registry: &registry,
2771        primary_agent: reviewer_agent,
2772        output_validator: Some(validate_output),
2773        workspace,
2774    };
2775
2776    let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2777
2778    if exit_code != 0 {
2779        return Ok(ConflictResolutionResult::Failed);
2780    }
2781
2782    // Check if conflicts are resolved after agent run
2783    // The validator already checked this, but we verify again to determine the result type
2784    let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2785
2786    if remaining_conflicts.is_empty() {
2787        // Conflicts are resolved - check if agent provided JSON output
2788        match extract_last_result(workspace, Path::new(log_dir)) {
2789            Ok(Some(content)) => {
2790                logger.info("Agent provided JSON output with resolved files");
2791                Ok(ConflictResolutionResult::WithJson(content))
2792            }
2793            Ok(None) => {
2794                logger.info("Agent resolved conflicts via file edits (no JSON output)");
2795                Ok(ConflictResolutionResult::FileEditsOnly)
2796            }
2797            Err(e) => {
2798                // Extraction failed but conflicts are resolved - treat as file edits only
2799                logger.warn(&format!(
2800                    "Failed to extract JSON output but conflicts are resolved: {e}"
2801                ));
2802                Ok(ConflictResolutionResult::FileEditsOnly)
2803            }
2804        }
2805    } else {
2806        logger.warn(&format!(
2807            "{} conflict(s) remain after agent attempted resolution",
2808            remaining_conflicts.len()
2809        ));
2810        Ok(ConflictResolutionResult::Failed)
2811    }
2812}
2813
2814/// Parse and validate the resolved files from AI output.
2815///
2816/// JSON parsing failures are expected and handled gracefully - LibGit2 state
2817/// is used for verification, not JSON output. This function only parses the
2818/// JSON to write resolved files if available.
2819fn parse_and_validate_resolved_files(
2820    resolved_content: &str,
2821    logger: &Logger,
2822) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2823    let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2824        // Agent did not provide JSON output - fall back to LibGit2 verification
2825        // This is expected and normal, not an error condition
2826        anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2827    })?;
2828
2829    let resolved_files = match json.get("resolved_files") {
2830        Some(v) if v.is_object() => v.as_object().unwrap(),
2831        _ => {
2832            logger.info("Agent output missing 'resolved_files' object");
2833            anyhow::bail!("Agent output missing 'resolved_files' object");
2834        }
2835    };
2836
2837    if resolved_files.is_empty() {
2838        logger.info("No resolved files in JSON output");
2839        anyhow::bail!("No files were resolved by the agent");
2840    }
2841
2842    Ok(resolved_files.clone())
2843}
2844
2845/// Write resolved files to disk and stage them.
2846fn write_resolved_files(
2847    resolved_files: &serde_json::Map<String, serde_json::Value>,
2848    logger: &Logger,
2849) -> anyhow::Result<usize> {
2850    use std::fs;
2851
2852    let mut files_written = 0;
2853    for (path, content) in resolved_files {
2854        if let Some(content_str) = content.as_str() {
2855            fs::write(path, content_str).map_err(|e| {
2856                logger.error(&format!("Failed to write {path}: {e}"));
2857                anyhow::anyhow!("Failed to write {path}: {e}")
2858            })?;
2859            logger.info(&format!("Resolved and wrote: {path}"));
2860            files_written += 1;
2861            // Stage the resolved file
2862            if let Err(e) = crate::git_helpers::git_add_all() {
2863                logger.warn(&format!("Failed to stage {path}: {e}"));
2864            }
2865        }
2866    }
2867
2868    logger.success(&format!("Successfully resolved {files_written} file(s)"));
2869    Ok(files_written)
2870}