1use crate::config;
19use crate::contracts::{
20 GitRevertMode, Model, PhaseOverrideConfig, PhaseOverrides, ReasoningEffort, Runner,
21 RunnerCliOptionsPatch,
22};
23use crate::runner;
24use anyhow::Result;
25
26use super::args::{AgentArgs, RunAgentArgs};
27use super::parse::{parse_git_revert_mode, parse_runner, parse_runner_cli_patch};
28use super::repoprompt::{
29 RepopromptFlags, repoprompt_flags_from_mode, resolve_repoprompt_flags_from_agent_config,
30};
31
32macro_rules! resolve_bool_flag {
37 ($enable:expr, $disable:expr) => {
38 if $enable {
39 Some(true)
40 } else if $disable {
41 Some(false)
42 } else {
43 None
44 }
45 };
46}
47
48macro_rules! resolve_simple_flag {
52 ($flag:expr) => {
53 if $flag { Some(true) } else { None }
54 };
55}
56
57#[derive(Debug, Clone, Default)]
61pub struct AgentOverrides {
62 pub profile: Option<String>,
64 pub runner: Option<Runner>,
65 pub model: Option<Model>,
66 pub reasoning_effort: Option<ReasoningEffort>,
67 pub runner_cli: RunnerCliOptionsPatch,
68 pub phases: Option<u8>,
73 pub repoprompt_plan_required: Option<bool>,
74 pub repoprompt_tool_injection: Option<bool>,
75 pub git_revert_mode: Option<GitRevertMode>,
76 pub git_commit_push_enabled: Option<bool>,
77 pub include_draft: Option<bool>,
78 pub notify_on_complete: Option<bool>,
80 pub notify_on_fail: Option<bool>,
82 pub notify_on_loop_complete: Option<bool>,
84 pub notify_sound: Option<bool>,
86 pub lfs_check: Option<bool>,
88 pub no_progress: Option<bool>,
90 pub phase_overrides: Option<PhaseOverrides>,
92}
93
94pub fn resolve_run_agent_overrides(args: &RunAgentArgs) -> Result<AgentOverrides> {
98 use crate::runner;
99
100 let profile = args.profile.clone();
101
102 let runner = match args.runner.as_deref() {
103 Some(value) => Some(parse_runner(value)?),
104 None => None,
105 };
106
107 let model = match args.model.as_deref() {
108 Some(value) => Some(runner::parse_model(value)?),
109 None => None,
110 };
111
112 let reasoning_effort = match args.effort.as_deref() {
113 Some(value) => Some(runner::parse_reasoning_effort(value)?),
114 None => None,
115 };
116 let runner_cli = parse_runner_cli_patch(&args.runner_cli)?;
117
118 if let (Some(runner_kind), Some(model)) = (runner.as_ref(), model.as_ref()) {
119 runner::validate_model_for_runner(runner_kind, model)?;
120 }
121
122 let repoprompt_override = args.repo_prompt.map(repoprompt_flags_from_mode);
123
124 let git_revert_mode = match args.git_revert_mode.as_deref() {
125 Some(value) => Some(parse_git_revert_mode(value)?),
126 None => None,
127 };
128
129 let git_commit_push_enabled =
130 resolve_bool_flag!(args.git_commit_push_on, args.git_commit_push_off);
131 let include_draft = resolve_simple_flag!(args.include_draft);
132
133 let phases = if args.quick { Some(1) } else { args.phases };
135
136 let notify_on_complete = resolve_bool_flag!(args.notify, args.no_notify);
138 let notify_on_fail = resolve_bool_flag!(args.notify_fail, args.no_notify_fail);
139 let notify_sound = resolve_simple_flag!(args.notify_sound);
140 let lfs_check = resolve_simple_flag!(args.lfs_check);
141 let no_progress = resolve_simple_flag!(args.no_progress);
142
143 let phase_overrides = resolve_phase_overrides(args)?;
145
146 Ok(AgentOverrides {
147 profile,
148 runner,
149 model,
150 reasoning_effort,
151 runner_cli,
152 phases,
153 repoprompt_plan_required: repoprompt_override.map(|flags| flags.plan_required),
154 repoprompt_tool_injection: repoprompt_override.map(|flags| flags.tool_injection),
155 git_revert_mode,
156 git_commit_push_enabled,
157 include_draft,
158 notify_on_complete,
159 notify_on_fail,
160 notify_on_loop_complete: None,
161 notify_sound,
162 lfs_check,
163 no_progress,
164 phase_overrides,
165 })
166}
167
168pub fn resolve_agent_overrides(args: &AgentArgs) -> Result<AgentOverrides> {
172 use crate::runner;
173
174 let runner = match args.runner.as_deref() {
175 Some(value) => Some(parse_runner(value)?),
176 None => None,
177 };
178
179 let model = match args.model.as_deref() {
180 Some(value) => Some(runner::parse_model(value)?),
181 None => None,
182 };
183
184 let reasoning_effort = match args.effort.as_deref() {
185 Some(value) => Some(runner::parse_reasoning_effort(value)?),
186 None => None,
187 };
188
189 if let (Some(runner_kind), Some(model)) = (runner.as_ref(), model.as_ref()) {
190 runner::validate_model_for_runner(runner_kind, model)?;
191 }
192
193 let repoprompt_override = args.repo_prompt.map(repoprompt_flags_from_mode);
194 let runner_cli = parse_runner_cli_patch(&args.runner_cli)?;
195
196 Ok(AgentOverrides {
197 profile: None,
198 runner,
199 model,
200 reasoning_effort,
201 runner_cli,
202 phases: None,
203 repoprompt_plan_required: repoprompt_override.map(|flags| flags.plan_required),
204 repoprompt_tool_injection: repoprompt_override.map(|flags| flags.tool_injection),
205 git_revert_mode: None,
206 git_commit_push_enabled: None,
207 include_draft: None,
208 notify_on_complete: None,
209 notify_on_fail: None,
210 notify_on_loop_complete: None,
211 notify_sound: None,
212 lfs_check: None,
213 no_progress: None,
214 phase_overrides: None,
215 })
216}
217
218fn resolve_single_phase_override(
223 runner: Option<&str>,
224 model: Option<&str>,
225 effort: Option<&str>,
226) -> Result<Option<PhaseOverrideConfig>> {
227 if runner.is_none() && model.is_none() && effort.is_none() {
228 return Ok(None);
229 }
230
231 Ok(Some(PhaseOverrideConfig {
232 runner: runner.map(parse_runner).transpose()?,
233 model: model.map(runner::parse_model).transpose()?,
234 reasoning_effort: effort.map(runner::parse_reasoning_effort).transpose()?,
235 }))
236}
237
238fn resolve_phase_overrides(args: &RunAgentArgs) -> Result<Option<PhaseOverrides>> {
243 let phase1 = resolve_single_phase_override(
244 args.runner_phase1.as_deref(),
245 args.model_phase1.as_deref(),
246 args.effort_phase1.as_deref(),
247 )?;
248 let phase2 = resolve_single_phase_override(
249 args.runner_phase2.as_deref(),
250 args.model_phase2.as_deref(),
251 args.effort_phase2.as_deref(),
252 )?;
253 let phase3 = resolve_single_phase_override(
254 args.runner_phase3.as_deref(),
255 args.model_phase3.as_deref(),
256 args.effort_phase3.as_deref(),
257 )?;
258
259 if phase1.is_none() && phase2.is_none() && phase3.is_none() {
260 Ok(None)
261 } else {
262 Ok(Some(PhaseOverrides {
263 phase1,
264 phase2,
265 phase3,
266 }))
267 }
268}
269
270pub fn resolve_repoprompt_flags_from_overrides(
272 overrides: &AgentOverrides,
273 resolved: &config::Resolved,
274) -> RepopromptFlags {
275 let config_flags = resolve_repoprompt_flags_from_agent_config(&resolved.config.agent);
276 let plan_required = overrides
277 .repoprompt_plan_required
278 .unwrap_or(config_flags.plan_required);
279 let tool_injection = overrides
280 .repoprompt_tool_injection
281 .unwrap_or(config_flags.tool_injection);
282 RepopromptFlags {
283 plan_required,
284 tool_injection,
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::agent::args::RunnerCliArgs;
292 use crate::contracts::{
293 GitRevertMode, Model, ReasoningEffort, Runner, RunnerApprovalMode, RunnerPlanMode,
294 RunnerSandboxMode,
295 };
296
297 #[test]
298 fn resolve_agent_overrides_parses_valid_args() {
299 let args = AgentArgs {
300 runner: Some("opencode".to_string()),
301 model: Some("gpt-5.2".to_string()),
302 effort: None,
303 repo_prompt: None,
304 runner_cli: RunnerCliArgs::default(),
305 };
306
307 let overrides = resolve_agent_overrides(&args).unwrap();
308 assert_eq!(overrides.runner, Some(Runner::Opencode));
309 assert_eq!(overrides.model, Some(Model::Gpt52));
310 assert_eq!(overrides.reasoning_effort, None);
311 assert_eq!(overrides.repoprompt_plan_required, None);
312 assert_eq!(overrides.repoprompt_tool_injection, None);
313 assert_eq!(overrides.git_revert_mode, None);
314 assert_eq!(overrides.git_commit_push_enabled, None);
315 assert_eq!(overrides.include_draft, None);
316 }
317
318 #[test]
319 fn resolve_agent_overrides_sets_rp_flags() {
320 use crate::agent::repoprompt::RepoPromptMode;
321 let args = AgentArgs {
322 runner: None,
323 model: None,
324 effort: None,
325 repo_prompt: Some(RepoPromptMode::Plan),
326 runner_cli: RunnerCliArgs::default(),
327 };
328
329 let overrides = resolve_agent_overrides(&args).unwrap();
330 assert_eq!(overrides.repoprompt_plan_required, Some(true));
331 assert_eq!(overrides.repoprompt_tool_injection, Some(true));
332 assert_eq!(overrides.git_revert_mode, None);
333 assert_eq!(overrides.git_commit_push_enabled, None);
334 assert_eq!(overrides.include_draft, None);
335 }
336
337 #[test]
338 fn resolve_agent_overrides_maps_repo_prompt_modes() {
339 use crate::agent::repoprompt::RepoPromptMode;
340 let tools_args = AgentArgs {
341 runner: None,
342 model: None,
343 effort: None,
344 repo_prompt: Some(RepoPromptMode::Tools),
345 runner_cli: RunnerCliArgs::default(),
346 };
347 let tools_overrides = resolve_agent_overrides(&tools_args).unwrap();
348 assert_eq!(tools_overrides.repoprompt_plan_required, Some(false));
349 assert_eq!(tools_overrides.repoprompt_tool_injection, Some(true));
350
351 let off_args = AgentArgs {
352 runner: None,
353 model: None,
354 effort: None,
355 repo_prompt: Some(RepoPromptMode::Off),
356 runner_cli: RunnerCliArgs::default(),
357 };
358 let off_overrides = resolve_agent_overrides(&off_args).unwrap();
359 assert_eq!(off_overrides.repoprompt_plan_required, Some(false));
360 assert_eq!(off_overrides.repoprompt_tool_injection, Some(false));
361 }
362
363 #[test]
364 fn resolve_agent_overrides_parses_runner_cli_args() {
365 let args = AgentArgs {
366 runner: None,
367 model: None,
368 effort: None,
369 repo_prompt: None,
370 runner_cli: RunnerCliArgs {
371 approval_mode: Some("auto-edits".to_string()),
372 sandbox: Some("disabled".to_string()),
373 ..RunnerCliArgs::default()
374 },
375 };
376
377 let overrides = resolve_agent_overrides(&args).unwrap();
378 assert_eq!(
379 overrides.runner_cli.approval_mode,
380 Some(RunnerApprovalMode::AutoEdits)
381 );
382 assert_eq!(
383 overrides.runner_cli.sandbox,
384 Some(RunnerSandboxMode::Disabled)
385 );
386 }
387
388 #[test]
389 fn resolve_run_agent_overrides_includes_phases() {
390 let args = RunAgentArgs {
391 profile: None,
392 runner: Some("codex".to_string()),
393 model: Some("gpt-5.2-codex".to_string()),
394 effort: Some("high".to_string()),
395 runner_cli: RunnerCliArgs::default(),
396 phases: Some(2),
397 quick: false,
398 repo_prompt: None,
399 git_revert_mode: Some("enabled".to_string()),
400 git_commit_push_on: false,
401 git_commit_push_off: true,
402 include_draft: true,
403 notify: false,
404 no_notify: false,
405 notify_fail: false,
406 no_notify_fail: false,
407 notify_sound: false,
408 lfs_check: false,
409 no_progress: false,
410 runner_phase1: None,
411 model_phase1: None,
412 effort_phase1: None,
413 runner_phase2: None,
414 model_phase2: None,
415 effort_phase2: None,
416 runner_phase3: None,
417 model_phase3: None,
418 effort_phase3: None,
419 };
420
421 let overrides = resolve_run_agent_overrides(&args).unwrap();
422 assert_eq!(overrides.runner, Some(Runner::Codex));
423 assert_eq!(overrides.model, Some(Model::Gpt52Codex));
424 assert_eq!(overrides.reasoning_effort, Some(ReasoningEffort::High));
425 assert_eq!(overrides.phases, Some(2));
426 assert_eq!(overrides.git_revert_mode, Some(GitRevertMode::Enabled));
427 assert_eq!(overrides.git_commit_push_enabled, Some(false));
428 assert_eq!(overrides.include_draft, Some(true));
429 }
430
431 #[test]
432 fn resolve_run_agent_overrides_parses_runner_cli_args() {
433 let args = RunAgentArgs {
434 profile: None,
435 runner: None,
436 model: None,
437 effort: None,
438 runner_cli: RunnerCliArgs {
439 approval_mode: Some("yolo".to_string()),
440 plan_mode: Some("enabled".to_string()),
441 ..RunnerCliArgs::default()
442 },
443 phases: None,
444 quick: false,
445 repo_prompt: None,
446 git_revert_mode: None,
447 git_commit_push_on: false,
448 git_commit_push_off: false,
449 include_draft: false,
450 notify: false,
451 no_notify: false,
452 notify_fail: false,
453 no_notify_fail: false,
454 notify_sound: false,
455 lfs_check: false,
456 no_progress: false,
457 runner_phase1: None,
458 model_phase1: None,
459 effort_phase1: None,
460 runner_phase2: None,
461 model_phase2: None,
462 effort_phase2: None,
463 runner_phase3: None,
464 model_phase3: None,
465 effort_phase3: None,
466 };
467
468 let overrides = resolve_run_agent_overrides(&args).unwrap();
469 assert_eq!(
470 overrides.runner_cli.approval_mode,
471 Some(RunnerApprovalMode::Yolo)
472 );
473 assert_eq!(
474 overrides.runner_cli.plan_mode,
475 Some(RunnerPlanMode::Enabled)
476 );
477 }
478
479 #[test]
480 fn resolve_run_agent_overrides_quick_flag_sets_phases_to_one() {
481 let args = RunAgentArgs {
482 profile: None,
483 runner: None,
484 model: None,
485 effort: None,
486 runner_cli: RunnerCliArgs::default(),
487 phases: None,
488 quick: true,
489 repo_prompt: None,
490 git_revert_mode: None,
491 git_commit_push_on: false,
492 git_commit_push_off: false,
493 include_draft: false,
494 notify: false,
495 no_notify: false,
496 notify_fail: false,
497 no_notify_fail: false,
498 notify_sound: false,
499 lfs_check: false,
500 no_progress: false,
501 runner_phase1: None,
502 model_phase1: None,
503 effort_phase1: None,
504 runner_phase2: None,
505 model_phase2: None,
506 effort_phase2: None,
507 runner_phase3: None,
508 model_phase3: None,
509 effort_phase3: None,
510 };
511
512 let overrides = resolve_run_agent_overrides(&args).unwrap();
513 assert_eq!(overrides.phases, Some(1));
514 }
515
516 #[test]
517 fn resolve_run_agent_overrides_phases_override_takes_precedence_when_quick_false() {
518 let args = RunAgentArgs {
519 profile: None,
520 runner: None,
521 model: None,
522 effort: None,
523 runner_cli: RunnerCliArgs::default(),
524 phases: Some(3),
525 quick: false,
526 repo_prompt: None,
527 git_revert_mode: None,
528 git_commit_push_on: false,
529 git_commit_push_off: false,
530 include_draft: false,
531 notify: false,
532 no_notify: false,
533 notify_fail: false,
534 no_notify_fail: false,
535 notify_sound: false,
536 lfs_check: false,
537 no_progress: false,
538 runner_phase1: None,
539 model_phase1: None,
540 effort_phase1: None,
541 runner_phase2: None,
542 model_phase2: None,
543 effort_phase2: None,
544 runner_phase3: None,
545 model_phase3: None,
546 effort_phase3: None,
547 };
548
549 let overrides = resolve_run_agent_overrides(&args).unwrap();
550 assert_eq!(overrides.phases, Some(3));
551 }
552
553 #[test]
554 fn resolve_run_agent_overrides_phase_flags_parsed_correctly() {
555 let args = RunAgentArgs {
556 profile: None,
557 runner: Some("claude".to_string()),
558 model: Some("sonnet".to_string()),
559 effort: None,
560 runner_cli: RunnerCliArgs::default(),
561 phases: Some(3),
562 quick: false,
563 repo_prompt: None,
564 git_revert_mode: None,
565 git_commit_push_on: false,
566 git_commit_push_off: false,
567 include_draft: false,
568 notify: false,
569 no_notify: false,
570 notify_fail: false,
571 no_notify_fail: false,
572 notify_sound: false,
573 lfs_check: false,
574 no_progress: false,
575 runner_phase1: Some("codex".to_string()),
576 model_phase1: Some("gpt-5.2-codex".to_string()),
577 effort_phase1: Some("high".to_string()),
578 runner_phase2: Some("claude".to_string()),
579 model_phase2: Some("opus".to_string()),
580 effort_phase2: None,
581 runner_phase3: Some("codex".to_string()),
582 model_phase3: Some("gpt-5.2-codex".to_string()),
583 effort_phase3: Some("medium".to_string()),
584 };
585
586 let overrides = resolve_run_agent_overrides(&args).unwrap();
587
588 assert_eq!(overrides.runner, Some(Runner::Claude));
590 assert_eq!(overrides.model, Some(Model::Custom("sonnet".to_string())));
591
592 let phase_overrides = overrides
594 .phase_overrides
595 .expect("phase_overrides should be set");
596
597 let phase1 = phase_overrides.phase1.expect("phase1 should be set");
599 assert_eq!(phase1.runner, Some(Runner::Codex));
600 assert_eq!(phase1.model, Some(Model::Gpt52Codex));
601 assert_eq!(phase1.reasoning_effort, Some(ReasoningEffort::High));
602
603 let phase2 = phase_overrides.phase2.expect("phase2 should be set");
605 assert_eq!(phase2.runner, Some(Runner::Claude));
606 assert_eq!(phase2.model, Some(Model::Custom("opus".to_string())));
607 assert_eq!(phase2.reasoning_effort, None);
608
609 let phase3 = phase_overrides.phase3.expect("phase3 should be set");
611 assert_eq!(phase3.runner, Some(Runner::Codex));
612 assert_eq!(phase3.model, Some(Model::Gpt52Codex));
613 assert_eq!(phase3.reasoning_effort, Some(ReasoningEffort::Medium));
614 }
615
616 #[test]
617 fn resolve_run_agent_overrides_phase_flags_partial() {
618 let args = RunAgentArgs {
620 profile: None,
621 runner: None,
622 model: None,
623 effort: None,
624 runner_cli: RunnerCliArgs::default(),
625 phases: None,
626 quick: false,
627 repo_prompt: None,
628 git_revert_mode: None,
629 git_commit_push_on: false,
630 git_commit_push_off: false,
631 include_draft: false,
632 notify: false,
633 no_notify: false,
634 notify_fail: false,
635 no_notify_fail: false,
636 notify_sound: false,
637 lfs_check: false,
638 no_progress: false,
639 runner_phase1: Some("codex".to_string()),
640 model_phase1: None,
641 effort_phase1: None,
642 runner_phase2: None,
643 model_phase2: None,
644 effort_phase2: None,
645 runner_phase3: None,
646 model_phase3: None,
647 effort_phase3: None,
648 };
649
650 let overrides = resolve_run_agent_overrides(&args).unwrap();
651
652 let phase_overrides = overrides
653 .phase_overrides
654 .expect("phase_overrides should be set");
655
656 let phase1 = phase_overrides.phase1.expect("phase1 should be set");
658 assert_eq!(phase1.runner, Some(Runner::Codex));
659 assert_eq!(phase1.model, None);
660 assert_eq!(phase1.reasoning_effort, None);
661
662 assert!(phase_overrides.phase2.is_none());
664 assert!(phase_overrides.phase3.is_none());
665 }
666
667 #[test]
668 fn resolve_run_agent_overrides_empty_phase_flags_returns_none() {
669 let args = RunAgentArgs {
671 profile: None,
672 runner: None,
673 model: None,
674 effort: None,
675 runner_cli: RunnerCliArgs::default(),
676 phases: None,
677 quick: false,
678 repo_prompt: None,
679 git_revert_mode: None,
680 git_commit_push_on: false,
681 git_commit_push_off: false,
682 include_draft: false,
683 notify: false,
684 no_notify: false,
685 notify_fail: false,
686 no_notify_fail: false,
687 notify_sound: false,
688 lfs_check: false,
689 no_progress: false,
690 runner_phase1: None,
691 model_phase1: None,
692 effort_phase1: None,
693 runner_phase2: None,
694 model_phase2: None,
695 effort_phase2: None,
696 runner_phase3: None,
697 model_phase3: None,
698 effort_phase3: None,
699 };
700
701 let overrides = resolve_run_agent_overrides(&args).unwrap();
702 assert!(overrides.phase_overrides.is_none());
703 }
704
705 #[test]
706 fn resolve_run_agent_overrides_invalid_runner_phase_includes_phase_in_error() {
707 let args = RunAgentArgs {
709 profile: None,
710 runner: None,
711 model: None,
712 effort: None,
713 runner_cli: RunnerCliArgs::default(),
714 phases: None,
715 quick: false,
716 repo_prompt: None,
717 git_revert_mode: None,
718 git_commit_push_on: false,
719 git_commit_push_off: false,
720 include_draft: false,
721 notify: false,
722 no_notify: false,
723 notify_fail: false,
724 no_notify_fail: false,
725 notify_sound: false,
726 lfs_check: false,
727 no_progress: false,
728 runner_phase1: Some("invalid_runner".to_string()),
729 model_phase1: None,
730 effort_phase1: None,
731 runner_phase2: None,
732 model_phase2: None,
733 effort_phase2: None,
734 runner_phase3: None,
735 model_phase3: None,
736 effort_phase3: None,
737 };
738
739 let result = resolve_run_agent_overrides(&args);
740 assert!(result.is_err());
741 let err = result.unwrap_err().to_string();
742 assert!(err.contains("Invalid runner"));
743 }
744}