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::{AgentDrain, AgentRegistry};
8use crate::checkpoint::execution_history::ExecutionHistory;
9use crate::checkpoint::RunContext;
10use crate::config::Config;
11use crate::guidelines::ReviewGuidelines;
12use crate::logger::{Colors, Logger};
13use crate::logging::RunLogContext;
14use crate::pipeline::Timer;
15use crate::prompts::template_context::TemplateContext;
16use crate::workspace::Workspace;
17use crate::ProcessExecutor;
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    /// Process executor for external process execution.
72    pub executor: &'a dyn ProcessExecutor,
73    /// Arc-wrapped executor for spawning into threads (e.g., idle timeout monitor).
74    pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
75    /// Repository root path for explicit file operations.
76    ///
77    /// This eliminates CWD dependencies by providing an explicit path for all
78    /// file operations. Code should use `repo_root.join("relative/path")` instead
79    /// of `Path::new("relative/path")`.
80    pub repo_root: &'a Path,
81    /// Workspace for explicit path resolution and file operations.
82    ///
83    /// Provides convenient methods for file operations and path resolution
84    /// without depending on the current working directory.
85    ///
86    /// This uses trait object (`&dyn Workspace`) for proper dependency injection:
87    /// - Production code passes `&WorkspaceFs` (real filesystem)
88    /// - Tests can pass `&MemoryWorkspace` (in-memory storage)
89    pub workspace: &'a dyn Workspace,
90    /// Arc-wrapped workspace for spawning into threads (e.g., file activity monitor).
91    pub workspace_arc: std::sync::Arc<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    /// Cloud reporter for progress updates (None in CLI mode).
99    ///
100    /// When cloud mode is disabled, this is None and no cloud reporting occurs.
101    /// When enabled, this is Some(&dyn `CloudReporter`) for API communication.
102    pub cloud_reporter: Option<&'a dyn crate::cloud::CloudReporter>,
103    /// Cloud configuration.
104    ///
105    /// When cloud mode is disabled (enabled=false), all cloud-specific
106    /// logic is skipped throughout the pipeline.
107    pub cloud: &'a crate::config::types::CloudConfig,
108    /// Git environment for configuring authentication variables.
109    ///
110    /// Used by cloud handlers to configure GIT_SSH_COMMAND and GIT_TERMINAL_PROMPT
111    /// without calling `std::env::set_var` directly.
112    pub env: &'a dyn crate::runtime::environment::GitEnvironment,
113}
114
115impl PhaseContext<'_> {
116    /// Record a completed developer iteration.
117    pub fn record_developer_iteration(&mut self) {
118        self.run_context = self.run_context.clone().record_developer_iteration();
119    }
120
121    /// Record a completed reviewer pass.
122    pub fn record_reviewer_pass(&mut self) {
123        self.run_context = self.run_context.clone().record_reviewer_pass();
124    }
125}
126
127/// Get the primary commit agent from the registry.
128///
129/// This function returns the name of the primary commit agent.
130/// If a commit-specific agent is configured, it uses that. Otherwise, it falls back
131/// to using the reviewer chain (since commit generation is typically done after review).
132#[must_use]
133pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
134    if let Some(commit_binding) = ctx.registry.resolved_drain(AgentDrain::Commit) {
135        return commit_binding.agents.first().cloned();
136    }
137
138    // Fallback to using reviewer agents for commit generation
139    let reviewer_agents = ctx
140        .registry
141        .resolved_drain(AgentDrain::Review)
142        .map_or(&[] as &[String], |binding| binding.agents.as_slice());
143    if !reviewer_agents.is_empty() {
144        return reviewer_agents.first().cloned();
145    }
146
147    // Last resort: use the current reviewer agent
148    Some(ctx.reviewer_agent.to_string())
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::config::Config;
155    use crate::executor::MockProcessExecutor;
156    use crate::logger::{Colors, Logger};
157    use crate::pipeline::Timer;
158    use crate::prompts::template_context::TemplateContext;
159    use std::path::PathBuf;
160
161    /// Test fixture for creating `PhaseContext` in tests.
162    ///
163    /// Uses `MemoryWorkspace` instead of `WorkspaceFs` for proper dependency injection.
164    /// This allows tests to run without touching the real filesystem.
165    struct TestFixture {
166        config: Config,
167        colors: Colors,
168        logger: Logger,
169        timer: Timer,
170        template_context: TemplateContext,
171        executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
172        repo_root: PathBuf,
173        workspace: MemoryWorkspace,
174        workspace_arc: std::sync::Arc<dyn Workspace>,
175        run_log_context: crate::logging::RunLogContext,
176    }
177
178    impl TestFixture {
179        fn new() -> Self {
180            let colors = Colors { enabled: false };
181            let executor_arc = std::sync::Arc::new(MockProcessExecutor::new())
182                as std::sync::Arc<dyn crate::executor::ProcessExecutor>;
183            let repo_root = PathBuf::from("/test/repo");
184            // Use MemoryWorkspace for testing - no real filesystem access
185            let workspace = MemoryWorkspace::new(repo_root.clone());
186            let workspace_arc =
187                std::sync::Arc::new(workspace.clone()) as std::sync::Arc<dyn Workspace>;
188            let run_log_context = crate::logging::RunLogContext::new(&workspace).unwrap();
189            Self {
190                config: Config::default(),
191                colors,
192                logger: Logger::new(colors),
193                timer: Timer::new(),
194                template_context: TemplateContext::default(),
195                executor_arc,
196                repo_root,
197                workspace,
198                workspace_arc,
199                run_log_context,
200            }
201        }
202    }
203
204    #[test]
205    fn test_get_primary_commit_agent_uses_commit_chain_first() {
206        let toml_str = r#"
207            [agent_chain]
208            commit = ["commit-agent-1", "commit-agent-2"]
209            reviewer = ["reviewer-agent"]
210            developer = ["developer-agent"]
211        "#;
212        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
213        let registry = AgentRegistry::new()
214            .unwrap()
215            .apply_unified_config(&unified)
216            .unwrap();
217
218        let mut fixture = TestFixture::new();
219        let git_env = crate::runtime::environment::mock::MockGitEnvironment::new();
220        let ctx = PhaseContext {
221            config: &fixture.config,
222            registry: &registry,
223            logger: &fixture.logger,
224            colors: &fixture.colors,
225            timer: &mut fixture.timer,
226            developer_agent: "developer-agent",
227            reviewer_agent: "reviewer-agent",
228            review_guidelines: None,
229            template_context: &fixture.template_context,
230            run_context: RunContext::new(),
231            execution_history: ExecutionHistory::new(),
232            executor: fixture.executor_arc.as_ref(),
233            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
234            repo_root: &fixture.repo_root,
235            workspace: &fixture.workspace,
236            workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
237            run_log_context: &fixture.run_log_context,
238            cloud_reporter: None,
239            cloud: &crate::config::types::CloudConfig::disabled(),
240            env: &git_env,
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 toml_str = r#"
254            [agent_chain]
255            reviewer = ["reviewer-agent-1", "reviewer-agent-2"]
256            developer = ["developer-agent"]
257        "#;
258        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
259        let registry = AgentRegistry::new()
260            .unwrap()
261            .apply_unified_config(&unified)
262            .unwrap();
263
264        let mut fixture = TestFixture::new();
265        let git_env = crate::runtime::environment::mock::MockGitEnvironment::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: "developer-agent",
273            reviewer_agent: "reviewer-agent-1",
274            review_guidelines: None,
275            template_context: &fixture.template_context,
276            run_context: RunContext::new(),
277            execution_history: ExecutionHistory::new(),
278            executor: fixture.executor_arc.as_ref(),
279            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
280            repo_root: &fixture.repo_root,
281            workspace: &fixture.workspace,
282            workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
283            run_log_context: &fixture.run_log_context,
284            cloud_reporter: None,
285            cloud: &crate::config::types::CloudConfig::disabled(),
286            env: &git_env,
287        };
288
289        let result = get_primary_commit_agent(&ctx);
290        assert_eq!(
291            result,
292            Some("reviewer-agent-1".to_string()),
293            "Should fall back to first agent from reviewer chain when commit chain is not configured"
294        );
295    }
296
297    #[test]
298    fn test_get_primary_commit_agent_uses_context_reviewer_as_last_resort() {
299        let registry = AgentRegistry::new().unwrap();
300        // Default registry with no custom chains configured
301
302        let mut fixture = TestFixture::new();
303        let git_env = crate::runtime::environment::mock::MockGitEnvironment::new();
304        let ctx = PhaseContext {
305            config: &fixture.config,
306            registry: &registry,
307            logger: &fixture.logger,
308            colors: &fixture.colors,
309            timer: &mut fixture.timer,
310            developer_agent: "fallback-developer",
311            reviewer_agent: "fallback-reviewer",
312            review_guidelines: None,
313            template_context: &fixture.template_context,
314            run_context: RunContext::new(),
315            execution_history: ExecutionHistory::new(),
316            executor: fixture.executor_arc.as_ref(),
317            executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
318            repo_root: &fixture.repo_root,
319            workspace: &fixture.workspace,
320            workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
321            run_log_context: &fixture.run_log_context,
322            cloud_reporter: None,
323            cloud: &crate::config::types::CloudConfig::disabled(),
324            env: &git_env,
325        };
326
327        let result = get_primary_commit_agent(&ctx);
328
329        // When no chains are configured, it should fall back to the context's reviewer_agent
330        // OR the default reviewer from the registry (if it has a default)
331        // The key point is it should NOT use developer agent
332        assert!(
333            result.is_some(),
334            "Should return Some agent even with no chains configured"
335        );
336
337        // Verify it's not using the developer agent
338        assert_ne!(
339            result.as_deref(),
340            Some("fallback-developer"),
341            "Should NOT fall back to developer agent - should use reviewer"
342        );
343    }
344}