Skip to main content

ralph_workflow/reducer/state/
pipeline.rs

1// Pipeline state and outcome types.
2//
3// Contains PipelineState, validated outcomes, and checkpoint conversion.
4
5#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
6pub struct ReviewValidatedOutcome {
7    pub pass: u32,
8    pub issues_found: bool,
9    pub clean_no_issues: bool,
10    #[serde(default)]
11    pub issues: Vec<String>,
12    #[serde(default)]
13    pub no_issues_found: Option<String>,
14}
15
16#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
17pub struct PlanningValidatedOutcome {
18    pub iteration: u32,
19    pub valid: bool,
20    #[serde(default)]
21    pub markdown: Option<String>,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
25pub struct DevelopmentValidatedOutcome {
26    pub iteration: u32,
27    pub status: DevelopmentStatus,
28    pub summary: String,
29    pub files_changed: Option<Vec<String>>,
30    pub next_steps: Option<String>,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
34pub struct FixValidatedOutcome {
35    pub pass: u32,
36    pub status: FixStatus,
37    pub summary: Option<String>,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
41pub struct CommitValidatedOutcome {
42    pub attempt: u32,
43    pub message: Option<String>,
44    pub reason: Option<String>,
45}
46
47#[derive(Clone, Serialize, Deserialize, Debug, Default)]
48pub struct PromptInputsState {
49    #[serde(default)]
50    pub planning: Option<MaterializedPlanningInputs>,
51    #[serde(default)]
52    pub development: Option<MaterializedDevelopmentInputs>,
53    #[serde(default)]
54    pub review: Option<MaterializedReviewInputs>,
55    #[serde(default)]
56    pub commit: Option<MaterializedCommitInputs>,
57    /// Materialized last invalid XML output for XSD retry prompts.
58    ///
59    /// This is used to dedupe retries and keep oversize handling reducer-visible.
60    #[serde(default)]
61    pub xsd_retry_last_output: Option<MaterializedXsdRetryLastOutput>,
62}
63
64#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
65pub struct MaterializedPlanningInputs {
66    pub iteration: u32,
67    pub prompt: MaterializedPromptInput,
68}
69
70#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
71pub struct MaterializedDevelopmentInputs {
72    pub iteration: u32,
73    pub prompt: MaterializedPromptInput,
74    pub plan: MaterializedPromptInput,
75}
76
77#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
78pub struct MaterializedReviewInputs {
79    pub pass: u32,
80    pub plan: MaterializedPromptInput,
81    pub diff: MaterializedPromptInput,
82}
83
84#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
85pub struct MaterializedCommitInputs {
86    pub attempt: u32,
87    pub diff: MaterializedPromptInput,
88}
89
90#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
91pub struct MaterializedXsdRetryLastOutput {
92    pub phase: PipelinePhase,
93    pub scope_id: u32,
94    pub last_output: MaterializedPromptInput,
95}
96
97/// Immutable pipeline state - the single source of truth for pipeline progress.
98///
99/// This struct captures complete execution context and doubles as the checkpoint
100/// data structure for resume functionality. Serialize it to JSON to save state;
101/// deserialize to resume interrupted runs.
102///
103/// # Invariants
104///
105/// - `iteration` is always `<= total_iterations`
106/// - `reviewer_pass` is always `<= total_reviewer_passes`
107/// - `agent_chain` maintains fallback order and retry counts
108/// - State transitions only occur through the [`reduce`](super::reduce) function
109///
110/// # See Also
111///
112/// - [`reduce`](super::reduce) for state transitions
113/// - [`determine_next_effect`](super::determine_next_effect) for effect derivation
114#[derive(Clone, Serialize, Deserialize, Debug)]
115pub struct PipelineState {
116    pub phase: PipelinePhase,
117    pub previous_phase: Option<PipelinePhase>,
118    pub iteration: u32,
119    pub total_iterations: u32,
120    pub reviewer_pass: u32,
121    pub total_reviewer_passes: u32,
122    pub review_issues_found: bool,
123    /// Tracks whether the planning prompt was prepared for the current iteration.
124    #[serde(default)]
125    pub planning_prompt_prepared_iteration: Option<u32>,
126    /// Tracks whether `.agent/tmp/plan.xml` was cleaned for the current iteration.
127    #[serde(default)]
128    pub planning_xml_cleaned_iteration: Option<u32>,
129    /// Tracks whether the planning agent was invoked for the current iteration.
130    #[serde(default)]
131    pub planning_agent_invoked_iteration: Option<u32>,
132    /// Tracks whether `.agent/tmp/plan.xml` was successfully extracted for the iteration.
133    #[serde(default)]
134    pub planning_xml_extracted_iteration: Option<u32>,
135    /// Stores the validated outcome for the current planning iteration.
136    #[serde(default)]
137    pub planning_validated_outcome: Option<PlanningValidatedOutcome>,
138    /// Tracks whether PLAN.md has been written for the current iteration.
139    #[serde(default)]
140    pub planning_markdown_written_iteration: Option<u32>,
141    /// Tracks whether `.agent/tmp/plan.xml` was archived for the current iteration.
142    #[serde(default)]
143    pub planning_xml_archived_iteration: Option<u32>,
144    /// Tracks whether development context was prepared for the current iteration.
145    ///
146    /// Used to sequence single-task development effects.
147    #[serde(default)]
148    pub development_context_prepared_iteration: Option<u32>,
149    /// Tracks whether the development prompt was prepared for the current iteration.
150    #[serde(default)]
151    pub development_prompt_prepared_iteration: Option<u32>,
152    /// Tracks whether `.agent/tmp/development_result.xml` was cleaned for the current iteration.
153    #[serde(default)]
154    pub development_xml_cleaned_iteration: Option<u32>,
155    /// Tracks whether the developer agent was invoked for the current iteration.
156    #[serde(default)]
157    pub development_agent_invoked_iteration: Option<u32>,
158    /// Tracks whether the analysis agent was invoked for the current iteration.
159    ///
160    /// Analysis agent runs after EVERY development iteration to produce
161    /// an objective assessment of progress by comparing git diff against PLAN.md.
162    /// This ensures continuous verification throughout the development phase.
163    #[serde(default)]
164    pub analysis_agent_invoked_iteration: Option<u32>,
165    /// Tracks whether `.agent/tmp/development_result.xml` was extracted for the current iteration.
166    #[serde(default)]
167    pub development_xml_extracted_iteration: Option<u32>,
168    /// Stores the validated development outcome for the current iteration.
169    #[serde(default)]
170    pub development_validated_outcome: Option<DevelopmentValidatedOutcome>,
171    /// Tracks whether the development XML was archived for the current iteration.
172    #[serde(default)]
173    pub development_xml_archived_iteration: Option<u32>,
174    /// Tracks whether review context was prepared for the current pass.
175    ///
176    /// Used to sequence single-task review effects (PrepareReviewContext -> ...).
177    #[serde(default)]
178    pub review_context_prepared_pass: Option<u32>,
179    /// Tracks whether the review prompt was prepared for the current pass.
180    #[serde(default)]
181    pub review_prompt_prepared_pass: Option<u32>,
182    /// Tracks whether `.agent/tmp/issues.xml` was cleaned for the current pass.
183    #[serde(default)]
184    pub review_issues_xml_cleaned_pass: Option<u32>,
185    /// Tracks whether the reviewer agent was invoked for the current pass.
186    #[serde(default)]
187    pub review_agent_invoked_pass: Option<u32>,
188    /// Tracks whether `.agent/tmp/issues.xml` was successfully extracted for the current pass.
189    #[serde(default)]
190    pub review_issues_xml_extracted_pass: Option<u32>,
191    /// Stores the validated outcome for the current review pass.
192    ///
193    /// This is used to sequence post-validation single-task effects (write markdown,
194    /// archive XML) before the reducer advances to the next pass/phase.
195    #[serde(default)]
196    pub review_validated_outcome: Option<ReviewValidatedOutcome>,
197    /// Tracks whether ISSUES.md has been written for the current pass.
198    #[serde(default)]
199    pub review_issues_markdown_written_pass: Option<u32>,
200    /// Tracks whether review issue snippets were extracted for the current pass.
201    #[serde(default)]
202    pub review_issue_snippets_extracted_pass: Option<u32>,
203    #[serde(default)]
204    pub review_issues_xml_archived_pass: Option<u32>,
205
206    #[serde(default)]
207    pub fix_prompt_prepared_pass: Option<u32>,
208
209    #[serde(default)]
210    pub fix_result_xml_cleaned_pass: Option<u32>,
211
212    #[serde(default)]
213    pub fix_agent_invoked_pass: Option<u32>,
214
215    #[serde(default)]
216    pub fix_result_xml_extracted_pass: Option<u32>,
217
218    #[serde(default)]
219    pub fix_validated_outcome: Option<FixValidatedOutcome>,
220
221    #[serde(default)]
222    pub fix_result_xml_archived_pass: Option<u32>,
223    /// Tracks whether the commit prompt was prepared for the current commit attempt.
224    #[serde(default)]
225    pub commit_prompt_prepared: bool,
226    /// Tracks whether the commit diff has been computed for the current attempt.
227    #[serde(default)]
228    pub commit_diff_prepared: bool,
229    /// Tracks whether the computed commit diff was empty.
230    #[serde(default)]
231    pub commit_diff_empty: bool,
232    /// Content identifier (sha256 hex) of the prepared commit diff.
233    ///
234    /// This is recorded when the diff is prepared and is used by orchestration guards
235    /// to avoid reusing stale materialized prompt inputs across checkpoint resumes or
236    /// when tmp artifacts change.
237    #[serde(default)]
238    pub commit_diff_content_id_sha256: Option<String>,
239    /// Tracks whether the commit agent was invoked for the current commit attempt.
240    #[serde(default)]
241    pub commit_agent_invoked: bool,
242    /// Tracks whether `.agent/tmp/commit_message.xml` was cleaned for the current attempt.
243    #[serde(default)]
244    pub commit_xml_cleaned: bool,
245    /// Tracks whether `.agent/tmp/commit_message.xml` was extracted for the current attempt.
246    #[serde(default)]
247    pub commit_xml_extracted: bool,
248    /// Stores the validated commit outcome for the current attempt.
249    #[serde(default)]
250    pub commit_validated_outcome: Option<CommitValidatedOutcome>,
251    /// Tracks whether commit XML has been archived for the current attempt.
252    #[serde(default)]
253    pub commit_xml_archived: bool,
254    pub context_cleaned: bool,
255    pub agent_chain: AgentChainState,
256    pub rebase: RebaseState,
257    pub commit: CommitState,
258    pub execution_history: Vec<ExecutionStep>,
259    /// Count of CheckpointSaved events applied to state.
260    ///
261    /// This is a reducer-visible record of checkpoint saves, intended for
262    /// observability and tests that enforce checkpointing happens via effects.
263    #[serde(default)]
264    pub checkpoint_saved_count: u32,
265    /// Continuation state for development iterations.
266    ///
267    /// Tracks context from previous attempts when status is "partial" or "failed"
268    /// to enable continuation-aware prompting.
269    #[serde(default)]
270    pub continuation: ContinuationState,
271
272    /// Run-level execution metrics.
273    ///
274    /// This is the single source of truth for all iteration/attempt/retry/fallback
275    /// statistics. Updated deterministically by the reducer based on events.
276    #[serde(default)]
277    pub metrics: RunMetrics,
278
279    /// Whether TriggerDevFixFlow has been executed in the current AwaitingDevFix phase.
280    ///
281    /// This flag is set to true when DevFixTriggered event is reduced.
282    /// It ensures the event loop allows at least one iteration to execute
283    /// TriggerDevFixFlow before checking completion, preventing premature
284    /// exit when max iterations is imminent.
285    #[serde(default)]
286    pub dev_fix_triggered: bool,
287
288    /// Canonical, reducer-visible prompt inputs after oversize materialization.
289    ///
290    /// This is the single source of truth for any inline-vs-reference and
291    /// model-budget truncation decisions. Effects must not silently re-truncate
292    /// or re-reference content on retries; instead, they should consume these
293    /// materialized inputs (or materialize them exactly once per content id).
294    #[serde(default)]
295    pub prompt_inputs: PromptInputsState,
296}
297
298impl PipelineState {
299    pub fn initial(developer_iters: u32, reviewer_reviews: u32) -> Self {
300        Self::initial_with_continuation(developer_iters, reviewer_reviews, ContinuationState::new())
301    }
302
303    /// Create initial state with custom continuation limits from config.
304    ///
305    /// Use this when you need to load XSD retry and continuation limits from unified config.
306    /// Example:
307    /// ```ignore
308    /// // Config semantics: max_dev_continuations counts continuation attempts *beyond*
309    /// // the initial attempt. ContinuationState::max_continue_count semantics are
310    /// // "maximum total attempts including initial".
311    /// let continuation = ContinuationState::with_limits(
312    ///     config.general.max_xsd_retries,
313    ///     1 + config.general.max_dev_continuations,
314    ///     config.general.max_same_agent_retries,
315    /// );
316    /// let state = PipelineState::initial_with_continuation(dev_iters, reviews, continuation);
317    /// ```
318    pub fn initial_with_continuation(
319        developer_iters: u32,
320        reviewer_reviews: u32,
321        continuation: ContinuationState,
322    ) -> Self {
323        // Determine initial phase based on what work needs to be done
324        let initial_phase = if developer_iters == 0 {
325            // No development iterations → skip Planning and Development
326            if reviewer_reviews == 0 {
327                // No review passes either → go straight to commit
328                PipelinePhase::CommitMessage
329            } else {
330                PipelinePhase::Review
331            }
332        } else {
333            PipelinePhase::Planning
334        };
335
336        Self {
337            phase: initial_phase,
338            previous_phase: None,
339            iteration: 0,
340            total_iterations: developer_iters,
341            reviewer_pass: 0,
342            total_reviewer_passes: reviewer_reviews,
343            review_issues_found: false,
344            planning_prompt_prepared_iteration: None,
345            planning_xml_cleaned_iteration: None,
346            planning_agent_invoked_iteration: None,
347            planning_xml_extracted_iteration: None,
348            planning_validated_outcome: None,
349            planning_markdown_written_iteration: None,
350            planning_xml_archived_iteration: None,
351            development_context_prepared_iteration: None,
352            development_prompt_prepared_iteration: None,
353            development_xml_cleaned_iteration: None,
354            development_agent_invoked_iteration: None,
355            analysis_agent_invoked_iteration: None,
356            development_xml_extracted_iteration: None,
357            development_validated_outcome: None,
358            development_xml_archived_iteration: None,
359            review_context_prepared_pass: None,
360            review_prompt_prepared_pass: None,
361            review_issues_xml_cleaned_pass: None,
362            review_agent_invoked_pass: None,
363            review_issues_xml_extracted_pass: None,
364            review_validated_outcome: None,
365            review_issues_markdown_written_pass: None,
366            review_issue_snippets_extracted_pass: None,
367            review_issues_xml_archived_pass: None,
368            fix_prompt_prepared_pass: None,
369            fix_result_xml_cleaned_pass: None,
370            fix_agent_invoked_pass: None,
371            fix_result_xml_extracted_pass: None,
372            fix_validated_outcome: None,
373            fix_result_xml_archived_pass: None,
374            commit_prompt_prepared: false,
375            commit_diff_prepared: false,
376            commit_diff_empty: false,
377            commit_diff_content_id_sha256: None,
378            commit_agent_invoked: false,
379            commit_xml_cleaned: false,
380            commit_xml_extracted: false,
381            commit_validated_outcome: None,
382            commit_xml_archived: false,
383            context_cleaned: false,
384            agent_chain: AgentChainState::initial(),
385            rebase: RebaseState::NotStarted,
386            commit: CommitState::NotStarted,
387            execution_history: Vec::new(),
388            checkpoint_saved_count: 0,
389            continuation: continuation.clone(),
390            dev_fix_triggered: false,
391            prompt_inputs: PromptInputsState::default(),
392            metrics: RunMetrics::new(developer_iters, reviewer_reviews, &continuation),
393        }
394    }
395
396    /// Returns true if the pipeline is in a terminal state for event loop purposes.
397    ///
398    /// # Terminal States
399    ///
400    /// - **Complete phase**: Always terminal (successful completion)
401    /// - **Interrupted phase**: Terminal under these conditions:
402    ///   1. A checkpoint has been saved (normal Ctrl+C interruption path)
403    ///   2. Transitioning from AwaitingDevFix phase (failure handling completed)
404    ///
405    /// # AwaitingDevFix → Interrupted Path
406    ///
407    /// When the pipeline encounters a terminal failure (e.g., AgentChainExhausted),
408    /// it transitions through AwaitingDevFix phase where:
409    /// 1. TriggerDevFixFlow effect writes completion marker to filesystem
410    /// 2. Dev-fix agent is dispatched (optional remediation attempt)
411    /// 3. CompletionMarkerEmitted event transitions to Interrupted phase
412    ///
413    /// At this point, the completion marker has been written, signaling external
414    /// orchestration that the pipeline has terminated. The SaveCheckpoint effect
415    /// will execute next, but the phase is already considered terminal because
416    /// the failure has been properly signaled.
417    ///
418    /// # Edge Cases
419    ///
420    /// An Interrupted phase without a checkpoint and without previous_phase context
421    /// is NOT considered terminal. This can occur when resuming from a checkpoint
422    /// that was interrupted mid-execution.
423    ///
424    /// # Non-Terminating Pipeline Architecture
425    ///
426    /// The pipeline is designed to never exit early. All failure paths route through
427    /// `AwaitingDevFix` → `TriggerDevFixFlow` → completion marker write → `Interrupted`.
428    /// This ensures orchestration can reliably detect completion via the marker file,
429    /// even when budget is exhausted or all agents fail.
430    ///
431    /// Terminal states:
432    /// - `Complete`: Normal successful completion
433    /// - `Interrupted` with checkpoint saved: Resumable state
434    /// - `Interrupted` from `AwaitingDevFix`: Completion marker written, failure signaled
435    pub fn is_complete(&self) -> bool {
436        matches!(self.phase, PipelinePhase::Complete)
437            || (matches!(self.phase, PipelinePhase::Interrupted)
438                && (self.checkpoint_saved_count > 0
439                    // CRITICAL: AwaitingDevFix→Interrupted transition means completion marker
440                    // was written during TriggerDevFixFlow. This is terminal even without
441                    // checkpoint because the failure has been properly signaled to orchestration.
442                    // This prevents "Pipeline exited without completion marker" bug.
443                    || matches!(self.previous_phase, Some(PipelinePhase::AwaitingDevFix))))
444    }
445
446    pub fn current_head(&self) -> String {
447        self.rebase
448            .current_head()
449            .unwrap_or_else(|| "HEAD".to_string())
450    }
451}
452
453impl From<PipelineCheckpoint> for PipelineState {
454    fn from(checkpoint: PipelineCheckpoint) -> Self {
455        let rebase_state = map_checkpoint_rebase_state(&checkpoint.rebase_state);
456        let agent_chain = AgentChainState::initial();
457
458        PipelineState {
459            phase: map_checkpoint_phase(checkpoint.phase),
460            previous_phase: None,
461            iteration: checkpoint.iteration,
462            total_iterations: checkpoint.total_iterations,
463            reviewer_pass: checkpoint.reviewer_pass,
464            total_reviewer_passes: checkpoint.total_reviewer_passes,
465            review_issues_found: false,
466            planning_prompt_prepared_iteration: None,
467            planning_xml_cleaned_iteration: None,
468            planning_agent_invoked_iteration: None,
469            planning_xml_extracted_iteration: None,
470            planning_validated_outcome: None,
471            planning_markdown_written_iteration: None,
472            planning_xml_archived_iteration: None,
473            development_context_prepared_iteration: None,
474            development_prompt_prepared_iteration: None,
475            development_xml_cleaned_iteration: None,
476            development_agent_invoked_iteration: None,
477            analysis_agent_invoked_iteration: None,
478            development_xml_extracted_iteration: None,
479            development_validated_outcome: None,
480            development_xml_archived_iteration: None,
481            review_context_prepared_pass: None,
482            review_prompt_prepared_pass: None,
483            review_issues_xml_cleaned_pass: None,
484            review_agent_invoked_pass: None,
485            review_issues_xml_extracted_pass: None,
486            review_validated_outcome: None,
487            review_issues_markdown_written_pass: None,
488            review_issue_snippets_extracted_pass: None,
489            review_issues_xml_archived_pass: None,
490            fix_prompt_prepared_pass: None,
491            fix_result_xml_cleaned_pass: None,
492            fix_agent_invoked_pass: None,
493            fix_result_xml_extracted_pass: None,
494            fix_validated_outcome: None,
495            fix_result_xml_archived_pass: None,
496            commit_prompt_prepared: false,
497            commit_diff_prepared: false,
498            commit_diff_empty: false,
499            commit_diff_content_id_sha256: None,
500            commit_agent_invoked: false,
501            commit_xml_cleaned: false,
502            commit_xml_extracted: false,
503            commit_validated_outcome: None,
504            commit_xml_archived: false,
505            context_cleaned: false,
506            agent_chain,
507            rebase: rebase_state,
508            commit: CommitState::NotStarted,
509            execution_history: checkpoint
510                .execution_history
511                .map(|h| h.steps)
512                .unwrap_or_default(),
513            checkpoint_saved_count: 0,
514            continuation: ContinuationState::new(),
515            dev_fix_triggered: false,
516            prompt_inputs: checkpoint.prompt_inputs.unwrap_or_default(),
517            metrics: {
518                let continuation = ContinuationState::new();
519                RunMetrics {
520                    dev_iterations_completed: checkpoint.actual_developer_runs,
521                    review_passes_completed: checkpoint.actual_reviewer_runs,
522                    max_dev_iterations: checkpoint.total_iterations,
523                    max_review_passes: checkpoint.total_reviewer_passes,
524                    max_xsd_retry_count: continuation.max_xsd_retry_count,
525                    max_dev_continuation_count: continuation.max_continue_count,
526                    max_fix_continuation_count: continuation.max_fix_continue_count,
527                    max_same_agent_retry_count: continuation.max_same_agent_retry_count,
528                    ..RunMetrics::default()
529                }
530            },
531        }
532    }
533}
534
535fn map_checkpoint_phase(phase: CheckpointPhase) -> PipelinePhase {
536    match phase {
537        CheckpointPhase::Rebase => PipelinePhase::Planning,
538        CheckpointPhase::Planning => PipelinePhase::Planning,
539        CheckpointPhase::Development => PipelinePhase::Development,
540        CheckpointPhase::Review => PipelinePhase::Review,
541        CheckpointPhase::CommitMessage => PipelinePhase::CommitMessage,
542        CheckpointPhase::FinalValidation => PipelinePhase::FinalValidation,
543        CheckpointPhase::Complete => PipelinePhase::Complete,
544        CheckpointPhase::PreRebase => PipelinePhase::Planning,
545        CheckpointPhase::PreRebaseConflict => PipelinePhase::Planning,
546        CheckpointPhase::PostRebase => PipelinePhase::CommitMessage,
547        CheckpointPhase::PostRebaseConflict => PipelinePhase::CommitMessage,
548        CheckpointPhase::AwaitingDevFix => PipelinePhase::AwaitingDevFix,
549        CheckpointPhase::Interrupted => PipelinePhase::Interrupted,
550    }
551}
552
553fn map_checkpoint_rebase_state(rebase_state: &CheckpointRebaseState) -> RebaseState {
554    match rebase_state {
555        CheckpointRebaseState::NotStarted => RebaseState::NotStarted,
556        CheckpointRebaseState::PreRebaseInProgress { upstream_branch } => RebaseState::InProgress {
557            original_head: "HEAD".to_string(),
558            target_branch: upstream_branch.clone(),
559        },
560        CheckpointRebaseState::PreRebaseCompleted { commit_oid } => RebaseState::Completed {
561            new_head: commit_oid.clone(),
562        },
563        CheckpointRebaseState::PostRebaseInProgress { upstream_branch } => {
564            RebaseState::InProgress {
565                original_head: "HEAD".to_string(),
566                target_branch: upstream_branch.clone(),
567            }
568        }
569        CheckpointRebaseState::PostRebaseCompleted { commit_oid } => RebaseState::Completed {
570            new_head: commit_oid.clone(),
571        },
572        CheckpointRebaseState::HasConflicts { files } => RebaseState::Conflicted {
573            original_head: "HEAD".to_string(),
574            target_branch: "main".to_string(),
575            files: files.iter().map(PathBuf::from).collect(),
576            resolution_attempts: 0,
577        },
578        CheckpointRebaseState::Failed { .. } => RebaseState::Skipped,
579    }
580}