ralph_workflow/reducer/event/mod.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 **fact** about what happened, not a command about
5//! what to do next.
6//!
7//! # Event-Sourced Reducer Architecture
8//!
9//! Ralph's pipeline follows an event-sourced reducer pattern:
10//!
11//! ```text
12//! State → Orchestrate → Effect → Handle → Event → Reduce → State'
13//! ```
14//!
15//! ## The Core Contract
16//!
17//! | Component | Pure? | Responsibility |
18//! |-----------|-------|----------------|
19//! | **Orchestration** | ✓ | Derives next effect from state only |
20//! | **Handler** | ✗ | Executes effect, emits events describing outcome |
21//! | **Reducer** | ✓ | Decides new state based on event |
22//!
23//! **Events are facts, not commands:**
24//!
25//! ```rust,ignore
26//! // ✓ GOOD - Describes what happened (fact)
27//! AgentEvent::InvocationFailed { retriable: true, error }
28//!
29//! // ✗ BAD - Commands what to do next (decision)
30//! AgentEvent::RetryAgent { with_backoff: true }
31//! ```
32//!
33//! **The handler reports, the reducer decides:**
34//!
35//! - Handler: "Agent invocation failed (retriable)"
36//! - Reducer: "Increment retry count, stay in same phase"
37//! - Orchestration: "Retry count < max? Emit InvokeAgent effect"
38//!
39//! # Event Categories
40//!
41//! Events are organized into logical categories for type-safe routing to
42//! category-specific reducers. Each category has a dedicated enum:
43//!
44//! - [`LifecycleEvent`] - Pipeline start/stop/resume
45//! - [`PlanningEvent`] - Plan generation events
46//! - [`DevelopmentEvent`] - Development iteration and continuation events
47//! - [`ReviewEvent`] - Review pass and fix attempt events
48//! - [`AgentEvent`] - Agent invocation and chain management events
49//! - [`RebaseEvent`] - Git rebase operation events
50//! - [`CommitEvent`] - Commit generation events
51//! - [`AwaitingDevFixEvent`] - Dev-fix flow events
52//! - [`PromptInputEvent`] - Prompt materialization events
53//! - [`ErrorEvent`] - Typed error events from handlers
54//!
55//! The main [`PipelineEvent`] enum wraps these category enums to enable
56//! type-safe dispatch in the reducer.
57//!
58//! # Why This File Is Large (514 lines)
59//!
60//! This file exceeds the 500-line recommended limit (currently 514 lines) but is an acceptable
61//! exception to the 300-line guideline because it's a **comprehensive enum module** with 10+ event
62//! category types that must remain together for:
63//! - Type-safe event category dispatch in reducers
64//! - Exhaustiveness checking across all event variants
65//! - Single source of truth for the event vocabulary
66//!
67//! Splitting would break pattern matching and scatter the event contract across many files, which
68//! would be significantly worse for maintainability than the current size. The tradeoff of exceeding
69//! the 500-line limit is justified by the cohesion and type-safety benefits.
70//!
71//! # Module Organization
72//!
73//! - [`types`] - Core event type definitions (all event enums)
74//! - [`constructors`] - Convenience constructors for building events
75//! - `development` - DevelopmentEvent and constructors
76//! - `review` - ReviewEvent and constructors
77//! - `agent` - AgentEvent and constructors
78//! - `error` - ErrorEvent and error types
79//!
80//! # Example: Handler Emitting Events
81//!
82//! ```rust,ignore
83//! use ralph_workflow::reducer::event::{PipelineEvent, AgentEvent};
84//! use ralph_workflow::reducer::effect::EffectResult;
85//!
86//! fn handle_invoke_agent(ctx: &mut PhaseContext) -> Result<EffectResult> {
87//! match invoke_agent_process(ctx) {
88//! Ok(output) => {
89//! // Report fact: invocation succeeded
90//! Ok(EffectResult::event(PipelineEvent::Agent(
91//! AgentEvent::InvocationSucceeded {
92//! role: ctx.role,
93//! output,
94//! }
95//! )))
96//! }
97//! Err(e) if is_retriable(&e) => {
98//! // Report fact: invocation failed (retriable)
99//! Ok(EffectResult::event(PipelineEvent::Agent(
100//! AgentEvent::InvocationFailed {
101//! role: ctx.role,
102//! error: e.to_string(),
103//! retriable: true,
104//! }
105//! )))
106//! }
107//! Err(e) => {
108//! // Report fact: invocation failed (not retriable)
109//! Ok(EffectResult::event(PipelineEvent::Agent(
110//! AgentEvent::InvocationFailed {
111//! role: ctx.role,
112//! error: e.to_string(),
113//! retriable: false,
114//! }
115//! )))
116//! }
117//! }
118//! }
119//! ```
120//!
121//! # Example: Reducer Making Decisions
122//!
123//! ```rust,ignore
124//! use ralph_workflow::reducer::event::{PipelineEvent, AgentEvent};
125//! use ralph_workflow::reducer::state::PipelineState;
126//!
127//! fn reduce(state: PipelineState, event: PipelineEvent) -> PipelineState {
128//! match event {
129//! PipelineEvent::Agent(AgentEvent::InvocationFailed { retriable, .. }) => {
130//! if retriable && state.retry_count < state.max_retries {
131//! // Decision: retry same agent
132//! PipelineState {
133//! retry_count: state.retry_count + 1,
134//! ..state
135//! }
136//! } else if retriable {
137//! // Decision: switch to next agent in chain
138//! PipelineState {
139//! agent_chain_index: state.agent_chain_index + 1,
140//! retry_count: 0,
141//! ..state
142//! }
143//! } else {
144//! // Decision: non-retriable failure, transition to AwaitingDevFix
145//! PipelineState {
146//! phase: PipelinePhase::AwaitingDevFix,
147//! ..state
148//! }
149//! }
150//! }
151//! _ => state,
152//! }
153//! }
154//! ```
155//!
156//! # Frozen Policy
157//!
158//! Both [`LifecycleEvent`] and [`PipelineEvent`] are **FROZEN** - adding new variants
159//! is prohibited. See their documentation for rationale and alternatives.
160//!
161//! # See Also
162//!
163//! - `docs/architecture/event-loop-and-reducers.md` - Detailed architecture doc
164//! - `reducer::state_reduction` - Reducer implementations
165//! - `reducer::orchestration` - Effect orchestration logic
166//! - `reducer::handler` - Effect handler implementations
167
168use crate::agents::AgentRole;
169use crate::reducer::state::DevelopmentStatus;
170use serde::{Deserialize, Serialize};
171use std::path::PathBuf;
172
173// ============================================================================
174// Type Definitions Module
175// ============================================================================
176
177#[path = "types.rs"]
178mod types;
179
180// Re-export all type definitions
181pub use types::{
182 AgentErrorKind, AwaitingDevFixEvent, CheckpointTrigger, CommitEvent, ConflictStrategy,
183 LifecycleEvent, MaterializedPromptInput, PlanningEvent, PromptInputEvent, PromptInputKind,
184 RebaseEvent, RebasePhase,
185};
186
187// ============================================================================
188// Category Event Modules
189// ============================================================================
190
191#[path = "development.rs"]
192mod development;
193pub use development::DevelopmentEvent;
194
195#[path = "review.rs"]
196mod review;
197pub use review::ReviewEvent;
198
199#[path = "agent.rs"]
200mod agent;
201pub use agent::AgentEvent;
202
203#[path = "error.rs"]
204mod error;
205pub use error::ErrorEvent;
206pub use error::WorkspaceIoErrorKind;
207
208// ============================================================================
209// Constructor Module
210// ============================================================================
211
212#[path = "constructors.rs"]
213mod constructors;
214
215// ============================================================================
216// Main Event Enum and Supporting Types
217// ============================================================================
218
219/// Pipeline phases for checkpoint tracking.
220///
221/// These phases represent the major stages of the Ralph pipeline.
222/// Reducers transition between phases based on events.
223///
224/// # Phase Transitions
225///
226/// ```text
227/// Planning → Development → Review → CommitMessage → FinalValidation → Finalizing → Complete
228/// ↓ ↓ ↓
229/// AwaitingDevFix → Interrupted
230/// ```
231///
232/// # Phase Descriptions
233///
234/// - **Planning**: Generate implementation plan for the iteration
235/// - **Development**: Execute plan, write code
236/// - **Review**: Review code changes, identify issues
237/// - **CommitMessage**: Generate commit message
238/// - **FinalValidation**: Final checks before completion
239/// - **Finalizing**: Cleanup operations (restore permissions, etc.)
240/// - **Complete**: Pipeline completed successfully
241/// - **AwaitingDevFix**: Terminal failure occurred, dev agent diagnosing
242/// - **Interrupted**: Pipeline terminated (success or failure)
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244pub enum PipelinePhase {
245 Planning,
246 Development,
247 Review,
248 CommitMessage,
249 FinalValidation,
250 /// Finalizing phase for cleanup operations before completion.
251 ///
252 /// This phase handles:
253 /// - Restoring PROMPT.md write permissions
254 /// - Any other cleanup that must go through the effect system
255 Finalizing,
256 Complete,
257 /// Awaiting development agent to fix pipeline failure.
258 ///
259 /// This phase occurs when the pipeline encounters a terminal failure condition
260 /// (e.g., agent chain exhausted) but before transitioning to Interrupted. It
261 /// signals that the development agent should be invoked to diagnose and fix
262 /// the failure root cause.
263 ///
264 /// ## Failure Handling Flow
265 ///
266 /// 1. ErrorEvent::AgentChainExhausted occurs in any phase
267 /// 2. Reducer transitions state to AwaitingDevFix
268 /// 3. Orchestration determines Effect::TriggerDevFixFlow
269 /// 4. Handler executes TriggerDevFixFlow:
270 /// a. Writes completion marker to .agent/tmp/completion_marker (failure status)
271 /// b. Emits DevFixTriggered event
272 /// c. Dispatches dev-fix agent
273 /// d. Emits DevFixCompleted event
274 /// e. Emits CompletionMarkerEmitted event
275 /// 5. DevFixTriggered/DevFixCompleted events: no state change (stays in AwaitingDevFix)
276 /// 6. CompletionMarkerEmitted event: transitions to Interrupted
277 /// 7. Orchestration determines Effect::SaveCheckpoint for Interrupted
278 /// 8. Handler saves checkpoint, increments checkpoint_saved_count
279 /// 9. Event loop recognizes is_complete() == true and exits successfully
280 ///
281 /// ## Event Loop Termination Guarantees
282 ///
283 /// The event loop MUST NOT exit with completed=false when in AwaitingDevFix phase.
284 /// The failure handling flow is designed to always complete with:
285 /// - Completion marker written to filesystem
286 /// - State transitioned to Interrupted
287 /// - Checkpoint saved (checkpoint_saved_count > 0)
288 /// - Event loop returning completed=true
289 ///
290 /// If the event loop exits with completed=false from AwaitingDevFix, this indicates
291 /// a critical bug (e.g., max iterations reached before checkpoint saved).
292 ///
293 /// ## Completion Marker Requirement
294 ///
295 /// The completion marker MUST be written before transitioning to Interrupted.
296 /// This ensures external orchestration systems (CI, monitoring) can detect
297 /// pipeline termination even if the event loop exits unexpectedly.
298 ///
299 /// ## Agent Chain Exhaustion Handling
300 ///
301 /// When in AwaitingDevFix phase with an exhausted agent chain, orchestration
302 /// falls through to phase-specific logic (TriggerDevFixFlow) instead of reporting
303 /// exhaustion again. This prevents infinite loops where exhaustion is reported
304 /// repeatedly.
305 ///
306 /// Transitions:
307 /// - From: Any phase where AgentChainExhausted error occurs
308 /// - To: Interrupted (after dev-fix attempt completes or fails)
309 AwaitingDevFix,
310 Interrupted,
311}
312
313impl std::fmt::Display for PipelinePhase {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Planning => write!(f, "Planning"),
317 Self::Development => write!(f, "Development"),
318 Self::Review => write!(f, "Review"),
319 Self::CommitMessage => write!(f, "Commit Message"),
320 Self::FinalValidation => write!(f, "Final Validation"),
321 Self::Finalizing => write!(f, "Finalizing"),
322 Self::Complete => write!(f, "Complete"),
323 Self::AwaitingDevFix => write!(f, "Awaiting Dev Fix"),
324 Self::Interrupted => write!(f, "Interrupted"),
325 }
326 }
327}
328
329/// Pipeline events representing all state transitions.
330///
331/// Events are organized into logical categories for type-safe routing
332/// to category-specific reducers. Each category has a dedicated inner enum.
333///
334/// # Event Categories
335///
336/// - `Lifecycle` - Pipeline start/stop/resume
337/// - `Planning` - Plan generation events
338/// - `Development` - Development iteration and continuation events
339/// - `Review` - Review pass and fix attempt events
340/// - `Agent` - Agent invocation and chain management events
341/// - `Rebase` - Git rebase operation events
342/// - `Commit` - Commit generation events
343/// - Miscellaneous events (context cleanup, checkpoints, finalization)
344///
345/// # Example
346///
347/// ```rust,ignore
348/// // Type-safe event construction
349/// let event = PipelineEvent::Agent(AgentEvent::InvocationStarted {
350/// role: AgentRole::Developer,
351/// agent: "claude".to_string(),
352/// model: Some("opus".to_string()),
353/// });
354///
355/// // Pattern matching routes to category handlers
356/// match event {
357/// PipelineEvent::Agent(agent_event) => reduce_agent_event(state, agent_event),
358/// // ...
359/// }
360/// ```
361///
362/// # ⚠️ FROZEN - DO NOT ADD VARIANTS ⚠️
363///
364/// This enum is **FROZEN**. Adding new top-level variants is **PROHIBITED**.
365///
366/// ## Why is this frozen?
367///
368/// `PipelineEvent` provides category-based event routing to the reducer. The existing
369/// categories (Lifecycle, Planning, Development, Review, etc.) cover all pipeline phases.
370/// Adding new top-level variants would indicate a missing architectural abstraction or
371/// an attempt to bypass phase-specific event handling.
372///
373/// ## What to do instead
374///
375/// 1. **Express events through existing categories** - Use the category enums:
376/// - `PlanningEvent` for planning phase observations
377/// - `DevelopmentEvent` for development phase observations
378/// - `ReviewEvent` for review phase observations
379/// - `CommitEvent` for commit generation observations
380/// - `AgentEvent` for agent invocation observations
381/// - `RebaseEvent` for rebase state machine transitions
382///
383/// 2. **Return errors for unrecoverable failures** - Don't create events for conditions
384/// that should terminate the pipeline. Return `Err` from the effect handler instead.
385///
386/// 3. **Extend category enums if needed** - If you truly need a new event within an
387/// existing phase, add it to that phase's category enum (e.g., add a new variant to
388/// `ReviewEvent` rather than creating a new top-level category).
389///
390/// ## Enforcement
391///
392/// The freeze policy is enforced by the `pipeline_event_is_frozen` test in this module,
393/// which will fail to compile if new variants are added. This is intentional.
394///
395/// See `LifecycleEvent` documentation for additional context on the freeze policy rationale.
396#[derive(Clone, Serialize, Deserialize, Debug)]
397pub enum PipelineEvent {
398 /// Pipeline lifecycle events (start, stop, resume).
399 Lifecycle(LifecycleEvent),
400 /// Planning phase events.
401 Planning(PlanningEvent),
402 /// Development phase events.
403 Development(DevelopmentEvent),
404 /// Review phase events.
405 Review(ReviewEvent),
406 /// Prompt input materialization events.
407 PromptInput(PromptInputEvent),
408 /// Agent invocation and chain events.
409 Agent(AgentEvent),
410 /// Rebase operation events.
411 Rebase(RebaseEvent),
412 /// Commit generation events.
413 Commit(CommitEvent),
414 /// AwaitingDevFix phase events.
415 AwaitingDevFix(AwaitingDevFixEvent),
416
417 // ========================================================================
418 // Miscellaneous events that don't fit a category
419 // ========================================================================
420 /// Context cleanup completed.
421 ContextCleaned,
422 /// Checkpoint saved.
423 CheckpointSaved {
424 /// What triggered the checkpoint save.
425 trigger: CheckpointTrigger,
426 },
427 /// Finalization phase started.
428 FinalizingStarted,
429 /// PROMPT.md permissions restored.
430 PromptPermissionsRestored,
431 /// Loop recovery triggered (tight loop detected and broken).
432 LoopRecoveryTriggered {
433 /// String representation of the detected loop.
434 detected_loop: String,
435 /// Number of times the loop was repeated.
436 loop_count: u32,
437 },
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_pipeline_phase_display() {
446 assert_eq!(format!("{}", PipelinePhase::Planning), "Planning");
447 assert_eq!(format!("{}", PipelinePhase::Development), "Development");
448 assert_eq!(format!("{}", PipelinePhase::Review), "Review");
449 assert_eq!(
450 format!("{}", PipelinePhase::CommitMessage),
451 "Commit Message"
452 );
453 assert_eq!(
454 format!("{}", PipelinePhase::FinalValidation),
455 "Final Validation"
456 );
457 assert_eq!(format!("{}", PipelinePhase::Finalizing), "Finalizing");
458 assert_eq!(format!("{}", PipelinePhase::Complete), "Complete");
459 assert_eq!(
460 format!("{}", PipelinePhase::AwaitingDevFix),
461 "Awaiting Dev Fix"
462 );
463 assert_eq!(format!("{}", PipelinePhase::Interrupted), "Interrupted");
464 }
465
466 /// This test enforces the FROZEN policy on LifecycleEvent.
467 ///
468 /// If you're here because this test failed to compile after adding
469 /// a variant, you are violating the freeze policy. See the FROZEN
470 /// comment on LifecycleEvent for alternatives.
471 #[test]
472 fn lifecycle_event_is_frozen() {
473 fn exhaustive_match(e: LifecycleEvent) -> &'static str {
474 match e {
475 LifecycleEvent::Started => "started",
476 LifecycleEvent::Resumed { .. } => "resumed",
477 LifecycleEvent::Completed => "completed",
478 LifecycleEvent::GitignoreEntriesEnsured { .. } => "gitignore_ensured",
479 // DO NOT ADD _ WILDCARD - intentionally exhaustive
480 }
481 }
482 // Just needs to compile; actual call proves exhaustiveness
483 let _ = exhaustive_match(LifecycleEvent::Started);
484 }
485
486 /// This test enforces the FROZEN policy on PipelineEvent.
487 ///
488 /// If you're here because this test failed to compile after adding
489 /// a variant, you are violating the freeze policy. See the FROZEN
490 /// comment on PipelineEvent for alternatives.
491 #[test]
492 fn pipeline_event_is_frozen() {
493 fn exhaustive_match(e: PipelineEvent) -> &'static str {
494 match e {
495 PipelineEvent::Lifecycle(_) => "lifecycle",
496 PipelineEvent::Planning(_) => "planning",
497 PipelineEvent::Development(_) => "development",
498 PipelineEvent::Review(_) => "review",
499 PipelineEvent::PromptInput(_) => "prompt_input",
500 PipelineEvent::Agent(_) => "agent",
501 PipelineEvent::Rebase(_) => "rebase",
502 PipelineEvent::Commit(_) => "commit",
503 PipelineEvent::AwaitingDevFix(_) => "awaiting_dev_fix",
504 PipelineEvent::ContextCleaned => "context_cleaned",
505 PipelineEvent::CheckpointSaved { .. } => "checkpoint_saved",
506 PipelineEvent::FinalizingStarted => "finalizing_started",
507 PipelineEvent::PromptPermissionsRestored => "prompt_permissions_restored",
508 PipelineEvent::LoopRecoveryTriggered { .. } => "loop_recovery_triggered",
509 // DO NOT ADD _ WILDCARD - intentionally exhaustive
510 }
511 }
512 let _ = exhaustive_match(PipelineEvent::ContextCleaned);
513 }
514}