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::logging::RunLogContext;
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> {
49 pub config: &'a Config,
51 pub registry: &'a AgentRegistry,
53 pub logger: &'a Logger,
55 pub colors: &'a Colors,
57 pub timer: &'a mut Timer,
59 pub developer_agent: &'a str,
61 pub reviewer_agent: &'a str,
63 pub review_guidelines: Option<&'a ReviewGuidelines>,
65 pub template_context: &'a TemplateContext,
67 pub run_context: RunContext,
69 pub execution_history: ExecutionHistory,
71 pub prompt_history: std::collections::HashMap<String, String>,
73 pub executor: &'a dyn ProcessExecutor,
75 pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
77 pub repo_root: &'a Path,
83 pub workspace: &'a dyn Workspace,
92 pub run_log_context: &'a RunLogContext,
98}
99
100impl PhaseContext<'_> {
101 pub fn record_developer_iteration(&mut self) {
103 self.run_context.record_developer_iteration();
104 }
105
106 pub fn record_reviewer_pass(&mut self) {
108 self.run_context.record_reviewer_pass();
109 }
110
111 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 pub fn clone_prompt_history(&self) -> std::collections::HashMap<String, String> {
130 self.prompt_history.clone()
131 }
132}
133
134pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
140 let fallback_config = ctx.registry.fallback_config();
141
142 let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
144 if !commit_agents.is_empty() {
145 return commit_agents.first().cloned();
147 }
148
149 let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
151 if !reviewer_agents.is_empty() {
152 return reviewer_agents.first().cloned();
153 }
154
155 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 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 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 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: ®istry,
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 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: ®istry,
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 let mut fixture = TestFixture::new();
299 let ctx = PhaseContext {
300 config: &fixture.config,
301 registry: ®istry,
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 assert!(
325 result.is_some(),
326 "Should return Some agent even with no chains configured"
327 );
328
329 assert_ne!(
331 result.as_deref(),
332 Some("fallback-developer"),
333 "Should NOT fall back to developer agent - should use reviewer"
334 );
335 }
336}