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