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;
30mod rebase;
31pub mod resume;
32pub mod validation;
33
34use crate::agents::AgentRegistry;
35use crate::app::finalization::finalize_pipeline;
36use crate::banner::print_welcome_banner;
37use crate::checkpoint::{
38    save_checkpoint_with_workspace, CheckpointBuilder, PipelineCheckpoint, PipelinePhase,
39};
40use crate::cli::{
41    create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
42    handle_list_available_agents, handle_list_providers, handle_show_baseline,
43    handle_template_commands, prompt_template_selection, Args,
44};
45
46use crate::executor::ProcessExecutor;
47use crate::files::protection::monitoring::PromptMonitor;
48use crate::files::{
49    create_prompt_backup_with_workspace, make_prompt_read_only_with_workspace,
50    update_status_with_workspace, validate_prompt_md_with_workspace,
51};
52use crate::git_helpers::{
53    abort_rebase, continue_rebase, get_conflicted_files, is_main_or_master_branch,
54    reset_start_commit, RebaseResult,
55};
56#[cfg(not(feature = "test-utils"))]
57use crate::git_helpers::{
58    cleanup_orphaned_marker, get_start_commit_summary, save_start_commit, start_agent_phase,
59};
60use crate::logger::Colors;
61use crate::logger::Logger;
62use crate::phases::PhaseContext;
63use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
64use crate::prompts::template_context::TemplateContext;
65
66use config_init::initialize_config;
67use context::PipelineContext;
68use detection::detect_project_stack;
69use plumbing::handle_generate_commit_msg;
70use rebase::{run_initial_rebase, run_rebase_to_default, try_resolve_conflicts_without_phase_ctx};
71use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
72use validation::{
73    resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
74};
75
76fn discover_repo_root_for_workspace<H: effect::AppEffectHandler>(
77    override_dir: Option<&std::path::Path>,
78    handler: &mut H,
79) -> anyhow::Result<std::path::PathBuf> {
80    use effect::{AppEffect, AppEffectResult};
81
82    if let Some(dir) = override_dir {
83        match handler.execute(AppEffect::SetCurrentDir {
84            path: dir.to_path_buf(),
85        }) {
86            AppEffectResult::Ok => {}
87            AppEffectResult::Error(e) => anyhow::bail!(e),
88            other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
89        }
90    }
91
92    match handler.execute(AppEffect::GitRequireRepo) {
93        AppEffectResult::Ok => {}
94        AppEffectResult::Error(e) => anyhow::bail!("Not in a git repository: {e}"),
95        other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
96    }
97
98    match handler.execute(AppEffect::GitGetRepoRoot) {
99        AppEffectResult::Path(p) => Ok(p),
100        AppEffectResult::Error(e) => anyhow::bail!("Failed to get repo root: {e}"),
101        other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
102    }
103}
104
105/// Main application entry point.
106///
107/// Orchestrates the entire Ralph pipeline:
108/// 1. Configuration initialization
109/// 2. Agent validation
110/// 3. Plumbing commands (if requested)
111/// 4. Development phase
112/// 5. Review & fix phase
113/// 6. Final validation
114/// 7. Commit phase
115///
116/// # Arguments
117///
118/// * `args` - The parsed CLI arguments
119/// * `executor` - Process executor for external process execution
120///
121/// # Returns
122///
123/// Returns `Ok(())` on success or an error if any phase fails.
124pub fn run(args: Args, executor: std::sync::Arc<dyn ProcessExecutor>) -> anyhow::Result<()> {
125    let colors = Colors::new();
126    let logger = Logger::new(colors);
127
128    // Set working directory first if override is provided
129    // This ensures all subsequent operations (including config init) use the correct directory
130    if let Some(ref override_dir) = args.working_dir_override {
131        std::env::set_current_dir(override_dir)?;
132    }
133
134    // Initialize configuration and agent registry
135    let Some(init_result) = initialize_config(&args, colors, &logger)? else {
136        return Ok(()); // Early exit (--init/--init-global/--init-legacy)
137    };
138
139    let config_init::ConfigInitResult {
140        config,
141        registry,
142        config_path,
143        config_sources,
144    } = init_result;
145
146    // Resolve required agent names
147    let validated = resolve_required_agents(&config)?;
148    let developer_agent = validated.developer_agent;
149    let reviewer_agent = validated.reviewer_agent;
150
151    // Handle listing commands (these can run without git repo)
152    if handle_listing_commands(&args, &registry, colors) {
153        return Ok(());
154    }
155
156    // Handle --diagnose
157    if args.recovery.diagnose {
158        handle_diagnose(
159            colors,
160            &config,
161            &registry,
162            &config_path,
163            &config_sources,
164            &*executor,
165        );
166        return Ok(());
167    }
168
169    // Validate agent chains
170    validate_agent_chains(&registry, colors);
171
172    // Create effect handler for production operations
173    let mut handler = effect_handler::RealAppEffectHandler::new();
174
175    // Get repo root early for workspace creation (needed by plumbing commands)
176    // This uses the same logic as setup_working_dir_via_handler but captures the repo_root.
177    let early_repo_root =
178        discover_repo_root_for_workspace(args.working_dir_override.as_deref(), &mut handler)?;
179
180    // Create workspace for plumbing commands (and later for the full pipeline)
181    let workspace: std::sync::Arc<dyn crate::workspace::Workspace> =
182        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(early_repo_root));
183
184    // Handle plumbing commands with workspace support
185    if handle_plumbing_commands(
186        &args,
187        &logger,
188        colors,
189        &mut handler,
190        Some(workspace.as_ref()),
191    )? {
192        return Ok(());
193    }
194
195    // Validate agents and set up git repo and PROMPT.md
196    // Note: repo_root is discovered again here (same as early_repo_root) but also
197    // does additional setup like PROMPT.md creation that plumbing commands don't need
198    let Some(repo_root) = validate_and_setup_agents(
199        AgentSetupParams {
200            config: &config,
201            registry: &registry,
202            developer_agent: &developer_agent,
203            reviewer_agent: &reviewer_agent,
204            config_path: &config_path,
205            colors,
206            logger: &logger,
207            working_dir_override: args.working_dir_override.as_deref(),
208        },
209        &mut handler,
210    )?
211    else {
212        return Ok(());
213    };
214
215    // Prepare pipeline context or exit early
216    // Note: Reuse workspace created earlier (same repo root)
217    (prepare_pipeline_or_exit(PipelinePreparationParams {
218        args,
219        config,
220        registry,
221        developer_agent,
222        reviewer_agent,
223        repo_root,
224        logger,
225        colors,
226        executor,
227        handler: &mut handler,
228        workspace,
229    })?)
230    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::app::effect::{AppEffect, AppEffectHandler, AppEffectResult};
237
238    #[derive(Debug)]
239    struct TestRepoRootHandler {
240        captured: Vec<AppEffect>,
241        repo_root: std::path::PathBuf,
242    }
243
244    impl TestRepoRootHandler {
245        fn new(repo_root: std::path::PathBuf) -> Self {
246            Self {
247                captured: Vec::new(),
248                repo_root,
249            }
250        }
251    }
252
253    impl AppEffectHandler for TestRepoRootHandler {
254        fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
255            self.captured.push(effect.clone());
256            match effect {
257                AppEffect::SetCurrentDir { .. } => AppEffectResult::Ok,
258                AppEffect::GitRequireRepo => AppEffectResult::Ok,
259                AppEffect::GitGetRepoRoot => AppEffectResult::Path(self.repo_root.clone()),
260                other => panic!("unexpected effect in test handler: {other:?}"),
261            }
262        }
263    }
264
265    #[test]
266    fn discover_repo_root_for_workspace_prefers_git_repo_root_over_override_dir() {
267        let override_dir = std::path::PathBuf::from("/override/subdir");
268        let repo_root = std::path::PathBuf::from("/repo");
269        let mut handler = TestRepoRootHandler::new(repo_root.clone());
270
271        let got = discover_repo_root_for_workspace(Some(&override_dir), &mut handler).unwrap();
272        assert_eq!(got, repo_root);
273
274        assert!(matches!(
275            handler.captured.get(0),
276            Some(AppEffect::SetCurrentDir { .. })
277        ));
278        assert!(handler
279            .captured
280            .iter()
281            .any(|e| matches!(e, AppEffect::GitRequireRepo)));
282        assert!(handler
283            .captured
284            .iter()
285            .any(|e| matches!(e, AppEffect::GitGetRepoRoot)));
286    }
287}
288
289/// Test-only entry point that accepts a pre-built Config.
290///
291/// This function is for integration testing only. It bypasses environment variable
292/// loading and uses the provided Config directly, enabling deterministic tests
293/// that don't rely on process-global state.
294///
295/// This function handles ALL commands including early-exit commands (--init, --diagnose,
296/// --reset-start-commit, etc.) so that tests can use a single entry point.
297///
298/// # Arguments
299///
300/// * `args` - The parsed CLI arguments
301/// * `executor` - Process executor for external process execution  
302/// * `config` - Pre-built configuration (bypasses env var loading)
303/// * `registry` - Pre-built agent registry
304///
305/// # Returns
306///
307/// Returns `Ok(())` on success or an error if any phase fails.
308#[cfg(feature = "test-utils")]
309pub fn run_with_config(
310    args: Args,
311    executor: std::sync::Arc<dyn ProcessExecutor>,
312    config: crate::config::Config,
313    registry: AgentRegistry,
314) -> anyhow::Result<()> {
315    // Use real path resolver and effect handler by default for backward compatibility
316    let mut handler = effect_handler::RealAppEffectHandler::new();
317    run_with_config_and_resolver(
318        args,
319        executor,
320        config,
321        registry,
322        &crate::config::RealConfigEnvironment,
323        &mut handler,
324        None, // Use default WorkspaceFs
325    )
326}
327
328/// Test-only entry point that accepts a pre-built Config and a custom path resolver.
329///
330/// This function is for integration testing only. It bypasses environment variable
331/// loading and uses the provided Config and path resolver directly, enabling
332/// deterministic tests that don't rely on process-global state or env vars.
333///
334/// This function handles ALL commands including early-exit commands (--init, --diagnose,
335/// --reset-start-commit, etc.) so that tests can use a single entry point.
336///
337/// # Arguments
338///
339/// * `args` - The parsed CLI arguments
340/// * `executor` - Process executor for external process execution
341/// * `config` - Pre-built configuration (bypasses env var loading)
342/// * `registry` - Pre-built agent registry
343/// * `path_resolver` - Custom path resolver for init commands
344/// * `handler` - Effect handler for git/filesystem operations
345/// * `workspace` - Optional workspace for file operations (if `None`, uses `WorkspaceFs`)
346///
347/// # Returns
348///
349/// Returns `Ok(())` on success or an error if any phase fails.
350#[cfg(feature = "test-utils")]
351pub fn run_with_config_and_resolver<
352    P: crate::config::ConfigEnvironment,
353    H: effect::AppEffectHandler,
354>(
355    args: Args,
356    executor: std::sync::Arc<dyn ProcessExecutor>,
357    config: crate::config::Config,
358    registry: AgentRegistry,
359    path_resolver: &P,
360    handler: &mut H,
361    workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
362) -> anyhow::Result<()> {
363    use crate::cli::{
364        handle_extended_help, handle_init_global_with, handle_init_prompt_with,
365        handle_list_work_guides, handle_smart_init_with,
366    };
367
368    let colors = Colors::new();
369    let logger = Logger::new(colors);
370
371    // Set working directory first if override is provided
372    if let Some(ref override_dir) = args.working_dir_override {
373        std::env::set_current_dir(override_dir)?;
374    }
375
376    // Handle --extended-help / --man flag: display extended help and exit.
377    if args.recovery.extended_help {
378        handle_extended_help();
379        if args.work_guide_list.list_work_guides {
380            println!();
381            handle_list_work_guides(colors);
382        }
383        return Ok(());
384    }
385
386    // Handle --list-work-guides / --list-templates flag
387    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
388        return Ok(());
389    }
390
391    // Handle --init-prompt flag: create PROMPT.md from template and exit
392    if let Some(ref template_name) = args.init_prompt {
393        if handle_init_prompt_with(
394            template_name,
395            args.unified_init.force_init,
396            colors,
397            path_resolver,
398        )? {
399            return Ok(());
400        }
401    }
402
403    // Handle smart --init flag: intelligently determine what to initialize
404    if args.unified_init.init.is_some()
405        && handle_smart_init_with(
406            args.unified_init.init.as_deref(),
407            args.unified_init.force_init,
408            colors,
409            path_resolver,
410        )?
411    {
412        return Ok(());
413    }
414
415    // Handle --init-config flag: explicit config creation and exit
416    if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
417        return Ok(());
418    }
419
420    // Handle --init-global flag: create unified config if it doesn't exist and exit
421    if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
422        return Ok(());
423    }
424
425    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
426    if args.legacy_init.init_legacy {
427        let repo_root = match handler.execute(effect::AppEffect::GitGetRepoRoot) {
428            effect::AppEffectResult::Path(p) => Some(p),
429            _ => None,
430        };
431        let legacy_path = repo_root.map_or_else(
432            || std::path::PathBuf::from(".agent/agents.toml"),
433            |root| root.join(".agent/agents.toml"),
434        );
435        if crate::cli::handle_init_legacy(colors, &legacy_path)? {
436            return Ok(());
437        }
438    }
439
440    // Use provided config directly (no env var loading)
441    let config_path = std::path::PathBuf::from("test-config");
442
443    // Resolve required agent names
444    let validated = resolve_required_agents(&config)?;
445    let developer_agent = validated.developer_agent;
446    let reviewer_agent = validated.reviewer_agent;
447
448    // Handle listing commands (these can run without git repo)
449    if handle_listing_commands(&args, &registry, colors) {
450        return Ok(());
451    }
452
453    // Handle --diagnose
454    if args.recovery.diagnose {
455        handle_diagnose(colors, &config, &registry, &config_path, &[], &*executor);
456        return Ok(());
457    }
458
459    // Handle plumbing commands (--reset-start-commit, --show-commit-msg, etc.)
460    // Pass workspace reference for testability with MemoryWorkspace
461    if handle_plumbing_commands(
462        &args,
463        &logger,
464        colors,
465        handler,
466        workspace.as_ref().map(|w| w.as_ref()),
467    )? {
468        return Ok(());
469    }
470
471    // Validate agents and set up git repo and PROMPT.md
472    let Some(repo_root) = validate_and_setup_agents(
473        AgentSetupParams {
474            config: &config,
475            registry: &registry,
476            developer_agent: &developer_agent,
477            reviewer_agent: &reviewer_agent,
478            config_path: &config_path,
479            colors,
480            logger: &logger,
481            working_dir_override: args.working_dir_override.as_deref(),
482        },
483        handler,
484    )?
485    else {
486        return Ok(());
487    };
488
489    // Create workspace for explicit path resolution, or use injected workspace
490    let workspace = workspace.unwrap_or_else(|| {
491        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
492    });
493
494    // Prepare pipeline context or exit early
495    (prepare_pipeline_or_exit(PipelinePreparationParams {
496        args,
497        config,
498        registry,
499        developer_agent,
500        reviewer_agent,
501        repo_root,
502        logger,
503        colors,
504        executor,
505        handler,
506        workspace,
507    })?)
508    .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
509}
510
511/// Parameters for `run_with_config_and_handlers`.
512///
513/// Groups related parameters to reduce function argument count.
514#[cfg(feature = "test-utils")]
515pub struct RunWithHandlersParams<'a, 'ctx, P, A, E>
516where
517    P: crate::config::ConfigEnvironment,
518    A: effect::AppEffectHandler,
519    E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
520{
521    pub args: Args,
522    pub executor: std::sync::Arc<dyn ProcessExecutor>,
523    pub config: crate::config::Config,
524    pub registry: AgentRegistry,
525    pub path_resolver: &'a P,
526    pub app_handler: &'a mut A,
527    pub effect_handler: &'a mut E,
528    pub workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
529    /// Phantom data to bind the `'ctx` lifetime from `EffectHandler<'ctx>`.
530    pub _marker: std::marker::PhantomData<&'ctx ()>,
531}
532
533/// Run with both AppEffectHandler AND EffectHandler for full isolation.
534///
535/// This function is the ultimate test entry point that allows injecting BOTH:
536/// - `AppEffectHandler` for CLI-layer operations (git require repo, set cwd, etc.)
537/// - `EffectHandler` for reducer-layer operations (create commit, run rebase, etc.)
538///
539/// Using both handlers ensures tests make ZERO real git calls at any layer.
540///
541/// # Example
542///
543/// ```ignore
544/// use ralph_workflow::app::mock_effect_handler::MockAppEffectHandler;
545/// use ralph_workflow::reducer::mock_effect_handler::MockEffectHandler;
546///
547/// let mut app_handler = MockAppEffectHandler::new().with_head_oid("abc123");
548/// let mut effect_handler = MockEffectHandler::new(PipelineState::initial(1, 0));
549///
550/// run_with_config_and_handlers(RunWithHandlersParams {
551///     args, executor, config, registry, path_resolver: &env,
552///     app_handler: &mut app_handler, effect_handler: &mut effect_handler,
553///     workspace: None,
554/// })?;
555///
556/// // Verify no real git operations at either layer
557/// assert!(app_handler.captured().iter().any(|e| matches!(e, AppEffect::GitRequireRepo)));
558/// assert!(effect_handler.captured_effects().iter().any(|e| matches!(e, Effect::CreateCommit { .. })));
559/// ```
560#[cfg(feature = "test-utils")]
561pub fn run_with_config_and_handlers<'a, 'ctx, P, A, E>(
562    params: RunWithHandlersParams<'a, 'ctx, P, A, E>,
563) -> anyhow::Result<()>
564where
565    P: crate::config::ConfigEnvironment,
566    A: effect::AppEffectHandler,
567    E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
568{
569    let RunWithHandlersParams {
570        args,
571        executor,
572        config,
573        registry,
574        path_resolver,
575        app_handler,
576        effect_handler,
577        workspace,
578        ..
579    } = params;
580    use crate::cli::{
581        handle_extended_help, handle_init_global_with, handle_init_prompt_with,
582        handle_list_work_guides, handle_smart_init_with,
583    };
584
585    let colors = Colors::new();
586    let logger = Logger::new(colors);
587
588    // Set working directory first if override is provided
589    if let Some(ref override_dir) = args.working_dir_override {
590        std::env::set_current_dir(override_dir)?;
591    }
592
593    // Handle --extended-help / --man flag
594    if args.recovery.extended_help {
595        handle_extended_help();
596        if args.work_guide_list.list_work_guides {
597            println!();
598            handle_list_work_guides(colors);
599        }
600        return Ok(());
601    }
602
603    // Handle --list-work-guides / --list-templates flag
604    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
605        return Ok(());
606    }
607
608    // Handle --init-prompt flag
609    if let Some(ref template_name) = args.init_prompt {
610        if handle_init_prompt_with(
611            template_name,
612            args.unified_init.force_init,
613            colors,
614            path_resolver,
615        )? {
616            return Ok(());
617        }
618    }
619
620    // Handle smart --init flag
621    if args.unified_init.init.is_some()
622        && handle_smart_init_with(
623            args.unified_init.init.as_deref(),
624            args.unified_init.force_init,
625            colors,
626            path_resolver,
627        )?
628    {
629        return Ok(());
630    }
631
632    // Handle --init-config flag
633    if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
634        return Ok(());
635    }
636
637    // Handle --init-global flag
638    if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
639        return Ok(());
640    }
641
642    // Handle --init-legacy flag
643    if args.legacy_init.init_legacy {
644        let repo_root = match app_handler.execute(effect::AppEffect::GitGetRepoRoot) {
645            effect::AppEffectResult::Path(p) => Some(p),
646            _ => None,
647        };
648        let legacy_path = repo_root.map_or_else(
649            || std::path::PathBuf::from(".agent/agents.toml"),
650            |root| root.join(".agent/agents.toml"),
651        );
652        if crate::cli::handle_init_legacy(colors, &legacy_path)? {
653            return Ok(());
654        }
655    }
656
657    // Use provided config directly
658    let config_path = std::path::PathBuf::from("test-config");
659
660    // Resolve required agent names
661    let validated = resolve_required_agents(&config)?;
662    let developer_agent = validated.developer_agent;
663    let reviewer_agent = validated.reviewer_agent;
664
665    // Handle listing commands
666    if handle_listing_commands(&args, &registry, colors) {
667        return Ok(());
668    }
669
670    // Handle --diagnose
671    if args.recovery.diagnose {
672        handle_diagnose(colors, &config, &registry, &config_path, &[], &*executor);
673        return Ok(());
674    }
675
676    // Handle plumbing commands with app_handler
677    // Pass workspace reference for testability with MemoryWorkspace
678    if handle_plumbing_commands(
679        &args,
680        &logger,
681        colors,
682        app_handler,
683        workspace.as_ref().map(|w| w.as_ref()),
684    )? {
685        return Ok(());
686    }
687
688    // Validate agents and set up git repo with app_handler
689    let Some(repo_root) = validate_and_setup_agents(
690        AgentSetupParams {
691            config: &config,
692            registry: &registry,
693            developer_agent: &developer_agent,
694            reviewer_agent: &reviewer_agent,
695            config_path: &config_path,
696            colors,
697            logger: &logger,
698            working_dir_override: args.working_dir_override.as_deref(),
699        },
700        app_handler,
701    )?
702    else {
703        return Ok(());
704    };
705
706    // Create workspace for explicit path resolution, or use injected workspace
707    let workspace = workspace.unwrap_or_else(|| {
708        std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
709    });
710
711    // Prepare pipeline context or exit early
712    let ctx = prepare_pipeline_or_exit(PipelinePreparationParams {
713        args,
714        config,
715        registry,
716        developer_agent,
717        reviewer_agent,
718        repo_root,
719        logger,
720        colors,
721        executor,
722        handler: app_handler,
723        workspace,
724    })?;
725
726    // Run pipeline with the injected effect_handler
727    match ctx {
728        Some(ctx) => run_pipeline_with_effect_handler(&ctx, effect_handler),
729        None => Ok(()),
730    }
731}
732
733/// Handles listing commands that don't require the full pipeline.
734///
735/// Returns `true` if a listing command was handled and we should exit.
736fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
737    if args.agent_list.list_agents {
738        handle_list_agents(registry);
739        return true;
740    }
741    if args.agent_list.list_available_agents {
742        handle_list_available_agents(registry);
743        return true;
744    }
745    if args.provider_list.list_providers {
746        handle_list_providers(colors);
747        return true;
748    }
749
750    // Handle template commands
751    let template_cmds = &args.template_commands;
752    if template_cmds.init_templates_enabled()
753        || template_cmds.validate
754        || template_cmds.show.is_some()
755        || template_cmds.list
756        || template_cmds.list_all
757        || template_cmds.variables.is_some()
758        || template_cmds.render.is_some()
759    {
760        let _ = handle_template_commands(template_cmds, colors);
761        return true;
762    }
763
764    false
765}
766
767/// Handles plumbing commands that require git repo but not full validation.
768///
769/// Returns `Ok(true)` if a plumbing command was handled and we should exit.
770/// Returns `Ok(false)` if we should continue to the main pipeline.
771///
772/// # Workspace Support
773///
774/// When `workspace` is `Some`, the workspace-aware versions of plumbing commands
775/// are used, enabling testing with `MemoryWorkspace`. When `None`, the direct
776/// filesystem versions are used (production behavior).
777fn handle_plumbing_commands<H: effect::AppEffectHandler>(
778    args: &Args,
779    logger: &Logger,
780    colors: Colors,
781    handler: &mut H,
782    workspace: Option<&dyn crate::workspace::Workspace>,
783) -> anyhow::Result<bool> {
784    use plumbing::{handle_apply_commit_with_handler, handle_show_commit_msg_with_workspace};
785
786    // Helper to set up working directory for plumbing commands using the effect handler
787    fn setup_working_dir_via_handler<H: effect::AppEffectHandler>(
788        override_dir: Option<&std::path::Path>,
789        handler: &mut H,
790    ) -> anyhow::Result<()> {
791        use effect::{AppEffect, AppEffectResult};
792
793        if let Some(dir) = override_dir {
794            match handler.execute(AppEffect::SetCurrentDir {
795                path: dir.to_path_buf(),
796            }) {
797                AppEffectResult::Ok => Ok(()),
798                AppEffectResult::Error(e) => anyhow::bail!(e),
799                other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
800            }
801        } else {
802            // Require git repo
803            match handler.execute(AppEffect::GitRequireRepo) {
804                AppEffectResult::Ok => {}
805                AppEffectResult::Error(e) => anyhow::bail!(e),
806                other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
807            }
808            // Get repo root
809            let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
810                AppEffectResult::Path(p) => p,
811                AppEffectResult::Error(e) => anyhow::bail!(e),
812                other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
813            };
814            // Set current dir to repo root
815            match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
816                AppEffectResult::Ok => Ok(()),
817                AppEffectResult::Error(e) => anyhow::bail!(e),
818                other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
819            }
820        }
821    }
822
823    // Show commit message
824    if args.commit_display.show_commit_msg {
825        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
826        let ws = workspace.ok_or_else(|| {
827            anyhow::anyhow!(
828                "--show-commit-msg requires workspace context. Run this command after the pipeline has initialized."
829            )
830        })?;
831        return handle_show_commit_msg_with_workspace(ws).map(|()| true);
832    }
833
834    // Apply commit
835    if args.commit_plumbing.apply_commit {
836        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
837        let ws = workspace.ok_or_else(|| {
838            anyhow::anyhow!(
839                "--apply-commit requires workspace context. Run this command after the pipeline has initialized."
840            )
841        })?;
842        return handle_apply_commit_with_handler(ws, handler, logger, colors).map(|()| true);
843    }
844
845    // Reset start commit
846    if args.commit_display.reset_start_commit {
847        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
848
849        // Use the effect handler for reset_start_commit
850        return match handler.execute(effect::AppEffect::GitResetStartCommit) {
851            effect::AppEffectResult::String(oid) => {
852                // Simple case - just got the OID back
853                let short_oid = &oid[..8.min(oid.len())];
854                logger.success(&format!("Starting commit reference reset ({})", short_oid));
855                logger.info(".agent/start_commit has been updated");
856                Ok(true)
857            }
858            effect::AppEffectResult::Error(e) => {
859                logger.error(&format!("Failed to reset starting commit: {e}"));
860                anyhow::bail!("Failed to reset starting commit");
861            }
862            other => {
863                // Fallback to old implementation for other result types
864                // This allows gradual migration
865                drop(other);
866                match reset_start_commit() {
867                    Ok(result) => {
868                        let short_oid = &result.oid[..8.min(result.oid.len())];
869                        if result.fell_back_to_head {
870                            logger.success(&format!(
871                                "Starting commit reference reset to current HEAD ({})",
872                                short_oid
873                            ));
874                            logger.info("On main/master branch - using HEAD as baseline");
875                        } else if let Some(ref branch) = result.default_branch {
876                            logger.success(&format!(
877                                "Starting commit reference reset to merge-base with '{}' ({})",
878                                branch, short_oid
879                            ));
880                            logger.info("Baseline set to common ancestor with default branch");
881                        } else {
882                            logger.success(&format!(
883                                "Starting commit reference reset ({})",
884                                short_oid
885                            ));
886                        }
887                        logger.info(".agent/start_commit has been updated");
888                        Ok(true)
889                    }
890                    Err(e) => {
891                        logger.error(&format!("Failed to reset starting commit: {e}"));
892                        anyhow::bail!("Failed to reset starting commit");
893                    }
894                }
895            }
896        };
897    }
898
899    // Show baseline state
900    if args.commit_display.show_baseline {
901        setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
902
903        return match handle_show_baseline() {
904            Ok(()) => Ok(true),
905            Err(e) => {
906                logger.error(&format!("Failed to show baseline: {e}"));
907                anyhow::bail!("Failed to show baseline");
908            }
909        };
910    }
911
912    Ok(false)
913}
914
915/// Parameters for preparing the pipeline context.
916///
917/// Groups related parameters to avoid too many function arguments.
918struct PipelinePreparationParams<'a, H: effect::AppEffectHandler> {
919    args: Args,
920    config: crate::config::Config,
921    registry: AgentRegistry,
922    developer_agent: String,
923    reviewer_agent: String,
924    repo_root: std::path::PathBuf,
925    logger: Logger,
926    colors: Colors,
927    executor: std::sync::Arc<dyn ProcessExecutor>,
928    handler: &'a mut H,
929    /// Workspace for explicit path resolution.
930    ///
931    /// Production code passes `Arc::new(WorkspaceFs::new(...))`.
932    /// Tests can pass `Arc::new(MemoryWorkspace::new(...))`.
933    workspace: std::sync::Arc<dyn crate::workspace::Workspace>,
934}
935
936/// Prepares the pipeline context after agent validation.
937///
938/// Returns `Some(ctx)` if pipeline should run, or `None` if we should exit early.
939fn prepare_pipeline_or_exit<H: effect::AppEffectHandler>(
940    params: PipelinePreparationParams<'_, H>,
941) -> anyhow::Result<Option<PipelineContext>> {
942    let PipelinePreparationParams {
943        args,
944        config,
945        registry,
946        developer_agent,
947        reviewer_agent,
948        repo_root,
949        mut logger,
950        colors,
951        executor,
952        handler,
953        workspace,
954    } = params;
955
956    // Ensure required files and directories exist via effects
957    effectful::ensure_files_effectful(handler, config.isolation_mode)
958        .map_err(|e| anyhow::anyhow!("{}", e))?;
959
960    // Reset context for isolation mode via effects
961    if config.isolation_mode {
962        effectful::reset_context_for_isolation_effectful(handler)
963            .map_err(|e| anyhow::anyhow!("{}", e))?;
964    }
965
966    logger = logger.with_log_file(".agent/logs/pipeline.log");
967
968    // Handle --dry-run
969    if args.recovery.dry_run {
970        let developer_display = registry.display_name(&developer_agent);
971        let reviewer_display = registry.display_name(&reviewer_agent);
972        handle_dry_run(
973            &logger,
974            colors,
975            &config,
976            &developer_display,
977            &reviewer_display,
978            &repo_root,
979        )?;
980        return Ok(None);
981    }
982
983    // Create template context for user template overrides
984    let template_context =
985        TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
986
987    // Handle --rebase-only
988    if args.rebase_flags.rebase_only {
989        handle_rebase_only(
990            &args,
991            &config,
992            &template_context,
993            &logger,
994            colors,
995            std::sync::Arc::clone(&executor),
996            &repo_root,
997        )?;
998        return Ok(None);
999    }
1000
1001    // Handle --generate-commit-msg
1002    if args.commit_plumbing.generate_commit_msg {
1003        handle_generate_commit_msg(plumbing::CommitGenerationConfig {
1004            config: &config,
1005            template_context: &template_context,
1006            workspace: &*workspace,
1007            registry: &registry,
1008            logger: &logger,
1009            colors,
1010            developer_agent: &developer_agent,
1011            _reviewer_agent: &reviewer_agent,
1012            executor: std::sync::Arc::clone(&executor),
1013        })?;
1014        return Ok(None);
1015    }
1016
1017    // Get display names before moving registry
1018    let developer_display = registry.display_name(&developer_agent);
1019    let reviewer_display = registry.display_name(&reviewer_agent);
1020
1021    // Build pipeline context (workspace was injected via params)
1022    let ctx = PipelineContext {
1023        args,
1024        config,
1025        registry,
1026        developer_agent,
1027        reviewer_agent,
1028        developer_display,
1029        reviewer_display,
1030        repo_root,
1031        workspace,
1032        logger,
1033        colors,
1034        template_context,
1035        executor,
1036    };
1037    Ok(Some(ctx))
1038}
1039
1040/// Parameters for agent validation and setup.
1041struct AgentSetupParams<'a> {
1042    config: &'a crate::config::Config,
1043    registry: &'a AgentRegistry,
1044    developer_agent: &'a str,
1045    reviewer_agent: &'a str,
1046    config_path: &'a std::path::Path,
1047    colors: Colors,
1048    logger: &'a Logger,
1049    /// If Some, use this path as the working directory without discovering the repo root
1050    /// or changing the global CWD. This enables test parallelism.
1051    working_dir_override: Option<&'a std::path::Path>,
1052}
1053
1054/// Validates agent commands and workflow capability, then sets up git repo and PROMPT.md.
1055///
1056/// Returns `Some(repo_root)` if setup succeeded and should continue.
1057/// Returns `None` if the user declined PROMPT.md creation (to exit early).
1058fn validate_and_setup_agents<H: effect::AppEffectHandler>(
1059    params: AgentSetupParams<'_>,
1060    handler: &mut H,
1061) -> anyhow::Result<Option<std::path::PathBuf>> {
1062    let AgentSetupParams {
1063        config,
1064        registry,
1065        developer_agent,
1066        reviewer_agent,
1067        config_path,
1068        colors,
1069        logger,
1070        working_dir_override,
1071    } = params;
1072    // Validate agent commands exist
1073    validate_agent_commands(
1074        config,
1075        registry,
1076        developer_agent,
1077        reviewer_agent,
1078        config_path,
1079    )?;
1080
1081    // Validate agents are workflow-capable
1082    validate_can_commit(
1083        config,
1084        registry,
1085        developer_agent,
1086        reviewer_agent,
1087        config_path,
1088    )?;
1089
1090    // Determine repo root - use override if provided (for testing), otherwise discover
1091    let repo_root = if let Some(override_dir) = working_dir_override {
1092        // Testing mode: use provided directory and change CWD to it via handler
1093        handler.execute(effect::AppEffect::SetCurrentDir {
1094            path: override_dir.to_path_buf(),
1095        });
1096        override_dir.to_path_buf()
1097    } else {
1098        // Production mode: discover repo root and change CWD via handler
1099        let require_result = handler.execute(effect::AppEffect::GitRequireRepo);
1100        if let effect::AppEffectResult::Error(e) = require_result {
1101            anyhow::bail!("Not in a git repository: {}", e);
1102        }
1103
1104        let root_result = handler.execute(effect::AppEffect::GitGetRepoRoot);
1105        let root = match root_result {
1106            effect::AppEffectResult::Path(p) => p,
1107            effect::AppEffectResult::Error(e) => {
1108                anyhow::bail!("Failed to get repo root: {}", e);
1109            }
1110            _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
1111        };
1112
1113        handler.execute(effect::AppEffect::SetCurrentDir { path: root.clone() });
1114        root
1115    };
1116
1117    // Set up PROMPT.md if needed (may return None to exit early)
1118    let should_continue = setup_git_and_prompt_file(config, colors, logger, handler)?;
1119    if should_continue.is_none() {
1120        return Ok(None);
1121    }
1122
1123    Ok(Some(repo_root))
1124}
1125
1126/// In interactive mode, prompts to create PROMPT.md from a template before `ensure_files()`.
1127///
1128/// Returns `Ok(Some(()))` if setup succeeded and should continue.
1129/// Returns `Ok(None)` if the user declined PROMPT.md creation (to exit early).
1130fn setup_git_and_prompt_file<H: effect::AppEffectHandler>(
1131    config: &crate::config::Config,
1132    colors: Colors,
1133    logger: &Logger,
1134    handler: &mut H,
1135) -> anyhow::Result<Option<()>> {
1136    let prompt_exists =
1137        effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{}", e))?;
1138
1139    // In interactive mode, prompt to create PROMPT.md from a template BEFORE ensure_files().
1140    // If the user declines (or we can't prompt), exit without creating a placeholder PROMPT.md.
1141    if config.behavior.interactive && !prompt_exists {
1142        if let Some(template_name) = prompt_template_selection(colors) {
1143            create_prompt_from_template(&template_name, colors)?;
1144            println!();
1145            logger.info(
1146                "PROMPT.md created. Please edit it with your task details, then run ralph again.",
1147            );
1148            logger.info("Tip: Edit PROMPT.md, then run: ralph");
1149            return Ok(None);
1150        }
1151        println!();
1152        logger.error("PROMPT.md not found in current directory.");
1153        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1154        println!();
1155        logger.info("To get started:");
1156        logger.info("  ralph --init                    # Smart setup wizard");
1157        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1158        logger.info("  ralph --list-work-guides          # See all Work Guides");
1159        println!();
1160        return Ok(None);
1161    }
1162
1163    // Non-interactive mode: show helpful error if PROMPT.md doesn't exist
1164    if !prompt_exists {
1165        logger.error("PROMPT.md not found in current directory.");
1166        logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1167        println!();
1168        logger.info("Quick start:");
1169        logger.info("  ralph --init                    # Smart setup wizard");
1170        logger.info("  ralph --init bug-fix             # Create from Work Guide");
1171        logger.info("  ralph --list-work-guides          # See all Work Guides");
1172        println!();
1173        logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1174        println!();
1175        return Ok(None);
1176    }
1177
1178    Ok(Some(()))
1179}
1180
1181/// Runs the full development/review/commit pipeline using reducer-based event loop.
1182fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1183    // Use MainEffectHandler for production
1184    run_pipeline_with_default_handler(ctx)
1185}
1186
1187/// Runs the pipeline with the default MainEffectHandler.
1188///
1189/// This is the production entry point - it creates a MainEffectHandler internally.
1190fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1191    use crate::app::event_loop::EventLoopConfig;
1192    #[cfg(not(feature = "test-utils"))]
1193    use crate::reducer::MainEffectHandler;
1194    use crate::reducer::PipelineState;
1195
1196    // First, offer interactive resume if checkpoint exists without --resume flag
1197    let resume_result = offer_resume_if_checkpoint_exists(
1198        &ctx.args,
1199        &ctx.config,
1200        &ctx.registry,
1201        &ctx.logger,
1202        &ctx.developer_agent,
1203        &ctx.reviewer_agent,
1204    );
1205
1206    // If interactive resume didn't happen, check for --resume flag
1207    let resume_result = match resume_result {
1208        Some(result) => Some(result),
1209        None => handle_resume_with_validation(
1210            &ctx.args,
1211            &ctx.config,
1212            &ctx.registry,
1213            &ctx.logger,
1214            &ctx.developer_display,
1215            &ctx.reviewer_display,
1216        ),
1217    };
1218
1219    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1220
1221    // Create run context - either new or from checkpoint
1222    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1223        use crate::checkpoint::RunContext;
1224        RunContext::from_checkpoint(checkpoint)
1225    } else {
1226        use crate::checkpoint::RunContext;
1227        RunContext::new()
1228    };
1229
1230    // Apply checkpoint configuration restoration if resuming
1231    let config = if let Some(ref checkpoint) = resume_checkpoint {
1232        use crate::checkpoint::apply_checkpoint_to_config;
1233        let mut restored_config = ctx.config.clone();
1234        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1235        ctx.logger.info("Restored configuration from checkpoint:");
1236        if checkpoint.cli_args.developer_iters > 0 {
1237            ctx.logger.info(&format!(
1238                "  Developer iterations: {} (from checkpoint)",
1239                checkpoint.cli_args.developer_iters
1240            ));
1241        }
1242        if checkpoint.cli_args.reviewer_reviews > 0 {
1243            ctx.logger.info(&format!(
1244                "  Reviewer passes: {} (from checkpoint)",
1245                checkpoint.cli_args.reviewer_reviews
1246            ));
1247        }
1248        restored_config
1249    } else {
1250        ctx.config.clone()
1251    };
1252
1253    // Restore environment variables from checkpoint if resuming
1254    if let Some(ref checkpoint) = resume_checkpoint {
1255        use crate::checkpoint::restore::restore_environment_from_checkpoint;
1256        let restored_count = restore_environment_from_checkpoint(checkpoint);
1257        if restored_count > 0 {
1258            ctx.logger.info(&format!(
1259                "  Restored {} environment variable(s) from checkpoint",
1260                restored_count
1261            ));
1262        }
1263    }
1264
1265    // Set up git helpers and agent phase
1266    // Use workspace-aware versions when test-utils feature is enabled
1267    // to avoid real git operations that would cause test failures.
1268    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1269
1270    #[cfg(feature = "test-utils")]
1271    {
1272        use crate::git_helpers::{
1273            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1274        };
1275        // Use workspace-based operations that don't require real git
1276        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1277        create_marker_with_workspace(&*ctx.workspace)?;
1278        // Skip hook installation and git wrapper in test mode
1279    }
1280    #[cfg(not(feature = "test-utils"))]
1281    {
1282        cleanup_orphaned_marker(&ctx.logger)?;
1283        start_agent_phase(&mut git_helpers)?;
1284    }
1285    let mut agent_phase_guard =
1286        AgentPhaseGuard::new(&mut git_helpers, &ctx.logger, &*ctx.workspace);
1287
1288    // Print welcome banner and validate PROMPT.md
1289    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1290    print_pipeline_info_with_config(ctx, &config);
1291    validate_prompt_and_setup_backup(ctx)?;
1292
1293    // Set up PROMPT.md monitoring
1294    let mut prompt_monitor = setup_prompt_monitor(ctx);
1295
1296    // Detect project stack and review guidelines
1297    let (_project_stack, review_guidelines) =
1298        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1299
1300    print_review_guidelines(ctx, review_guidelines.as_ref());
1301    println!();
1302
1303    // Create phase context and save starting commit
1304    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1305    let mut phase_ctx = create_phase_context_with_config(
1306        ctx,
1307        &config,
1308        &mut timer,
1309        &mut stats,
1310        review_guidelines.as_ref(),
1311        &run_context,
1312        resume_checkpoint.as_ref(),
1313    );
1314    save_start_commit_or_warn(ctx);
1315
1316    // Set up interrupt context for checkpoint saving on Ctrl+C
1317    // This must be done after phase_ctx is created
1318    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1319        checkpoint.phase
1320    } else {
1321        PipelinePhase::Planning
1322    };
1323    setup_interrupt_context_for_pipeline(
1324        initial_phase,
1325        config.developer_iters,
1326        config.reviewer_reviews,
1327        &phase_ctx.execution_history,
1328        &phase_ctx.prompt_history,
1329        &run_context,
1330    );
1331
1332    // Ensure interrupt context is cleared on completion
1333    let _interrupt_guard = defer_clear_interrupt_context();
1334
1335    // Determine if we should run rebase based on checkpoint or current args
1336    let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1337        // Use checkpoint's skip_rebase value if it has meaningful cli_args
1338        if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1339            !checkpoint.cli_args.skip_rebase
1340        } else {
1341            // Fallback to current args
1342            ctx.args.rebase_flags.with_rebase
1343        }
1344    } else {
1345        ctx.args.rebase_flags.with_rebase
1346    };
1347
1348    // Run pre-development rebase (only if explicitly requested via --with-rebase)
1349    if should_run_rebase {
1350        run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1351        // Update interrupt context after rebase
1352        update_interrupt_context_from_phase(
1353            &phase_ctx,
1354            PipelinePhase::Planning,
1355            config.developer_iters,
1356            config.reviewer_reviews,
1357            &run_context,
1358        );
1359    } else {
1360        // Save initial checkpoint when rebase is disabled
1361        if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1362            let builder = CheckpointBuilder::new()
1363                .phase(PipelinePhase::Planning, 0, config.developer_iters)
1364                .reviewer_pass(0, config.reviewer_reviews)
1365                .skip_rebase(true) // Rebase is disabled
1366                .capture_from_context(
1367                    &config,
1368                    &ctx.registry,
1369                    &ctx.developer_agent,
1370                    &ctx.reviewer_agent,
1371                    &ctx.logger,
1372                    &run_context,
1373                )
1374                .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1375                .with_execution_history(phase_ctx.execution_history.clone())
1376                .with_prompt_history(phase_ctx.clone_prompt_history());
1377
1378            if let Some(checkpoint) = builder.build() {
1379                let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1380            }
1381        }
1382        // Update interrupt context after initial checkpoint
1383        update_interrupt_context_from_phase(
1384            &phase_ctx,
1385            PipelinePhase::Planning,
1386            config.developer_iters,
1387            config.reviewer_reviews,
1388            &run_context,
1389        );
1390    }
1391
1392    // ============================================
1393    // RUN PIPELINE PHASES VIA REDUCER EVENT LOOP
1394    // ============================================
1395
1396    // Initialize pipeline state
1397    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1398        // Migrate from old checkpoint format to new reducer state
1399        PipelineState::from(checkpoint.clone())
1400    } else {
1401        // Create new initial state
1402        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1403    };
1404
1405    // Configure event loop
1406    let event_loop_config = EventLoopConfig {
1407        max_iterations: event_loop::MAX_EVENT_LOOP_ITERATIONS,
1408        enable_checkpointing: config.features.checkpoint_enabled,
1409    };
1410
1411    // Clone execution_history and prompt_history BEFORE running event loop (to avoid borrow issues)
1412    let execution_history_before = phase_ctx.execution_history.clone();
1413    let prompt_history_before = phase_ctx.clone_prompt_history();
1414
1415    // Create effect handler and run event loop.
1416    // Under test-utils feature, use MockEffectHandler to avoid real git operations.
1417    #[cfg(feature = "test-utils")]
1418    let loop_result = {
1419        use crate::app::event_loop::run_event_loop_with_handler;
1420        use crate::reducer::mock_effect_handler::MockEffectHandler;
1421        let mut handler = MockEffectHandler::new(initial_state.clone());
1422        let phase_ctx_ref = &mut phase_ctx;
1423        run_event_loop_with_handler(
1424            phase_ctx_ref,
1425            Some(initial_state),
1426            event_loop_config,
1427            &mut handler,
1428        )
1429    };
1430    #[cfg(not(feature = "test-utils"))]
1431    let loop_result = {
1432        use crate::app::event_loop::run_event_loop_with_handler;
1433        let mut handler = MainEffectHandler::new(initial_state.clone());
1434        let phase_ctx_ref = &mut phase_ctx;
1435        run_event_loop_with_handler(
1436            phase_ctx_ref,
1437            Some(initial_state),
1438            event_loop_config,
1439            &mut handler,
1440        )
1441    };
1442
1443    // Handle event loop result
1444    let loop_result = loop_result?;
1445    if loop_result.completed {
1446        ctx.logger
1447            .success("Pipeline completed successfully via reducer event loop");
1448        ctx.logger.info(&format!(
1449            "Total events processed: {}",
1450            loop_result.events_processed
1451        ));
1452    } else {
1453        ctx.logger.warn("Pipeline exited without completion marker");
1454    }
1455
1456    // Save Complete checkpoint before clearing (for idempotent resume)
1457    if config.features.checkpoint_enabled {
1458        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1459        let builder = CheckpointBuilder::new()
1460            .phase(
1461                PipelinePhase::Complete,
1462                config.developer_iters,
1463                config.developer_iters,
1464            )
1465            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1466            .skip_rebase(skip_rebase)
1467            .capture_from_context(
1468                &config,
1469                &ctx.registry,
1470                &ctx.developer_agent,
1471                &ctx.reviewer_agent,
1472                &ctx.logger,
1473                &run_context,
1474            )
1475            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1476
1477        let builder = builder
1478            .with_execution_history(execution_history_before)
1479            .with_prompt_history(prompt_history_before);
1480
1481        if let Some(checkpoint) = builder.build() {
1482            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1483        }
1484    }
1485
1486    // Post-pipeline operations
1487    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1488    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1489
1490    // Commit phase
1491    finalize_pipeline(
1492        &mut agent_phase_guard,
1493        &ctx.logger,
1494        ctx.colors,
1495        &config,
1496        finalization::RuntimeStats {
1497            timer: &timer,
1498            stats: &stats,
1499        },
1500        prompt_monitor,
1501        Some(&*ctx.workspace),
1502    );
1503    Ok(())
1504}
1505
1506/// Runs the pipeline with a custom effect handler for testing.
1507///
1508/// This function is only available with the `test-utils` feature and allows
1509/// injecting a `MockEffectHandler` to prevent real git operations during tests.
1510///
1511/// # Arguments
1512///
1513/// * `ctx` - Pipeline context
1514/// * `effect_handler` - Custom effect handler (e.g., `MockEffectHandler`)
1515///
1516/// # Type Parameters
1517///
1518/// * `H` - Effect handler type that implements `EffectHandler` and `StatefulHandler`
1519#[cfg(feature = "test-utils")]
1520pub fn run_pipeline_with_effect_handler<'ctx, H>(
1521    ctx: &PipelineContext,
1522    effect_handler: &mut H,
1523) -> anyhow::Result<()>
1524where
1525    H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1526{
1527    use crate::app::event_loop::EventLoopConfig;
1528    use crate::reducer::PipelineState;
1529
1530    // First, offer interactive resume if checkpoint exists without --resume flag
1531    let resume_result = offer_resume_if_checkpoint_exists(
1532        &ctx.args,
1533        &ctx.config,
1534        &ctx.registry,
1535        &ctx.logger,
1536        &ctx.developer_agent,
1537        &ctx.reviewer_agent,
1538    );
1539
1540    // If interactive resume didn't happen, check for --resume flag
1541    let resume_result = match resume_result {
1542        Some(result) => Some(result),
1543        None => handle_resume_with_validation(
1544            &ctx.args,
1545            &ctx.config,
1546            &ctx.registry,
1547            &ctx.logger,
1548            &ctx.developer_display,
1549            &ctx.reviewer_display,
1550        ),
1551    };
1552
1553    let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1554
1555    // Create run context - either new or from checkpoint
1556    let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1557        use crate::checkpoint::RunContext;
1558        RunContext::from_checkpoint(checkpoint)
1559    } else {
1560        use crate::checkpoint::RunContext;
1561        RunContext::new()
1562    };
1563
1564    // Apply checkpoint configuration restoration if resuming
1565    let config = if let Some(ref checkpoint) = resume_checkpoint {
1566        use crate::checkpoint::apply_checkpoint_to_config;
1567        let mut restored_config = ctx.config.clone();
1568        apply_checkpoint_to_config(&mut restored_config, checkpoint);
1569        restored_config
1570    } else {
1571        ctx.config.clone()
1572    };
1573
1574    // Set up git helpers and agent phase
1575    // Use workspace-aware versions when test-utils feature is enabled
1576    // to avoid real git operations that would cause test failures.
1577    let mut git_helpers = crate::git_helpers::GitHelpers::new();
1578
1579    #[cfg(feature = "test-utils")]
1580    {
1581        use crate::git_helpers::{
1582            cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1583        };
1584        // Use workspace-based operations that don't require real git
1585        cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1586        create_marker_with_workspace(&*ctx.workspace)?;
1587        // Skip hook installation and git wrapper in test mode
1588    }
1589    #[cfg(not(feature = "test-utils"))]
1590    {
1591        cleanup_orphaned_marker(&ctx.logger)?;
1592        start_agent_phase(&mut git_helpers)?;
1593    }
1594    let mut agent_phase_guard =
1595        AgentPhaseGuard::new(&mut git_helpers, &ctx.logger, &*ctx.workspace);
1596
1597    // Print welcome banner and validate PROMPT.md
1598    print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1599    print_pipeline_info_with_config(ctx, &config);
1600    validate_prompt_and_setup_backup(ctx)?;
1601
1602    // Set up PROMPT.md monitoring
1603    let mut prompt_monitor = setup_prompt_monitor(ctx);
1604
1605    // Detect project stack and review guidelines
1606    let (_project_stack, review_guidelines) =
1607        detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1608
1609    print_review_guidelines(ctx, review_guidelines.as_ref());
1610    println!();
1611
1612    // Create phase context and save starting commit
1613    let (mut timer, mut stats) = (Timer::new(), Stats::new());
1614    let mut phase_ctx = create_phase_context_with_config(
1615        ctx,
1616        &config,
1617        &mut timer,
1618        &mut stats,
1619        review_guidelines.as_ref(),
1620        &run_context,
1621        resume_checkpoint.as_ref(),
1622    );
1623    save_start_commit_or_warn(ctx);
1624
1625    // Set up interrupt context for checkpoint saving on Ctrl+C
1626    let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1627        checkpoint.phase
1628    } else {
1629        PipelinePhase::Planning
1630    };
1631    setup_interrupt_context_for_pipeline(
1632        initial_phase,
1633        config.developer_iters,
1634        config.reviewer_reviews,
1635        &phase_ctx.execution_history,
1636        &phase_ctx.prompt_history,
1637        &run_context,
1638    );
1639
1640    // Ensure interrupt context is cleared on completion
1641    let _interrupt_guard = defer_clear_interrupt_context();
1642
1643    // Initialize pipeline state
1644    let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1645        PipelineState::from(checkpoint.clone())
1646    } else {
1647        PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1648    };
1649
1650    // Configure event loop
1651    let event_loop_config = EventLoopConfig {
1652        max_iterations: event_loop::MAX_EVENT_LOOP_ITERATIONS,
1653        enable_checkpointing: config.features.checkpoint_enabled,
1654    };
1655
1656    // Clone execution_history and prompt_history BEFORE running event loop
1657    let execution_history_before = phase_ctx.execution_history.clone();
1658    let prompt_history_before = phase_ctx.clone_prompt_history();
1659
1660    // Run event loop with the provided handler
1661    effect_handler.update_state(initial_state.clone());
1662    let loop_result = {
1663        use crate::app::event_loop::run_event_loop_with_handler;
1664        let phase_ctx_ref = &mut phase_ctx;
1665        run_event_loop_with_handler(
1666            phase_ctx_ref,
1667            Some(initial_state),
1668            event_loop_config,
1669            effect_handler,
1670        )
1671    };
1672
1673    // Handle event loop result
1674    let loop_result = loop_result?;
1675    if loop_result.completed {
1676        ctx.logger
1677            .success("Pipeline completed successfully via reducer event loop");
1678        ctx.logger.info(&format!(
1679            "Total events processed: {}",
1680            loop_result.events_processed
1681        ));
1682    } else {
1683        ctx.logger.warn("Pipeline exited without completion marker");
1684    }
1685
1686    // Save Complete checkpoint before clearing (for idempotent resume)
1687    if config.features.checkpoint_enabled {
1688        let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1689        let builder = CheckpointBuilder::new()
1690            .phase(
1691                PipelinePhase::Complete,
1692                config.developer_iters,
1693                config.developer_iters,
1694            )
1695            .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1696            .skip_rebase(skip_rebase)
1697            .capture_from_context(
1698                &config,
1699                &ctx.registry,
1700                &ctx.developer_agent,
1701                &ctx.reviewer_agent,
1702                &ctx.logger,
1703                &run_context,
1704            )
1705            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1706
1707        let builder = builder
1708            .with_execution_history(execution_history_before)
1709            .with_prompt_history(prompt_history_before);
1710
1711        if let Some(checkpoint) = builder.build() {
1712            let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1713        }
1714    }
1715
1716    // Post-pipeline operations
1717    check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1718    update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1719
1720    // Commit phase
1721    finalize_pipeline(
1722        &mut agent_phase_guard,
1723        &ctx.logger,
1724        ctx.colors,
1725        &config,
1726        finalization::RuntimeStats {
1727            timer: &timer,
1728            stats: &stats,
1729        },
1730        prompt_monitor,
1731        Some(&*ctx.workspace),
1732    );
1733    Ok(())
1734}
1735
1736/// Set up the interrupt context with initial pipeline state.
1737///
1738/// This function initializes the global interrupt context so that if
1739/// the user presses Ctrl+C, the interrupt handler can save a checkpoint.
1740fn setup_interrupt_context_for_pipeline(
1741    phase: PipelinePhase,
1742    total_iterations: u32,
1743    total_reviewer_passes: u32,
1744    execution_history: &crate::checkpoint::ExecutionHistory,
1745    prompt_history: &std::collections::HashMap<String, String>,
1746    run_context: &crate::checkpoint::RunContext,
1747) {
1748    use crate::interrupt::{set_interrupt_context, InterruptContext};
1749
1750    // Determine initial iteration based on phase
1751    let (iteration, reviewer_pass) = match phase {
1752        PipelinePhase::Development => (1, 0),
1753        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1754            (total_iterations, 1)
1755        }
1756        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1757            (total_iterations, total_reviewer_passes)
1758        }
1759        _ => (0, 0),
1760    };
1761
1762    let context = InterruptContext {
1763        phase,
1764        iteration,
1765        total_iterations,
1766        reviewer_pass,
1767        total_reviewer_passes,
1768        run_context: run_context.clone(),
1769        execution_history: execution_history.clone(),
1770        prompt_history: prompt_history.clone(),
1771    };
1772
1773    set_interrupt_context(context);
1774}
1775
1776/// Update the interrupt context from the current phase context.
1777///
1778/// This function should be called after each major phase to keep the
1779/// interrupt context up-to-date with the latest execution history.
1780fn update_interrupt_context_from_phase(
1781    phase_ctx: &crate::phases::PhaseContext,
1782    phase: PipelinePhase,
1783    total_iterations: u32,
1784    total_reviewer_passes: u32,
1785    run_context: &crate::checkpoint::RunContext,
1786) {
1787    use crate::interrupt::{set_interrupt_context, InterruptContext};
1788
1789    // Determine current iteration based on phase
1790    let (iteration, reviewer_pass) = match phase {
1791        PipelinePhase::Development => {
1792            // Estimate iteration from actual runs
1793            let iter = run_context.actual_developer_runs.max(1);
1794            (iter, 0)
1795        }
1796        PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1797            (total_iterations, run_context.actual_reviewer_runs.max(1))
1798        }
1799        PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1800            (total_iterations, total_reviewer_passes)
1801        }
1802        _ => (0, 0),
1803    };
1804
1805    let context = InterruptContext {
1806        phase,
1807        iteration,
1808        total_iterations,
1809        reviewer_pass,
1810        total_reviewer_passes,
1811        run_context: run_context.clone(),
1812        execution_history: phase_ctx.execution_history.clone(),
1813        prompt_history: phase_ctx.clone_prompt_history(),
1814    };
1815
1816    set_interrupt_context(context);
1817}
1818
1819/// Helper to defer clearing interrupt context until function exit.
1820///
1821/// Uses a scope guard pattern to ensure the interrupt context is cleared
1822/// when the pipeline completes successfully, preventing an "interrupted"
1823/// checkpoint from being saved after normal completion.
1824fn defer_clear_interrupt_context() -> InterruptContextGuard {
1825    InterruptContextGuard
1826}
1827
1828/// RAII guard for clearing interrupt context on drop.
1829///
1830/// Ensures the interrupt context is cleared when the guard is dropped,
1831/// preventing an "interrupted" checkpoint from being saved after normal
1832/// pipeline completion.
1833struct InterruptContextGuard;
1834
1835impl Drop for InterruptContextGuard {
1836    fn drop(&mut self) {
1837        crate::interrupt::clear_interrupt_context();
1838    }
1839}
1840
1841/// Validate PROMPT.md and set up backup/protection.
1842fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1843    let prompt_validation = validate_prompt_md_with_workspace(
1844        &*ctx.workspace,
1845        ctx.config.behavior.strict_validation,
1846        ctx.args.interactive,
1847    );
1848    for err in &prompt_validation.errors {
1849        ctx.logger.error(err);
1850    }
1851    for warn in &prompt_validation.warnings {
1852        ctx.logger.warn(warn);
1853    }
1854    if !prompt_validation.is_valid() {
1855        anyhow::bail!("PROMPT.md validation errors");
1856    }
1857
1858    // Create a backup of PROMPT.md to protect against accidental deletion.
1859    match create_prompt_backup_with_workspace(&*ctx.workspace) {
1860        Ok(None) => {}
1861        Ok(Some(warning)) => {
1862            ctx.logger.warn(&format!(
1863                "PROMPT.md backup created but: {warning}. Continuing anyway."
1864            ));
1865        }
1866        Err(e) => {
1867            ctx.logger.warn(&format!(
1868                "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1869            ));
1870        }
1871    }
1872
1873    // Make PROMPT.md read-only to protect against accidental deletion.
1874    match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1875        None => {}
1876        Some(warning) => {
1877            ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1878        }
1879    }
1880
1881    Ok(())
1882}
1883
1884/// Set up PROMPT.md monitoring for deletion detection.
1885fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1886    match PromptMonitor::new() {
1887        Ok(mut monitor) => {
1888            if let Err(e) = monitor.start() {
1889                ctx.logger.warn(&format!(
1890                    "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1891                ));
1892                None
1893            } else {
1894                if ctx.config.verbosity.is_debug() {
1895                    ctx.logger.info("Started real-time PROMPT.md monitoring");
1896                }
1897                Some(monitor)
1898            }
1899        }
1900        Err(e) => {
1901            ctx.logger.warn(&format!(
1902                "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1903            ));
1904            None
1905        }
1906    }
1907}
1908
1909/// Print review guidelines if detected.
1910fn print_review_guidelines(
1911    ctx: &PipelineContext,
1912    review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1913) {
1914    if let Some(guidelines) = review_guidelines {
1915        ctx.logger.info(&format!(
1916            "Review guidelines: {}{}{}",
1917            ctx.colors.dim(),
1918            guidelines.summary(),
1919            ctx.colors.reset()
1920        ));
1921    }
1922}
1923
1924/// Create the phase context with a modified config (for resume restoration).
1925fn create_phase_context_with_config<'ctx>(
1926    ctx: &'ctx PipelineContext,
1927    config: &'ctx crate::config::Config,
1928    timer: &'ctx mut Timer,
1929    stats: &'ctx mut Stats,
1930    review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1931    run_context: &'ctx crate::checkpoint::RunContext,
1932    resume_checkpoint: Option<&PipelineCheckpoint>,
1933) -> PhaseContext<'ctx> {
1934    // Restore execution history and prompt history from checkpoint if available
1935    let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1936        let exec_history = checkpoint
1937            .execution_history
1938            .clone()
1939            .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1940        let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1941        (exec_history, prompt_hist)
1942    } else {
1943        (
1944            crate::checkpoint::execution_history::ExecutionHistory::new(),
1945            std::collections::HashMap::new(),
1946        )
1947    };
1948
1949    PhaseContext {
1950        config,
1951        registry: &ctx.registry,
1952        logger: &ctx.logger,
1953        colors: &ctx.colors,
1954        timer,
1955        stats,
1956        developer_agent: &ctx.developer_agent,
1957        reviewer_agent: &ctx.reviewer_agent,
1958        review_guidelines,
1959        template_context: &ctx.template_context,
1960        run_context: run_context.clone(),
1961        execution_history,
1962        prompt_history,
1963        executor: &*ctx.executor,
1964        executor_arc: std::sync::Arc::clone(&ctx.executor),
1965        repo_root: &ctx.repo_root,
1966        workspace: &*ctx.workspace,
1967    }
1968}
1969
1970/// Print pipeline info with a specific config.
1971fn print_pipeline_info_with_config(ctx: &PipelineContext, _config: &crate::config::Config) {
1972    ctx.logger.info(&format!(
1973        "Working directory: {}{}{}",
1974        ctx.colors.cyan(),
1975        ctx.repo_root.display(),
1976        ctx.colors.reset()
1977    ));
1978}
1979
1980/// Save starting commit or warn if it fails.
1981///
1982/// Under `test-utils` feature, this function uses mock data to avoid real git operations.
1983fn save_start_commit_or_warn(ctx: &PipelineContext) {
1984    // Skip real git operations when test-utils feature is enabled.
1985    // These functions call git2::Repository::discover which requires a real git repo.
1986    #[cfg(feature = "test-utils")]
1987    {
1988        // In tests, just log a mock message
1989        if ctx.config.verbosity.is_debug() {
1990            ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1991        }
1992        ctx.logger
1993            .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1994    }
1995
1996    #[cfg(not(feature = "test-utils"))]
1997    {
1998        match save_start_commit() {
1999            Ok(()) => {
2000                if ctx.config.verbosity.is_debug() {
2001                    ctx.logger
2002                        .info("Saved starting commit for incremental diff generation");
2003                }
2004            }
2005            Err(e) => {
2006                ctx.logger.warn(&format!(
2007                    "Failed to save starting commit: {e}. \
2008                     Incremental diffs may be unavailable as a result."
2009                ));
2010                ctx.logger.info(
2011                    "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
2012                );
2013            }
2014        }
2015
2016        // Display start commit information to user
2017        match get_start_commit_summary() {
2018            Ok(summary) => {
2019                if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
2020                {
2021                    ctx.logger.info(&summary.format_compact());
2022                    if summary.is_stale {
2023                        ctx.logger.warn(
2024                            "Start commit is stale. Consider running: ralph --reset-start-commit",
2025                        );
2026                    } else if summary.commits_since > 5 {
2027                        ctx.logger
2028                            .info("Tip: Run 'ralph --show-baseline' for more details");
2029                    }
2030                }
2031            }
2032            Err(e) => {
2033                // Only show error in debug mode since this is informational
2034                if ctx.config.verbosity.is_debug() {
2035                    ctx.logger
2036                        .warn(&format!("Failed to get start commit summary: {e}"));
2037                }
2038            }
2039        }
2040    }
2041}
2042
2043/// Check for PROMPT.md restoration after a phase.
2044fn check_prompt_restoration(
2045    ctx: &PipelineContext,
2046    prompt_monitor: &mut Option<PromptMonitor>,
2047    phase: &str,
2048) {
2049    if let Some(ref mut monitor) = prompt_monitor {
2050        if monitor.check_and_restore() {
2051            ctx.logger.warn(&format!(
2052                "PROMPT.md was deleted and restored during {phase} phase"
2053            ));
2054        }
2055    }
2056}
2057
2058/// Handle --rebase-only flag.
2059///
2060/// This function performs a rebase to the default branch with AI conflict resolution and exits,
2061/// without running the full pipeline.
2062pub fn handle_rebase_only(
2063    _args: &Args,
2064    config: &crate::config::Config,
2065    template_context: &TemplateContext,
2066    logger: &Logger,
2067    colors: Colors,
2068    executor: std::sync::Arc<dyn ProcessExecutor>,
2069    repo_root: &std::path::Path,
2070) -> anyhow::Result<()> {
2071    // Check if we're on main/master branch
2072    if is_main_or_master_branch()? {
2073        logger.warn("Already on main/master branch - rebasing on main is not recommended");
2074        logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
2075        logger.info("  git worktree add ../feature-branch feature-branch");
2076        logger.info("This allows multiple AI agents to work on different features simultaneously.");
2077        logger.info("Proceeding with rebase anyway as requested...");
2078    }
2079
2080    logger.header("Rebase to default branch", Colors::cyan);
2081
2082    match run_rebase_to_default(logger, colors, &*executor) {
2083        Ok(RebaseResult::Success) => {
2084            logger.success("Rebase completed successfully");
2085            Ok(())
2086        }
2087        Ok(RebaseResult::NoOp { reason }) => {
2088            logger.info(&format!("No rebase needed: {reason}"));
2089            Ok(())
2090        }
2091        Ok(RebaseResult::Failed(err)) => {
2092            logger.error(&format!("Rebase failed: {err}"));
2093            anyhow::bail!("Rebase failed: {err}")
2094        }
2095        Ok(RebaseResult::Conflicts(_conflicts)) => {
2096            // Get the actual conflicted files
2097            let conflicted_files = get_conflicted_files()?;
2098            if conflicted_files.is_empty() {
2099                logger.warn("Rebase reported conflicts but no conflicted files found");
2100                let _ = abort_rebase(&*executor);
2101                return Ok(());
2102            }
2103
2104            logger.warn(&format!(
2105                "Rebase resulted in {} conflict(s), attempting AI resolution",
2106                conflicted_files.len()
2107            ));
2108
2109            // For --rebase-only, we don't have a full PhaseContext, so we use a wrapper
2110            match try_resolve_conflicts_without_phase_ctx(
2111                &conflicted_files,
2112                config,
2113                template_context,
2114                logger,
2115                colors,
2116                std::sync::Arc::clone(&executor),
2117                repo_root,
2118            ) {
2119                Ok(true) => {
2120                    // Conflicts resolved, continue the rebase
2121                    logger.info("Continuing rebase after conflict resolution");
2122                    match continue_rebase(&*executor) {
2123                        Ok(()) => {
2124                            logger.success("Rebase completed successfully after AI resolution");
2125                            Ok(())
2126                        }
2127                        Err(e) => {
2128                            logger.error(&format!("Failed to continue rebase: {e}"));
2129                            let _ = abort_rebase(&*executor);
2130                            anyhow::bail!("Rebase failed after conflict resolution")
2131                        }
2132                    }
2133                }
2134                Ok(false) => {
2135                    // AI resolution failed
2136                    logger.error("AI conflict resolution failed, aborting rebase");
2137                    let _ = abort_rebase(&*executor);
2138                    anyhow::bail!("Rebase conflicts could not be resolved by AI")
2139                }
2140                Err(e) => {
2141                    logger.error(&format!("Conflict resolution error: {e}"));
2142                    let _ = abort_rebase(&*executor);
2143                    anyhow::bail!("Rebase conflict resolution failed: {e}")
2144                }
2145            }
2146        }
2147        Err(e) => {
2148            logger.error(&format!("Rebase failed: {e}"));
2149            anyhow::bail!("Rebase failed: {e}")
2150        }
2151    }
2152}