1use crate::checkpoint::execution_history::ExecutionHistory;
7use crate::checkpoint::state::{PipelineCheckpoint, PipelinePhase, RebaseState};
8use crate::config::Config;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ResumeContext {
17 pub phase: PipelinePhase,
19 pub iteration: u32,
21 pub total_iterations: u32,
23 pub reviewer_pass: u32,
25 pub total_reviewer_passes: u32,
27 pub resume_count: u32,
29 pub rebase_state: RebaseState,
31 pub run_id: String,
33 pub prompt_history: Option<std::collections::HashMap<String, String>>,
35 pub execution_history: Option<ExecutionHistory>,
37}
38
39impl ResumeContext {
40 pub fn phase_name(&self) -> String {
42 match self.phase {
43 PipelinePhase::Rebase => "Rebase".to_string(),
44 PipelinePhase::Planning => "Planning".to_string(),
45 PipelinePhase::Development => format!(
46 "Development iteration {}/{}",
47 self.iteration + 1,
48 self.total_iterations
49 ),
50 PipelinePhase::Review => format!(
51 "Review (pass {}/{})",
52 self.reviewer_pass + 1,
53 self.total_reviewer_passes
54 ),
55 PipelinePhase::CommitMessage => "Commit Message Generation".to_string(),
56 PipelinePhase::FinalValidation => "Final Validation".to_string(),
57 PipelinePhase::Complete => "Complete".to_string(),
58 PipelinePhase::PreRebase => "Pre-Rebase".to_string(),
59 PipelinePhase::PreRebaseConflict => "Pre-Rebase Conflict".to_string(),
60 PipelinePhase::PostRebase => "Post-Rebase".to_string(),
61 PipelinePhase::PostRebaseConflict => "Post-Rebase Conflict".to_string(),
62 PipelinePhase::AwaitingDevFix => "Awaiting Dev Fix".to_string(),
63 PipelinePhase::Interrupted => "Interrupted".to_string(),
64 }
65 }
66}
67
68impl PipelineCheckpoint {
69 pub fn resume_context(&self) -> ResumeContext {
74 ResumeContext {
75 phase: self.phase,
76 iteration: self.iteration,
77 total_iterations: self.total_iterations,
78 reviewer_pass: self.reviewer_pass,
79 total_reviewer_passes: self.total_reviewer_passes,
80 resume_count: self.resume_count,
81 rebase_state: self.rebase_state.clone(),
82 run_id: self.run_id.clone(),
83 prompt_history: self.prompt_history.clone(),
84 execution_history: self.execution_history.clone(),
85 }
86 }
87}
88
89pub fn apply_checkpoint_to_config(config: &mut Config, checkpoint: &PipelineCheckpoint) {
91 let cli_args = &checkpoint.cli_args;
92
93 config.developer_iters = cli_args.developer_iters;
96 config.reviewer_reviews = cli_args.reviewer_reviews;
97
98 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
104 config.developer_model = Some(model.clone());
105 }
106 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
107 config.reviewer_model = Some(model.clone());
108 }
109
110 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
112 config.developer_provider = Some(provider.clone());
113 }
114 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
115 config.reviewer_provider = Some(provider.clone());
116 }
117
118 config.developer_context = checkpoint.developer_agent_config.context_level;
120 config.reviewer_context = checkpoint.reviewer_agent_config.context_level;
121
122 if let Some(ref name) = checkpoint.git_user_name {
124 config.git_user_name = Some(name.clone());
125 }
126 if let Some(ref email) = checkpoint.git_user_email {
127 config.git_user_email = Some(email.clone());
128 }
129
130 config.isolation_mode = cli_args.isolation_mode;
132
133 config.verbosity = crate::config::types::Verbosity::from(cli_args.verbosity);
135
136 config.show_streaming_metrics = cli_args.show_streaming_metrics;
138
139 if let Some(ref parser) = cli_args.reviewer_json_parser {
141 config.reviewer_json_parser = Some(parser.clone());
142 }
143}
144
145pub fn restore_environment_from_checkpoint(checkpoint: &PipelineCheckpoint) -> usize {
149 let Some(ref env_snap) = checkpoint.env_snapshot else {
150 return 0;
151 };
152
153 let mut restored = 0;
154
155 for (key, value) in &env_snap.ralph_vars {
157 if crate::checkpoint::state::is_sensitive_env_key(key) {
158 continue;
159 }
160 std::env::set_var(key, value);
161 restored += 1;
162 }
163
164 for (key, value) in &env_snap.other_vars {
166 if crate::checkpoint::state::is_sensitive_env_key(key) {
167 continue;
168 }
169 std::env::set_var(key, value);
170 restored += 1;
171 }
172
173 restored
174}
175
176pub fn calculate_start_iteration(checkpoint: &PipelineCheckpoint, max_iterations: u32) -> u32 {
178 match checkpoint.phase {
179 PipelinePhase::Planning | PipelinePhase::Development => {
180 checkpoint.iteration.clamp(1, max_iterations)
181 }
182 _ => max_iterations,
184 }
185}
186
187pub fn calculate_start_reviewer_pass(checkpoint: &PipelineCheckpoint, max_passes: u32) -> u32 {
201 match checkpoint.phase {
202 PipelinePhase::Review => checkpoint.reviewer_pass.clamp(1, max_passes.max(1)),
203 PipelinePhase::Planning
205 | PipelinePhase::Development
206 | PipelinePhase::PreRebase
207 | PipelinePhase::PreRebaseConflict => 1,
208 _ => max_passes,
210 }
211}
212
213pub fn should_skip_phase(phase: PipelinePhase, checkpoint: &PipelineCheckpoint) -> bool {
217 phase_rank(phase) < phase_rank(checkpoint.phase)
218}
219
220fn phase_rank(phase: PipelinePhase) -> u32 {
224 match phase {
225 PipelinePhase::Planning => 0,
226 PipelinePhase::Development => 1,
227 PipelinePhase::Review => 2,
228 PipelinePhase::CommitMessage => 3,
229 PipelinePhase::FinalValidation => 4,
230 PipelinePhase::Complete => 5,
231 PipelinePhase::AwaitingDevFix => 6,
232 PipelinePhase::Interrupted => 7,
233 PipelinePhase::PreRebase | PipelinePhase::PreRebaseConflict => 2,
235 PipelinePhase::Rebase | PipelinePhase::PostRebase | PipelinePhase::PostRebaseConflict => 2,
237 }
238}
239#[cfg(test)]
240#[derive(Debug, Clone)]
241pub struct RestoredContext {
242 pub phase: PipelinePhase,
243 pub resume_iteration: u32,
244 pub total_iterations: u32,
245 pub resume_reviewer_pass: u32,
246 pub total_reviewer_passes: u32,
247 pub developer_agent: String,
248 pub reviewer_agent: String,
249 pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
250}
251
252#[cfg(test)]
253impl RestoredContext {
254 pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
256 let cli_args = if checkpoint.cli_args.developer_iters > 0
258 || checkpoint.cli_args.reviewer_reviews > 0
259 {
260 Some(checkpoint.cli_args.clone())
261 } else {
262 None
263 };
264
265 Self {
266 phase: checkpoint.phase,
267 resume_iteration: checkpoint.iteration,
268 total_iterations: checkpoint.total_iterations,
269 resume_reviewer_pass: checkpoint.reviewer_pass,
270 total_reviewer_passes: checkpoint.total_reviewer_passes,
271 developer_agent: checkpoint.developer_agent.clone(),
272 reviewer_agent: checkpoint.reviewer_agent.clone(),
273 cli_args,
274 }
275 }
276
277 pub fn should_use_checkpoint_iterations(&self) -> bool {
282 self.cli_args
283 .as_ref()
284 .is_some_and(|args| args.developer_iters > 0)
285 }
286
287 pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
289 self.cli_args
290 .as_ref()
291 .is_some_and(|args| args.reviewer_reviews > 0)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::checkpoint::state::{
299 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, EnvironmentSnapshot, RebaseState,
300 };
301
302 fn make_test_checkpoint(phase: PipelinePhase, iteration: u32, pass: u32) -> PipelineCheckpoint {
303 let cli_args = CliArgsSnapshot::new(5, 3, None, true, 2, false, None);
304 let dev_config =
305 AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
306 let rev_config =
307 AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
308 let run_id = uuid::Uuid::new_v4().to_string();
309
310 PipelineCheckpoint::from_params(CheckpointParams {
311 phase,
312 iteration,
313 total_iterations: 5,
314 reviewer_pass: pass,
315 total_reviewer_passes: 3,
316 developer_agent: "claude",
317 reviewer_agent: "codex",
318 cli_args,
319 developer_agent_config: dev_config,
320 reviewer_agent_config: rev_config,
321 rebase_state: RebaseState::default(),
322 git_user_name: None,
323 git_user_email: None,
324 run_id: &run_id,
325 parent_run_id: None,
326 resume_count: 0,
327 actual_developer_runs: iteration,
328 actual_reviewer_runs: pass,
329 working_dir: "/test/repo".to_string(),
330 prompt_md_checksum: None,
331 config_path: None,
332 config_checksum: None,
333 })
334 }
335
336 #[test]
337 fn test_restored_context_from_checkpoint() {
338 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
339 let context = RestoredContext::from_checkpoint(&checkpoint);
340
341 assert_eq!(context.phase, PipelinePhase::Development);
342 assert_eq!(context.resume_iteration, 3);
343 assert_eq!(context.total_iterations, 5);
344 assert_eq!(context.resume_reviewer_pass, 0);
345 assert_eq!(context.developer_agent, "claude");
346 assert!(context.cli_args.is_some());
347 }
348
349 #[test]
350 fn test_should_use_checkpoint_iterations() {
351 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
352 let context = RestoredContext::from_checkpoint(&checkpoint);
353
354 assert!(context.should_use_checkpoint_iterations());
355 }
356
357 #[test]
358 fn test_calculate_start_iteration_development() {
359 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
360 let start = calculate_start_iteration(&checkpoint, 5);
361 assert_eq!(start, 3);
362 }
363
364 #[test]
365 fn test_calculate_start_iteration_later_phase() {
366 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
367 let start = calculate_start_iteration(&checkpoint, 5);
368 assert_eq!(start, 5); }
370
371 #[test]
372 fn test_calculate_start_reviewer_pass() {
373 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 2);
374 let start = calculate_start_reviewer_pass(&checkpoint, 3);
375 assert_eq!(start, 2);
376 }
377
378 #[test]
379 fn test_calculate_start_reviewer_pass_early_phase() {
380 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
381 let start = calculate_start_reviewer_pass(&checkpoint, 3);
382 assert_eq!(start, 1); }
384
385 #[test]
386 fn test_should_skip_phase() {
387 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
388
389 assert!(should_skip_phase(PipelinePhase::Planning, &checkpoint));
391 assert!(should_skip_phase(PipelinePhase::Development, &checkpoint));
392
393 assert!(!should_skip_phase(PipelinePhase::Review, &checkpoint));
395 assert!(!should_skip_phase(
396 PipelinePhase::FinalValidation,
397 &checkpoint
398 ));
399 }
400
401 #[test]
402 fn test_resume_context_from_checkpoint() {
403 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 1);
404 let resume_ctx = checkpoint.resume_context();
405
406 assert_eq!(resume_ctx.phase, PipelinePhase::Development);
407 assert_eq!(resume_ctx.iteration, 3);
408 assert_eq!(resume_ctx.total_iterations, 5);
409 assert_eq!(resume_ctx.reviewer_pass, 1);
410 assert_eq!(resume_ctx.total_reviewer_passes, 3);
411 assert_eq!(resume_ctx.resume_count, 0);
412 assert_eq!(resume_ctx.run_id, checkpoint.run_id);
413 assert!(resume_ctx.prompt_history.is_none());
414 }
415
416 #[test]
417 fn test_resume_context_phase_name_development() {
418 let ctx = ResumeContext {
419 phase: PipelinePhase::Development,
420 iteration: 2,
421 total_iterations: 5,
422 reviewer_pass: 0,
423 total_reviewer_passes: 3,
424 resume_count: 0,
425 rebase_state: RebaseState::default(),
426 run_id: "test".to_string(),
427 prompt_history: None,
428 execution_history: None,
429 };
430
431 assert_eq!(ctx.phase_name(), "Development iteration 3/5");
432 }
433
434 #[test]
435 fn test_resume_context_phase_name_review() {
436 let ctx = ResumeContext {
437 phase: PipelinePhase::Review,
438 iteration: 5,
439 total_iterations: 5,
440 reviewer_pass: 1,
441 total_reviewer_passes: 3,
442 resume_count: 0,
443 rebase_state: RebaseState::default(),
444 run_id: "test".to_string(),
445 prompt_history: None,
446 execution_history: None,
447 };
448
449 assert_eq!(ctx.phase_name(), "Review (pass 2/3)");
450 }
451
452 #[test]
453 fn test_restore_environment_skips_sensitive_vars() {
454 let mut checkpoint = make_test_checkpoint(PipelinePhase::Development, 1, 0);
455 let mut snapshot = EnvironmentSnapshot::default();
456 snapshot
457 .ralph_vars
458 .insert("RALPH_SAFE_SETTING".to_string(), "ok".to_string());
459 snapshot
460 .ralph_vars
461 .insert("RALPH_API_TOKEN".to_string(), "secret".to_string());
462 snapshot
463 .other_vars
464 .insert("EDITOR".to_string(), "vim".to_string());
465 snapshot
466 .other_vars
467 .insert("GIT_PASSWORD".to_string(), "nope".to_string());
468 checkpoint.env_snapshot = Some(snapshot);
469
470 std::env::remove_var("RALPH_SAFE_SETTING");
471 std::env::remove_var("RALPH_API_TOKEN");
472 std::env::remove_var("EDITOR");
473 std::env::remove_var("GIT_PASSWORD");
474
475 let restored = restore_environment_from_checkpoint(&checkpoint);
476 assert_eq!(restored, 2);
477 assert_eq!(
478 std::env::var("RALPH_SAFE_SETTING").ok().as_deref(),
479 Some("ok")
480 );
481 assert!(std::env::var("RALPH_API_TOKEN").is_err());
482 assert_eq!(std::env::var("EDITOR").ok().as_deref(), Some("vim"));
483 assert!(std::env::var("GIT_PASSWORD").is_err());
484
485 std::env::remove_var("RALPH_SAFE_SETTING");
486 std::env::remove_var("EDITOR");
487 }
488}