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 {
257 phase_rank(phase) < phase_rank(checkpoint.phase)
258}
259
260fn phase_rank(phase: PipelinePhase) -> u32 {
264 match phase {
265 PipelinePhase::Planning => 0,
266 PipelinePhase::Development => 1,
267 PipelinePhase::Review => 2,
268 PipelinePhase::CommitMessage => 3,
269 PipelinePhase::FinalValidation => 4,
270 PipelinePhase::Complete => 5,
271 PipelinePhase::Interrupted => 6,
272 PipelinePhase::Fix
274 | PipelinePhase::ReviewAgain
275 | PipelinePhase::PreRebase
276 | PipelinePhase::PreRebaseConflict => 2,
277 PipelinePhase::Rebase | PipelinePhase::PostRebase | PipelinePhase::PostRebaseConflict => 2,
279 }
280}
281#[cfg(test)]
288#[derive(Debug, Clone)]
289pub struct RestoredContext {
290 pub phase: PipelinePhase,
292 pub resume_iteration: u32,
294 pub total_iterations: u32,
296 pub resume_reviewer_pass: u32,
298 pub total_reviewer_passes: u32,
300 pub developer_agent: String,
302 pub reviewer_agent: String,
304 pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
306}
307
308#[cfg(test)]
309impl RestoredContext {
310 pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
312 let cli_args = if checkpoint.cli_args.developer_iters > 0
314 || checkpoint.cli_args.reviewer_reviews > 0
315 || !checkpoint.cli_args.commit_msg.is_empty()
316 {
317 Some(checkpoint.cli_args.clone())
318 } else {
319 None
320 };
321
322 Self {
323 phase: checkpoint.phase,
324 resume_iteration: checkpoint.iteration,
325 total_iterations: checkpoint.total_iterations,
326 resume_reviewer_pass: checkpoint.reviewer_pass,
327 total_reviewer_passes: checkpoint.total_reviewer_passes,
328 developer_agent: checkpoint.developer_agent.clone(),
329 reviewer_agent: checkpoint.reviewer_agent.clone(),
330 cli_args,
331 }
332 }
333
334 pub fn should_use_checkpoint_iterations(&self) -> bool {
339 self.cli_args
340 .as_ref()
341 .is_some_and(|args| args.developer_iters > 0)
342 }
343
344 pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
346 self.cli_args
347 .as_ref()
348 .is_some_and(|args| args.reviewer_reviews > 0)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::checkpoint::state::{
356 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, RebaseState,
357 };
358
359 fn make_test_checkpoint(phase: PipelinePhase, iteration: u32, pass: u32) -> PipelineCheckpoint {
360 let cli_args = CliArgsSnapshot::new(
361 5,
362 3,
363 "test commit".to_string(),
364 None,
365 false,
366 true,
367 2,
368 false,
369 None,
370 );
371 let dev_config =
372 AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
373 let rev_config =
374 AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
375 let run_id = uuid::Uuid::new_v4().to_string();
376
377 PipelineCheckpoint::from_params(CheckpointParams {
378 phase,
379 iteration,
380 total_iterations: 5,
381 reviewer_pass: pass,
382 total_reviewer_passes: 3,
383 developer_agent: "claude",
384 reviewer_agent: "codex",
385 cli_args,
386 developer_agent_config: dev_config,
387 reviewer_agent_config: rev_config,
388 rebase_state: RebaseState::default(),
389 git_user_name: None,
390 git_user_email: None,
391 run_id: &run_id,
392 parent_run_id: None,
393 resume_count: 0,
394 actual_developer_runs: iteration,
395 actual_reviewer_runs: pass,
396 })
397 }
398
399 #[test]
400 fn test_restored_context_from_checkpoint() {
401 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
402 let context = RestoredContext::from_checkpoint(&checkpoint);
403
404 assert_eq!(context.phase, PipelinePhase::Development);
405 assert_eq!(context.resume_iteration, 3);
406 assert_eq!(context.total_iterations, 5);
407 assert_eq!(context.resume_reviewer_pass, 0);
408 assert_eq!(context.developer_agent, "claude");
409 assert!(context.cli_args.is_some());
410 }
411
412 #[test]
413 fn test_should_use_checkpoint_iterations() {
414 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
415 let context = RestoredContext::from_checkpoint(&checkpoint);
416
417 assert!(context.should_use_checkpoint_iterations());
418 }
419
420 #[test]
421 fn test_calculate_start_iteration_development() {
422 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
423 let start = calculate_start_iteration(&checkpoint, 5);
424 assert_eq!(start, 3);
425 }
426
427 #[test]
428 fn test_calculate_start_iteration_later_phase() {
429 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
430 let start = calculate_start_iteration(&checkpoint, 5);
431 assert_eq!(start, 5); }
433
434 #[test]
435 fn test_calculate_start_reviewer_pass() {
436 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 2);
437 let start = calculate_start_reviewer_pass(&checkpoint, 3);
438 assert_eq!(start, 2);
439 }
440
441 #[test]
442 fn test_calculate_start_reviewer_pass_early_phase() {
443 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 0);
444 let start = calculate_start_reviewer_pass(&checkpoint, 3);
445 assert_eq!(start, 1); }
447
448 #[test]
449 fn test_should_skip_phase() {
450 let checkpoint = make_test_checkpoint(PipelinePhase::Review, 5, 1);
451
452 assert!(should_skip_phase(PipelinePhase::Planning, &checkpoint));
454 assert!(should_skip_phase(PipelinePhase::Development, &checkpoint));
455
456 assert!(!should_skip_phase(PipelinePhase::Review, &checkpoint));
458 assert!(!should_skip_phase(
459 PipelinePhase::FinalValidation,
460 &checkpoint
461 ));
462 }
463
464 #[test]
465 fn test_resume_context_from_checkpoint() {
466 let checkpoint = make_test_checkpoint(PipelinePhase::Development, 3, 1);
467 let resume_ctx = checkpoint.resume_context();
468
469 assert_eq!(resume_ctx.phase, PipelinePhase::Development);
470 assert_eq!(resume_ctx.iteration, 3);
471 assert_eq!(resume_ctx.total_iterations, 5);
472 assert_eq!(resume_ctx.reviewer_pass, 1);
473 assert_eq!(resume_ctx.total_reviewer_passes, 3);
474 assert_eq!(resume_ctx.resume_count, 0);
475 assert_eq!(resume_ctx.run_id, checkpoint.run_id);
476 assert!(resume_ctx.prompt_history.is_none());
477 }
478
479 #[test]
480 fn test_resume_context_phase_name_development() {
481 let ctx = ResumeContext {
482 phase: PipelinePhase::Development,
483 iteration: 2,
484 total_iterations: 5,
485 reviewer_pass: 0,
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(), "Development iteration 3/5");
495 }
496
497 #[test]
498 fn test_resume_context_phase_name_review() {
499 let ctx = ResumeContext {
500 phase: PipelinePhase::Review,
501 iteration: 5,
502 total_iterations: 5,
503 reviewer_pass: 1,
504 total_reviewer_passes: 3,
505 resume_count: 0,
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(), "Review (pass 2/3)");
513 }
514
515 #[test]
516 fn test_resume_context_phase_name_review_again() {
517 let ctx = ResumeContext {
518 phase: PipelinePhase::ReviewAgain,
519 iteration: 5,
520 total_iterations: 5,
521 reviewer_pass: 2,
522 total_reviewer_passes: 3,
523 resume_count: 1,
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(), "Verification review 3/3");
531 }
532
533 #[test]
534 fn test_resume_context_phase_name_fix() {
535 let ctx = ResumeContext {
536 phase: PipelinePhase::Fix,
537 iteration: 5,
538 total_iterations: 5,
539 reviewer_pass: 1,
540 total_reviewer_passes: 3,
541 resume_count: 0,
542 rebase_state: RebaseState::default(),
543 run_id: "test".to_string(),
544 prompt_history: None,
545 execution_history: None,
546 };
547
548 assert_eq!(ctx.phase_name(), "Fix");
549 }
550}