1use crate::agents::AgentRegistry;
7use crate::checkpoint::execution_history::ExecutionHistory;
8use crate::checkpoint::file_state::FileSystemState;
9use crate::checkpoint::state::{
10 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, PipelineCheckpoint, PipelinePhase,
11 RebaseState,
12};
13use crate::checkpoint::RunContext;
14use crate::config::{Config, ReviewDepth};
15use crate::executor::ProcessExecutor;
16use crate::logger::Logger;
17use crate::workspace::Workspace;
18use std::sync::Arc;
19
20pub struct CheckpointBuilder {
35 phase: Option<PipelinePhase>,
36 iteration: u32,
37 total_iterations: u32,
38 reviewer_pass: u32,
39 total_reviewer_passes: u32,
40 developer_agent: Option<String>,
41 reviewer_agent: Option<String>,
42 cli_args: Option<CliArgsSnapshot>,
43 developer_agent_config: Option<AgentConfigSnapshot>,
44 reviewer_agent_config: Option<AgentConfigSnapshot>,
45 rebase_state: RebaseState,
46 config_path: Option<std::path::PathBuf>,
47 git_user_name: Option<String>,
48 git_user_email: Option<String>,
49 run_context: Option<RunContext>,
51 execution_history: Option<ExecutionHistory>,
53 prompt_history: Option<std::collections::HashMap<String, String>>,
54 skip_rebase: Option<bool>,
56 executor: Option<Arc<dyn ProcessExecutor>>,
58}
59
60impl Default for CheckpointBuilder {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl CheckpointBuilder {
67 pub fn new() -> Self {
69 Self {
70 phase: None,
71 iteration: 1,
72 total_iterations: 1,
73 reviewer_pass: 0,
74 total_reviewer_passes: 0,
75 developer_agent: None,
76 reviewer_agent: None,
77 cli_args: None,
78 developer_agent_config: None,
79 reviewer_agent_config: None,
80 rebase_state: RebaseState::default(),
81 config_path: None,
82 git_user_name: None,
83 git_user_email: None,
84 run_context: None,
85 execution_history: None,
86 prompt_history: None,
87 skip_rebase: None,
88 executor: None,
89 }
90 }
91
92 pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
94 self.phase = Some(phase);
95 self.iteration = iteration;
96 self.total_iterations = total_iterations;
97 self
98 }
99
100 pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
102 self.reviewer_pass = pass;
103 self.total_reviewer_passes = total;
104 self
105 }
106
107 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
109 self.developer_agent = Some(developer.to_string());
110 self.reviewer_agent = Some(reviewer.to_string());
111 self
112 }
113
114 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
116 self.cli_args = Some(args);
117 self
118 }
119
120 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
122 self.developer_agent_config = Some(config);
123 self
124 }
125
126 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
128 self.reviewer_agent_config = Some(config);
129 self
130 }
131
132 pub fn rebase_state(mut self, state: RebaseState) -> Self {
134 self.rebase_state = state;
135 self
136 }
137
138 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
140 self.config_path = path;
141 self
142 }
143
144 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
146 self.git_user_name = name.map(String::from);
147 self.git_user_email = email.map(String::from);
148 self
149 }
150
151 pub fn skip_rebase(mut self, value: bool) -> Self {
153 self.skip_rebase = Some(value);
154 self
155 }
156
157 pub fn with_executor(mut self, executor: Arc<dyn ProcessExecutor>) -> Self {
159 self.executor = Some(executor);
160 self
161 }
162
163 pub fn capture_cli_args(mut self, config: &Config) -> Self {
165 let review_depth_str = review_depth_to_string(config.review_depth);
166 let skip_rebase = self.skip_rebase.unwrap_or(false);
167
168 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
169 config.developer_iters,
170 config.reviewer_reviews,
171 config.commit_msg.clone(),
172 review_depth_str,
173 skip_rebase,
174 config.isolation_mode,
175 )
176 .verbosity(config.verbosity as u8)
177 .show_streaming_metrics(config.show_streaming_metrics)
178 .reviewer_json_parser(config.reviewer_json_parser.clone())
179 .build();
180 self.cli_args = Some(snapshot);
181 self
182 }
183
184 pub fn capture_from_context(
189 mut self,
190 config: &Config,
191 registry: &AgentRegistry,
192 developer_name: &str,
193 reviewer_name: &str,
194 logger: &Logger,
195 run_context: &RunContext,
196 ) -> Self {
197 self.run_context = Some(run_context.clone());
199
200 self = self.capture_cli_args(config);
202
203 if let Some(agent_config) = registry.resolve_config(developer_name) {
205 let snapshot = AgentConfigSnapshot::new(
206 developer_name.to_string(),
207 agent_config.cmd.clone(),
208 agent_config.output_flag.clone(),
209 Some(agent_config.yolo_flag.clone()),
210 agent_config.can_commit,
211 )
212 .with_model_override(config.developer_model.clone())
213 .with_provider_override(config.developer_provider.clone())
214 .with_context_level(config.developer_context);
215 self.developer_agent_config = Some(snapshot);
216 self.developer_agent = Some(developer_name.to_string());
217 } else {
218 logger.warn(&format!(
219 "Developer agent '{}' not found in registry",
220 developer_name
221 ));
222 }
223
224 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
226 let snapshot = AgentConfigSnapshot::new(
227 reviewer_name.to_string(),
228 agent_config.cmd.clone(),
229 agent_config.output_flag.clone(),
230 Some(agent_config.yolo_flag.clone()),
231 agent_config.can_commit,
232 )
233 .with_model_override(config.reviewer_model.clone())
234 .with_provider_override(config.reviewer_provider.clone())
235 .with_context_level(config.reviewer_context);
236 self.reviewer_agent_config = Some(snapshot);
237 self.reviewer_agent = Some(reviewer_name.to_string());
238 } else {
239 logger.warn(&format!(
240 "Reviewer agent '{}' not found in registry",
241 reviewer_name
242 ));
243 }
244
245 self.git_user_name = config.git_user_name.clone();
247 self.git_user_email = config.git_user_email.clone();
248
249 self
250 }
251
252 pub fn with_executor_from_context(mut self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
257 self.executor = Some(executor_arc);
258 self
259 }
260
261 pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
266 self.execution_history = Some(history);
267 self
268 }
269
270 pub fn with_prompt_history(
278 mut self,
279 history: std::collections::HashMap<String, String>,
280 ) -> Self {
281 self.prompt_history = if history.is_empty() {
282 None
283 } else {
284 Some(history)
285 };
286 self
287 }
288
289 pub fn build(self) -> Option<PipelineCheckpoint> {
297 self.build_internal(None)
298 }
299
300 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
310 self.build_internal(Some(workspace))
311 }
312
313 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
315 let phase = self.phase?;
316 let developer_agent = self.developer_agent?;
317 let reviewer_agent = self.reviewer_agent?;
318 let cli_args = self.cli_args?;
319 let developer_config = self.developer_agent_config?;
320 let reviewer_config = self.reviewer_agent_config?;
321
322 let git_user_name = self.git_user_name.as_deref();
323 let git_user_email = self.git_user_email.as_deref();
324
325 let run_context = self.run_context.unwrap_or_default();
327
328 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
329 phase,
330 iteration: self.iteration,
331 total_iterations: self.total_iterations,
332 reviewer_pass: self.reviewer_pass,
333 total_reviewer_passes: self.total_reviewer_passes,
334 developer_agent: &developer_agent,
335 reviewer_agent: &reviewer_agent,
336 cli_args,
337 developer_agent_config: developer_config,
338 reviewer_agent_config: reviewer_config,
339 rebase_state: self.rebase_state,
340 git_user_name,
341 git_user_email,
342 run_id: &run_context.run_id,
343 parent_run_id: run_context.parent_run_id.as_deref(),
344 resume_count: run_context.resume_count,
345 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
346 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
347 });
348
349 if let Some(path) = self.config_path {
350 checkpoint = checkpoint.with_config(Some(path));
351 }
352
353 checkpoint.execution_history = self.execution_history;
355
356 checkpoint.prompt_history = self.prompt_history;
358
359 let executor_ref = self.executor.as_ref().map(|e| e.as_ref());
363 checkpoint.file_system_state = if let Some(ws) = workspace {
364 let executor = executor_ref.unwrap_or_else(|| {
365 static DEFAULT_EXECUTOR: std::sync::LazyLock<crate::executor::RealProcessExecutor> =
368 std::sync::LazyLock::new(crate::executor::RealProcessExecutor::new);
369 &*DEFAULT_EXECUTOR
370 });
371 Some(FileSystemState::capture_with_workspace(ws, executor))
372 } else {
373 Some(FileSystemState::capture_with_optional_executor_impl(
374 executor_ref,
375 ))
376 };
377
378 checkpoint.env_snapshot =
380 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
381
382 Some(checkpoint)
383 }
384}
385
386fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
388 match depth {
389 ReviewDepth::Standard => Some("standard".to_string()),
390 ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
391 ReviewDepth::Security => Some("security".to_string()),
392 ReviewDepth::Incremental => Some("incremental".to_string()),
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::review_depth_to_string;
399 use crate::checkpoint::state::{AgentConfigSnapshot, CliArgsSnapshot};
400 use crate::checkpoint::CheckpointBuilder;
401 use crate::checkpoint::PipelinePhase;
402 use crate::config::ReviewDepth;
403
404 #[test]
405 fn test_builder_basic() {
406 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
407 let dev_config =
408 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
409 let rev_config =
410 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
411
412 let checkpoint = CheckpointBuilder::new()
413 .phase(PipelinePhase::Development, 2, 5)
414 .reviewer_pass(1, 2)
415 .agents("dev", "rev")
416 .cli_args(cli_args)
417 .developer_config(dev_config)
418 .reviewer_config(rev_config)
419 .build()
420 .unwrap();
421
422 assert_eq!(checkpoint.phase, PipelinePhase::Development);
423 assert_eq!(checkpoint.iteration, 2);
424 assert_eq!(checkpoint.total_iterations, 5);
425 assert_eq!(checkpoint.reviewer_pass, 1);
426 assert_eq!(checkpoint.total_reviewer_passes, 2);
427 }
428
429 #[test]
430 fn test_builder_missing_required_field() {
431 let result = CheckpointBuilder::new().build();
433 assert!(result.is_none());
434 }
435
436 #[test]
437 fn test_review_depth_to_string() {
438 assert_eq!(
439 review_depth_to_string(ReviewDepth::Standard),
440 Some("standard".to_string())
441 );
442 assert_eq!(
443 review_depth_to_string(ReviewDepth::Comprehensive),
444 Some("comprehensive".to_string())
445 );
446 assert_eq!(
447 review_depth_to_string(ReviewDepth::Security),
448 Some("security".to_string())
449 );
450 assert_eq!(
451 review_depth_to_string(ReviewDepth::Incremental),
452 Some("incremental".to_string())
453 );
454 }
455
456 #[test]
457 fn test_builder_with_prompt_history() {
458 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
459 let dev_config =
460 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
461 let rev_config =
462 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
463
464 let mut prompts = std::collections::HashMap::new();
465 prompts.insert(
466 "development_1".to_string(),
467 "Implement feature X".to_string(),
468 );
469
470 let checkpoint = CheckpointBuilder::new()
471 .phase(PipelinePhase::Development, 2, 5)
472 .reviewer_pass(1, 2)
473 .agents("dev", "rev")
474 .cli_args(cli_args)
475 .developer_config(dev_config)
476 .reviewer_config(rev_config)
477 .with_prompt_history(prompts)
478 .build()
479 .unwrap();
480
481 assert_eq!(checkpoint.phase, PipelinePhase::Development);
482 assert!(checkpoint.prompt_history.is_some());
483 let history = checkpoint.prompt_history.as_ref().unwrap();
484 assert_eq!(history.len(), 1);
485 assert_eq!(
486 history.get("development_1"),
487 Some(&"Implement feature X".to_string())
488 );
489 }
490
491 #[test]
492 fn test_builder_with_prompt_history_multiple() {
493 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
494 let dev_config =
495 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
496 let rev_config =
497 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
498
499 let mut prompts = std::collections::HashMap::new();
500 prompts.insert(
501 "development_1".to_string(),
502 "Implement feature X".to_string(),
503 );
504 prompts.insert("review_1".to_string(), "Review the changes".to_string());
505
506 let checkpoint = CheckpointBuilder::new()
507 .phase(PipelinePhase::Development, 2, 5)
508 .reviewer_pass(1, 2)
509 .agents("dev", "rev")
510 .cli_args(cli_args)
511 .developer_config(dev_config)
512 .reviewer_config(rev_config)
513 .with_prompt_history(prompts)
514 .build()
515 .unwrap();
516
517 assert!(checkpoint.prompt_history.is_some());
518 let history = checkpoint.prompt_history.as_ref().unwrap();
519 assert_eq!(history.len(), 2);
520 assert_eq!(
521 history.get("development_1"),
522 Some(&"Implement feature X".to_string())
523 );
524 assert_eq!(
525 history.get("review_1"),
526 Some(&"Review the changes".to_string())
527 );
528 }
529
530 #[cfg(feature = "test-utils")]
535 mod workspace_tests {
536 use super::*;
537 use crate::workspace::MemoryWorkspace;
538
539 #[test]
540 fn test_builder_with_workspace_captures_file_state() {
541 let workspace =
543 MemoryWorkspace::new_test().with_file("PROMPT.md", "# Test prompt content");
544
545 let cli_args =
546 CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
547 let dev_config =
548 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
549 let rev_config =
550 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
551
552 let checkpoint = CheckpointBuilder::new()
553 .phase(PipelinePhase::Development, 2, 5)
554 .reviewer_pass(1, 2)
555 .agents("dev", "rev")
556 .cli_args(cli_args)
557 .developer_config(dev_config)
558 .reviewer_config(rev_config)
559 .build_with_workspace(&workspace)
560 .unwrap();
561
562 assert!(checkpoint.file_system_state.is_some());
564 let fs_state = checkpoint.file_system_state.as_ref().unwrap();
565
566 assert!(fs_state.files.contains_key("PROMPT.md"));
568 let snapshot = &fs_state.files["PROMPT.md"];
569 assert!(snapshot.exists);
570 assert_eq!(snapshot.size, 21); }
572
573 #[test]
574 fn test_builder_with_workspace_captures_agent_files() {
575 let workspace = MemoryWorkspace::new_test()
577 .with_file("PROMPT.md", "# Test prompt")
578 .with_file(".agent/PLAN.md", "# Plan")
579 .with_file(".agent/ISSUES.md", "# Issues");
580
581 let cli_args =
582 CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
583 let dev_config =
584 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
585 let rev_config =
586 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
587
588 let checkpoint = CheckpointBuilder::new()
589 .phase(PipelinePhase::Review, 2, 5)
590 .reviewer_pass(1, 2)
591 .agents("dev", "rev")
592 .cli_args(cli_args)
593 .developer_config(dev_config)
594 .reviewer_config(rev_config)
595 .build_with_workspace(&workspace)
596 .unwrap();
597
598 let fs_state = checkpoint.file_system_state.as_ref().unwrap();
599
600 assert!(fs_state.files.contains_key(".agent/PLAN.md"));
602 assert!(fs_state.files.contains_key(".agent/ISSUES.md"));
603
604 let plan_snapshot = &fs_state.files[".agent/PLAN.md"];
605 assert!(plan_snapshot.exists);
606
607 let issues_snapshot = &fs_state.files[".agent/ISSUES.md"];
608 assert!(issues_snapshot.exists);
609 }
610 }
611}