Skip to main content

ralph_workflow/reducer/
event.rs

1//! Pipeline event types for reducer architecture.
2//!
3//! Defines all possible events that can occur during pipeline execution.
4//! Each event represents a state transition that the reducer handles.
5//!
6//! # Event Categories
7//!
8//! Events are organized into logical categories for type-safe routing to
9//! category-specific reducers. Each category has a dedicated inner enum:
10//!
11//! - [`LifecycleEvent`] - Pipeline start/stop/resume
12//! - [`PlanningEvent`] - Plan generation events
13//! - [`DevelopmentEvent`] - Development iteration and continuation events
14//! - [`ReviewEvent`] - Review pass and fix attempt events
15//! - [`AgentEvent`] - Agent invocation and chain management events
16//! - [`RebaseEvent`] - Git rebase operation events
17//! - [`CommitEvent`] - Commit generation events
18//!
19//! The main [`PipelineEvent`] enum wraps these category enums to enable
20//! type-safe dispatch in the reducer.
21
22use crate::agents::AgentRole;
23use crate::reducer::state::{DevelopmentStatus, MaterializedPromptInput, PromptInputKind};
24use serde::{Deserialize, Serialize};
25use std::path::PathBuf;
26
27// ============================================================================
28// Event Category Enums
29// ============================================================================
30
31/// Pipeline lifecycle events (start, stop, resume).
32///
33/// These events control the overall pipeline execution lifecycle,
34/// distinct from phase-specific transitions. Use these for:
35///
36/// - Starting or resuming a pipeline run
37/// - Completing a successful pipeline execution
38///
39/// # When to Use
40///
41/// - `Started`: When a fresh pipeline run begins
42/// - `Resumed`: When resuming from a checkpoint
43/// - `Completed`: When all phases complete successfully
44///
45/// # ⚠️ FROZEN - DO NOT ADD VARIANTS ⚠️
46///
47/// This enum is **FROZEN**. Adding new variants is **PROHIBITED**.
48///
49/// ## Why is this frozen?
50///
51/// Lifecycle events control pipeline flow (start/stop/completion). Allowing effect
52/// handlers to emit new lifecycle events would violate the core architectural principle:
53/// **handlers describe what happened; reducers decide what happens next.**
54///
55/// ## What to do instead
56///
57/// If you need to express new observations or failures:
58///
59/// 1. **Reuse existing phase/category events** - Use `PlanningEvent`, `DevelopmentEvent`,
60///    `ReviewEvent`, `CommitEvent`, etc. to describe what happened within that phase.
61///    Example: `PlanningEvent::PlanXmlMissing` instead of creating a generic "Aborted" event.
62///
63/// 2. **Return errors from the event loop** - For truly unrecoverable failures (permission
64///    errors, invariant violations), return `Err` from the effect handler. The outer runner
65///    will handle termination, not the reducer.
66///
67/// 3. **Handle in orchestration** - Some conditions don't need events at all and can be
68///    handled in the effect handler or runner logic.
69///
70/// ## Enforcement
71///
72/// The freeze policy is enforced by the `lifecycle_event_is_frozen` test in this module,
73/// which will fail to compile if new variants are added. This is intentional.
74#[derive(Clone, Serialize, Deserialize, Debug)]
75pub enum LifecycleEvent {
76    /// Pipeline execution started fresh (not from checkpoint).
77    Started,
78    /// Pipeline execution resumed from a previous state.
79    Resumed {
80        /// Whether this resume is from a persisted checkpoint.
81        from_checkpoint: bool,
82    },
83    /// Pipeline execution completed successfully.
84    Completed,
85}
86
87/// Planning phase events.
88///
89/// Events related to plan generation and validation within the Planning phase.
90/// The planning phase generates a plan for the current development iteration.
91///
92/// # State Transitions
93///
94/// - `PhaseStarted`: Sets phase to Planning
95/// - `GenerationCompleted(valid=true)`: Transitions to Development
96/// - `GenerationCompleted(valid=false)`: Stays in Planning for retry
97/// - `PhaseCompleted`: Transitions to Development
98#[derive(Clone, Serialize, Deserialize, Debug)]
99pub enum PlanningEvent {
100    /// Planning phase has started.
101    PhaseStarted,
102    /// Planning phase completed, ready to proceed.
103    PhaseCompleted,
104    /// Planning prompt prepared for an iteration.
105    PromptPrepared {
106        /// The iteration number this plan is for.
107        iteration: u32,
108    },
109    /// Planning agent invoked for an iteration.
110    AgentInvoked {
111        /// The iteration number this plan is for.
112        iteration: u32,
113    },
114    /// Planning XML extracted for an iteration.
115    PlanXmlExtracted {
116        /// The iteration number this plan is for.
117        iteration: u32,
118    },
119    /// Planning XML missing for an iteration.
120    PlanXmlMissing {
121        /// The iteration number this plan is for.
122        iteration: u32,
123        /// The invalid output attempt count.
124        attempt: u32,
125    },
126    /// Planning XML validated for an iteration.
127    PlanXmlValidated {
128        /// The iteration number this plan is for.
129        iteration: u32,
130        /// Whether the generated plan passed validation.
131        valid: bool,
132        /// Markdown generated from the validated plan XML.
133        markdown: Option<String>,
134    },
135    /// Planning markdown written for an iteration.
136    PlanMarkdownWritten {
137        /// The iteration number this plan is for.
138        iteration: u32,
139    },
140    /// Planning XML archived for an iteration.
141    PlanXmlArchived {
142        /// The iteration number this plan is for.
143        iteration: u32,
144    },
145    /// Planning XML cleaned before invoking the planning agent.
146    PlanXmlCleaned {
147        /// The iteration number this plan is for.
148        iteration: u32,
149    },
150    /// Plan generation completed with validation result.
151    GenerationCompleted {
152        /// The iteration number this plan was for.
153        iteration: u32,
154        /// Whether the generated plan passed validation.
155        valid: bool,
156    },
157
158    /// Output validation failed (missing/empty or otherwise invalid plan output).
159    ///
160    /// Emitted when planning output cannot be validated. The reducer decides
161    /// whether to retry (same agent) or switch agents based on the attempt count.
162    OutputValidationFailed {
163        /// Current iteration number.
164        iteration: u32,
165        /// Current invalid output attempt number.
166        attempt: u32,
167    },
168}
169
170/// Prompt input oversize detection and materialization events.
171///
172/// These events make reducer-visible any transformation that affects the
173/// agent-visible prompt content (inline vs file reference, truncation, etc.).
174#[derive(Clone, Serialize, Deserialize, Debug)]
175pub enum PromptInputEvent {
176    OversizeDetected {
177        phase: PipelinePhase,
178        kind: PromptInputKind,
179        content_id_sha256: String,
180        size_bytes: u64,
181        limit_bytes: u64,
182        policy: String,
183    },
184    PlanningInputsMaterialized {
185        iteration: u32,
186        prompt: MaterializedPromptInput,
187    },
188    DevelopmentInputsMaterialized {
189        iteration: u32,
190        prompt: MaterializedPromptInput,
191        plan: MaterializedPromptInput,
192    },
193    ReviewInputsMaterialized {
194        pass: u32,
195        plan: MaterializedPromptInput,
196        diff: MaterializedPromptInput,
197    },
198    CommitInputsMaterialized {
199        attempt: u32,
200        diff: MaterializedPromptInput,
201    },
202    XsdRetryLastOutputMaterialized {
203        /// Phase that produced the invalid output being retried.
204        phase: PipelinePhase,
205        /// Scope id within the phase (iteration/pass/attempt).
206        scope_id: u32,
207        /// Materialized representation of the last invalid output.
208        last_output: MaterializedPromptInput,
209    },
210    /// A typed error event returned by an effect handler.
211    ///
212    /// Effect handlers surface failures by returning `Err(ErrorEvent::... .into())`.
213    /// The event loop extracts the underlying `ErrorEvent` and re-emits it through
214    /// this existing category so the reducer can decide recovery strategy without
215    /// adding new top-level `PipelineEvent` variants.
216    HandlerError {
217        /// Phase during which the error occurred (best-effort; derived from current state).
218        phase: PipelinePhase,
219        /// The typed error event.
220        error: ErrorEvent,
221    },
222}
223
224#[path = "event/development.rs"]
225mod development;
226pub use development::DevelopmentEvent;
227
228#[path = "event/review.rs"]
229mod review;
230pub use review::ReviewEvent;
231
232#[path = "event/agent.rs"]
233mod agent;
234pub use agent::AgentEvent;
235
236#[path = "event/error.rs"]
237mod error;
238pub use error::ErrorEvent;
239pub use error::WorkspaceIoErrorKind;
240
241/// Rebase operation events.
242///
243/// Events related to git rebase operations including conflict detection
244/// and resolution. Rebase operations can occur at multiple points in the
245/// pipeline (initial and post-review).
246///
247/// # State Machine
248///
249/// ```text
250/// NotStarted -> InProgress -> Conflicted -> InProgress -> Completed
251///                    |                           |
252///                    +---------> Skipped <-------+
253///                    |
254///                    +---------> Failed (resets to NotStarted)
255/// ```
256#[derive(Clone, Serialize, Deserialize, Debug)]
257pub enum RebaseEvent {
258    /// Rebase operation started.
259    Started {
260        /// The rebase phase (initial or post-review).
261        phase: RebasePhase,
262        /// The target branch to rebase onto.
263        target_branch: String,
264    },
265    /// Merge conflict detected during rebase.
266    ConflictDetected {
267        /// The files with conflicts.
268        files: Vec<PathBuf>,
269    },
270    /// Merge conflicts were resolved.
271    ConflictResolved {
272        /// The files that were resolved.
273        files: Vec<PathBuf>,
274    },
275    /// Rebase completed successfully.
276    Succeeded {
277        /// The rebase phase that completed.
278        phase: RebasePhase,
279        /// The new HEAD after rebase.
280        new_head: String,
281    },
282    /// Rebase failed and was reset.
283    Failed {
284        /// The rebase phase that failed.
285        phase: RebasePhase,
286        /// The reason for failure.
287        reason: String,
288    },
289    /// Rebase was aborted and state restored.
290    Aborted {
291        /// The rebase phase that was aborted.
292        phase: RebasePhase,
293        /// The commit that was restored.
294        restored_to: String,
295    },
296    /// Rebase was skipped (e.g., already up to date).
297    Skipped {
298        /// The rebase phase that was skipped.
299        phase: RebasePhase,
300        /// The reason for skipping.
301        reason: String,
302    },
303}
304
305/// Commit generation events.
306///
307/// Events related to commit message generation, validation, and creation.
308/// Commit generation occurs after development iterations and review fixes.
309///
310/// # State Machine
311///
312/// ```text
313/// NotStarted -> Generating -> Generated -> Committed
314///                    |              |
315///                    +--> (retry) --+
316///                    |
317///                    +--> Skipped
318/// ```
319#[derive(Clone, Serialize, Deserialize, Debug)]
320pub enum CommitEvent {
321    /// Commit message generation started.
322    GenerationStarted,
323    /// Commit diff computed for commit generation.
324    DiffPrepared {
325        /// True when the diff is empty.
326        empty: bool,
327        /// Content identifier (sha256 hex) of the prepared diff content.
328        ///
329        /// This is used to guard against reusing stale materialized inputs when the
330        /// diff content changes across checkpoints or retries.
331        content_id_sha256: String,
332    },
333    /// Commit diff computation failed.
334    DiffFailed {
335        /// The error message for the diff failure.
336        error: String,
337    },
338    /// Commit diff is no longer available and must be recomputed.
339    ///
340    /// This is used for recoverability when `.agent/tmp` artifacts are cleaned between
341    /// checkpoints or when required diff files go missing during resume.
342    DiffInvalidated {
343        /// Reason for invalidation.
344        reason: String,
345    },
346    /// Commit prompt prepared for a commit attempt.
347    PromptPrepared {
348        /// The attempt number.
349        attempt: u32,
350    },
351    /// Commit agent invoked for a commit attempt.
352    AgentInvoked {
353        /// The attempt number.
354        attempt: u32,
355    },
356    /// Commit message XML extracted for a commit attempt.
357    CommitXmlExtracted {
358        /// The attempt number.
359        attempt: u32,
360    },
361    /// Commit message XML missing for a commit attempt.
362    CommitXmlMissing {
363        /// The attempt number.
364        attempt: u32,
365    },
366    /// Commit message XML validated successfully.
367    CommitXmlValidated {
368        /// The generated commit message.
369        message: String,
370        /// The attempt number.
371        attempt: u32,
372    },
373    /// Commit message XML validation failed.
374    CommitXmlValidationFailed {
375        /// The reason for validation failure.
376        reason: String,
377        /// The attempt number.
378        attempt: u32,
379    },
380    /// Commit message XML archived.
381    CommitXmlArchived {
382        /// The attempt number.
383        attempt: u32,
384    },
385    /// Commit message XML cleaned before invoking the commit agent.
386    CommitXmlCleaned {
387        /// The attempt number.
388        attempt: u32,
389    },
390    /// Commit message was generated.
391    MessageGenerated {
392        /// The generated commit message.
393        message: String,
394        /// The attempt number.
395        attempt: u32,
396    },
397    /// Commit message validation failed.
398    MessageValidationFailed {
399        /// The reason for validation failure.
400        reason: String,
401        /// The attempt number that failed.
402        attempt: u32,
403    },
404    /// Commit was created successfully.
405    Created {
406        /// The commit hash.
407        hash: String,
408        /// The commit message used.
409        message: String,
410    },
411    /// Commit generation failed completely.
412    GenerationFailed {
413        /// The reason for failure.
414        reason: String,
415    },
416    /// Commit was skipped (e.g., no changes to commit).
417    Skipped {
418        /// The reason for skipping.
419        reason: String,
420    },
421}
422
423/// Events for AwaitingDevFix phase.
424///
425/// This phase handles pipeline failure remediation by invoking the development
426/// agent to diagnose and fix the root cause before termination.
427#[derive(Clone, Serialize, Deserialize, Debug)]
428pub enum AwaitingDevFixEvent {
429    /// Dev-fix flow was triggered.
430    DevFixTriggered {
431        failed_phase: PipelinePhase,
432        failed_role: AgentRole,
433    },
434    /// Dev-fix flow was skipped (not yet implemented or disabled).
435    DevFixSkipped { reason: String },
436    /// Dev-fix flow completed (may or may not have fixed the issue).
437    DevFixCompleted {
438        success: bool,
439        summary: Option<String>,
440    },
441    /// Dev-fix agent is unavailable (quota/usage limit).
442    DevFixAgentUnavailable {
443        failed_phase: PipelinePhase,
444        reason: String,
445    },
446    /// Completion marker was emitted to filesystem.
447    CompletionMarkerEmitted { is_failure: bool },
448}
449
450// ============================================================================
451// Supporting Types
452// ============================================================================
453
454/// Pipeline phases for checkpoint tracking.
455///
456/// These phases represent the major stages of the Ralph pipeline.
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
458pub enum PipelinePhase {
459    Planning,
460    Development,
461    Review,
462    CommitMessage,
463    FinalValidation,
464    /// Finalizing phase for cleanup operations before completion.
465    ///
466    /// This phase handles:
467    /// - Restoring PROMPT.md write permissions
468    /// - Any other cleanup that must go through the effect system
469    Finalizing,
470    Complete,
471    /// Awaiting development agent to fix pipeline failure.
472    ///
473    /// This phase occurs when the pipeline encounters a terminal failure condition
474    /// (e.g., agent chain exhausted) but before transitioning to Interrupted. It
475    /// signals that the development agent should be invoked to diagnose and fix
476    /// the failure root cause.
477    ///
478    /// ## Failure Handling Flow
479    ///
480    /// 1. ErrorEvent::AgentChainExhausted occurs in any phase
481    /// 2. Reducer transitions state to AwaitingDevFix
482    /// 3. Orchestration determines Effect::TriggerDevFixFlow
483    /// 4. Handler executes TriggerDevFixFlow:
484    ///    a. Writes completion marker to .agent/tmp/completion_marker (failure status)
485    ///    b. Emits DevFixTriggered event
486    ///    c. Dispatches dev-fix agent
487    ///    d. Emits DevFixCompleted event
488    ///    e. Emits CompletionMarkerEmitted event
489    /// 5. DevFixTriggered/DevFixCompleted events: no state change (stays in AwaitingDevFix)
490    /// 6. CompletionMarkerEmitted event: transitions to Interrupted
491    /// 7. Orchestration determines Effect::SaveCheckpoint for Interrupted
492    /// 8. Handler saves checkpoint, increments checkpoint_saved_count
493    /// 9. Event loop recognizes is_complete() == true and exits successfully
494    ///
495    /// ## Event Loop Termination Guarantees
496    ///
497    /// The event loop MUST NOT exit with completed=false when in AwaitingDevFix phase.
498    /// The failure handling flow is designed to always complete with:
499    /// - Completion marker written to filesystem
500    /// - State transitioned to Interrupted
501    /// - Checkpoint saved (checkpoint_saved_count > 0)
502    /// - Event loop returning completed=true
503    ///
504    /// If the event loop exits with completed=false from AwaitingDevFix, this indicates
505    /// a critical bug (e.g., max iterations reached before checkpoint saved).
506    ///
507    /// ## Completion Marker Requirement
508    ///
509    /// The completion marker MUST be written before transitioning to Interrupted.
510    /// This ensures external orchestration systems (CI, monitoring) can detect
511    /// pipeline termination even if the event loop exits unexpectedly.
512    ///
513    /// ## Agent Chain Exhaustion Handling
514    ///
515    /// When in AwaitingDevFix phase with an exhausted agent chain, orchestration
516    /// falls through to phase-specific logic (TriggerDevFixFlow) instead of reporting
517    /// exhaustion again. This prevents infinite loops where exhaustion is reported
518    /// repeatedly.
519    ///
520    /// Transitions:
521    /// - From: Any phase where AgentChainExhausted error occurs
522    /// - To: Interrupted (after dev-fix attempt completes or fails)
523    AwaitingDevFix,
524    Interrupted,
525}
526
527impl std::fmt::Display for PipelinePhase {
528    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529        match self {
530            Self::Planning => write!(f, "Planning"),
531            Self::Development => write!(f, "Development"),
532            Self::Review => write!(f, "Review"),
533            Self::CommitMessage => write!(f, "Commit Message"),
534            Self::FinalValidation => write!(f, "Final Validation"),
535            Self::Finalizing => write!(f, "Finalizing"),
536            Self::Complete => write!(f, "Complete"),
537            Self::AwaitingDevFix => write!(f, "Awaiting Dev Fix"),
538            Self::Interrupted => write!(f, "Interrupted"),
539        }
540    }
541}
542
543/// Pipeline events representing all state transitions.
544///
545/// Events are organized into logical categories for type-safe routing
546/// to category-specific reducers. Each category has a dedicated inner enum.
547///
548/// # Event Categories
549///
550/// - `Lifecycle` - Pipeline start/stop/resume
551/// - `Planning` - Plan generation events
552/// - `Development` - Development iteration and continuation events
553/// - `Review` - Review pass and fix attempt events
554/// - `Agent` - Agent invocation and chain management events
555/// - `Rebase` - Git rebase operation events
556/// - `Commit` - Commit generation events
557/// - Miscellaneous events (context cleanup, checkpoints, finalization)
558///
559/// # Example
560///
561/// ```ignore
562/// // Type-safe event construction
563/// let event = PipelineEvent::Agent(AgentEvent::InvocationStarted {
564///     role: AgentRole::Developer,
565///     agent: "claude".to_string(),
566///     model: Some("opus".to_string()),
567/// });
568///
569/// // Pattern matching routes to category handlers
570/// match event {
571///     PipelineEvent::Agent(agent_event) => reduce_agent_event(state, agent_event),
572///     // ...
573/// }
574/// ```
575///
576/// # ⚠️ FROZEN - DO NOT ADD VARIANTS ⚠️
577///
578/// This enum is **FROZEN**. Adding new top-level variants is **PROHIBITED**.
579///
580/// ## Why is this frozen?
581///
582/// `PipelineEvent` provides category-based event routing to the reducer. The existing
583/// categories (Lifecycle, Planning, Development, Review, etc.) cover all pipeline phases.
584/// Adding new top-level variants would indicate a missing architectural abstraction or
585/// an attempt to bypass phase-specific event handling.
586///
587/// ## What to do instead
588///
589/// 1. **Express events through existing categories** - Use the category enums:
590///    - `PlanningEvent` for planning phase observations
591///    - `DevelopmentEvent` for development phase observations
592///    - `ReviewEvent` for review phase observations
593///    - `CommitEvent` for commit generation observations
594///    - `AgentEvent` for agent invocation observations
595///    - `RebaseEvent` for rebase state machine transitions
596///
597/// 2. **Return errors for unrecoverable failures** - Don't create events for conditions
598///    that should terminate the pipeline. Return `Err` from the effect handler instead.
599///
600/// 3. **Extend category enums if needed** - If you truly need a new event within an
601///    existing phase, add it to that phase's category enum (e.g., add a new variant to
602///    `ReviewEvent` rather than creating a new top-level category).
603///
604/// ## Enforcement
605///
606/// The freeze policy is enforced by the `pipeline_event_is_frozen` test in this module,
607/// which will fail to compile if new variants are added. This is intentional.
608///
609/// See `LifecycleEvent` documentation for additional context on the freeze policy rationale.
610#[derive(Clone, Serialize, Deserialize, Debug)]
611pub enum PipelineEvent {
612    /// Pipeline lifecycle events (start, stop, resume).
613    Lifecycle(LifecycleEvent),
614    /// Planning phase events.
615    Planning(PlanningEvent),
616    /// Development phase events.
617    Development(DevelopmentEvent),
618    /// Review phase events.
619    Review(ReviewEvent),
620    /// Prompt input materialization events.
621    PromptInput(PromptInputEvent),
622    /// Agent invocation and chain events.
623    Agent(AgentEvent),
624    /// Rebase operation events.
625    Rebase(RebaseEvent),
626    /// Commit generation events.
627    Commit(CommitEvent),
628    /// AwaitingDevFix phase events.
629    AwaitingDevFix(AwaitingDevFixEvent),
630
631    // ========================================================================
632    // Miscellaneous events that don't fit a category
633    // ========================================================================
634    /// Context cleanup completed.
635    ContextCleaned,
636    /// Checkpoint saved.
637    CheckpointSaved {
638        /// What triggered the checkpoint save.
639        trigger: CheckpointTrigger,
640    },
641    /// Finalization phase started.
642    FinalizingStarted,
643    /// PROMPT.md permissions restored.
644    PromptPermissionsRestored,
645    /// Loop recovery triggered (tight loop detected and broken).
646    LoopRecoveryTriggered {
647        /// String representation of the detected loop.
648        detected_loop: String,
649        /// Number of times the loop was repeated.
650        loop_count: u32,
651    },
652}
653
654// ============================================================================
655// Convenience Constructors
656// ============================================================================
657
658#[path = "event/constructors.rs"]
659mod constructors;
660
661/// Rebase phase (initial or post-review).
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
663pub enum RebasePhase {
664    Initial,
665    PostReview,
666}
667
668/// Error kind for agent failures.
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
670pub enum AgentErrorKind {
671    Network,
672    Authentication,
673    RateLimit,
674    Timeout,
675    InternalError,
676    ModelUnavailable,
677    ParsingError,
678    FileSystem,
679}
680
681/// Conflict resolution strategy.
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
683pub enum ConflictStrategy {
684    Abort,
685    Continue,
686    Skip,
687}
688
689/// Checkpoint save trigger.
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
691pub enum CheckpointTrigger {
692    PhaseTransition,
693    IterationComplete,
694    BeforeRebase,
695    Interrupt,
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    #[test]
703    fn test_pipeline_phase_display() {
704        assert_eq!(format!("{}", PipelinePhase::Planning), "Planning");
705        assert_eq!(format!("{}", PipelinePhase::Development), "Development");
706        assert_eq!(format!("{}", PipelinePhase::Review), "Review");
707        assert_eq!(
708            format!("{}", PipelinePhase::CommitMessage),
709            "Commit Message"
710        );
711        assert_eq!(
712            format!("{}", PipelinePhase::FinalValidation),
713            "Final Validation"
714        );
715        assert_eq!(format!("{}", PipelinePhase::Finalizing), "Finalizing");
716        assert_eq!(format!("{}", PipelinePhase::Complete), "Complete");
717        assert_eq!(
718            format!("{}", PipelinePhase::AwaitingDevFix),
719            "Awaiting Dev Fix"
720        );
721        assert_eq!(format!("{}", PipelinePhase::Interrupted), "Interrupted");
722    }
723
724    /// This test enforces the FROZEN policy on LifecycleEvent.
725    ///
726    /// If you're here because this test failed to compile after adding
727    /// a variant, you are violating the freeze policy. See the FROZEN
728    /// comment on LifecycleEvent for alternatives.
729    #[test]
730    fn lifecycle_event_is_frozen() {
731        fn exhaustive_match(e: LifecycleEvent) -> &'static str {
732            match e {
733                LifecycleEvent::Started => "started",
734                LifecycleEvent::Resumed { .. } => "resumed",
735                LifecycleEvent::Completed => "completed",
736                // DO NOT ADD _ WILDCARD - intentionally exhaustive
737            }
738        }
739        // Just needs to compile; actual call proves exhaustiveness
740        let _ = exhaustive_match(LifecycleEvent::Started);
741    }
742
743    /// This test enforces the FROZEN policy on PipelineEvent.
744    ///
745    /// If you're here because this test failed to compile after adding
746    /// a variant, you are violating the freeze policy. See the FROZEN
747    /// comment on PipelineEvent for alternatives.
748    #[test]
749    fn pipeline_event_is_frozen() {
750        fn exhaustive_match(e: PipelineEvent) -> &'static str {
751            match e {
752                PipelineEvent::Lifecycle(_) => "lifecycle",
753                PipelineEvent::Planning(_) => "planning",
754                PipelineEvent::Development(_) => "development",
755                PipelineEvent::Review(_) => "review",
756                PipelineEvent::PromptInput(_) => "prompt_input",
757                PipelineEvent::Agent(_) => "agent",
758                PipelineEvent::Rebase(_) => "rebase",
759                PipelineEvent::Commit(_) => "commit",
760                PipelineEvent::AwaitingDevFix(_) => "awaiting_dev_fix",
761                PipelineEvent::ContextCleaned => "context_cleaned",
762                PipelineEvent::CheckpointSaved { .. } => "checkpoint_saved",
763                PipelineEvent::FinalizingStarted => "finalizing_started",
764                PipelineEvent::PromptPermissionsRestored => "prompt_permissions_restored",
765                PipelineEvent::LoopRecoveryTriggered { .. } => "loop_recovery_triggered",
766                // DO NOT ADD _ WILDCARD - intentionally exhaustive
767            }
768        }
769        let _ = exhaustive_match(PipelineEvent::ContextCleaned);
770    }
771}