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