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 executor: &'a dyn ProcessExecutor,
73 pub executor_arc: std::sync::Arc<dyn ProcessExecutor>,
75 pub repo_root: &'a Path,
81 pub workspace: &'a dyn Workspace,
90 pub workspace_arc: std::sync::Arc<dyn Workspace>,
92 pub run_log_context: &'a RunLogContext,
98 pub cloud_reporter: Option<&'a dyn crate::cloud::CloudReporter>,
103 pub cloud: &'a crate::config::types::CloudConfig,
108}
109
110impl PhaseContext<'_> {
111 pub const fn record_developer_iteration(&mut self) {
113 self.run_context.record_developer_iteration();
114 }
115
116 pub const fn record_reviewer_pass(&mut self) {
118 self.run_context.record_reviewer_pass();
119 }
120}
121
122#[must_use]
128pub fn get_primary_commit_agent(ctx: &PhaseContext<'_>) -> Option<String> {
129 let fallback_config = ctx.registry.fallback_config();
130
131 let commit_agents = fallback_config.get_fallbacks(AgentRole::Commit);
133 if !commit_agents.is_empty() {
134 return commit_agents.first().cloned();
136 }
137
138 let reviewer_agents = fallback_config.get_fallbacks(AgentRole::Reviewer);
140 if !reviewer_agents.is_empty() {
141 return reviewer_agents.first().cloned();
142 }
143
144 Some(ctx.reviewer_agent.to_string())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::config::Config;
152 use crate::executor::MockProcessExecutor;
153 use crate::logger::{Colors, Logger};
154 use crate::pipeline::Timer;
155 use crate::prompts::template_context::TemplateContext;
156 use std::path::PathBuf;
157
158 struct TestFixture {
163 config: Config,
164 colors: Colors,
165 logger: Logger,
166 timer: Timer,
167 template_context: TemplateContext,
168 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
169 repo_root: PathBuf,
170 workspace: MemoryWorkspace,
171 workspace_arc: std::sync::Arc<dyn Workspace>,
172 run_log_context: crate::logging::RunLogContext,
173 }
174
175 impl TestFixture {
176 fn new() -> Self {
177 let colors = Colors { enabled: false };
178 let executor_arc = std::sync::Arc::new(MockProcessExecutor::new())
179 as std::sync::Arc<dyn crate::executor::ProcessExecutor>;
180 let repo_root = PathBuf::from("/test/repo");
181 let workspace = MemoryWorkspace::new(repo_root.clone());
183 let workspace_arc =
184 std::sync::Arc::new(workspace.clone()) as std::sync::Arc<dyn Workspace>;
185 let run_log_context = crate::logging::RunLogContext::new(&workspace).unwrap();
186 Self {
187 config: Config::default(),
188 colors,
189 logger: Logger::new(colors),
190 timer: Timer::new(),
191 template_context: TemplateContext::default(),
192 executor_arc,
193 repo_root,
194 workspace,
195 workspace_arc,
196 run_log_context,
197 }
198 }
199 }
200
201 #[test]
202 fn test_get_primary_commit_agent_uses_commit_chain_first() {
203 let mut registry = AgentRegistry::new().unwrap();
204
205 let toml_str = r#"
207 [agent_chain]
208 commit = ["commit-agent-1", "commit-agent-2"]
209 reviewer = ["reviewer-agent"]
210 developer = ["developer-agent"]
211 "#;
212 let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
213 registry.apply_unified_config(&unified);
214
215 let mut fixture = TestFixture::new();
216 let ctx = PhaseContext {
217 config: &fixture.config,
218 registry: ®istry,
219 logger: &fixture.logger,
220 colors: &fixture.colors,
221 timer: &mut fixture.timer,
222 developer_agent: "developer-agent",
223 reviewer_agent: "reviewer-agent",
224 review_guidelines: None,
225 template_context: &fixture.template_context,
226 run_context: RunContext::new(),
227 execution_history: ExecutionHistory::new(),
228 executor: fixture.executor_arc.as_ref(),
229 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
230 repo_root: &fixture.repo_root,
231 workspace: &fixture.workspace,
232 workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
233 run_log_context: &fixture.run_log_context,
234 cloud_reporter: None,
235 cloud: &crate::config::types::CloudConfig::disabled(),
236 };
237
238 let result = get_primary_commit_agent(&ctx);
239 assert_eq!(
240 result,
241 Some("commit-agent-1".to_string()),
242 "Should use first agent from commit chain when configured"
243 );
244 }
245
246 #[test]
247 fn test_get_primary_commit_agent_falls_back_to_reviewer_chain() {
248 let mut registry = AgentRegistry::new().unwrap();
249
250 let toml_str = r#"
252 [agent_chain]
253 reviewer = ["reviewer-agent-1", "reviewer-agent-2"]
254 developer = ["developer-agent"]
255 "#;
256 let unified: crate::config::UnifiedConfig = toml::from_str(toml_str).unwrap();
257 registry.apply_unified_config(&unified);
258
259 let mut fixture = TestFixture::new();
260 let ctx = PhaseContext {
261 config: &fixture.config,
262 registry: ®istry,
263 logger: &fixture.logger,
264 colors: &fixture.colors,
265 timer: &mut fixture.timer,
266 developer_agent: "developer-agent",
267 reviewer_agent: "reviewer-agent-1",
268 review_guidelines: None,
269 template_context: &fixture.template_context,
270 run_context: RunContext::new(),
271 execution_history: ExecutionHistory::new(),
272 executor: fixture.executor_arc.as_ref(),
273 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
274 repo_root: &fixture.repo_root,
275 workspace: &fixture.workspace,
276 workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
277 run_log_context: &fixture.run_log_context,
278 cloud_reporter: None,
279 cloud: &crate::config::types::CloudConfig::disabled(),
280 };
281
282 let result = get_primary_commit_agent(&ctx);
283 assert_eq!(
284 result,
285 Some("reviewer-agent-1".to_string()),
286 "Should fall back to first agent from reviewer chain when commit chain is not configured"
287 );
288 }
289
290 #[test]
291 fn test_get_primary_commit_agent_uses_context_reviewer_as_last_resort() {
292 let registry = AgentRegistry::new().unwrap();
293 let mut fixture = TestFixture::new();
296 let ctx = PhaseContext {
297 config: &fixture.config,
298 registry: ®istry,
299 logger: &fixture.logger,
300 colors: &fixture.colors,
301 timer: &mut fixture.timer,
302 developer_agent: "fallback-developer",
303 reviewer_agent: "fallback-reviewer",
304 review_guidelines: None,
305 template_context: &fixture.template_context,
306 run_context: RunContext::new(),
307 execution_history: ExecutionHistory::new(),
308 executor: fixture.executor_arc.as_ref(),
309 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
310 repo_root: &fixture.repo_root,
311 workspace: &fixture.workspace,
312 workspace_arc: std::sync::Arc::clone(&fixture.workspace_arc),
313 run_log_context: &fixture.run_log_context,
314 cloud_reporter: None,
315 cloud: &crate::config::types::CloudConfig::disabled(),
316 };
317
318 let result = get_primary_commit_agent(&ctx);
319
320 assert!(
324 result.is_some(),
325 "Should return Some agent even with no chains configured"
326 );
327
328 assert_ne!(
330 result.as_deref(),
331 Some("fallback-developer"),
332 "Should NOT fall back to developer agent - should use reviewer"
333 );
334 }
335}