Skip to main content

ralph/cli/run/
handle.rs

1//! Dispatch logic for `ralph run`.
2//!
3//! Responsibilities:
4//! - Resolve config/profile context for run commands.
5//! - Route parsed clap commands into the runtime entrypoints.
6//!
7//! Not handled here:
8//! - Clap type definitions.
9//! - Help-text content.
10//!
11//! Invariants/assumptions:
12//! - Profile resolution happens once before per-command dispatch.
13//! - Parallel worker internal flags are validated before execution.
14
15use anyhow::{Result, anyhow};
16
17use crate::{agent, commands::run as run_cmd, config, debuglog};
18
19use super::args::{ParallelSubcommand, RunCommand, RunLoopArgs, RunOneArgs};
20
21pub fn handle_run(cmd: RunCommand, force: bool) -> Result<()> {
22    let profile = selected_profile(&cmd);
23    let resolved = config::resolve_from_cwd_with_profile(profile)?;
24
25    match cmd {
26        RunCommand::Resume(args) => {
27            maybe_enable_debug(args.debug, &resolved)?;
28            let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
29            run_cmd::run_loop(
30                &resolved,
31                run_cmd::RunLoopOptions {
32                    max_tasks: 0,
33                    agent_overrides: overrides,
34                    force: args.force || force,
35                    auto_resume: true,
36                    starting_completed: 0,
37                    non_interactive: args.non_interactive,
38                    parallel_workers: None,
39                    wait_when_blocked: false,
40                    wait_poll_ms: 1000,
41                    wait_timeout_seconds: 0,
42                    notify_when_unblocked: false,
43                    wait_when_empty: false,
44                    empty_poll_ms: 30_000,
45                    run_event_handler: None,
46                },
47            )
48        }
49        RunCommand::One(args) => handle_run_one(args, force, &resolved),
50        RunCommand::Loop(args) => handle_run_loop(args, force, &resolved),
51        RunCommand::Parallel(args) => match args.command {
52            ParallelSubcommand::Status(status_args) => {
53                run_cmd::parallel_status(&resolved, status_args.json)
54            }
55            ParallelSubcommand::Retry(retry_args) => {
56                run_cmd::parallel_retry(&resolved, &retry_args.task)
57            }
58        },
59    }
60}
61
62fn selected_profile(cmd: &RunCommand) -> Option<&str> {
63    match cmd {
64        RunCommand::Resume(args) => args.agent.profile.as_deref(),
65        RunCommand::One(args) => args.agent.profile.as_deref(),
66        RunCommand::Loop(args) => args.agent.profile.as_deref(),
67        RunCommand::Parallel(_) => None,
68    }
69}
70
71fn maybe_enable_debug(debug: bool, resolved: &config::Resolved) -> Result<()> {
72    if debug {
73        debuglog::enable(&resolved.repo_root)?;
74    }
75    Ok(())
76}
77
78fn handle_run_one(args: RunOneArgs, force: bool, resolved: &config::Resolved) -> Result<()> {
79    maybe_enable_debug(args.debug, resolved)?;
80    let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
81
82    if args.dry_run {
83        if args.parallel_worker {
84            return Err(anyhow!("--dry-run cannot be used with --parallel-worker"));
85        }
86        return run_cmd::dry_run_one(resolved, &overrides, args.id.as_deref());
87    }
88
89    if args.parallel_worker {
90        return handle_parallel_worker_run_one(args, force, resolved, overrides);
91    }
92
93    let resume_options = run_cmd::RunOneResumeOptions::detect(args.resume, args.non_interactive);
94    if let Some(task_id) = args.id.as_deref() {
95        run_cmd::run_one_with_id(
96            resolved,
97            &overrides,
98            force,
99            task_id,
100            resume_options,
101            None,
102            None,
103            None,
104        )?;
105    } else {
106        run_cmd::run_one(resolved, &overrides, force, resume_options)?;
107    }
108
109    Ok(())
110}
111
112fn handle_parallel_worker_run_one(
113    args: RunOneArgs,
114    force: bool,
115    resolved: &config::Resolved,
116    overrides: crate::agent::AgentOverrides,
117) -> Result<()> {
118    let task_id = args
119        .id
120        .as_deref()
121        .ok_or_else(|| anyhow!("--parallel-worker requires --id <TASK_ID>"))?;
122    let target_branch = args
123        .parallel_target_branch
124        .as_deref()
125        .ok_or_else(|| anyhow!("--parallel-worker requires --parallel-target-branch"))?;
126
127    let mut worker_resolved = resolved.clone();
128    worker_resolved.queue_path = args
129        .coordinator_queue_path
130        .clone()
131        .ok_or_else(|| anyhow!("--parallel-worker requires --coordinator-queue-path"))?;
132    worker_resolved.done_path = args
133        .coordinator_done_path
134        .clone()
135        .ok_or_else(|| anyhow!("--parallel-worker requires --coordinator-done-path"))?;
136
137    log::debug!(
138        "parallel worker using queue/done paths: queue={}, done={}, target_branch={}",
139        worker_resolved.queue_path.display(),
140        worker_resolved.done_path.display(),
141        target_branch
142    );
143
144    run_cmd::run_one_parallel_worker(&worker_resolved, &overrides, force, task_id, target_branch)?;
145    Ok(())
146}
147
148fn handle_run_loop(args: RunLoopArgs, force: bool, resolved: &config::Resolved) -> Result<()> {
149    maybe_enable_debug(args.debug, resolved)?;
150    let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
151
152    if args.dry_run {
153        return run_cmd::dry_run_loop(resolved, &overrides);
154    }
155
156    run_cmd::run_loop(
157        resolved,
158        run_cmd::RunLoopOptions {
159            max_tasks: args.max_tasks,
160            agent_overrides: overrides,
161            force,
162            auto_resume: args.resume,
163            starting_completed: 0,
164            non_interactive: args.non_interactive,
165            parallel_workers: args.parallel,
166            wait_when_blocked: args.wait_when_blocked,
167            wait_poll_ms: args.wait_poll_ms,
168            wait_timeout_seconds: args.wait_timeout_seconds,
169            notify_when_unblocked: args.notify_when_unblocked,
170            wait_when_empty: args.wait_when_empty,
171            empty_poll_ms: args.empty_poll_ms,
172            run_event_handler: None,
173        },
174    )
175}