ralph_workflow/checkpoint/
builder.rs

1//! Checkpoint builder for convenient checkpoint creation.
2//!
3//! This module provides a builder pattern for creating checkpoints
4//! from various contexts in the pipeline.
5
6use 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::logger::Logger;
16
17/// Builder for creating pipeline checkpoints.
18///
19/// Provides a convenient interface for capturing all necessary state
20/// when creating checkpoints during pipeline execution.
21///
22/// # Example
23///
24/// ```ignore
25/// let checkpoint = CheckpointBuilder::new()
26///     .phase(PipelinePhase::Development, 3, 5)
27///     .reviewer_pass(1, 2)
28///     .capture_from_config(&ctx, &registry, "claude", "codex")
29///     .build();
30/// ```
31pub struct CheckpointBuilder {
32    phase: Option<PipelinePhase>,
33    iteration: u32,
34    total_iterations: u32,
35    reviewer_pass: u32,
36    total_reviewer_passes: u32,
37    developer_agent: Option<String>,
38    reviewer_agent: Option<String>,
39    cli_args: Option<CliArgsSnapshot>,
40    developer_agent_config: Option<AgentConfigSnapshot>,
41    reviewer_agent_config: Option<AgentConfigSnapshot>,
42    rebase_state: RebaseState,
43    config_path: Option<std::path::PathBuf>,
44    git_user_name: Option<String>,
45    git_user_email: Option<String>,
46    // Run context for tracking execution lineage and state
47    run_context: Option<RunContext>,
48    // Hardened resume fields
49    execution_history: Option<ExecutionHistory>,
50    prompt_history: Option<std::collections::HashMap<String, String>>,
51    // Optional skip_rebase flag for CLI args capture
52    skip_rebase: Option<bool>,
53}
54
55impl Default for CheckpointBuilder {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl CheckpointBuilder {
62    /// Create a new checkpoint builder with default values.
63    pub fn new() -> Self {
64        Self {
65            phase: None,
66            iteration: 1,
67            total_iterations: 1,
68            reviewer_pass: 0,
69            total_reviewer_passes: 0,
70            developer_agent: None,
71            reviewer_agent: None,
72            cli_args: None,
73            developer_agent_config: None,
74            reviewer_agent_config: None,
75            rebase_state: RebaseState::default(),
76            config_path: None,
77            git_user_name: None,
78            git_user_email: None,
79            run_context: None,
80            execution_history: None,
81            prompt_history: None,
82            skip_rebase: None,
83        }
84    }
85
86    /// Set the phase and iteration information.
87    pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
88        self.phase = Some(phase);
89        self.iteration = iteration;
90        self.total_iterations = total_iterations;
91        self
92    }
93
94    /// Set the reviewer pass information.
95    pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
96        self.reviewer_pass = pass;
97        self.total_reviewer_passes = total;
98        self
99    }
100
101    /// Set the agent names.
102    #[cfg(test)]
103    pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
104        self.developer_agent = Some(developer.to_string());
105        self.reviewer_agent = Some(reviewer.to_string());
106        self
107    }
108
109    /// Set the CLI arguments snapshot.
110    #[cfg(test)]
111    pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
112        self.cli_args = Some(args);
113        self
114    }
115
116    /// Set the developer agent configuration snapshot.
117    #[cfg(test)]
118    pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
119        self.developer_agent_config = Some(config);
120        self
121    }
122
123    /// Set the reviewer agent configuration snapshot.
124    #[cfg(test)]
125    pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
126        self.reviewer_agent_config = Some(config);
127        self
128    }
129
130    /// Set the rebase state.
131    #[cfg(test)]
132    pub fn rebase_state(mut self, state: RebaseState) -> Self {
133        self.rebase_state = state;
134        self
135    }
136
137    /// Set the config path.
138    #[cfg(test)]
139    pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
140        self.config_path = path;
141        self
142    }
143
144    /// Set the git user name and email.
145    #[cfg(test)]
146    pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
147        self.git_user_name = name.map(String::from);
148        self.git_user_email = email.map(String::from);
149        self
150    }
151
152    /// Set the skip_rebase flag for CLI args capture.
153    pub fn skip_rebase(mut self, value: bool) -> Self {
154        self.skip_rebase = Some(value);
155        self
156    }
157
158    /// Capture CLI arguments from a Config.
159    pub fn capture_cli_args(mut self, config: &Config) -> Self {
160        let review_depth_str = review_depth_to_string(config.review_depth);
161        let skip_rebase = self.skip_rebase.unwrap_or(false);
162
163        let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
164            config.developer_iters,
165            config.reviewer_reviews,
166            config.commit_msg.clone(),
167            review_depth_str,
168            skip_rebase,
169            config.isolation_mode,
170        )
171        .verbosity(config.verbosity as u8)
172        .show_streaming_metrics(config.show_streaming_metrics)
173        .reviewer_json_parser(config.reviewer_json_parser.clone())
174        .build();
175        self.cli_args = Some(snapshot);
176        self
177    }
178
179    /// Capture all configuration from a PhaseContext and AgentRegistry.
180    ///
181    /// This is a convenience method that captures CLI args and both agent configs.
182    /// It takes a PhaseContext which provides access to config, registry, and agents.
183    pub fn capture_from_context(
184        mut self,
185        config: &Config,
186        registry: &AgentRegistry,
187        developer_name: &str,
188        reviewer_name: &str,
189        logger: &Logger,
190        run_context: &RunContext,
191    ) -> Self {
192        // Store run context (cloned for builder ownership)
193        self.run_context = Some(run_context.clone());
194
195        // Capture CLI args
196        self = self.capture_cli_args(config);
197
198        // Capture developer agent config
199        if let Some(agent_config) = registry.resolve_config(developer_name) {
200            let snapshot = AgentConfigSnapshot::new(
201                developer_name.to_string(),
202                agent_config.cmd.clone(),
203                agent_config.output_flag.clone(),
204                Some(agent_config.yolo_flag.clone()),
205                agent_config.can_commit,
206            )
207            .with_model_override(config.developer_model.clone())
208            .with_provider_override(config.developer_provider.clone())
209            .with_context_level(config.developer_context);
210            self.developer_agent_config = Some(snapshot);
211            self.developer_agent = Some(developer_name.to_string());
212        } else {
213            logger.warn(&format!(
214                "Developer agent '{}' not found in registry",
215                developer_name
216            ));
217        }
218
219        // Capture reviewer agent config
220        if let Some(agent_config) = registry.resolve_config(reviewer_name) {
221            let snapshot = AgentConfigSnapshot::new(
222                reviewer_name.to_string(),
223                agent_config.cmd.clone(),
224                agent_config.output_flag.clone(),
225                Some(agent_config.yolo_flag.clone()),
226                agent_config.can_commit,
227            )
228            .with_model_override(config.reviewer_model.clone())
229            .with_provider_override(config.reviewer_provider.clone())
230            .with_context_level(config.reviewer_context);
231            self.reviewer_agent_config = Some(snapshot);
232            self.reviewer_agent = Some(reviewer_name.to_string());
233        } else {
234            logger.warn(&format!(
235                "Reviewer agent '{}' not found in registry",
236                reviewer_name
237            ));
238        }
239
240        // Capture git identity
241        self.git_user_name = config.git_user_name.clone();
242        self.git_user_email = config.git_user_email.clone();
243
244        self
245    }
246
247    /// Attach execution history from a PhaseContext.
248    ///
249    /// This method captures the execution history from the phase context
250    /// and attaches it to the checkpoint.
251    pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
252        self.execution_history = Some(history);
253        self
254    }
255
256    /// Set the entire prompt history from a HashMap.
257    ///
258    /// This is useful when transferring prompts from a PhaseContext.
259    ///
260    /// # Arguments
261    ///
262    /// * `history` - HashMap of prompt keys to prompt text
263    pub fn with_prompt_history(
264        mut self,
265        history: std::collections::HashMap<String, String>,
266    ) -> Self {
267        self.prompt_history = if history.is_empty() {
268            None
269        } else {
270            Some(history)
271        };
272        self
273    }
274
275    /// Build the checkpoint.
276    ///
277    /// Returns None if required fields (phase, agent configs) are missing.
278    /// Generates a new RunContext if not set.
279    pub fn build(self) -> Option<PipelineCheckpoint> {
280        let phase = self.phase?;
281        let developer_agent = self.developer_agent?;
282        let reviewer_agent = self.reviewer_agent?;
283        let cli_args = self.cli_args?;
284        let developer_config = self.developer_agent_config?;
285        let reviewer_config = self.reviewer_agent_config?;
286
287        let git_user_name = self.git_user_name.as_deref();
288        let git_user_email = self.git_user_email.as_deref();
289
290        // Use provided run context or generate a new one
291        let run_context = self.run_context.unwrap_or_default();
292
293        let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
294            phase,
295            iteration: self.iteration,
296            total_iterations: self.total_iterations,
297            reviewer_pass: self.reviewer_pass,
298            total_reviewer_passes: self.total_reviewer_passes,
299            developer_agent: &developer_agent,
300            reviewer_agent: &reviewer_agent,
301            cli_args,
302            developer_agent_config: developer_config,
303            reviewer_agent_config: reviewer_config,
304            rebase_state: self.rebase_state,
305            git_user_name,
306            git_user_email,
307            run_id: &run_context.run_id,
308            parent_run_id: run_context.parent_run_id.as_deref(),
309            resume_count: run_context.resume_count,
310            actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
311            actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
312        });
313
314        if let Some(path) = self.config_path {
315            checkpoint = checkpoint.with_config(Some(path));
316        }
317
318        // Populate execution history
319        checkpoint.execution_history = self.execution_history;
320
321        // Populate prompt history
322        checkpoint.prompt_history = self.prompt_history;
323
324        // Capture and populate file system state
325        checkpoint.file_system_state = Some(FileSystemState::capture_current());
326
327        // Capture and populate environment snapshot
328        checkpoint.env_snapshot =
329            Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
330
331        Some(checkpoint)
332    }
333}
334
335/// Convert ReviewDepth to a string representation.
336fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
337    match depth {
338        ReviewDepth::Standard => Some("standard".to_string()),
339        ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
340        ReviewDepth::Security => Some("security".to_string()),
341        ReviewDepth::Incremental => Some("incremental".to_string()),
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_builder_basic() {
351        let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
352        let dev_config =
353            AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
354        let rev_config =
355            AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
356
357        let checkpoint = CheckpointBuilder::new()
358            .phase(PipelinePhase::Development, 2, 5)
359            .reviewer_pass(1, 2)
360            .agents("dev", "rev")
361            .cli_args(cli_args)
362            .developer_config(dev_config)
363            .reviewer_config(rev_config)
364            .build()
365            .unwrap();
366
367        assert_eq!(checkpoint.phase, PipelinePhase::Development);
368        assert_eq!(checkpoint.iteration, 2);
369        assert_eq!(checkpoint.total_iterations, 5);
370        assert_eq!(checkpoint.reviewer_pass, 1);
371        assert_eq!(checkpoint.total_reviewer_passes, 2);
372    }
373
374    #[test]
375    fn test_builder_missing_required_field() {
376        // Missing phase - should return None
377        let result = CheckpointBuilder::new().build();
378        assert!(result.is_none());
379    }
380
381    #[test]
382    fn test_review_depth_to_string() {
383        assert_eq!(
384            review_depth_to_string(ReviewDepth::Standard),
385            Some("standard".to_string())
386        );
387        assert_eq!(
388            review_depth_to_string(ReviewDepth::Comprehensive),
389            Some("comprehensive".to_string())
390        );
391        assert_eq!(
392            review_depth_to_string(ReviewDepth::Security),
393            Some("security".to_string())
394        );
395        assert_eq!(
396            review_depth_to_string(ReviewDepth::Incremental),
397            Some("incremental".to_string())
398        );
399    }
400
401    #[test]
402    fn test_builder_with_prompt_history() {
403        let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
404        let dev_config =
405            AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
406        let rev_config =
407            AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
408
409        let mut prompts = std::collections::HashMap::new();
410        prompts.insert(
411            "development_1".to_string(),
412            "Implement feature X".to_string(),
413        );
414
415        let checkpoint = CheckpointBuilder::new()
416            .phase(PipelinePhase::Development, 2, 5)
417            .reviewer_pass(1, 2)
418            .agents("dev", "rev")
419            .cli_args(cli_args)
420            .developer_config(dev_config)
421            .reviewer_config(rev_config)
422            .with_prompt_history(prompts)
423            .build()
424            .unwrap();
425
426        assert_eq!(checkpoint.phase, PipelinePhase::Development);
427        assert!(checkpoint.prompt_history.is_some());
428        let history = checkpoint.prompt_history.as_ref().unwrap();
429        assert_eq!(history.len(), 1);
430        assert_eq!(
431            history.get("development_1"),
432            Some(&"Implement feature X".to_string())
433        );
434    }
435
436    #[test]
437    fn test_builder_with_prompt_history_multiple() {
438        let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
439        let dev_config =
440            AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
441        let rev_config =
442            AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
443
444        let mut prompts = std::collections::HashMap::new();
445        prompts.insert(
446            "development_1".to_string(),
447            "Implement feature X".to_string(),
448        );
449        prompts.insert("review_1".to_string(), "Review the changes".to_string());
450
451        let checkpoint = CheckpointBuilder::new()
452            .phase(PipelinePhase::Development, 2, 5)
453            .reviewer_pass(1, 2)
454            .agents("dev", "rev")
455            .cli_args(cli_args)
456            .developer_config(dev_config)
457            .reviewer_config(rev_config)
458            .with_prompt_history(prompts)
459            .build()
460            .unwrap();
461
462        assert!(checkpoint.prompt_history.is_some());
463        let history = checkpoint.prompt_history.as_ref().unwrap();
464        assert_eq!(history.len(), 2);
465        assert_eq!(
466            history.get("development_1"),
467            Some(&"Implement feature X".to_string())
468        );
469        assert_eq!(
470            history.get("review_1"),
471            Some(&"Review the changes".to_string())
472        );
473    }
474}