Skip to main content

ralph_workflow/phases/
context.rs

1//! Phase execution context.
2//!
3//! This module defines the shared context that is passed to each phase
4//! of the pipeline. It contains references to configuration, registry,
5//! logging utilities, and runtime state that all phases need access to.
6
7use crate::agents::{AgentRegistry, AgentRole};
8use crate::checkpoint::execution_history::ExecutionHistory;
9use crate::checkpoint::RunContext;
10use crate::config::Config;
11use crate::executor::ProcessExecutor;
12use crate::guidelines::ReviewGuidelines;
13use crate::logger::{Colors, Logger};
14use crate::pipeline::Timer;
15use crate::prompts::template_context::TemplateContext;
16use crate::workspace::Workspace;
17// MemoryWorkspace is used in test fixtures for proper DI
18#[cfg(test)]
19use crate::workspace::MemoryWorkspace;
20use std::path::Path;
21
22/// Shared context for all pipeline phases.
23///
24/// This struct holds references to all the shared state that phases need
25/// to access. It is passed by mutable reference to each phase function.
26pub struct PhaseContext<'a> {
27    /// Configuration settings for the pipeline.
28    pub config: &'a Config,
29    /// Agent registry for looking up agent configurations.
30    pub registry: &'a AgentRegistry,
31    /// Logger for output and diagnostics.
32    pub logger: &'a Logger,
33    /// Terminal color configuration.
34    pub colors: &'a Colors,
35    /// Timer for tracking elapsed time.
36    pub timer: &'a mut Timer,
37    /// Name of the developer agent.
38    pub developer_agent: &'a str,
39    /// Name of the reviewer agent.
40    pub reviewer_agent: &'a str,
41    /// Review guidelines based on detected project stack.
42    pub review_guidelines: Option<&'a ReviewGuidelines>,
43    /// Template context for loading user templates.
44    pub template_context: &'a TemplateContext,
45    /// Run context for tracking execution lineage and state.
46    pub run_context: RunContext,
47    /// Execution history for tracking pipeline steps.
48    pub execution_history: ExecutionHistory,
49    /// Prompt history for storing prompts used during execution.
50    pub prompt_history: std::collections::HashMap<String, String>,
51    /// Process executor for external process execution.
52    pub executor: &'a dyn ProcessExecutor,
53    /// Arc-wrapped executor for spawning into threads (e.g., idle timeout monitor).
54    pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
55    /// Repository root path for explicit file operations.
56    ///
57    /// This eliminates CWD dependencies by providing an explicit path for all
58    /// file operations. Code should use `repo_root.join("relative/path")` instead
59    /// of `Path::new("relative/path")`.
60    pub repo_root: &'a Path,
61    /// Workspace for explicit path resolution and file operations.
62    ///
63    /// Provides convenient methods for file operations and path resolution
64    /// without depending on the current working directory.
65    ///
66    /// This uses trait object (`&dyn Workspace`) for proper dependency injection:
67    /// - Production code passes `&WorkspaceFs` (real filesystem)
68    /// - Tests can pass `&MemoryWorkspace` (in-memory storage)
69    pub workspace: &'a dyn Workspace,
70}
71
72impl PhaseContext<'_> {
73    /// Record a completed developer iteration.
74    pub fn record_developer_iteration(&mut self) {
75        self.run_context.record_developer_iteration();
76    }
77
78    /// Record a completed reviewer pass.
79    pub fn record_reviewer_pass(&mut self) {
80        self.run_context.record_reviewer_pass();
81    }
82
83    /// Capture a prompt in the prompt history.
84    ///
85    /// This method stores a prompt with a key for later retrieval on resume.
86    /// The key should uniquely identify the prompt (e.g., "development_1", "review_2").
87    ///
88    /// # Arguments
89    ///
90    /// * `key` - Unique identifier for this prompt
91    /// * `prompt` - The prompt text to store
92    pub fn capture_prompt(&mut self, key: &str, prompt: &str) {
93        self.prompt_history
94            .insert(key.to_string(), prompt.to_string());
95    }
96
97    /// Clone the prompt history without consuming it.
98    ///
99    /// This is used when building checkpoints to include the prompts
100    /// while keeping them in the context for subsequent checkpoint saves.
101    pub fn clone_prompt_history(&self) -> std::collections::HashMap<String, String> {
102        self.prompt_history.clone()
103    }
104}
105
106/// Get the primary commit agent from the registry.
107///
108/// This function returns the name of the primary commit agent.
109/// If a commit-specific agent is configured, it uses that. Otherwise, it falls back
110/// to using the reviewer chain (since commit generation is typically done after review).
111pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
112    let fallback_config = ctx.registry.fallback_config();
113
114    // First, try to get commit-specific agents
115    let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
116    if !commit_agents.is_empty() {
117        // Return the first commit agent as the primary
118        return commit_agents.first().cloned();
119    }
120
121    // Fallback to using reviewer agents for commit generation
122    let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
123    if !reviewer_agents.is_empty() {
124        return reviewer_agents.first().cloned();
125    }
126
127    // Last resort: use the current reviewer agent
128    Some(ctx.reviewer_agent.to_string())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::config::Config;
135    use crate::executor::MockProcessExecutor;
136    use crate::logger::{Colors, Logger};
137    use crate::pipeline::Timer;
138    use crate::prompts::template_context::TemplateContext;
139    use std::path::PathBuf;
140
141    /// Test fixture for creating `PhaseContext` in tests.
142    ///
143    /// Uses `MemoryWorkspace` instead of `WorkspaceFs` for proper dependency injection.
144    /// This allows tests to run without touching the real filesystem.
145    struct TestFixture {
146        config: Config,
147        colors: Colors,
148        logger: Logger,
149        timer: Timer,
150        template_context: TemplateContext,
151        executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
152        repo_root: PathBuf,
153        workspace: MemoryWorkspace,
154    }
155
156    impl TestFixture {
157        fn new() -> Self {
158            let colors = Colors { enabled: false };
159            let executor_arc = std::sync::Arc::new(MockProcessExecutor::new())
160                as std::sync::Arc<dyn crate::executor::ProcessExecutor>;
161            let repo_root = PathBuf::from("/test/repo");
162            // Use MemoryWorkspace for testing - no real filesystem access
163            let workspace = MemoryWorkspace::new(repo_root.clone());
164            Self {
165                config: Config::default(),
166                colors,
167                logger: Logger::new(colors),
168                timer: Timer::new(),
169                template_context: TemplateContext::default(),
170                executor_arc,
171                repo_root,
172                workspace,
173            }
174        }
175    }
176
177    #[test]
178    fn test_get_primary_commit_agent_uses_commit_chain_first() {
179        let mut registry = AgentRegistry::new().unwrap();
180
181        // Configure a commit chain
182        let toml_str = r#"
183            [agent_chain]
184            commit = ["commit-agent-1", "commit-agent-2"]
185            reviewer = ["reviewer-agent"]
186            developer = ["developer-agent"]
187        "#;
188        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
189        registry.apply_unified_config(&unified);
190
191        let mut fixture = TestFixture::new();
192        let ctx = PhaseContext {
193            config: &fixture.config,
194            registry: &registry,
195            logger: &fixture.logger,
196            colors: &fixture.colors,
197            timer: &mut fixture.timer,
198            developer_agent: "developer-agent",
199            reviewer_agent: "reviewer-agent",
200            review_guidelines: None,
201            template_context: &fixture.template_context,
202            run_context: RunContext::new(),
203            execution_history: ExecutionHistory::new(),
204            prompt_history: std::collections::HashMap::new(),
205            executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
206            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
207            repo_root: &fixture.repo_root,
208            workspace: &fixture.workspace,
209        };
210
211        let result = get_primary_commit_agent(&ctx);
212        assert_eq!(
213            result,
214            Some("commit-agent-1".to_string()),
215            "Should use first agent from commit chain when configured"
216        );
217    }
218
219    #[test]
220    fn test_get_primary_commit_agent_falls_back_to_reviewer_chain() {
221        let mut registry = AgentRegistry::new().unwrap();
222
223        // Configure reviewer chain but NO commit chain
224        let toml_str = r#"
225            [agent_chain]
226            reviewer = ["reviewer-agent-1", "reviewer-agent-2"]
227            developer = ["developer-agent"]
228        "#;
229        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
230        registry.apply_unified_config(&unified);
231
232        let mut fixture = TestFixture::new();
233        let ctx = PhaseContext {
234            config: &fixture.config,
235            registry: &registry,
236            logger: &fixture.logger,
237            colors: &fixture.colors,
238            timer: &mut fixture.timer,
239            developer_agent: "developer-agent",
240            reviewer_agent: "reviewer-agent-1",
241            review_guidelines: None,
242            template_context: &fixture.template_context,
243            run_context: RunContext::new(),
244            execution_history: ExecutionHistory::new(),
245            prompt_history: std::collections::HashMap::new(),
246            executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
247            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
248            repo_root: &fixture.repo_root,
249            workspace: &fixture.workspace,
250        };
251
252        let result = get_primary_commit_agent(&ctx);
253        assert_eq!(
254            result,
255            Some("reviewer-agent-1".to_string()),
256            "Should fall back to first agent from reviewer chain when commit chain is not configured"
257        );
258    }
259
260    #[test]
261    fn test_get_primary_commit_agent_uses_context_reviewer_as_last_resort() {
262        let registry = AgentRegistry::new().unwrap();
263        // Default registry with no custom chains configured
264
265        let mut fixture = TestFixture::new();
266        let ctx = PhaseContext {
267            config: &fixture.config,
268            registry: &registry,
269            logger: &fixture.logger,
270            colors: &fixture.colors,
271            timer: &mut fixture.timer,
272            developer_agent: "fallback-developer",
273            reviewer_agent: "fallback-reviewer",
274            review_guidelines: None,
275            template_context: &fixture.template_context,
276            run_context: RunContext::new(),
277            execution_history: ExecutionHistory::new(),
278            prompt_history: std::collections::HashMap::new(),
279            executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
280            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
281            repo_root: &fixture.repo_root,
282            workspace: &fixture.workspace,
283        };
284
285        let result = get_primary_commit_agent(&ctx);
286
287        // When no chains are configured, it should fall back to the context's reviewer_agent
288        // OR the default reviewer from the registry (if it has a default)
289        // The key point is it should NOT use developer agent
290        assert!(
291            result.is_some(),
292            "Should return Some agent even with no chains configured"
293        );
294
295        // Verify it's not using the developer agent
296        assert_ne!(
297            result.as_deref(),
298            Some("fallback-developer"),
299            "Should NOT fall back to developer agent - should use reviewer"
300        );
301    }
302}