1use std::path::PathBuf;
17
18use anyhow::Result;
19use clap::{Args, Subcommand};
20
21use crate::{agent, commands::run as run_cmd, config, debuglog};
22
23pub fn handle_run(cmd: RunCommand, force: bool) -> Result<()> {
24 let profile = match &cmd {
26 RunCommand::Resume(args) => args.agent.profile.as_deref(),
27 RunCommand::One(args) => args.agent.profile.as_deref(),
28 RunCommand::Loop(args) => args.agent.profile.as_deref(),
29 RunCommand::Parallel(_) => None,
30 };
31 let resolved = config::resolve_from_cwd_with_profile(profile)?;
32 match cmd {
33 RunCommand::Resume(args) => {
34 if args.debug {
35 debuglog::enable(&resolved.repo_root)?;
36 }
37 let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
39
40 run_cmd::run_loop(
42 &resolved,
43 run_cmd::RunLoopOptions {
44 max_tasks: 0, agent_overrides: overrides,
46 force: args.force || force,
47 auto_resume: true,
48 starting_completed: 0,
49 non_interactive: args.non_interactive,
50 parallel_workers: None,
51 wait_when_blocked: false,
52 wait_poll_ms: 1000,
53 wait_timeout_seconds: 0,
54 notify_when_unblocked: false,
55 wait_when_empty: false,
56 empty_poll_ms: 30_000,
57 },
58 )
59 }
60 RunCommand::One(args) => {
61 if args.debug {
62 debuglog::enable(&resolved.repo_root)?;
63 }
64 let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
66
67 if args.dry_run {
68 if args.parallel_worker {
69 return Err(anyhow::anyhow!(
70 "--dry-run cannot be used with --parallel-worker"
71 ));
72 }
73 run_cmd::dry_run_one(&resolved, &overrides, args.id.as_deref())
74 } else {
75 if args.parallel_worker {
76 let task_id = args.id.as_deref().ok_or_else(|| {
77 anyhow::anyhow!("--parallel-worker requires --id <TASK_ID>")
78 })?;
79 let target_branch =
80 args.parallel_target_branch.as_deref().ok_or_else(|| {
81 anyhow::anyhow!("--parallel-worker requires --parallel-target-branch")
82 })?;
83
84 let mut worker_resolved = resolved.clone();
85 worker_resolved.queue_path =
86 args.coordinator_queue_path.clone().ok_or_else(|| {
87 anyhow::anyhow!("--parallel-worker requires --coordinator-queue-path")
88 })?;
89 worker_resolved.done_path =
90 args.coordinator_done_path.clone().ok_or_else(|| {
91 anyhow::anyhow!("--parallel-worker requires --coordinator-done-path")
92 })?;
93 log::debug!(
94 "parallel worker using queue/done paths: queue={}, done={}, target_branch={}",
95 worker_resolved.queue_path.display(),
96 worker_resolved.done_path.display(),
97 target_branch
98 );
99
100 run_cmd::run_one_parallel_worker(
101 &worker_resolved,
102 &overrides,
103 force,
104 task_id,
105 target_branch,
106 )?;
107 return Ok(());
108 }
109
110 if let Some(task_id) = args.id.as_deref() {
111 run_cmd::run_one_with_id(&resolved, &overrides, force, task_id, None, None)?;
112 } else {
113 run_cmd::run_one(&resolved, &overrides, force, None)?;
114 }
115 Ok(())
116 }
117 }
118 RunCommand::Loop(args) => {
119 if args.debug {
120 debuglog::enable(&resolved.repo_root)?;
121 }
122 let overrides = agent::resolve_run_agent_overrides(&args.agent)?;
124
125 if args.dry_run {
126 run_cmd::dry_run_loop(&resolved, &overrides)
127 } else {
128 run_cmd::run_loop(
129 &resolved,
130 run_cmd::RunLoopOptions {
131 max_tasks: args.max_tasks,
132 agent_overrides: overrides,
133 force,
134 auto_resume: args.resume,
135 starting_completed: 0,
136 non_interactive: args.non_interactive,
137 parallel_workers: args.parallel,
138 wait_when_blocked: args.wait_when_blocked,
139 wait_poll_ms: args.wait_poll_ms,
140 wait_timeout_seconds: args.wait_timeout_seconds,
141 notify_when_unblocked: args.notify_when_unblocked,
142 wait_when_empty: args.wait_when_empty,
143 empty_poll_ms: args.empty_poll_ms,
144 },
145 )
146 }
147 }
148 RunCommand::Parallel(args) => match args.command {
149 ParallelSubcommand::Status(status_args) => {
150 run_cmd::parallel_status(&resolved, status_args.json)
151 }
152 ParallelSubcommand::Retry(retry_args) => {
153 run_cmd::parallel_retry(&resolved, &retry_args.task, force)
154 }
155 },
156 }
157}
158
159#[derive(Args)]
160#[command(
161 about = "Run Ralph supervisor (executes queued tasks via codex/opencode/gemini/claude/cursor/kimi/pi)",
162 after_long_help = "Runner selection:\n\
163 - `ralph run` selects runner/model/effort with this precedence:\n\
164 1) CLI overrides (flags on `run one` / `run loop`)\n\
165 2) task's `agent` override (runner/model plus `model_effort` if set)\n\
166 3) otherwise: resolved config defaults (`agent.runner`, `agent.model`, `agent.reasoning_effort`).\n\
167 \n\
168 Notes:\n\
169 - Allowed runners: codex, opencode, gemini, claude, cursor, kimi, pi\n\
170 - Allowed models: gpt-5.4, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.3, gpt-5.2-codex, gpt-5.2, zai-coding-plan/glm-4.7, gemini-3-pro-preview, gemini-3-flash-preview, sonnet, opus, kimi-for-coding (codex supports only gpt-5.4 + gpt-5.3-codex + gpt-5.3-codex-spark + gpt-5.3 + gpt-5.2-codex + gpt-5.2; opencode/gemini/claude/cursor/kimi/pi accept arbitrary model ids)\n\
171 - `--effort` is codex-only and is ignored for other runners.\n\
172 - `--git-revert-mode` controls whether Ralph reverts uncommitted changes on errors (ask, enabled, disabled).\n\
173 - `--git-commit-push-on` / `--git-commit-push-off` control automatic git commit/push after successful runs.\n\
174 - `--parallel` runs loop tasks concurrently in workspaces (clone-based).\n\
175 - Workers push directly to the target branch after phase execution.\n\
176 - Clean-repo checks allow changes to `.ralph/config.{json,jsonc}` (plus `.ralph/queue.{json,jsonc}` and `.ralph/done.{json,jsonc}`); use `--force` to bypass entirely.\n\
177 \n\
178Phase-specific overrides:\n\
179 Use --runner-phaseN, --model-phaseN, --effort-phaseN to override settings for a specific phase.\n\
180 Phase-specific flags take precedence over global flags for that phase.\n\
181 Single-pass (--phases 1) uses Phase 2 overrides.\n\
182 \n\
183 Precedence per phase (highest to lowest):\n\
184 1) CLI phase override (--runner-phaseN, --model-phaseN, --effort-phaseN)\n\
185 2) Task phase override (task.agent.phase_overrides.phaseN.*)\n\
186 3) Config phase override (agent.phase_overrides.phaseN.*)\n\
187 4) CLI global override (--runner, --model, --effort)\n\
188 5) Task global override (task.agent.runner/model/model_effort)\n\
189 6) Config defaults (agent.*)\n\
190 \n\
191 To change defaults for this repo, edit .ralph/config.jsonc:\n\
192 version: 1\n\
193 agent:\n\
194 runner: codex\n\
195 model: gpt-5.4\n\
196 gemini_bin: gemini\n\
197 \n\
198Examples:\n\
199 ralph run one\n\
200 ralph run one --phases 2\n\
201 ralph run one --phases 1\n\
202 ralph run one --runner opencode --model gpt-5.2\n\
203 ralph run one --runner codex --model gpt-5.4 --effort high\n\
204 ralph run one --runner-phase1 codex --model-phase1 gpt-5.4 --effort-phase1 high\n\
205 ralph run one --runner-phase2 claude --model-phase2 opus\n\
206 ralph run one --runner gemini --model gemini-3-flash-preview\n\
207 ralph run one --runner pi --model gpt-5.2\n\
208 ralph run one --include-draft\n\
209 ralph run one --git-revert-mode disabled\n\
210 ralph run one --git-commit-push-off\n\
211 ralph run one --lfs-check\n\
212 ralph run loop --max-tasks 0\n\
213 ralph run loop --max-tasks 1 --runner opencode --model gpt-5.2\n\
214 ralph run loop --include-draft --max-tasks 1\n\
215 ralph run loop --git-revert-mode ask --max-tasks 1\n\
216 ralph run loop --git-commit-push-on --max-tasks 1\n\
217 ralph run loop --lfs-check --max-tasks 1\n\
218 ralph run loop --parallel --max-tasks 4\n\
219 ralph run loop --parallel 4 --max-tasks 8\n\
220 ralph run resume\n\
221 ralph run resume --force\n\
222 ralph run loop --resume --max-tasks 5"
223)]
224pub struct RunArgs {
225 #[command(subcommand)]
226 pub command: RunCommand,
227}
228
229#[derive(Subcommand)]
230pub enum RunCommand {
231 #[command(
233 about = "Resume an interrupted session from where it left off",
234 after_long_help = "Examples:
235 ralph run resume
236 ralph run resume --force"
237 )]
238 Resume(ResumeArgs),
239 #[command(
240 about = "Run exactly one task (the first todo in the configured queue file)",
241 after_long_help = "Runner selection (precedence):\n\
242 1) CLI overrides (--runner/--model/--effort)\n\
243 2) task.agent in the configured queue file (if present)\n\
244 3) selected profile (if --profile specified)\n\
245 4) config defaults (.ralph/config.jsonc then ~/.config/ralph/config.jsonc, with .json fallback)\n\
246\n\
247Examples:\n\
248 ralph run one\n\
249 ralph run one --id RQ-0001\n\
250 ralph run one --debug\n\
251 ralph run one --profile quick (codex/gpt-5.4, 1-phase, low effort)\n\
252 ralph run one --profile thorough (codex/gpt-5.4, 3-phase, high effort)\n\
253 ralph run one --phases 3 (plan/implement+CI/review+complete)\n\
254 ralph run one --phases 2 (plan/implement)\n\
255 ralph run one --phases 1 (single-pass)\n\
256 ralph run one --quick (single-pass, same as --phases 1)\n\
257 ralph run one --runner opencode --model gpt-5.2\n\
258 ralph run one --runner gemini --model gemini-3-flash-preview\n\
259 ralph run one --runner pi --model gpt-5.2\n\
260 ralph run one --runner codex --model gpt-5.4 --effort high\n\
261 ralph run one --runner-phase1 codex --model-phase1 gpt-5.4 --effort-phase1 high\n\
262 ralph run one --runner-phase2 claude --model-phase2 opus\n\
263 ralph run one --include-draft\n\
264 ralph run one --git-revert-mode enabled\n\
265 ralph run one --git-commit-push-off\n\
266 ralph run one --lfs-check\n\
267 ralph run one --repo-prompt plan\n\
268 ralph run one --repo-prompt off\n\
269 ralph run one --non-interactive\n\
270 ralph run one --dry-run\n\
271 ralph run one --dry-run --include-draft\n\
272 ralph run one --dry-run --id RQ-0001"
273 )]
274 One(RunOneArgs),
275 #[command(
276 about = "Run tasks repeatedly until no todo remain (or --max-tasks is reached)",
277 after_long_help = "Examples:\n\
278 ralph run loop --max-tasks 0\n\
279 ralph run loop --profile quick --max-tasks 5 (codex/gpt-5.4, 1-phase, low effort)\n\
280 ralph run loop --profile thorough --max-tasks 5 (codex/gpt-5.4, 3-phase, high effort)\n\
281 ralph run loop --phases 3 --max-tasks 0 (plan/implement+CI/review+complete)\n\
282 ralph run loop --phases 2 --max-tasks 0 (plan/implement)\n\
283 ralph run loop --phases 1 --max-tasks 1 (single-pass)\n\
284 ralph run loop --quick --max-tasks 1 (single-pass, same as --phases 1)\n\
285 ralph run loop --max-tasks 3\n\
286 ralph run loop --max-tasks 1 --debug\n\
287 ralph run loop --max-tasks 1 --runner opencode --model gpt-5.2\n\
288 ralph run loop --runner-phase1 codex --model-phase1 gpt-5.4 --effort-phase1 high --max-tasks 1\n\
289 ralph run loop --runner-phase2 claude --model-phase2 opus --max-tasks 1\n\
290 ralph run loop --include-draft --max-tasks 1\n\
291 ralph run loop --git-revert-mode disabled --max-tasks 1\n\
292 ralph run loop --git-commit-push-off --max-tasks 1\n\
293 ralph run loop --repo-prompt tools --max-tasks 1\n\
294 ralph run loop --repo-prompt off --max-tasks 1\n\
295 ralph run loop --lfs-check --max-tasks 1\n\
296 ralph run loop --dry-run\n\
297 ralph run loop --wait-when-blocked\n\
298 ralph run loop --wait-when-blocked --wait-timeout-seconds 600\n\
299 ralph run loop --wait-when-blocked --wait-poll-ms 250\n\
300 ralph run loop --wait-when-blocked --notify-when-unblocked"
301 )]
302 Loop(RunLoopArgs),
303 #[command(
305 about = "Manage parallel mode operations",
306 after_long_help = "Examples:\n\
307 ralph run parallel status\n\
308 ralph run parallel status --json\n\
309 ralph run parallel retry --task RQ-0001"
310 )]
311 Parallel(ParallelArgs),
312}
313
314#[derive(Args)]
318pub struct ResumeArgs {
319 #[arg(long)]
321 pub force: bool,
322
323 #[arg(long)]
325 pub debug: bool,
326
327 #[arg(long)]
329 pub non_interactive: bool,
330
331 #[command(flatten)]
332 pub agent: crate::agent::RunAgentArgs,
333}
334
335#[derive(Args)]
336pub struct RunOneArgs {
337 #[arg(long)]
339 pub debug: bool,
340
341 #[arg(long, value_name = "TASK_ID")]
343 pub id: Option<String>,
344
345 #[arg(long)]
347 pub non_interactive: bool,
348
349 #[arg(long, conflicts_with = "parallel_worker")]
352 pub dry_run: bool,
353
354 #[arg(
356 long,
357 hide = true,
358 requires_all = [
359 "id",
360 "coordinator_queue_path",
361 "coordinator_done_path",
362 "parallel_target_branch"
363 ]
364 )]
365 pub parallel_worker: bool,
366
367 #[arg(
369 long,
370 hide = true,
371 value_name = "PATH",
372 requires_all = ["parallel_worker", "coordinator_done_path"]
373 )]
374 pub coordinator_queue_path: Option<PathBuf>,
375
376 #[arg(
378 long,
379 hide = true,
380 value_name = "PATH",
381 requires_all = ["parallel_worker", "coordinator_queue_path"]
382 )]
383 pub coordinator_done_path: Option<PathBuf>,
384
385 #[arg(
387 long,
388 hide = true,
389 value_name = "BRANCH",
390 requires_all = ["parallel_worker", "coordinator_queue_path", "coordinator_done_path"]
391 )]
392 pub parallel_target_branch: Option<String>,
393
394 #[command(flatten)]
395 pub agent: crate::agent::RunAgentArgs,
396}
397
398#[derive(Args)]
399pub struct RunLoopArgs {
400 #[arg(long, default_value_t = 0)]
402 pub max_tasks: u32,
403
404 #[arg(long)]
406 pub debug: bool,
407
408 #[arg(long)]
410 pub resume: bool,
411
412 #[arg(long)]
414 pub non_interactive: bool,
415
416 #[arg(long, conflicts_with = "parallel")]
419 pub dry_run: bool,
420
421 #[arg(
423 long,
424 value_parser = clap::value_parser!(u8).range(2..),
425 num_args = 0..=1,
426 default_missing_value = "2",
427 value_name = "N",
428 )]
429 pub parallel: Option<u8>,
430
431 #[arg(long, conflicts_with = "parallel")]
434 pub wait_when_blocked: bool,
435
436 #[arg(
438 long,
439 default_value_t = 1000,
440 value_parser = clap::value_parser!(u64).range(50..),
441 value_name = "MS"
442 )]
443 pub wait_poll_ms: u64,
444
445 #[arg(long, default_value_t = 0, value_name = "SECONDS")]
447 pub wait_timeout_seconds: u64,
448
449 #[arg(long)]
451 pub notify_when_unblocked: bool,
452
453 #[arg(long, alias = "continuous", conflicts_with = "parallel")]
456 pub wait_when_empty: bool,
457
458 #[arg(
461 long,
462 default_value_t = 30_000,
463 value_parser = clap::value_parser!(u64).range(50..),
464 value_name = "MS"
465 )]
466 pub empty_poll_ms: u64,
467
468 #[command(flatten)]
469 pub agent: crate::agent::RunAgentArgs,
470}
471
472#[derive(Args)]
474pub struct ParallelArgs {
475 #[command(subcommand)]
476 pub command: ParallelSubcommand,
477}
478
479#[derive(Subcommand)]
481pub enum ParallelSubcommand {
482 #[command(
484 about = "Show status of parallel workers",
485 after_long_help = "Examples:\n\
486 ralph run parallel status\n\
487 ralph run parallel status --json"
488 )]
489 Status(ParallelStatusArgs),
490 #[command(
492 about = "Retry a blocked or failed parallel worker",
493 after_long_help = "Examples:\n\
494 ralph run parallel retry --task RQ-0001"
495 )]
496 Retry(ParallelRetryArgs),
497}
498
499#[derive(Args)]
501pub struct ParallelStatusArgs {
502 #[arg(long)]
504 pub json: bool,
505}
506
507#[derive(Args)]
509pub struct ParallelRetryArgs {
510 #[arg(long, value_name = "TASK_ID", required = true)]
512 pub task: String,
513}
514
515#[cfg(test)]
516mod tests {
517 use std::path::PathBuf;
518
519 use clap::{CommandFactory, Parser};
520
521 use crate::cli::{Cli, run::RunCommand};
522
523 #[test]
524 fn run_one_help_includes_phase_semantics() {
525 let mut cmd = Cli::command();
526 let run = cmd.find_subcommand_mut("run").expect("run subcommand");
527 let run_one = run.find_subcommand_mut("one").expect("run one subcommand");
528 let help = run_one.render_long_help().to_string();
529
530 assert!(
531 help.contains("ralph run one --phases 3 (plan/implement+CI/review+complete)"),
532 "missing phases=3 example: {help}"
533 );
534 assert!(
535 help.contains("ralph run one --phases 2 (plan/implement)"),
536 "missing phases=2 example: {help}"
537 );
538 assert!(
539 help.contains("ralph run one --phases 1 (single-pass)"),
540 "missing phases=1 example: {help}"
541 );
542 assert!(
543 help.contains("ralph run one --quick (single-pass, same as --phases 1)"),
544 "missing --quick example: {help}"
545 );
546 }
547
548 #[test]
549 fn run_loop_help_mentions_repo_prompt_examples() {
550 let mut cmd = Cli::command();
551 let run = cmd.find_subcommand_mut("run").expect("run subcommand");
552 let run_loop = run
553 .find_subcommand_mut("loop")
554 .expect("run loop subcommand");
555 let help = run_loop.render_long_help().to_string();
556
557 assert!(
558 help.contains("ralph run loop --repo-prompt tools --max-tasks 1"),
559 "missing repo-prompt tools example: {help}"
560 );
561 assert!(
562 help.contains("ralph run loop --repo-prompt off --max-tasks 1"),
563 "missing repo-prompt off example: {help}"
564 );
565 }
566
567 #[test]
568 fn run_one_non_interactive_parses() {
569 let args = vec!["ralph", "run", "one", "--non-interactive"];
570 let cli = Cli::parse_from(args);
571 match cli.command {
572 crate::cli::Command::Run(run_args) => match run_args.command {
573 RunCommand::One(one_args) => {
574 assert!(one_args.non_interactive);
575 }
576 _ => panic!("expected RunCommand::One"),
577 },
578 _ => panic!("expected Command::Run"),
579 }
580 }
581
582 #[test]
583 fn run_one_non_interactive_with_id_parses() {
584 let args = vec![
585 "ralph",
586 "run",
587 "one",
588 "--non-interactive",
589 "--id",
590 "RQ-0001",
591 ];
592 let cli = Cli::parse_from(args);
593 match cli.command {
594 crate::cli::Command::Run(run_args) => match run_args.command {
595 RunCommand::One(one_args) => {
596 assert!(one_args.non_interactive);
597 assert_eq!(one_args.id, Some("RQ-0001".to_string()));
598 }
599 _ => panic!("expected RunCommand::One"),
600 },
601 _ => panic!("expected Command::Run"),
602 }
603 }
604
605 #[test]
606 fn run_one_dry_run_parses() {
607 let args = vec!["ralph", "run", "one", "--dry-run"];
608 let cli = Cli::parse_from(args);
609 match cli.command {
610 crate::cli::Command::Run(run_args) => match run_args.command {
611 RunCommand::One(one_args) => {
612 assert!(one_args.dry_run);
613 }
614 _ => panic!("expected RunCommand::One"),
615 },
616 _ => panic!("expected Command::Run"),
617 }
618 }
619
620 #[test]
621 fn run_one_dry_run_with_id_parses() {
622 let args = vec!["ralph", "run", "one", "--dry-run", "--id", "RQ-0001"];
623 let cli = Cli::parse_from(args);
624 match cli.command {
625 crate::cli::Command::Run(run_args) => match run_args.command {
626 RunCommand::One(one_args) => {
627 assert!(one_args.dry_run);
628 assert_eq!(one_args.id, Some("RQ-0001".to_string()));
629 }
630 _ => panic!("expected RunCommand::One"),
631 },
632 _ => panic!("expected Command::Run"),
633 }
634 }
635
636 #[test]
637 fn run_loop_dry_run_parses() {
638 let args = vec!["ralph", "run", "loop", "--dry-run"];
639 let cli = Cli::parse_from(args);
640 match cli.command {
641 crate::cli::Command::Run(run_args) => match run_args.command {
642 RunCommand::Loop(loop_args) => {
643 assert!(loop_args.dry_run);
644 }
645 _ => panic!("expected RunCommand::Loop"),
646 },
647 _ => panic!("expected Command::Run"),
648 }
649 }
650
651 #[test]
652 fn run_loop_dry_run_conflicts_with_parallel() {
653 let args = vec!["ralph", "run", "loop", "--dry-run", "--parallel"];
654 let result = Cli::try_parse_from(args);
655 assert!(result.is_err(), "--dry-run and --parallel should conflict");
656 }
657
658 #[test]
659 fn run_one_help_includes_dry_run_examples() {
660 let mut cmd = Cli::command();
661 let run = cmd.find_subcommand_mut("run").expect("run subcommand");
662 let run_one = run.find_subcommand_mut("one").expect("run one subcommand");
663 let help = run_one.render_long_help().to_string();
664
665 assert!(
666 help.contains("ralph run one --dry-run"),
667 "missing dry-run example: {help}"
668 );
669 assert!(
670 help.contains("ralph run one --dry-run --include-draft"),
671 "missing dry-run --include-draft example: {help}"
672 );
673 assert!(
674 help.contains("ralph run one --dry-run --id RQ-0001"),
675 "missing dry-run --id example: {help}"
676 );
677 }
678
679 #[test]
680 fn run_loop_help_includes_dry_run_examples() {
681 let mut cmd = Cli::command();
682 let run = cmd.find_subcommand_mut("run").expect("run subcommand");
683 let run_loop = run
684 .find_subcommand_mut("loop")
685 .expect("run loop subcommand");
686 let help = run_loop.render_long_help().to_string();
687
688 assert!(
689 help.contains("ralph run loop --dry-run"),
690 "missing dry-run example: {help}"
691 );
692 }
693
694 #[test]
695 fn run_loop_wait_poll_ms_rejects_below_minimum() {
696 let args = vec!["ralph", "run", "loop", "--wait-poll-ms", "10"];
697 let result = Cli::try_parse_from(args);
698 assert!(
699 result.is_err(),
700 "--wait-poll-ms should reject values below 50"
701 );
702 }
703
704 #[test]
705 fn run_loop_empty_poll_ms_rejects_below_minimum() {
706 let args = vec!["ralph", "run", "loop", "--empty-poll-ms", "10"];
707 let result = Cli::try_parse_from(args);
708 assert!(
709 result.is_err(),
710 "--empty-poll-ms should reject values below 50"
711 );
712 }
713
714 #[test]
715 fn run_loop_wait_poll_ms_accepts_minimum() {
716 let args = vec!["ralph", "run", "loop", "--wait-poll-ms", "50"];
717 let cli = Cli::try_parse_from(args);
718 assert!(cli.is_ok(), "--wait-poll-ms should accept 50");
719 if let Ok(cli) = cli {
720 match cli.command {
721 crate::cli::Command::Run(run_args) => match run_args.command {
722 RunCommand::Loop(loop_args) => {
723 assert_eq!(loop_args.wait_poll_ms, 50);
724 }
725 _ => panic!("expected RunCommand::Loop"),
726 },
727 _ => panic!("expected Command::Run"),
728 }
729 }
730 }
731
732 #[test]
733 fn run_one_parallel_worker_with_coordinator_paths_parses() {
734 let args = vec![
735 "ralph",
736 "run",
737 "one",
738 "--parallel-worker",
739 "--id",
740 "RQ-0001",
741 "--coordinator-queue-path",
742 "/path/to/queue.json",
743 "--coordinator-done-path",
744 "/path/to/done.json",
745 "--parallel-target-branch",
746 "main",
747 ];
748 let cli = Cli::parse_from(args);
749 match cli.command {
750 crate::cli::Command::Run(run_args) => match run_args.command {
751 RunCommand::One(one_args) => {
752 assert!(one_args.parallel_worker);
753 assert_eq!(one_args.id, Some("RQ-0001".to_string()));
754 assert_eq!(
755 one_args.coordinator_queue_path,
756 Some(PathBuf::from("/path/to/queue.json"))
757 );
758 assert_eq!(
759 one_args.coordinator_done_path,
760 Some(PathBuf::from("/path/to/done.json"))
761 );
762 assert_eq!(one_args.parallel_target_branch, Some("main".to_string()));
763 }
764 _ => panic!("expected RunCommand::One"),
765 },
766 _ => panic!("expected Command::Run"),
767 }
768 }
769
770 #[test]
771 fn run_one_parallel_worker_requires_coordinator_paths() {
772 let args = vec![
773 "ralph",
774 "run",
775 "one",
776 "--parallel-worker",
777 "--id",
778 "RQ-0001",
779 ];
780 let result = Cli::try_parse_from(args);
781 assert!(
782 result.is_err(),
783 "--parallel-worker should require coordinator queue/done paths and target branch"
784 );
785 }
786
787 #[test]
788 fn run_one_parallel_worker_requires_both_coordinator_paths() {
789 let args = vec![
790 "ralph",
791 "run",
792 "one",
793 "--parallel-worker",
794 "--id",
795 "RQ-0001",
796 "--coordinator-queue-path",
797 "/path/to/queue.json",
798 ];
799 let result = Cli::try_parse_from(args);
800 assert!(
801 result.is_err(),
802 "missing --coordinator-done-path should fail parsing"
803 );
804 }
805
806 #[test]
807 fn run_one_parallel_worker_requires_target_branch() {
808 let args = vec![
809 "ralph",
810 "run",
811 "one",
812 "--parallel-worker",
813 "--id",
814 "RQ-0001",
815 "--coordinator-queue-path",
816 "/path/to/queue.json",
817 "--coordinator-done-path",
818 "/path/to/done.json",
819 ];
820 let result = Cli::try_parse_from(args);
821 assert!(
822 result.is_err(),
823 "missing --parallel-target-branch should fail parsing"
824 );
825 }
826}