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