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