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::Fix => "Fix".to_string(),
56 PipelinePhase::ReviewAgain => format!(
57 "Verification review {}/{}",
58 self.reviewer_pass + 1,
59 self.total_reviewer_passes
60 ),
61 PipelinePhase::CommitMessage => "Commit Message Generation".to_string(),
62 PipelinePhase::FinalValidation => "Final Validation".to_string(),
63 PipelinePhase::Complete => "Complete".to_string(),
64 PipelinePhase::PreRebase => "Pre-Rebase".to_string(),
65 PipelinePhase::PreRebaseConflict => "Pre-Rebase Conflict".to_string(),
66 PipelinePhase::PostRebase => "Post-Rebase".to_string(),
67 PipelinePhase::PostRebaseConflict => "Post-Rebase Conflict".to_string(),
68 PipelinePhase::Interrupted => "Interrupted".to_string(),
69 }
70 }
71}
72
73impl PipelineCheckpoint {
74 pub fn resume_context(&self) -> ResumeContext {
79 ResumeContext {
80 phase: self.phase,
81 iteration: self.iteration,
82 total_iterations: self.total_iterations,
83 reviewer_pass: self.reviewer_pass,
84 total_reviewer_passes: self.total_reviewer_passes,
85 resume_count: self.resume_count,
86 rebase_state: self.rebase_state.clone(),
87 run_id: self.run_id.clone(),
88 prompt_history: self.prompt_history.clone(),
89 execution_history: self.execution_history.clone(),
90 }
91 }
92}
93
94pub fn apply_checkpoint_to_config(config: &mut Config, checkpoint: &PipelineCheckpoint) {
109 let cli_args = &checkpoint.cli_args;
110
111 config.developer_iters = cli_args.developer_iters;
114 config.reviewer_reviews = cli_args.reviewer_reviews;
115
116 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
122 config.developer_model = Some(model.clone());
123 }
124 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
125 config.reviewer_model = Some(model.clone());
126 }
127
128 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
130 config.developer_provider = Some(provider.clone());
131 }
132 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
133 config.reviewer_provider = Some(provider.clone());
134 }
135
136 config.developer_context = checkpoint.developer_agent_config.context_level;
138 config.reviewer_context = checkpoint.reviewer_agent_config.context_level;
139
140 if let Some(ref name) = checkpoint.git_user_name {
142 config.git_user_name = Some(name.clone());
143 }
144 if let Some(ref email) = checkpoint.git_user_email {
145 config.git_user_email = Some(email.clone());
146 }
147
148 config.isolation_mode = cli_args.isolation_mode;
150
151 config.verbosity = crate::config::types::Verbosity::from(cli_args.verbosity);
153
154 config.show_streaming_metrics = cli_args.show_streaming_metrics;
156
157 if let Some(ref parser) = cli_args.reviewer_json_parser {
159 config.reviewer_json_parser = Some(parser.clone());
160 }
161}
162
163pub fn restore_environment_from_checkpoint(checkpoint: &PipelineCheckpoint) -> usize {
177 let Some(ref env_snap) = checkpoint.env_snapshot else {
178 return 0;
179 };
180
181 let mut restored = 0;
182
183 for (key, value) in &env_snap.ralph_vars {
185 std::env::set_var(key, value);
186 restored += 1;
187 }
188
189 for (key, value) in &env_snap.other_vars {
191 std::env::set_var(key, value);
192 restored += 1;
193 }
194
195 restored
196}
197
198pub fn calculate_start_iteration(checkpoint: &PipelineCheckpoint, max_iterations: u32) -> u32 {
212 match checkpoint.phase {
213 PipelinePhase::Planning | PipelinePhase::Development => {
214 checkpoint.iteration.clamp(1, max_iterations)
215 }
216 _ => max_iterations,
218 }
219}
220
221pub fn calculate_start_reviewer_pass(checkpoint: &PipelineCheckpoint, max_passes: u32) -> u32 {
235 match checkpoint.phase {
236 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
237 checkpoint.reviewer_pass.clamp(1, max_passes.max(1))
238 }
239 PipelinePhase::Planning
241 | PipelinePhase::Development
242 | PipelinePhase::PreRebase
243 | PipelinePhase::PreRebaseConflict => 1,
244 _ => max_passes,
246 }
247}
248
249pub fn should_skip_phase(phase: PipelinePhase, checkpoint: &PipelineCheckpoint) -> bool {
253 phase_rank(phase) < phase_rank(checkpoint.phase)
254}
255
256fn phase_rank(phase: PipelinePhase) -> u32 {
260 match phase {
261 PipelinePhase::Planning => 0,
262 PipelinePhase::Development => 1,
263 PipelinePhase::Review => 2,
264 PipelinePhase::CommitMessage => 3,
265 PipelinePhase::FinalValidation => 4,
266 PipelinePhase::Complete => 5,
267 PipelinePhase::Interrupted => 6,
268 PipelinePhase::Fix
270 | PipelinePhase::ReviewAgain
271 | PipelinePhase::PreRebase
272 | PipelinePhase::PreRebaseConflict => 2,
273 PipelinePhase::Rebase | PipelinePhase::PostRebase | PipelinePhase::PostRebaseConflict => 2,
275 }
276}
277#[cfg(test)]
284#[derive(Debug, Clone)]
285pub struct RestoredContext {
286 pub phase: PipelinePhase,
288 pub resume_iteration: u32,
290 pub total_iterations: u32,
292 pub resume_reviewer_pass: u32,
294 pub total_reviewer_passes: u32,
296 pub developer_agent: String,
298 pub reviewer_agent: String,
300 pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
302}
303
304#[cfg(test)]
305impl RestoredContext {
306 pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
308 let cli_args = if checkpoint.cli_args.developer_iters > 0
310 || checkpoint.cli_args.reviewer_reviews > 0
311 {
312 Some(checkpoint.cli_args.clone())
313 } else {
314 None
315 };
316
317 Self {
318 phase: checkpoint.phase,
319 resume_iteration: checkpoint.iteration,
320 total_iterations: checkpoint.total_iterations,
321 resume_reviewer_pass: checkpoint.reviewer_pass,
322 total_reviewer_passes: checkpoint.total_reviewer_passes,
323 developer_agent: checkpoint.developer_agent.clone(),
324 reviewer_agent: checkpoint.reviewer_agent.clone(),
325 cli_args,
326 }
327 }
328
329 pub fn should_use_checkpoint_iterations(&self) -> bool {
334 self.cli_args
335 .as_ref()
336 .is_some_and(|args| args.developer_iters > 0)
337 }
338
339 pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
341 self.cli_args
342 .as_ref()
343 .is_some_and(|args| args.reviewer_reviews > 0)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::checkpoint::state::{
351 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, RebaseState,
352 };
353
354 fn make_test_checkpoint(phase: PipelinePhase, iteration: u32, pass: u32) -> PipelineCheckpoint {
355 let cli_args = CliArgsSnapshot::new(5, 3, None, false, true, 2, false, None);
356 let dev_config =
357 AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
358 let rev_config =
359 AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
360 let run_id = uuid::Uuid::new_v4().to_string();
361
362 PipelineCheckpoint::from_params(CheckpointParams {
363 phase,
364 iteration,
365 total_iterations: 5,
366 reviewer_pass: pass,
367 total_reviewer_passes: 3,
368 developer_agent: "claude",
369 reviewer_agent: "codex",
370 cli_args,
371 developer_agent_config: dev_config,
372 reviewer_agent_config: rev_config,
373 rebase_state: RebaseState::default(),
374 git_user_name: None,
375 git_user_email: None,
376 run_id: &run_id,
377 parent_run_id: None,
378 resume_count: 0,
379 actual_developer_runs: iteration,
380 actual_reviewer_runs: pass,
381 })
382 }
383
384 #[test]
385 fn test_restored_context_from_checkpoint() {
386 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
387 let context = RestoredContext::from_checkpoint(&checkpoint);
388
389 assert_eq!(context.phase, PipelinePhase::Development);
390 assert_eq!(context.resume_iteration, 3);
391 assert_eq!(context.total_iterations, 5);
392 assert_eq!(context.resume_reviewer_pass, 0);
393 assert_eq!(context.developer_agent, "claude");
394 assert!(context.cli_args.is_some());
395 }
396
397 #[test]
398 fn test_should_use_checkpoint_iterations() {
399 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
400 let context = RestoredContext::from_checkpoint(&checkpoint);
401
402 assert!(context.should_use_checkpoint_iterations());
403 }
404
405 #[test]
406 fn test_calculate_start_iteration_development() {
407 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
408 let start = calculate_start_iteration(&checkpoint, 5);
409 assert_eq!(start, 3);
410 }
411
412 #[test]
413 fn test_calculate_start_iteration_later_phase() {
414 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
415 let start = calculate_start_iteration(&checkpoint, 5);
416 assert_eq!(start, 5); }
418
419 #[test]
420 fn test_calculate_start_reviewer_pass() {
421 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 2);
422 let start = calculate_start_reviewer_pass(&checkpoint, 3);
423 assert_eq!(start, 2);
424 }
425
426 #[test]
427 fn test_calculate_start_reviewer_pass_early_phase() {
428 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
429 let start = calculate_start_reviewer_pass(&checkpoint, 3);
430 assert_eq!(start, 1); }
432
433 #[test]
434 fn test_should_skip_phase() {
435 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
436
437 assert!(should_skip_phase(PipelinePhase::Planning, &checkpoint));
439 assert!(should_skip_phase(PipelinePhase::Development, &checkpoint));
440
441 assert!(!should_skip_phase(PipelinePhase::Review, &checkpoint));
443 assert!(!should_skip_phase(
444 PipelinePhase::FinalValidation,
445 &checkpoint
446 ));
447 }
448
449 #[test]
450 fn test_resume_context_from_checkpoint() {
451 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 1);
452 let resume_ctx = checkpoint.resume_context();
453
454 assert_eq!(resume_ctx.phase, PipelinePhase::Development);
455 assert_eq!(resume_ctx.iteration, 3);
456 assert_eq!(resume_ctx.total_iterations, 5);
457 assert_eq!(resume_ctx.reviewer_pass, 1);
458 assert_eq!(resume_ctx.total_reviewer_passes, 3);
459 assert_eq!(resume_ctx.resume_count, 0);
460 assert_eq!(resume_ctx.run_id, checkpoint.run_id);
461 assert!(resume_ctx.prompt_history.is_none());
462 }
463
464 #[test]
465 fn test_resume_context_phase_name_development() {
466 let ctx = ResumeContext {
467 phase: PipelinePhase::Development,
468 iteration: 2,
469 total_iterations: 5,
470 reviewer_pass: 0,
471 total_reviewer_passes: 3,
472 resume_count: 0,
473 rebase_state: RebaseState::default(),
474 run_id: "test".to_string(),
475 prompt_history: None,
476 execution_history: None,
477 };
478
479 assert_eq!(ctx.phase_name(), "Development iteration 3/5");
480 }
481
482 #[test]
483 fn test_resume_context_phase_name_review() {
484 let ctx = ResumeContext {
485 phase: PipelinePhase::Review,
486 iteration: 5,
487 total_iterations: 5,
488 reviewer_pass: 1,
489 total_reviewer_passes: 3,
490 resume_count: 0,
491 rebase_state: RebaseState::default(),
492 run_id: "test".to_string(),
493 prompt_history: None,
494 execution_history: None,
495 };
496
497 assert_eq!(ctx.phase_name(), "Review (pass 2/3)");
498 }
499
500 #[test]
501 fn test_resume_context_phase_name_review_again() {
502 let ctx = ResumeContext {
503 phase: PipelinePhase::ReviewAgain,
504 iteration: 5,
505 total_iterations: 5,
506 reviewer_pass: 2,
507 total_reviewer_passes: 3,
508 resume_count: 1,
509 rebase_state: RebaseState::default(),
510 run_id: "test".to_string(),
511 prompt_history: None,
512 execution_history: None,
513 };
514
515 assert_eq!(ctx.phase_name(), "Verification review 3/3");
516 }
517
518 #[test]
519 fn test_resume_context_phase_name_fix() {
520 let ctx = ResumeContext {
521 phase: PipelinePhase::Fix,
522 iteration: 5,
523 total_iterations: 5,
524 reviewer_pass: 1,
525 total_reviewer_passes: 3,
526 resume_count: 0,
527 rebase_state: RebaseState::default(),
528 run_id: "test".to_string(),
529 prompt_history: None,
530 execution_history: None,
531 };
532
533 assert_eq!(ctx.phase_name(), "Fix");
534 }
535}