Skip to main content

ralph/agent/
repoprompt.rs

1//! RepoPrompt mode and flag resolution.
2//!
3//! Responsibilities:
4//! - Define RepoPromptMode enum with clap ValueEnum support.
5//! - Define RepopromptFlags struct for resolved flag state.
6//! - Resolve RepoPrompt flags from CLI mode, config, and overrides.
7//!
8//! Not handled here:
9//! - CLI argument struct definitions (see `super::args`).
10//! - Override resolution logic (see `super::resolve`).
11//! - Runner/model parsing (see `crate::runner`).
12//!
13//! Invariants/assumptions:
14//! - RepoPromptMode::Tools enables tool injection without plan requirement.
15//! - RepoPromptMode::Plan enables both tool injection and plan requirement.
16//! - RepoPromptMode::Off disables both features.
17//! - Config values are used as fallback when CLI mode is not specified.
18
19use crate::config;
20use crate::contracts::AgentConfig;
21use clap::ValueEnum;
22
23/// RepoPrompt mode selection from CLI.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
25pub enum RepoPromptMode {
26    #[value(name = "tools")]
27    Tools,
28    #[value(name = "plan")]
29    Plan,
30    #[value(name = "off")]
31    Off,
32}
33
34/// Resolved RepoPrompt flags after processing mode/config/overrides.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct RepopromptFlags {
37    pub plan_required: bool,
38    pub tool_injection: bool,
39}
40
41/// Convert a RepoPromptMode to its corresponding flags.
42pub(crate) fn repoprompt_flags_from_mode(mode: RepoPromptMode) -> RepopromptFlags {
43    match mode {
44        RepoPromptMode::Tools => RepopromptFlags {
45            plan_required: false,
46            tool_injection: true,
47        },
48        RepoPromptMode::Plan => RepopromptFlags {
49            plan_required: true,
50            tool_injection: true,
51        },
52        RepoPromptMode::Off => RepopromptFlags {
53            plan_required: false,
54            tool_injection: false,
55        },
56    }
57}
58
59/// Resolve RepoPrompt flags from agent config defaults.
60pub(crate) fn resolve_repoprompt_flags_from_agent_config(agent: &AgentConfig) -> RepopromptFlags {
61    let plan_required = agent.repoprompt_plan_required.unwrap_or(false);
62    let tool_injection = agent.repoprompt_tool_injection.unwrap_or(false);
63    RepopromptFlags {
64        plan_required,
65        tool_injection,
66    }
67}
68
69/// Resolve RepoPrompt flags from CLI mode or config defaults.
70pub fn resolve_repoprompt_flags(
71    repo_prompt: Option<RepoPromptMode>,
72    resolved: &config::Resolved,
73) -> RepopromptFlags {
74    if let Some(mode) = repo_prompt {
75        return repoprompt_flags_from_mode(mode);
76    }
77    resolve_repoprompt_flags_from_agent_config(&resolved.config.agent)
78}
79
80/// Resolve whether RepoPrompt tooling reminder injection is required.
81pub fn resolve_rp_required(
82    repo_prompt: Option<RepoPromptMode>,
83    resolved: &config::Resolved,
84) -> bool {
85    resolve_repoprompt_flags(repo_prompt, resolved).tool_injection
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::contracts::{
92        AgentConfig, ClaudePermissionMode, Config, GitRevertMode, NotificationConfig, QueueConfig,
93        RunnerRetryConfig,
94    };
95    use tempfile::TempDir;
96
97    fn resolved_with_defaults() -> config::Resolved {
98        let dir = TempDir::new().expect("temp dir");
99        let repo_root = dir.path().to_path_buf();
100
101        let cfg = Config {
102            agent: AgentConfig {
103                runner: None,
104                model: None,
105                reasoning_effort: None,
106                iterations: None,
107                followup_reasoning_effort: None,
108                codex_bin: Some("codex".to_string()),
109                opencode_bin: Some("opencode".to_string()),
110                gemini_bin: Some("gemini".to_string()),
111                claude_bin: Some("claude".to_string()),
112                cursor_bin: Some("agent".to_string()),
113                kimi_bin: Some("kimi".to_string()),
114                pi_bin: Some("pi".to_string()),
115                phases: Some(2),
116                claude_permission_mode: Some(ClaudePermissionMode::BypassPermissions),
117                runner_cli: None,
118                phase_overrides: None,
119                instruction_files: None,
120                repoprompt_plan_required: None,
121                repoprompt_tool_injection: None,
122                ci_gate: Some(crate::contracts::CiGateConfig {
123                    enabled: Some(true),
124                    argv: Some(vec!["make".to_string(), "ci".to_string()]),
125                }),
126                git_revert_mode: Some(GitRevertMode::Ask),
127                git_commit_push_enabled: Some(true),
128                notification: NotificationConfig::default(),
129                webhook: crate::contracts::WebhookConfig::default(),
130                runner_retry: RunnerRetryConfig::default(),
131                session_timeout_hours: None,
132                scan_prompt_version: None,
133            },
134            queue: QueueConfig::default(),
135            ..Config::default()
136        };
137
138        config::Resolved {
139            config: cfg,
140            repo_root: repo_root.clone(),
141            queue_path: repo_root.join(".ralph/queue.json"),
142            done_path: repo_root.join(".ralph/done.json"),
143            id_prefix: "RQ".to_string(),
144            id_width: 4,
145            global_config_path: None,
146            project_config_path: Some(repo_root.join(".ralph/config.json")),
147        }
148    }
149
150    #[test]
151    fn resolve_rp_required_cli_plan_overrides_config() {
152        let resolved = resolved_with_defaults();
153        assert!(resolve_rp_required(Some(RepoPromptMode::Plan), &resolved));
154    }
155
156    #[test]
157    fn resolve_rp_required_cli_off_overrides_config() {
158        let resolved = resolved_with_defaults();
159        assert!(!resolve_rp_required(Some(RepoPromptMode::Off), &resolved));
160    }
161
162    #[test]
163    fn resolve_rp_required_uses_config_when_cli_not_set() {
164        let mut resolved = resolved_with_defaults();
165        resolved.config.agent.repoprompt_tool_injection = Some(true);
166        assert!(resolve_rp_required(None, &resolved));
167
168        resolved.config.agent.repoprompt_tool_injection = Some(false);
169        assert!(!resolve_rp_required(None, &resolved));
170    }
171
172    #[test]
173    fn resolve_repoprompt_flags_defaults_false_when_unset() {
174        let resolved = resolved_with_defaults();
175        let flags = resolve_repoprompt_flags(None, &resolved);
176        assert!(!flags.plan_required);
177        assert!(!flags.tool_injection);
178    }
179
180    #[test]
181    fn resolve_repoprompt_flags_uses_config_fields() {
182        let mut resolved = resolved_with_defaults();
183        resolved.config.agent.repoprompt_plan_required = Some(true);
184        resolved.config.agent.repoprompt_tool_injection = Some(false);
185
186        let flags = resolve_repoprompt_flags(None, &resolved);
187        assert!(flags.plan_required);
188        assert!(!flags.tool_injection);
189    }
190}