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 !cli_args.commit_msg.is_empty() {
117 config.commit_msg = cli_args.commit_msg.clone();
118 }
119
120 if let Some(ref model) = checkpoint.developer_agent_config.model_override {
126 config.developer_model = Some(model.clone());
127 }
128 if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
129 config.reviewer_model = Some(model.clone());
130 }
131
132 if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
134 config.developer_provider = Some(provider.clone());
135 }
136 if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
137 config.reviewer_provider = Some(provider.clone());
138 }
139
140 config.developer_context = checkpoint.developer_agent_config.context_level;
142 config.reviewer_context = checkpoint.reviewer_agent_config.context_level;
143
144 if let Some(ref name) = checkpoint.git_user_name {
146 config.git_user_name = Some(name.clone());
147 }
148 if let Some(ref email) = checkpoint.git_user_email {
149 config.git_user_email = Some(email.clone());
150 }
151
152 config.isolation_mode = cli_args.isolation_mode;
154
155 config.verbosity = crate::config::types::Verbosity::from(cli_args.verbosity);
157
158 config.show_streaming_metrics = cli_args.show_streaming_metrics;
160
161 if let Some(ref parser) = cli_args.reviewer_json_parser {
163 config.reviewer_json_parser = Some(parser.clone());
164 }
165}
166
167pub fn restore_environment_from_checkpoint(checkpoint: &PipelineCheckpoint) -> usize {
181 let Some(ref env_snap) = checkpoint.env_snapshot else {
182 return 0;
183 };
184
185 let mut restored = 0;
186
187 for (key, value) in &env_snap.ralph_vars {
189 std::env::set_var(key, value);
190 restored += 1;
191 }
192
193 for (key, value) in &env_snap.other_vars {
195 std::env::set_var(key, value);
196 restored += 1;
197 }
198
199 restored
200}
201
202pub fn calculate_start_iteration(checkpoint: &PipelineCheckpoint, max_iterations: u32) -> u32 {
216 match checkpoint.phase {
217 PipelinePhase::Planning | PipelinePhase::Development => {
218 checkpoint.iteration.clamp(1, max_iterations)
219 }
220 _ => max_iterations,
222 }
223}
224
225pub fn calculate_start_reviewer_pass(checkpoint: &PipelineCheckpoint, max_passes: u32) -> u32 {
239 match checkpoint.phase {
240 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
241 checkpoint.reviewer_pass.clamp(1, max_passes.max(1))
242 }
243 PipelinePhase::Planning
245 | PipelinePhase::Development
246 | PipelinePhase::PreRebase
247 | PipelinePhase::PreRebaseConflict => 1,
248 _ => max_passes,
250 }
251}
252
253pub fn should_skip_phase(phase: PipelinePhase, checkpoint: &PipelineCheckpoint) -> bool {
262 use crate::app::resume::phase_rank;
263 phase_rank(phase) < phase_rank(checkpoint.phase)
264}
265
266#[cfg(test)]
270#[derive(Debug, Clone)]
271pub struct RestoredContext {
272 pub phase: PipelinePhase,
274 pub resume_iteration: u32,
276 pub total_iterations: u32,
278 pub resume_reviewer_pass: u32,
280 pub total_reviewer_passes: u32,
282 pub developer_agent: String,
284 pub reviewer_agent: String,
286 pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
288}
289
290#[cfg(test)]
291impl RestoredContext {
292 pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
294 let cli_args = if checkpoint.cli_args.developer_iters > 0
296 || checkpoint.cli_args.reviewer_reviews > 0
297 || !checkpoint.cli_args.commit_msg.is_empty()
298 {
299 Some(checkpoint.cli_args.clone())
300 } else {
301 None
302 };
303
304 Self {
305 phase: checkpoint.phase,
306 resume_iteration: checkpoint.iteration,
307 total_iterations: checkpoint.total_iterations,
308 resume_reviewer_pass: checkpoint.reviewer_pass,
309 total_reviewer_passes: checkpoint.total_reviewer_passes,
310 developer_agent: checkpoint.developer_agent.clone(),
311 reviewer_agent: checkpoint.reviewer_agent.clone(),
312 cli_args,
313 }
314 }
315
316 pub fn should_use_checkpoint_iterations(&self) -> bool {
321 self.cli_args
322 .as_ref()
323 .is_some_and(|args| args.developer_iters > 0)
324 }
325
326 pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
328 self.cli_args
329 .as_ref()
330 .is_some_and(|args| args.reviewer_reviews > 0)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::checkpoint::state::{
338 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, RebaseState,
339 };
340
341 fn make_test_checkpoint(phase: PipelinePhase, iteration: u32, pass: u32) -> PipelineCheckpoint {
342 let cli_args = CliArgsSnapshot::new(
343 5,
344 3,
345 "test commit".to_string(),
346 None,
347 false,
348 true,
349 2,
350 false,
351 None,
352 );
353 let dev_config =
354 AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
355 let rev_config =
356 AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
357 let run_id = uuid::Uuid::new_v4().to_string();
358
359 PipelineCheckpoint::from_params(CheckpointParams {
360 phase,
361 iteration,
362 total_iterations: 5,
363 reviewer_pass: pass,
364 total_reviewer_passes: 3,
365 developer_agent: "claude",
366 reviewer_agent: "codex",
367 cli_args,
368 developer_agent_config: dev_config,
369 reviewer_agent_config: rev_config,
370 rebase_state: RebaseState::default(),
371 git_user_name: None,
372 git_user_email: None,
373 run_id: &run_id,
374 parent_run_id: None,
375 resume_count: 0,
376 actual_developer_runs: iteration,
377 actual_reviewer_runs: pass,
378 })
379 }
380
381 #[test]
382 fn test_restored_context_from_checkpoint() {
383 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
384 let context = RestoredContext::from_checkpoint(&checkpoint);
385
386 assert_eq!(context.phase, PipelinePhase::Development);
387 assert_eq!(context.resume_iteration, 3);
388 assert_eq!(context.total_iterations, 5);
389 assert_eq!(context.resume_reviewer_pass, 0);
390 assert_eq!(context.developer_agent, "claude");
391 assert!(context.cli_args.is_some());
392 }
393
394 #[test]
395 fn test_should_use_checkpoint_iterations() {
396 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
397 let context = RestoredContext::from_checkpoint(&checkpoint);
398
399 assert!(context.should_use_checkpoint_iterations());
400 }
401
402 #[test]
403 fn test_calculate_start_iteration_development() {
404 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
405 let start = calculate_start_iteration(&checkpoint, 5);
406 assert_eq!(start, 3);
407 }
408
409 #[test]
410 fn test_calculate_start_iteration_later_phase() {
411 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
412 let start = calculate_start_iteration(&checkpoint, 5);
413 assert_eq!(start, 5); }
415
416 #[test]
417 fn test_calculate_start_reviewer_pass() {
418 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 2);
419 let start = calculate_start_reviewer_pass(&checkpoint, 3);
420 assert_eq!(start, 2);
421 }
422
423 #[test]
424 fn test_calculate_start_reviewer_pass_early_phase() {
425 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
426 let start = calculate_start_reviewer_pass(&checkpoint, 3);
427 assert_eq!(start, 1); }
429
430 #[test]
431 fn test_should_skip_phase() {
432 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
433
434 assert!(should_skip_phase(PipelinePhase::Planning, &checkpoint));
436 assert!(should_skip_phase(PipelinePhase::Development, &checkpoint));
437
438 assert!(!should_skip_phase(PipelinePhase::Review, &checkpoint));
440 assert!(!should_skip_phase(
441 PipelinePhase::FinalValidation,
442 &checkpoint
443 ));
444 }
445
446 #[test]
447 fn test_resume_context_from_checkpoint() {
448 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 1);
449 let resume_ctx = checkpoint.resume_context();
450
451 assert_eq!(resume_ctx.phase, PipelinePhase::Development);
452 assert_eq!(resume_ctx.iteration, 3);
453 assert_eq!(resume_ctx.total_iterations, 5);
454 assert_eq!(resume_ctx.reviewer_pass, 1);
455 assert_eq!(resume_ctx.total_reviewer_passes, 3);
456 assert_eq!(resume_ctx.resume_count, 0);
457 assert_eq!(resume_ctx.run_id, checkpoint.run_id);
458 assert!(resume_ctx.prompt_history.is_none());
459 }
460
461 #[test]
462 fn test_resume_context_phase_name_development() {
463 let ctx = ResumeContext {
464 phase: PipelinePhase::Development,
465 iteration: 2,
466 total_iterations: 5,
467 reviewer_pass: 0,
468 total_reviewer_passes: 3,
469 resume_count: 0,
470 rebase_state: RebaseState::default(),
471 run_id: "test".to_string(),
472 prompt_history: None,
473 execution_history: None,
474 };
475
476 assert_eq!(ctx.phase_name(), "Development iteration 3/5");
477 }
478
479 #[test]
480 fn test_resume_context_phase_name_review() {
481 let ctx = ResumeContext {
482 phase: PipelinePhase::Review,
483 iteration: 5,
484 total_iterations: 5,
485 reviewer_pass: 1,
486 total_reviewer_passes: 3,
487 resume_count: 0,
488 rebase_state: RebaseState::default(),
489 run_id: "test".to_string(),
490 prompt_history: None,
491 execution_history: None,
492 };
493
494 assert_eq!(ctx.phase_name(), "Review (pass 2/3)");
495 }
496
497 #[test]
498 fn test_resume_context_phase_name_review_again() {
499 let ctx = ResumeContext {
500 phase: PipelinePhase::ReviewAgain,
501 iteration: 5,
502 total_iterations: 5,
503 reviewer_pass: 2,
504 total_reviewer_passes: 3,
505 resume_count: 1,
506 rebase_state: RebaseState::default(),
507 run_id: "test".to_string(),
508 prompt_history: None,
509 execution_history: None,
510 };
511
512 assert_eq!(ctx.phase_name(), "Verification review 3/3");
513 }
514
515 #[test]
516 fn test_resume_context_phase_name_fix() {
517 let ctx = ResumeContext {
518 phase: PipelinePhase::Fix,
519 iteration: 5,
520 total_iterations: 5,
521 reviewer_pass: 1,
522 total_reviewer_passes: 3,
523 resume_count: 0,
524 rebase_state: RebaseState::default(),
525 run_id: "test".to_string(),
526 prompt_history: None,
527 execution_history: None,
528 };
529
530 assert_eq!(ctx.phase_name(), "Fix");
531 }
532}