ralph/contracts/config/
agent.rs1use crate::contracts::config::{
12 GitRevertMode, NotificationConfig, PhaseOverrides, RunnerRetryConfig, ScanPromptVersion,
13 WebhookConfig,
14};
15use crate::contracts::model::{Model, ReasoningEffort};
16use crate::contracts::runner::{ClaudePermissionMode, Runner, RunnerCliConfigRoot};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq, Eq)]
23#[serde(default, deny_unknown_fields)]
24pub struct CiGateConfig {
25 pub enabled: Option<bool>,
27
28 pub argv: Option<Vec<String>>,
30}
31
32impl CiGateConfig {
33 pub fn is_enabled(&self) -> bool {
34 self.enabled.unwrap_or(true)
35 }
36
37 pub fn display_string(&self) -> String {
38 if !self.is_enabled() {
39 return "disabled".to_string();
40 }
41
42 if let Some(argv) = &self.argv {
43 return format_argv(argv);
44 }
45
46 "<unset>".to_string()
47 }
48
49 pub fn merge_from(&mut self, other: Self) {
50 if other.enabled.is_some() {
51 self.enabled = other.enabled;
52 }
53 if other.argv.is_some() {
54 self.argv = other.argv;
55 }
56 }
57}
58
59fn format_argv(argv: &[String]) -> String {
60 argv.iter()
61 .map(|part| {
62 if part.is_empty() {
63 "\"\"".to_string()
64 } else if part
65 .chars()
66 .any(|ch| ch.is_whitespace() || matches!(ch, '"' | '\'' | '\\'))
67 {
68 format!("{part:?}")
69 } else {
70 part.clone()
71 }
72 })
73 .collect::<Vec<_>>()
74 .join(" ")
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
79#[serde(default, deny_unknown_fields)]
80pub struct AgentConfig {
81 pub runner: Option<Runner>,
83
84 pub model: Option<Model>,
86
87 pub reasoning_effort: Option<ReasoningEffort>,
89
90 #[schemars(range(min = 1))]
92 pub iterations: Option<u8>,
93
94 pub followup_reasoning_effort: Option<ReasoningEffort>,
97
98 pub codex_bin: Option<String>,
100
101 pub opencode_bin: Option<String>,
103
104 pub gemini_bin: Option<String>,
106
107 pub claude_bin: Option<String>,
109
110 pub cursor_bin: Option<String>,
114
115 pub kimi_bin: Option<String>,
117
118 pub pi_bin: Option<String>,
120
121 pub claude_permission_mode: Option<ClaudePermissionMode>,
125
126 pub runner_cli: Option<RunnerCliConfigRoot>,
130
131 pub phase_overrides: Option<PhaseOverrides>,
136
137 pub instruction_files: Option<Vec<PathBuf>>,
142
143 pub repoprompt_plan_required: Option<bool>,
145
146 pub repoprompt_tool_injection: Option<bool>,
148
149 pub ci_gate: Option<CiGateConfig>,
151
152 pub git_revert_mode: Option<GitRevertMode>,
154
155 pub git_commit_push_enabled: Option<bool>,
157
158 #[schemars(range(min = 1, max = 3))]
161 pub phases: Option<u8>,
162
163 pub notification: NotificationConfig,
165
166 pub webhook: WebhookConfig,
168
169 #[schemars(range(min = 1))]
173 pub session_timeout_hours: Option<u64>,
174
175 pub scan_prompt_version: Option<ScanPromptVersion>,
177
178 pub runner_retry: RunnerRetryConfig,
180}
181
182impl AgentConfig {
183 pub fn ci_gate_enabled(&self) -> bool {
184 self.ci_gate
185 .as_ref()
186 .map(CiGateConfig::is_enabled)
187 .unwrap_or(true)
188 }
189
190 pub fn ci_gate_display_string(&self) -> String {
191 self.ci_gate
192 .as_ref()
193 .map(CiGateConfig::display_string)
194 .unwrap_or_else(|| "make ci".to_string())
195 }
196
197 pub fn merge_from(&mut self, other: Self) {
198 if other.runner.is_some() {
199 self.runner = other.runner;
200 }
201 if other.model.is_some() {
202 self.model = other.model;
203 }
204 if other.reasoning_effort.is_some() {
205 self.reasoning_effort = other.reasoning_effort;
206 }
207 if other.iterations.is_some() {
208 self.iterations = other.iterations;
209 }
210 if other.followup_reasoning_effort.is_some() {
211 self.followup_reasoning_effort = other.followup_reasoning_effort;
212 }
213 if other.codex_bin.is_some() {
214 self.codex_bin = other.codex_bin;
215 }
216 if other.opencode_bin.is_some() {
217 self.opencode_bin = other.opencode_bin;
218 }
219 if other.gemini_bin.is_some() {
220 self.gemini_bin = other.gemini_bin;
221 }
222 if other.claude_bin.is_some() {
223 self.claude_bin = other.claude_bin;
224 }
225 if other.cursor_bin.is_some() {
226 self.cursor_bin = other.cursor_bin;
227 }
228 if other.kimi_bin.is_some() {
229 self.kimi_bin = other.kimi_bin;
230 }
231 if other.pi_bin.is_some() {
232 self.pi_bin = other.pi_bin;
233 }
234 if other.phases.is_some() {
235 self.phases = other.phases;
236 }
237 if other.claude_permission_mode.is_some() {
238 self.claude_permission_mode = other.claude_permission_mode;
239 }
240 if let Some(other_runner_cli) = other.runner_cli {
241 match &mut self.runner_cli {
242 Some(existing) => existing.merge_from(other_runner_cli),
243 None => self.runner_cli = Some(other_runner_cli),
244 }
245 }
246 if let Some(other_phase_overrides) = other.phase_overrides {
247 match &mut self.phase_overrides {
248 Some(existing) => existing.merge_from(other_phase_overrides),
249 None => self.phase_overrides = Some(other_phase_overrides),
250 }
251 }
252 if other.instruction_files.is_some() {
253 self.instruction_files = other.instruction_files;
254 }
255 if other.repoprompt_plan_required.is_some() {
256 self.repoprompt_plan_required = other.repoprompt_plan_required;
257 }
258 if other.repoprompt_tool_injection.is_some() {
259 self.repoprompt_tool_injection = other.repoprompt_tool_injection;
260 }
261 if let Some(other_ci_gate) = other.ci_gate {
262 match &mut self.ci_gate {
263 Some(existing) => existing.merge_from(other_ci_gate),
264 None => self.ci_gate = Some(other_ci_gate),
265 }
266 }
267 if other.git_revert_mode.is_some() {
268 self.git_revert_mode = other.git_revert_mode;
269 }
270 if other.git_commit_push_enabled.is_some() {
271 self.git_commit_push_enabled = other.git_commit_push_enabled;
272 }
273 self.notification.merge_from(other.notification);
274 self.webhook.merge_from(other.webhook);
275 if other.session_timeout_hours.is_some() {
276 self.session_timeout_hours = other.session_timeout_hours;
277 }
278 if other.scan_prompt_version.is_some() {
279 self.scan_prompt_version = other.scan_prompt_version;
280 }
281 self.runner_retry.merge_from(other.runner_retry);
282 }
283}