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::Timer;
15use crate::prompts::template_context::TemplateContext;
16use crate::workspace::Workspace;
17#[cfg(test)]
19use crate::workspace::MemoryWorkspace;
20use std::path::Path;
21
22pub struct PhaseContext<'a> {
27 pub config: &'a Config,
29 pub registry: &'a AgentRegistry,
31 pub logger: &'a Logger,
33 pub colors: &'a Colors,
35 pub timer: &'a mut Timer,
37 pub developer_agent: &'a str,
39 pub reviewer_agent: &'a str,
41 pub review_guidelines: Option<&'a ReviewGuidelines>,
43 pub template_context: &'a TemplateContext,
45 pub run_context: RunContext,
47 pub execution_history: ExecutionHistory,
49 pub prompt_history: std::collections::HashMap<String, String>,
51 pub executor: &'a dyn ProcessExecutor,
53 pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
55 pub repo_root: &'a Path,
61 pub workspace: &'a dyn Workspace,
70}
71
72impl PhaseContext<'_> {
73 pub fn record_developer_iteration(&mut self) {
75 self.run_context.record_developer_iteration();
76 }
77
78 pub fn record_reviewer_pass(&mut self) {
80 self.run_context.record_reviewer_pass();
81 }
82
83 pub fn capture_prompt(&mut self, key: &str, prompt: &str) {
93 self.prompt_history
94 .insert(key.to_string(), prompt.to_string());
95 }
96
97 pub fn clone_prompt_history(&self) -> std::collections::HashMap<String, String> {
102 self.prompt_history.clone()
103 }
104}
105
106pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
112 let fallback_config = ctx.registry.fallback_config();
113
114 let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
116 if !commit_agents.is_empty() {
117 return commit_agents.first().cloned();
119 }
120
121 let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
123 if !reviewer_agents.is_empty() {
124 return reviewer_agents.first().cloned();
125 }
126
127 Some(ctx.reviewer_agent.to_string())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::config::Config;
135 use crate::executor::MockProcessExecutor;
136 use crate::logger::{Colors, Logger};
137 use crate::pipeline::Timer;
138 use crate::prompts::template_context::TemplateContext;
139 use std::path::PathBuf;
140
141 struct TestFixture {
146 config: Config,
147 colors: Colors,
148 logger: Logger,
149 timer: Timer,
150 template_context: TemplateContext,
151 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
152 repo_root: PathBuf,
153 workspace: MemoryWorkspace,
154 }
155
156 impl TestFixture {
157 fn new() -> Self {
158 let colors = Colors { enabled: false };
159 let executor_arc = std::sync::Arc::new(MockProcessExecutor::new())
160 as std::sync::Arc<dyn crate::executor::ProcessExecutor>;
161 let repo_root = PathBuf::from("/test/repo");
162 let workspace = MemoryWorkspace::new(repo_root.clone());
164 Self {
165 config: Config::default(),
166 colors,
167 logger: Logger::new(colors),
168 timer: Timer::new(),
169 template_context: TemplateContext::default(),
170 executor_arc,
171 repo_root,
172 workspace,
173 }
174 }
175 }
176
177 #[test]
178 fn test_get_primary_commit_agent_uses_commit_chain_first() {
179 let mut registry = AgentRegistry::new().unwrap();
180
181 let toml_str = r#"
183 [agent_chain]
184 commit = ["commit-agent-1", "commit-agent-2"]
185 reviewer = ["reviewer-agent"]
186 developer = ["developer-agent"]
187 "#;
188 let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
189 registry.apply_unified_config(&unified);
190
191 let mut fixture = TestFixture::new();
192 let ctx = PhaseContext {
193 config: &fixture.config,
194 registry: ®istry,
195 logger: &fixture.logger,
196 colors: &fixture.colors,
197 timer: &mut fixture.timer,
198 developer_agent: "developer-agent",
199 reviewer_agent: "reviewer-agent",
200 review_guidelines: None,
201 template_context: &fixture.template_context,
202 run_context: RunContext::new(),
203 execution_history: ExecutionHistory::new(),
204 prompt_history: std::collections::HashMap::new(),
205 executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
206 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
207 repo_root: &fixture.repo_root,
208 workspace: &fixture.workspace,
209 };
210
211 let result = get_primary_commit_agent(&ctx);
212 assert_eq!(
213 result,
214 Some("commit-agent-1".to_string()),
215 "Should use first agent from commit chain when configured"
216 );
217 }
218
219 #[test]
220 fn test_get_primary_commit_agent_falls_back_to_reviewer_chain() {
221 let mut registry = AgentRegistry::new().unwrap();
222
223 let toml_str = r#"
225 [agent_chain]
226 reviewer = ["reviewer-agent-1", "reviewer-agent-2"]
227 developer = ["developer-agent"]
228 "#;
229 let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
230 registry.apply_unified_config(&unified);
231
232 let mut fixture = TestFixture::new();
233 let ctx = PhaseContext {
234 config: &fixture.config,
235 registry: ®istry,
236 logger: &fixture.logger,
237 colors: &fixture.colors,
238 timer: &mut fixture.timer,
239 developer_agent: "developer-agent",
240 reviewer_agent: "reviewer-agent-1",
241 review_guidelines: None,
242 template_context: &fixture.template_context,
243 run_context: RunContext::new(),
244 execution_history: ExecutionHistory::new(),
245 prompt_history: std::collections::HashMap::new(),
246 executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
247 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
248 repo_root: &fixture.repo_root,
249 workspace: &fixture.workspace,
250 };
251
252 let result = get_primary_commit_agent(&ctx);
253 assert_eq!(
254 result,
255 Some("reviewer-agent-1".to_string()),
256 "Should fall back to first agent from reviewer chain when commit chain is not configured"
257 );
258 }
259
260 #[test]
261 fn test_get_primary_commit_agent_uses_context_reviewer_as_last_resort() {
262 let registry = AgentRegistry::new().unwrap();
263 let mut fixture = TestFixture::new();
266 let ctx = PhaseContext {
267 config: &fixture.config,
268 registry: ®istry,
269 logger: &fixture.logger,
270 colors: &fixture.colors,
271 timer: &mut fixture.timer,
272 developer_agent: "fallback-developer",
273 reviewer_agent: "fallback-reviewer",
274 review_guidelines: None,
275 template_context: &fixture.template_context,
276 run_context: RunContext::new(),
277 execution_history: ExecutionHistory::new(),
278 prompt_history: std::collections::HashMap::new(),
279 executor: &*std::sync::Arc::new(crate::executor::MockProcessExecutor::new()),
280 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
281 repo_root: &fixture.repo_root,
282 workspace: &fixture.workspace,
283 };
284
285 let result = get_primary_commit_agent(&ctx);
286
287 assert!(
291 result.is_some(),
292 "Should return Some agent even with no chains configured"
293 );
294
295 assert_ne!(
297 result.as_deref(),
298 Some("fallback-developer"),
299 "Should NOT fall back to developer agent - should use reviewer"
300 );
301 }
302}