Skip to main content

ralph/contracts/config/
mod.rs

1//! Configuration contracts for Ralph.
2//!
3//! Responsibilities:
4//! - Define config structs/enums and their merge behavior.
5//! - Provide defaults and schema helpers for configuration serialization.
6//!
7//! Not handled here:
8//! - Reading/writing config files or CLI parsing (see `crate::config`).
9//! - Queue/task contract definitions (see `super::queue` and `super::task`).
10//! - Runner definitions (see `super::runner`).
11//! - Model definitions (see `super::model`).
12//!
13//! Invariants/assumptions:
14//! - Config merge is leaf-wise: `Some` values override, `None` does not.
15//! - Serde/schemars attributes define the config contract.
16
17use crate::constants::defaults::DEFAULT_ID_WIDTH;
18use crate::constants::limits::{
19    DEFAULT_SIZE_WARNING_THRESHOLD_KB, DEFAULT_TASK_COUNT_WARNING_THRESHOLD,
20};
21use crate::constants::queue::{DEFAULT_DONE_FILE, DEFAULT_QUEUE_FILE};
22use crate::constants::timeouts::DEFAULT_SESSION_TIMEOUT_HOURS;
23use crate::contracts::model::{Model, ReasoningEffort};
24use crate::contracts::runner::{
25    ClaudePermissionMode, Runner, RunnerApprovalMode, RunnerCliConfigRoot, RunnerCliOptionsPatch,
26    RunnerOutputFormat, RunnerPlanMode, RunnerSandboxMode, RunnerVerbosity,
27    UnsupportedOptionPolicy,
28};
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31use std::collections::BTreeMap;
32use std::path::PathBuf;
33
34mod agent;
35mod enums;
36mod loop_;
37mod notification;
38mod parallel;
39mod phase;
40mod plugin;
41mod queue;
42mod retry;
43#[cfg(test)]
44mod tests;
45mod webhook;
46
47pub use agent::{AgentConfig, CiGateConfig};
48pub use enums::{GitRevertMode, ProjectType, ScanPromptVersion};
49pub use loop_::LoopConfig;
50pub use notification::NotificationConfig;
51pub use parallel::{ParallelConfig, default_push_backoff_ms};
52pub use phase::{PhaseOverrideConfig, PhaseOverrides};
53pub use plugin::{PluginConfig, PluginsConfig};
54pub use queue::{QueueAgingThresholds, QueueConfig};
55pub use retry::RunnerRetryConfig;
56pub use webhook::{WebhookConfig, WebhookEventSubscription, WebhookQueuePolicy};
57
58/// Root configuration struct for Ralph.
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60#[serde(default, deny_unknown_fields)]
61pub struct Config {
62    /// Schema version for config.
63    pub version: u32,
64
65    /// "code" or "docs". Drives prompt defaults and small workflow decisions.
66    pub project_type: Option<ProjectType>,
67
68    /// Queue-related configuration.
69    pub queue: QueueConfig,
70
71    /// Agent runner defaults (Claude, Codex, OpenCode, Gemini, or Cursor).
72    pub agent: AgentConfig,
73
74    /// Parallel run-loop configuration.
75    pub parallel: ParallelConfig,
76
77    /// Run loop waiting configuration (daemon/continuous mode).
78    #[serde(rename = "loop")]
79    pub loop_field: LoopConfig,
80
81    /// Plugin configuration (enable/disable + per-plugin settings).
82    pub plugins: PluginsConfig,
83
84    /// Optional named profiles for quick workflow switching.
85    ///
86    /// Each profile is an AgentConfig-shaped patch applied over `agent` when selected.
87    /// Profile values override base config but are overridden by CLI flags and task.agent.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub profiles: Option<BTreeMap<String, AgentConfig>>,
90}
91
92impl Default for Config {
93    fn default() -> Self {
94        use std::collections::BTreeMap;
95
96        Self {
97            version: 1,
98            project_type: Some(ProjectType::Code),
99            queue: QueueConfig {
100                file: Some(PathBuf::from(DEFAULT_QUEUE_FILE)),
101                done_file: Some(PathBuf::from(DEFAULT_DONE_FILE)),
102                id_prefix: Some("RQ".to_string()),
103                id_width: Some(DEFAULT_ID_WIDTH as u8),
104                size_warning_threshold_kb: Some(DEFAULT_SIZE_WARNING_THRESHOLD_KB),
105                task_count_warning_threshold: Some(DEFAULT_TASK_COUNT_WARNING_THRESHOLD),
106                max_dependency_depth: Some(10),
107                auto_archive_terminal_after_days: None,
108                aging_thresholds: Some(QueueAgingThresholds {
109                    warning_days: Some(7),
110                    stale_days: Some(14),
111                    rotten_days: Some(30),
112                }),
113            },
114            agent: AgentConfig {
115                runner: Some(Runner::Codex),
116                model: Some(Model::Gpt54),
117                reasoning_effort: Some(ReasoningEffort::Medium),
118                iterations: Some(1),
119                followup_reasoning_effort: None,
120                codex_bin: Some("codex".to_string()),
121                opencode_bin: Some("opencode".to_string()),
122                gemini_bin: Some("gemini".to_string()),
123                claude_bin: Some("claude".to_string()),
124                cursor_bin: Some("agent".to_string()),
125                kimi_bin: Some("kimi".to_string()),
126                pi_bin: Some("pi".to_string()),
127                phases: Some(3),
128                claude_permission_mode: Some(ClaudePermissionMode::BypassPermissions),
129                runner_cli: Some(RunnerCliConfigRoot {
130                    defaults: RunnerCliOptionsPatch {
131                        output_format: Some(RunnerOutputFormat::StreamJson),
132                        verbosity: Some(RunnerVerbosity::Normal),
133                        approval_mode: Some(RunnerApprovalMode::Yolo),
134                        sandbox: Some(RunnerSandboxMode::Default),
135                        plan_mode: Some(RunnerPlanMode::Default),
136                        unsupported_option_policy: Some(UnsupportedOptionPolicy::Warn),
137                    },
138                    runners: BTreeMap::from([
139                        (
140                            Runner::Codex,
141                            RunnerCliOptionsPatch {
142                                sandbox: Some(RunnerSandboxMode::Disabled),
143                                ..RunnerCliOptionsPatch::default()
144                            },
145                        ),
146                        (
147                            Runner::Claude,
148                            RunnerCliOptionsPatch {
149                                verbosity: Some(RunnerVerbosity::Verbose),
150                                ..RunnerCliOptionsPatch::default()
151                            },
152                        ),
153                        (
154                            Runner::Kimi,
155                            RunnerCliOptionsPatch {
156                                approval_mode: Some(RunnerApprovalMode::Yolo),
157                                ..RunnerCliOptionsPatch::default()
158                            },
159                        ),
160                        (
161                            Runner::Pi,
162                            RunnerCliOptionsPatch {
163                                approval_mode: Some(RunnerApprovalMode::Yolo),
164                                ..RunnerCliOptionsPatch::default()
165                            },
166                        ),
167                    ]),
168                }),
169                phase_overrides: None,
170                instruction_files: None,
171                repoprompt_plan_required: Some(false),
172                repoprompt_tool_injection: Some(false),
173                ci_gate: Some(CiGateConfig {
174                    enabled: Some(true),
175                    argv: Some(vec!["make".to_string(), "ci".to_string()]),
176                }),
177                git_revert_mode: Some(GitRevertMode::Ask),
178                git_commit_push_enabled: Some(true),
179                notification: NotificationConfig {
180                    enabled: Some(true),
181                    notify_on_complete: Some(true),
182                    notify_on_fail: Some(true),
183                    notify_on_loop_complete: Some(true),
184                    suppress_when_active: Some(true),
185                    sound_enabled: Some(false),
186                    sound_path: None,
187                    timeout_ms: Some(8000),
188                },
189                webhook: WebhookConfig::default(),
190                runner_retry: RunnerRetryConfig::default(),
191                session_timeout_hours: Some(DEFAULT_SESSION_TIMEOUT_HOURS),
192                scan_prompt_version: Some(ScanPromptVersion::V2),
193            },
194            parallel: ParallelConfig {
195                workers: None,
196                workspace_root: None,
197                max_push_attempts: Some(50),
198                push_backoff_ms: Some(default_push_backoff_ms()),
199                workspace_retention_hours: Some(24),
200            },
201            loop_field: LoopConfig {
202                wait_when_empty: Some(false),
203                empty_poll_ms: Some(30_000),
204                wait_when_blocked: Some(false),
205                wait_poll_ms: Some(1000),
206                wait_timeout_seconds: Some(0),
207                notify_when_unblocked: Some(false),
208            },
209            plugins: PluginsConfig::default(),
210            profiles: None,
211        }
212    }
213}