Skip to main content

ralph_workflow/app/
pipeline_setup.rs

1//! Pipeline setup boundary module.
2//!
3//! This module contains boundary functions for imperative pipeline setup operations.
4//! As a boundary module, it is exempt from functional programming lints.
5
6use crate::agents::AgentRegistry;
7use crate::app::config::{EventLoopConfig, MAX_EVENT_LOOP_ITERATIONS};
8use crate::app::context::PipelineContext;
9use crate::app::effect::AppEffectHandler;
10use crate::checkpoint::{PipelineCheckpoint, RunContext};
11use crate::cli::Args;
12use crate::config::Config;
13use crate::git_helpers::GitHelpers;
14use crate::guidelines::ReviewGuidelines;
15use crate::logger::Colors;
16use crate::phases::PhaseContext;
17use crate::pipeline::Timer;
18use crate::reducer::MainEffectHandler;
19use crate::reducer::PipelineState;
20use crate::workspace::Workspace;
21use std::path::PathBuf;
22use std::sync::Arc;
23
24pub struct PhaseContextWithTimer<'ctx> {
25    pub phase_ctx: PhaseContext<'ctx>,
26    pub timer: Timer,
27}
28
29fn prepare_git_helpers_for_workspace(
30    repo_root: &std::path::Path,
31    workspace: &dyn crate::workspace::Workspace,
32    logger: &crate::logger::Logger,
33    mut git_helpers: GitHelpers,
34    restore_prompt_permissions: bool,
35) -> GitHelpers {
36    crate::app::runner::pipeline_execution::prepare_agent_phase_for_workspace(
37        repo_root,
38        workspace,
39        logger,
40        &mut git_helpers,
41        restore_prompt_permissions,
42    );
43    git_helpers
44}
45
46fn mark_cleanup_guard_owned<'a>(
47    mut cleanup_guard: crate::app::runner::CommandExitCleanupGuard<'a>,
48) -> crate::app::runner::CommandExitCleanupGuard<'a> {
49    cleanup_guard.mark_owned();
50    cleanup_guard
51}
52
53pub fn setup_git_and_agent_phase<'a>(
54    git_helpers: &'a mut GitHelpers,
55    ctx: &'a PipelineContext,
56) -> crate::pipeline::AgentPhaseGuard<'a> {
57    crate::app::runner::pipeline_execution::prepare_agent_phase_for_workspace(
58        &ctx.repo_root,
59        &*ctx.workspace,
60        &ctx.logger,
61        git_helpers,
62        true,
63    );
64    crate::pipeline::AgentPhaseGuard::new(git_helpers, &ctx.logger, &*ctx.workspace)
65}
66
67pub fn setup_phase_context_with_timer<'ctx>(
68    timer: &'ctx mut crate::pipeline::Timer,
69    ctx: &'ctx PipelineContext,
70    config: &'ctx Config,
71    review_guidelines: Option<&'ctx ReviewGuidelines>,
72    run_context: &'ctx RunContext,
73    resume_checkpoint: Option<&'ctx PipelineCheckpoint>,
74    cloud_reporter: &'ctx dyn crate::cloud::CloudReporter,
75) -> crate::phases::PhaseContext<'ctx> {
76    crate::app::runner::pipeline_execution::create_phase_context_with_config(
77        ctx,
78        config,
79        timer,
80        review_guidelines,
81        run_context,
82        resume_checkpoint,
83        cloud_reporter,
84    )
85}
86
87pub fn create_main_effect_handler_boundary(initial_state: PipelineState) -> MainEffectHandler {
88    crate::app::runtime_factory::create_main_effect_handler(initial_state)
89}
90
91pub fn run_event_loop_with_handler_boundary<'r, 'ctx>(
92    phase_ctx: &'r mut PhaseContext<'ctx>,
93    initial_state: PipelineState,
94) -> anyhow::Result<crate::app::EventLoopResult> {
95    let event_loop_config = EventLoopConfig {
96        max_iterations: MAX_EVENT_LOOP_ITERATIONS,
97    };
98    crate::app::core::run_event_loop(phase_ctx, Some(initial_state), event_loop_config)
99}
100
101pub fn create_cleanup_guard_boundary<'a>(
102    logger: &'a crate::logger::Logger,
103    workspace: &'a dyn crate::workspace::Workspace,
104    owned: bool,
105) -> crate::app::runner::CommandExitCleanupGuard<'a> {
106    crate::app::runtime_factory::create_cleanup_guard(logger, workspace, owned)
107}
108
109pub fn create_git_helpers_boundary() -> GitHelpers {
110    crate::app::runtime_factory::create_git_helpers()
111}
112
113pub fn create_effect_handler_boundary() -> crate::app::effect_handler::RealAppEffectHandler {
114    crate::app::runtime_factory::create_effect_handler()
115}
116
117pub fn setup_agent_phase_for_workspace_boundary(
118    repo_root: &std::path::Path,
119    workspace: &dyn crate::workspace::Workspace,
120    logger: &crate::logger::Logger,
121) -> GitHelpers {
122    prepare_git_helpers_for_workspace(
123        repo_root,
124        workspace,
125        logger,
126        crate::app::runtime_factory::create_git_helpers(),
127        false,
128    )
129}
130
131pub struct RepoCommandBoundaryParams<'a> {
132    pub args: &'a Args,
133    pub config: &'a Config,
134    pub registry: &'a AgentRegistry,
135    pub developer_agent: &'a str,
136    pub reviewer_agent: &'a str,
137    pub logger: &'a crate::logger::Logger,
138    pub colors: Colors,
139    pub executor: &'a std::sync::Arc<dyn crate::executor::ProcessExecutor>,
140    pub app_handler: &'a mut dyn crate::app::effect::AppEffectHandler,
141    pub repo_root: &'a std::path::Path,
142    pub workspace: &'a std::sync::Arc<dyn crate::workspace::Workspace>,
143}
144
145pub fn handle_repo_commands_boundary(
146    params: RepoCommandBoundaryParams<'_>,
147) -> anyhow::Result<bool> {
148    let RepoCommandBoundaryParams {
149        args,
150        config,
151        registry,
152        developer_agent,
153        reviewer_agent,
154        logger,
155        colors,
156        executor,
157        app_handler,
158        repo_root,
159        workspace,
160    } = params;
161
162    if args.recovery.dry_run {
163        crate::cli::handle_dry_run(
164            logger,
165            colors,
166            config,
167            developer_agent,
168            reviewer_agent,
169            repo_root,
170            workspace.as_ref(),
171        )?;
172        return Ok(true);
173    }
174
175    if args.rebase_flags.rebase_only {
176        let _cleanup_guard = mark_cleanup_guard_owned(
177            crate::app::runtime_factory::create_cleanup_guard(logger, workspace.as_ref(), false),
178        );
179        let _git_helpers = prepare_git_helpers_for_workspace(
180            repo_root,
181            workspace.as_ref(),
182            logger,
183            crate::app::runtime_factory::create_git_helpers(),
184            false,
185        );
186        let template_context = crate::prompts::TemplateContext::from_user_templates_dir(
187            config.user_templates_dir().cloned(),
188        );
189        crate::app::runner::pipeline_execution::handle_rebase_only(
190            args,
191            config,
192            &template_context,
193            logger,
194            colors,
195            executor,
196            repo_root,
197        )?;
198        return Ok(true);
199    }
200
201    if args.commit_plumbing.generate_commit_msg {
202        let _cleanup_guard = mark_cleanup_guard_owned(
203            crate::app::runtime_factory::create_cleanup_guard(logger, workspace.as_ref(), false),
204        );
205        let _git_helpers = prepare_git_helpers_for_workspace(
206            repo_root,
207            workspace.as_ref(),
208            logger,
209            crate::app::runtime_factory::create_git_helpers(),
210            false,
211        );
212        let template_context = crate::prompts::TemplateContext::from_user_templates_dir(
213            config.user_templates_dir().cloned(),
214        );
215        crate::app::plumbing::handle_generate_commit_msg(
216            &crate::app::plumbing::CommitGenerationConfig {
217                config,
218                template_context: &template_context,
219                workspace: workspace.as_ref(),
220                workspace_arc: std::sync::Arc::clone(workspace),
221                registry,
222                logger,
223                colors,
224                developer_agent,
225                reviewer_agent,
226                executor: std::sync::Arc::clone(executor),
227            },
228            app_handler,
229        )?;
230        return Ok(true);
231    }
232
233    Ok(false)
234}
235
236pub struct PipelineAndRepoRoot {
237    pub ctx: PipelineContext,
238    pub repo_root: PathBuf,
239}
240
241pub struct RunPipelineWithHandlerParams {
242    pub args: Args,
243    pub config: Config,
244    pub registry: AgentRegistry,
245    pub developer_agent: String,
246    pub reviewer_agent: String,
247    pub developer_display: String,
248    pub reviewer_display: String,
249    pub config_path: std::path::PathBuf,
250    pub colors: crate::logger::Colors,
251    pub logger: crate::logger::Logger,
252    pub executor: Arc<dyn crate::executor::ProcessExecutor>,
253    pub template_context: crate::prompts::TemplateContext,
254}
255
256pub fn run_pipeline_with_handler_boundary(
257    params: RunPipelineWithHandlerParams,
258) -> anyhow::Result<PipelineAndRepoRoot> {
259    use crate::app::effect::{AppEffect, AppEffectResult};
260    use crate::app::runner::command_handlers::handle_plumbing_commands;
261    use crate::app::runner::pipeline_execution::{
262        command_requires_prompt_setup, handle_repo_commands_without_prompt_setup,
263        prepare_pipeline_or_exit,
264    };
265    use crate::app::runner::pipeline_execution::{PipelinePreparationParams, RepoCommandParams};
266    use crate::app::runner::AgentSetupParams;
267
268    let RunPipelineWithHandlerParams {
269        args,
270        config,
271        registry,
272        developer_agent,
273        reviewer_agent,
274        developer_display: _,
275        reviewer_display: _,
276        config_path,
277        colors,
278        logger,
279        executor,
280        template_context: _,
281    } = params;
282
283    let handler = &mut crate::app::runtime_factory::create_effect_handler();
284
285    let early_repo_root = {
286        if let Some(dir) = args.working_dir_override.as_deref() {
287            match handler.execute(AppEffect::SetCurrentDir {
288                path: dir.to_path_buf(),
289            }) {
290                AppEffectResult::Ok => {}
291                AppEffectResult::Error(e) => anyhow::bail!(e),
292                other => anyhow::bail!("unexpected result from SetCurrentDir: {other:?}"),
293            }
294        }
295
296        match handler.execute(AppEffect::GitRequireRepo) {
297            AppEffectResult::Ok => {}
298            AppEffectResult::Error(e) => anyhow::bail!("Not in a git repository: {e}"),
299            other => anyhow::bail!("unexpected result from GitRequireRepo: {other:?}"),
300        }
301
302        match handler.execute(AppEffect::GitGetRepoRoot) {
303            AppEffectResult::Path(p) => p,
304            AppEffectResult::Error(e) => anyhow::bail!("Failed to get repo root: {e}"),
305            other => anyhow::bail!("unexpected result from GitGetRepoRoot: {other:?}"),
306        }
307    };
308
309    let workspace: Arc<dyn Workspace> =
310        Arc::new(crate::workspace::WorkspaceFs::new(early_repo_root.clone()));
311
312    if handle_plumbing_commands(&args, &logger, colors, handler, Some(workspace.as_ref()))? {
313        anyhow::bail!("plumbing commands should not return from run_pipeline");
314    }
315
316    if !command_requires_prompt_setup(&args)
317        && handle_repo_commands_without_prompt_setup(RepoCommandParams {
318            args: &args,
319            config: &config,
320            registry: &registry,
321            developer_agent: &developer_agent,
322            reviewer_agent: &reviewer_agent,
323            logger: &logger,
324            colors,
325            executor: &executor,
326            app_handler: handler,
327            repo_root: workspace.root(),
328            workspace: &workspace,
329        })?
330    {
331        anyhow::bail!("repo commands should not return from run_pipeline");
332    }
333
334    let repo_root = match crate::app::runner::validate_and_setup_agents(
335        &AgentSetupParams {
336            config: &config,
337            registry: &registry,
338            developer_agent: &developer_agent,
339            reviewer_agent: &reviewer_agent,
340            config_path: &config_path,
341            colors,
342            logger: &logger,
343            working_dir_override: args.working_dir_override.as_deref(),
344        },
345        handler,
346    )? {
347        Some(root) => root,
348        None => anyhow::bail!("agent validation/setup failed"),
349    };
350
351    let params = PipelinePreparationParams {
352        args,
353        config,
354        registry,
355        developer_agent,
356        reviewer_agent,
357        colors,
358        logger,
359        executor,
360        repo_root,
361        workspace,
362        handler,
363    };
364
365    let ctx = prepare_pipeline_or_exit(params)?
366        .ok_or_else(|| anyhow::anyhow!("pipeline preparation returned None"))?;
367
368    Ok(PipelineAndRepoRoot {
369        ctx,
370        repo_root: early_repo_root,
371    })
372}