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