1use 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#[cfg(test)]
20use crate::workspace::MemoryWorkspace;
21use std::path::Path;
22
23pub struct PhaseContext<'a> {
28 pub config: &'a Config,
30 pub registry: &'a AgentRegistry,
32 pub logger: &'a Logger,
34 pub colors: &'a Colors,
36 pub timer: &'a mut Timer,
38 pub stats: &'a mut Stats,
40 pub developer_agent: &'a str,
42 pub reviewer_agent: &'a str,
44 pub review_guidelines: Option<&'a ReviewGuidelines>,
46 pub template_context: &'a TemplateContext,
48 pub run_context: RunContext,
50 pub execution_history: ExecutionHistory,
52 pub prompt_history: std::collections::HashMap<String, String>,
54 pub executor: &'a dyn ProcessExecutor,
56 pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
58 pub repo_root: &'a Path,
64 pub workspace: &'a dyn Workspace,
73}
74
75impl PhaseContext<'_> {
76 pub fn record_developer_iteration(&mut self) {
78 self.run_context.record_developer_iteration();
79 }
80
81 pub fn record_reviewer_pass(&mut self) {
83 self.run_context.record_reviewer_pass();
84 }
85
86 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 pub fn clone_prompt_history(&self) -> std::collections::HashMap<String, String> {
105 self.prompt_history.clone()
106 }
107}
108
109pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
115 let fallback_config = ctx.registry.fallback_config();
116
117 let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
119 if !commit_agents.is_empty() {
120 return commit_agents.first().cloned();
122 }
123
124 let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
126 if !reviewer_agents.is_empty() {
127 return reviewer_agents.first().cloned();
128 }
129
130 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 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 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 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: ®istry,
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 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: ®istry,
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 let mut fixture = TestFixture::new();
273 let ctx = PhaseContext {
274 config: &fixture.config,
275 registry: ®istry,
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 assert!(
299 result.is_some(),
300 "Should return Some agent even with no chains configured"
301 );
302
303 assert_ne!(
305 result.as_deref(),
306 Some("fallback-developer"),
307 "Should NOT fall back to developer agent - should use reviewer"
308 );
309 }
310}