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::config::Config;
9use crate::guidelines::ReviewGuidelines;
10use crate::logger::{Colors, Logger};
11use crate::pipeline::Stats;
12use crate::pipeline::Timer;
13use crate::prompts::template_context::TemplateContext;
14
15/// Shared context for all pipeline phases.
16///
17/// This struct holds references to all the shared state that phases need
18/// to access. It is passed by mutable reference to each phase function.
19pub struct PhaseContext<'a> {
20    /// Configuration settings for the pipeline.
21    pub config: &'a Config,
22    /// Agent registry for looking up agent configurations.
23    pub registry: &'a AgentRegistry,
24    /// Logger for output and diagnostics.
25    pub logger: &'a Logger,
26    /// Terminal color configuration.
27    pub colors: &'a Colors,
28    /// Timer for tracking elapsed time.
29    pub timer: &'a mut Timer,
30    /// Statistics for tracking pipeline progress.
31    pub stats: &'a mut Stats,
32    /// Name of the developer agent.
33    pub developer_agent: &'a str,
34    /// Name of the reviewer agent.
35    pub reviewer_agent: &'a str,
36    /// Review guidelines based on detected project stack.
37    pub review_guidelines: Option<&'a ReviewGuidelines>,
38    /// Template context for loading user templates.
39    pub template_context: &'a TemplateContext,
40}
41
42impl PhaseContext<'_> {}
43
44/// Get the primary commit agent from the registry.
45///
46/// This function returns the name of the primary commit agent.
47/// If a commit-specific agent is configured, it uses that. Otherwise, it falls back
48/// to using the reviewer chain (since commit generation is typically done after review).
49pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
50    let fallback_config = ctx.registry.fallback_config();
51
52    // First, try to get commit-specific agents
53    let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
54    if !commit_agents.is_empty() {
55        // Return the first commit agent as the primary
56        return commit_agents.first().cloned();
57    }
58
59    // Fallback to using reviewer agents for commit generation
60    let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
61    if !reviewer_agents.is_empty() {
62        return reviewer_agents.first().cloned();
63    }
64
65    // Last resort: use the current reviewer agent
66    Some(ctx.reviewer_agent.to_string())
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::config::Config;
73    use crate::logger::{Colors, Logger};
74    use crate::pipeline::{Stats, Timer};
75    use crate::prompts::template_context::TemplateContext;
76
77    /// Test fixture for creating `PhaseContext` in tests.
78    struct TestFixture {
79        config: Config,
80        colors: Colors,
81        logger: Logger,
82        timer: Timer,
83        stats: Stats,
84        template_context: TemplateContext,
85    }
86
87    impl TestFixture {
88        fn new() -> Self {
89            let colors = Colors { enabled: false };
90            Self {
91                config: Config::default(),
92                colors,
93                logger: Logger::new(colors),
94                timer: Timer::new(),
95                stats: Stats::default(),
96                template_context: TemplateContext::default(),
97            }
98        }
99    }
100
101    #[test]
102    fn test_get_primary_commit_agent_uses_commit_chain_first() {
103        let mut registry = AgentRegistry::new().unwrap();
104
105        // Configure a commit chain
106        let toml_str = r#"
107            [agent_chain]
108            commit = ["commit-agent-1", "commit-agent-2"]
109            reviewer = ["reviewer-agent"]
110            developer = ["developer-agent"]
111        "#;
112        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
113        registry.apply_unified_config(&unified);
114
115        let mut fixture = TestFixture::new();
116        let ctx = PhaseContext {
117            config: &fixture.config,
118            registry: &registry,
119            logger: &fixture.logger,
120            colors: &fixture.colors,
121            timer: &mut fixture.timer,
122            stats: &mut fixture.stats,
123            developer_agent: "developer-agent",
124            reviewer_agent: "reviewer-agent",
125            review_guidelines: None,
126            template_context: &fixture.template_context,
127        };
128
129        let result = get_primary_commit_agent(&ctx);
130        assert_eq!(
131            result,
132            Some("commit-agent-1".to_string()),
133            "Should use first agent from commit chain when configured"
134        );
135    }
136
137    #[test]
138    fn test_get_primary_commit_agent_falls_back_to_reviewer_chain() {
139        let mut registry = AgentRegistry::new().unwrap();
140
141        // Configure reviewer chain but NO commit chain
142        let toml_str = r#"
143            [agent_chain]
144            reviewer = ["reviewer-agent-1", "reviewer-agent-2"]
145            developer = ["developer-agent"]
146        "#;
147        let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
148        registry.apply_unified_config(&unified);
149
150        let mut fixture = TestFixture::new();
151        let ctx = PhaseContext {
152            config: &fixture.config,
153            registry: &registry,
154            logger: &fixture.logger,
155            colors: &fixture.colors,
156            timer: &mut fixture.timer,
157            stats: &mut fixture.stats,
158            developer_agent: "developer-agent",
159            reviewer_agent: "reviewer-agent-1",
160            review_guidelines: None,
161            template_context: &fixture.template_context,
162        };
163
164        let result = get_primary_commit_agent(&ctx);
165        assert_eq!(
166            result,
167            Some("reviewer-agent-1".to_string()),
168            "Should fall back to first agent from reviewer chain when commit chain is not configured"
169        );
170    }
171
172    #[test]
173    fn test_get_primary_commit_agent_uses_context_reviewer_as_last_resort() {
174        let registry = AgentRegistry::new().unwrap();
175        // Default registry with no custom chains configured
176
177        let mut fixture = TestFixture::new();
178        let ctx = PhaseContext {
179            config: &fixture.config,
180            registry: &registry,
181            logger: &fixture.logger,
182            colors: &fixture.colors,
183            timer: &mut fixture.timer,
184            stats: &mut fixture.stats,
185            developer_agent: "fallback-developer",
186            reviewer_agent: "fallback-reviewer",
187            review_guidelines: None,
188            template_context: &fixture.template_context,
189        };
190
191        let result = get_primary_commit_agent(&ctx);
192
193        // When no chains are configured, it should fall back to the context's reviewer_agent
194        // OR the default reviewer from the registry (if it has a default)
195        // The key point is it should NOT use developer agent
196        assert!(
197            result.is_some(),
198            "Should return Some agent even with no chains configured"
199        );
200
201        // Verify it's not using the developer agent
202        assert_ne!(
203            result.as_deref(),
204            Some("fallback-developer"),
205            "Should NOT fall back to developer agent - should use reviewer"
206        );
207    }
208}