Skip to main content

ralph_workflow/reducer/boundary/
mod.rs

1//! Effect handler implementation for pipeline side effects.
2//!
3//! This module implements the [`EffectHandler`] trait to execute pipeline effects
4//! through the reducer architecture. Effect handlers perform actual work (agent
5//! invocation, git operations, file I/O) and emit events that drive state transitions.
6//!
7//! # Architecture Contract
8//!
9//! ```text
10//! State → Orchestrator → Effect → Handler → Event → Reducer → State
11//!                                  ^^^^^^^
12//!                                  Impure execution (this module)
13//! ```
14//!
15//! ## Handler Responsibilities
16//!
17//! - **Execute effects**: Perform the I/O operation specified by the effect
18//! - **Report outcomes**: Emit events describing what happened (success/failure)
19//! - **Use workspace abstraction**: All filesystem access via `ctx.workspace`
20//! - **Single-task execution**: Execute exactly one effect, no hidden retry logic
21//!
22//! ## Reducer Responsibilities (NOT handler)
23//!
24//! - **Pure state transitions**: Process events to update state
25//! - **Policy decisions**: Retry, fallback, phase progression
26//! - **Control flow**: Determine what happens next based on events
27//!
28//! # Key Principle: Handlers Report, Reducers Decide
29//!
30//! Handlers must NOT contain decision logic. Examples:
31//!
32//! ```ignore
33//! // WRONG - Handler decides to retry
34//! fn handle_invoke_agent() -> Result<EffectResult> {
35//!     for attempt in 0..3 {  // NO! Reducer controls retry
36//!         if let Ok(output) = invoke_agent() {
37//!             return Ok(output);
38//!         }
39//!     }
40//! }
41//!
42//! // CORRECT - Handler reports outcome, reducer decides
43//! fn handle_invoke_agent() -> Result<EffectResult> {
44//!     match invoke_agent() {
45//!         Ok(output) => Ok(EffectResult::event(
46//!             AgentEvent::InvocationSucceeded { output }
47//!         )),
48//!         Err(e) => Ok(EffectResult::event(
49//!             AgentEvent::InvocationFailed { error: e, retriable: true }
50//!         )),
51//!     }
52//! }
53//! ```
54//!
55//! The reducer processes `InvocationFailed` and decides whether to retry
56//! (increment retry count, emit retry effect) or fallback (advance chain).
57//!
58//! # Workspace Abstraction
59//!
60//! All filesystem operations MUST use `ctx.workspace`:
61//!
62//! ```ignore
63//! // CORRECT
64//! ctx.workspace.write(path, content)?;
65//! let content = ctx.workspace.read(path)?;
66//!
67//! // WRONG - Never use std::fs in handlers
68//! std::fs::write(path, content)?;
69//! ```
70//!
71//! This abstraction enables:
72//! - In-memory testing with `MemoryWorkspace`
73//! - Proper error handling and path resolution
74//! - Consistent file operations across the pipeline
75//!
76//! See [`docs/agents/workspace-trait.md`] for details.
77//!
78//! # Testing Handlers
79//!
80//! Handlers require mocks for I/O (workspace) but NOT for reducer/orchestration:
81//!
82//! ```ignore
83//! #[test]
84//! fn test_invoke_agent_emits_success_event() {
85//!     let workspace = MemoryWorkspace::new_test();
86//!     let mut ctx = create_test_context(&workspace);
87//!
88//!     let result = handler.execute(
89//!         Effect::InvokeAgent { role, agent, prompt },
90//!         &mut ctx
91//!     )?;
92//!
93//!     assert!(matches!(
94//!         result.event,
95//!         PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
96//!     ));
97//! }
98//! ```
99//!
100//! # Module Organization
101//!
102//! - [`agent`] - Agent invocation and chain management
103//! - [`planning`] - Planning phase effects (prompt, XML, validation)
104//! - [`development`] - Development phase effects (iteration, continuation)
105//! - [`review`] - Review phase effects (issue detection, fix application)
106//! - [`commit`] - Commit phase effects (message generation, commit creation)
107//! - [`rebase`] - Rebase effects (conflict resolution, validation)
108//! - [`checkpoint`] - Checkpoint save/restore
109//! - [`context`] - Context preparation and cleanup
110//!
111//! [`docs/agents/workspace-trait.md`]: https://codeberg.org/mistlight/RalphWithReviewer/src/branch/main/docs/agents/workspace-trait.md
112
113mod agent;
114mod analysis;
115mod chain;
116mod checkpoint;
117mod cloud;
118mod commit;
119mod context;
120mod development;
121mod development_prompt;
122mod io_agent;
123mod io_commit;
124mod lifecycle;
125mod planning;
126mod rebase;
127pub(crate) mod retry_guidance;
128mod run_fix;
129mod run_review;
130mod run_review_prompt;
131
132#[cfg(test)]
133mod tests;
134
135use crate::phases::PhaseContext;
136use crate::prompts::{PromptHistoryEntry, PromptScopeKey};
137use crate::reducer::effect::{Effect, EffectHandler, EffectResult};
138use crate::reducer::event::{PipelineEvent, PipelinePhase};
139use crate::reducer::state::PipelineState;
140use crate::reducer::ui_event::UIEvent;
141use anyhow::Result;
142use std::hash::BuildHasher;
143
144fn execute_backoff_wait(
145    ctx: &mut PhaseContext<'_>,
146    role: crate::agents::AgentRole,
147    cycle: u32,
148    duration_ms: u64,
149) -> Result<EffectResult> {
150    ctx.registry
151        .retry_timer()
152        .sleep(std::time::Duration::from_millis(duration_ms));
153    Ok(EffectResult::event(
154        PipelineEvent::agent_retry_cycle_started(role, cycle),
155    ))
156}
157
158fn execute_write_continuation_context(
159    ctx: &mut PhaseContext<'_>,
160    data: &crate::reducer::effect::ContinuationContextData,
161) -> Result<EffectResult> {
162    development::write_continuation_context_to_workspace(ctx.workspace, ctx.logger, data)?;
163    Ok(EffectResult::event(
164        PipelineEvent::development_continuation_context_written(data.iteration, data.attempt),
165    ))
166}
167
168fn ensure_completion_marker_dir(ctx: &PhaseContext<'_>) {
169    if let Err(err) = ctx
170        .workspace
171        .create_dir_all(std::path::Path::new(".agent/tmp"))
172    {
173        ctx.logger.warn(&format!(
174            "Failed to create completion marker directory: {err}"
175        ));
176    }
177}
178
179fn write_completion_marker_content(
180    ctx: &PhaseContext<'_>,
181    content: &str,
182    is_failure: bool,
183) -> std::result::Result<(), String> {
184    let marker_path = std::path::Path::new(".agent/tmp/completion_marker");
185    match ctx.workspace.write(marker_path, content) {
186        Ok(()) => {
187            ctx.logger.info(&format!(
188                "Completion marker written: {}",
189                if is_failure { "failure" } else { "success" }
190            ));
191            Ok(())
192        }
193        Err(err) => {
194            ctx.logger
195                .warn(&format!("Failed to write completion marker: {err}"));
196            Err(err.to_string())
197        }
198    }
199}
200
201fn get_stored_or_generate_prompt_with_validation<F, S: BuildHasher>(
202    scope_key: &PromptScopeKey,
203    prompt_history: &std::collections::HashMap<String, PromptHistoryEntry, S>,
204    current_content_id: Option<&str>,
205    generator: F,
206) -> (String, bool, bool)
207where
208    F: FnOnce() -> (String, bool),
209{
210    let key = scope_key.to_string();
211    match prompt_history.get(&key) {
212        Some(entry) if !content_id_mismatch(entry, current_content_id) => {
213            (entry.content.clone(), true, false)
214        }
215        _ => {
216            let (prompt, should_validate) = generator();
217            (prompt, false, should_validate)
218        }
219    }
220}
221
222fn content_id_mismatch(entry: &PromptHistoryEntry, current_content_id: Option<&str>) -> bool {
223    current_content_id.is_some_and(|current_id| entry.content_id.as_deref() != Some(current_id))
224}
225
226/// Main effect handler implementation.
227///
228/// This handler executes effects by calling pipeline subsystems and emitting reducer events.
229pub struct MainEffectHandler {
230    /// Current pipeline state
231    pub state: PipelineState,
232    /// Event log for replay/debugging
233    pub event_log: Vec<PipelineEvent>,
234}
235
236impl MainEffectHandler {
237    /// Create a new effect handler.
238    #[must_use]
239    pub const fn new(state: PipelineState) -> Self {
240        Self {
241            state,
242            event_log: Vec::new(),
243        }
244    }
245}
246
247impl EffectHandler<'_> for MainEffectHandler {
248    fn execute(&mut self, effect: Effect, ctx: &mut PhaseContext<'_>) -> Result<EffectResult> {
249        let result = self.execute_effect(effect, ctx)?;
250        self.event_log.push(result.event.clone());
251        self.event_log
252            .extend(result.additional_events.iter().cloned());
253        Ok(result)
254    }
255}
256
257impl crate::app::event_loop::StatefulHandler for MainEffectHandler {
258    fn update_state(&mut self, state: PipelineState) {
259        self.state = state;
260    }
261}
262
263impl MainEffectHandler {
264    /// Helper to create phase transition UI event.
265    const fn phase_transition_ui(&self, to: PipelinePhase) -> UIEvent {
266        UIEvent::PhaseTransition {
267            from: Some(self.state.phase),
268            to,
269        }
270    }
271
272    fn write_completion_marker(
273        ctx: &PhaseContext<'_>,
274        content: &str,
275        is_failure: bool,
276    ) -> std::result::Result<(), String> {
277        ensure_completion_marker_dir(ctx);
278        write_completion_marker_content(ctx, content, is_failure)
279    }
280
281    fn execute_effect(
282        &mut self,
283        effect: Effect,
284        ctx: &mut PhaseContext<'_>,
285    ) -> Result<EffectResult> {
286        match effect {
287            Effect::AgentInvocation {
288                role,
289                agent,
290                model,
291                prompt,
292            } => self.execute_agent_invocation_effect(ctx, role, agent, model, prompt),
293            Effect::InitializeAgentChain { drain, .. } => {
294                Ok(self.initialize_agent_chain(ctx, drain))
295            }
296            e => self.execute_non_agent_effect(e, ctx),
297        }
298    }
299
300    fn execute_agent_invocation_effect(
301        &mut self,
302        ctx: &mut PhaseContext<'_>,
303        role: crate::agents::AgentRole,
304        agent: String,
305        model: Option<String>,
306        prompt: String,
307    ) -> Result<EffectResult> {
308        self.invoke_agent(
309            ctx,
310            crate::agents::AgentDrain::from(role),
311            role,
312            &agent,
313            model.as_deref(),
314            prompt,
315        )
316    }
317
318    fn execute_non_agent_effect(
319        &mut self,
320        effect: Effect,
321        ctx: &mut PhaseContext<'_>,
322    ) -> Result<EffectResult> {
323        match effect {
324            Effect::BackoffWait {
325                role,
326                cycle,
327                duration_ms,
328            } => execute_backoff_wait(ctx, role, cycle, duration_ms),
329            Effect::ReportAgentChainExhausted { role, phase, cycle } => Err(
330                crate::reducer::event::ErrorEvent::AgentChainExhausted { role, phase, cycle }
331                    .into(),
332            ),
333            e => self.execute_phase_effect(e, ctx),
334        }
335    }
336
337    fn execute_phase_effect(
338        &mut self,
339        effect: Effect,
340        ctx: &mut PhaseContext<'_>,
341    ) -> Result<EffectResult> {
342        match effect {
343            e @ (Effect::PreparePlanningPrompt { .. }
344            | Effect::MaterializePlanningInputs { .. }
345            | Effect::CleanupRequiredFiles { .. }
346            | Effect::InvokePlanningAgent { .. }
347            | Effect::ExtractPlanningXml { .. }
348            | Effect::ValidatePlanningXml { .. }
349            | Effect::WritePlanningMarkdown { .. }
350            | Effect::ArchivePlanningXml { .. }
351            | Effect::ApplyPlanningOutcome { .. }) => self.execute_planning_effect(e, ctx),
352            e @ (Effect::PrepareDevelopmentContext { .. }
353            | Effect::MaterializeDevelopmentInputs { .. }
354            | Effect::PrepareDevelopmentPrompt { .. }
355            | Effect::InvokeDevelopmentAgent { .. }
356            | Effect::InvokeAnalysisAgent { .. }
357            | Effect::ExtractDevelopmentXml { .. }
358            | Effect::ValidateDevelopmentXml { .. }
359            | Effect::ApplyDevelopmentOutcome { .. }
360            | Effect::ArchiveDevelopmentXml { .. }) => self.execute_development_effect(e, ctx),
361            e => self.execute_phase_effect_b(e, ctx),
362        }
363    }
364
365    fn execute_phase_effect_b(
366        &mut self,
367        effect: Effect,
368        ctx: &mut PhaseContext<'_>,
369    ) -> Result<EffectResult> {
370        match effect {
371            e @ (Effect::PrepareReviewContext { .. }
372            | Effect::MaterializeReviewInputs { .. }
373            | Effect::PrepareReviewPrompt { .. }
374            | Effect::InvokeReviewAgent { .. }
375            | Effect::ExtractReviewIssuesXml { .. }
376            | Effect::ValidateReviewIssuesXml { .. }
377            | Effect::WriteIssuesMarkdown { .. }
378            | Effect::ExtractReviewIssueSnippets { .. }
379            | Effect::ArchiveReviewIssuesXml { .. }
380            | Effect::ApplyReviewOutcome { .. }
381            | Effect::PrepareFixPrompt { .. }
382            | Effect::InvokeFixAgent { .. }
383            | Effect::InvokeFixAnalysisAgent { .. }
384            | Effect::ExtractFixResultXml { .. }
385            | Effect::ValidateFixResultXml { .. }
386            | Effect::ApplyFixOutcome { .. }
387            | Effect::ArchiveFixResultXml { .. }) => self.execute_review_effect(e, ctx),
388            e @ (Effect::PrepareCommitPrompt { .. }
389            | Effect::CheckCommitDiff
390            | Effect::MaterializeCommitInputs { .. }
391            | Effect::InvokeCommitAgent
392            | Effect::ExtractCommitXml
393            | Effect::ValidateCommitXml
394            | Effect::ApplyCommitMessageOutcome
395            | Effect::ArchiveCommitXml
396            | Effect::CreateCommit { .. }
397            | Effect::SkipCommit { .. }
398            | Effect::CheckResidualFiles { .. }
399            | Effect::CheckUncommittedChangesBeforeTermination) => {
400                self.execute_commit_effect(e, ctx)
401            }
402            e @ (Effect::RunRebase { .. } | Effect::ResolveRebaseConflicts { .. }) => {
403                self.execute_rebase_effect(e, ctx)
404            }
405            e => self.execute_lifecycle_effect(e, ctx),
406        }
407    }
408
409    fn execute_planning_effect(
410        &mut self,
411        effect: Effect,
412        ctx: &mut PhaseContext<'_>,
413    ) -> Result<EffectResult> {
414        match effect {
415            Effect::PreparePlanningPrompt {
416                iteration,
417                prompt_mode,
418            } => self.prepare_planning_prompt(ctx, iteration, prompt_mode),
419            Effect::MaterializePlanningInputs { iteration } => {
420                self.materialize_planning_inputs(ctx, iteration)
421            }
422            Effect::CleanupRequiredFiles { files } => Ok(self.cleanup_required_files(ctx, &files)),
423            Effect::InvokePlanningAgent { iteration } => self.invoke_planning_agent(ctx, iteration),
424            e => self.execute_planning_effect_b(e, ctx),
425        }
426    }
427
428    fn execute_planning_effect_b(
429        &mut self,
430        effect: Effect,
431        ctx: &mut PhaseContext<'_>,
432    ) -> Result<EffectResult> {
433        match effect {
434            Effect::ExtractPlanningXml { iteration } => {
435                Ok(self.extract_planning_xml(ctx, iteration))
436            }
437            Effect::ValidatePlanningXml { iteration } => self.validate_planning_xml(ctx, iteration),
438            Effect::WritePlanningMarkdown { iteration } => {
439                self.write_planning_markdown(ctx, iteration)
440            }
441            Effect::ArchivePlanningXml { iteration } => {
442                Ok(Self::archive_planning_xml(ctx, iteration))
443            }
444            Effect::ApplyPlanningOutcome { iteration, valid } => {
445                Ok(self.apply_planning_outcome(ctx, iteration, valid))
446            }
447            _ => unreachable!("execute_planning_effect called with non-planning effect"),
448        }
449    }
450
451    fn execute_development_effect(
452        &mut self,
453        effect: Effect,
454        ctx: &mut PhaseContext<'_>,
455    ) -> Result<EffectResult> {
456        match effect {
457            Effect::PrepareDevelopmentContext { iteration } => {
458                Ok(Self::prepare_development_context(ctx, iteration))
459            }
460            Effect::MaterializeDevelopmentInputs { iteration } => {
461                self.materialize_development_inputs(ctx, iteration)
462            }
463            Effect::PrepareDevelopmentPrompt {
464                iteration,
465                prompt_mode,
466            } => self.prepare_development_prompt(ctx, iteration, prompt_mode),
467            Effect::InvokeDevelopmentAgent { iteration } => {
468                self.invoke_development_agent(ctx, iteration)
469            }
470            e => self.execute_development_effect_b(e, ctx),
471        }
472    }
473
474    fn execute_development_effect_b(
475        &mut self,
476        effect: Effect,
477        ctx: &mut PhaseContext<'_>,
478    ) -> Result<EffectResult> {
479        match effect {
480            Effect::InvokeAnalysisAgent { iteration } => self.invoke_analysis_agent(ctx, iteration),
481            Effect::ExtractDevelopmentXml { iteration } => {
482                Ok(self.extract_development_xml(ctx, iteration))
483            }
484            Effect::ValidateDevelopmentXml { iteration } => {
485                Ok(self.validate_development_xml(ctx, iteration))
486            }
487            Effect::ApplyDevelopmentOutcome { iteration } => {
488                self.apply_development_outcome(ctx, iteration)
489            }
490            Effect::ArchiveDevelopmentXml { iteration } => {
491                Ok(Self::archive_development_xml(ctx, iteration))
492            }
493            _ => unreachable!("execute_development_effect called with non-development effect"),
494        }
495    }
496
497    fn execute_review_effect(
498        &mut self,
499        effect: Effect,
500        ctx: &mut PhaseContext<'_>,
501    ) -> Result<EffectResult> {
502        match effect {
503            Effect::PrepareReviewContext { pass } => Ok(self.prepare_review_context(ctx, pass)),
504            Effect::MaterializeReviewInputs { pass } => self.materialize_review_inputs(ctx, pass),
505            Effect::PrepareReviewPrompt { pass, prompt_mode } => {
506                self.prepare_review_prompt(ctx, pass, prompt_mode)
507            }
508            Effect::InvokeReviewAgent { pass } => self.invoke_review_agent(ctx, pass),
509            Effect::ExtractReviewIssuesXml { pass } => {
510                Ok(self.extract_review_issues_xml(ctx, pass))
511            }
512            Effect::ValidateReviewIssuesXml { pass } => {
513                Ok(self.validate_review_issues_xml(ctx, pass))
514            }
515            e => self.execute_fix_effect(e, ctx),
516        }
517    }
518
519    fn execute_fix_effect(
520        &mut self,
521        effect: Effect,
522        ctx: &mut PhaseContext<'_>,
523    ) -> Result<EffectResult> {
524        match effect {
525            Effect::WriteIssuesMarkdown { pass } => self.write_issues_markdown(ctx, pass),
526            Effect::ExtractReviewIssueSnippets { pass } => {
527                self.extract_review_issue_snippets(ctx, pass)
528            }
529            Effect::ArchiveReviewIssuesXml { pass } => {
530                Ok(Self::archive_review_issues_xml(ctx, pass))
531            }
532            e => self.execute_fix_outcome_or_agent_effect(e, ctx),
533        }
534    }
535
536    fn execute_fix_outcome_or_agent_effect(
537        &mut self,
538        effect: Effect,
539        ctx: &mut PhaseContext<'_>,
540    ) -> Result<EffectResult> {
541        match effect {
542            Effect::ApplyReviewOutcome {
543                pass,
544                issues_found,
545                clean_no_issues,
546            } => Ok(Self::apply_review_outcome(
547                ctx,
548                pass,
549                issues_found,
550                clean_no_issues,
551            )),
552            e => self.execute_fix_agent_effect(e, ctx),
553        }
554    }
555
556    fn execute_fix_agent_effect(
557        &mut self,
558        effect: Effect,
559        ctx: &mut PhaseContext<'_>,
560    ) -> Result<EffectResult> {
561        match effect {
562            Effect::PrepareFixPrompt { pass, prompt_mode } => {
563                self.prepare_fix_prompt(ctx, pass, prompt_mode)
564            }
565            Effect::InvokeFixAgent { pass } => self.invoke_fix_agent(ctx, pass),
566            Effect::InvokeFixAnalysisAgent { pass } => self.invoke_fix_analysis_agent(ctx, pass),
567            Effect::ExtractFixResultXml { pass } => Ok(self.extract_fix_result_xml(ctx, pass)),
568            Effect::ValidateFixResultXml { pass } => Ok(self.validate_fix_result_xml(ctx, pass)),
569            Effect::ApplyFixOutcome { pass } => self.apply_fix_outcome(ctx, pass),
570            Effect::ArchiveFixResultXml { pass } => Ok(self.archive_fix_result_xml(ctx, pass)),
571            _ => unreachable!("execute_fix_effect called with non-fix effect"),
572        }
573    }
574
575    fn execute_commit_effect(
576        &mut self,
577        effect: Effect,
578        ctx: &mut PhaseContext<'_>,
579    ) -> Result<EffectResult> {
580        match effect {
581            Effect::PrepareCommitPrompt { prompt_mode } => {
582                self.prepare_commit_prompt(ctx, prompt_mode)
583            }
584            Effect::CheckCommitDiff => Self::check_commit_diff(ctx),
585            Effect::MaterializeCommitInputs { attempt } => {
586                self.materialize_commit_inputs(ctx, attempt)
587            }
588            Effect::InvokeCommitAgent => self.invoke_commit_agent(ctx),
589            Effect::ExtractCommitXml => Ok(self.extract_commit_xml(ctx)),
590            Effect::ValidateCommitXml => Ok(self.validate_commit_xml(ctx)),
591            e => self.execute_commit_finalization_effect(e, ctx),
592        }
593    }
594
595    fn execute_commit_finalization_effect(
596        &mut self,
597        effect: Effect,
598        ctx: &mut PhaseContext<'_>,
599    ) -> Result<EffectResult> {
600        match effect {
601            Effect::ApplyCommitMessageOutcome => self.apply_commit_message_outcome(ctx),
602            Effect::ArchiveCommitXml => Ok(self.archive_commit_xml(ctx)),
603            Effect::CreateCommit {
604                message,
605                files,
606                excluded_files,
607            } => Self::create_commit(ctx, message, &files, &excluded_files),
608            Effect::SkipCommit { reason } => Ok(Self::skip_commit(ctx, reason)),
609            Effect::CheckResidualFiles { pass } => Self::check_residual_files(ctx, pass),
610            Effect::CheckUncommittedChangesBeforeTermination => {
611                Self::check_uncommitted_changes_before_termination(ctx)
612            }
613            _ => unreachable!("execute_commit_effect called with non-commit effect"),
614        }
615    }
616
617    fn execute_rebase_effect(
618        &mut self,
619        effect: Effect,
620        ctx: &mut PhaseContext<'_>,
621    ) -> Result<EffectResult> {
622        match effect {
623            Effect::RunRebase {
624                phase,
625                target_branch,
626            } => self.run_rebase(ctx, phase, &target_branch),
627            Effect::ResolveRebaseConflicts { strategy } => {
628                Ok(Self::resolve_rebase_conflicts(ctx, strategy))
629            }
630            _ => unreachable!("execute_rebase_effect called with non-rebase effect"),
631        }
632    }
633
634    fn execute_lifecycle_effect(
635        &mut self,
636        effect: Effect,
637        ctx: &mut PhaseContext<'_>,
638    ) -> Result<EffectResult> {
639        match effect {
640            Effect::ValidateFinalState => Ok(self.validate_final_state(ctx)),
641            Effect::SaveCheckpoint { trigger } => Ok(self.save_checkpoint(ctx, trigger)),
642            Effect::EnsureGitignoreEntries => Ok(Self::ensure_gitignore_entries(ctx)),
643            Effect::CleanupContext => Self::cleanup_context(ctx),
644            Effect::LockPromptPermissions => Ok(Self::lock_prompt_permissions(ctx)),
645            Effect::RestorePromptPermissions => Ok(self.restore_prompt_permissions(ctx)),
646            Effect::WriteContinuationContext(ref data) => {
647                execute_write_continuation_context(ctx, data)
648            }
649            Effect::CleanupContinuationContext => Self::cleanup_continuation_context(ctx),
650            Effect::WriteTimeoutContext {
651                role,
652                logfile_path,
653                context_path,
654            } => Self::write_timeout_context(ctx, role, &logfile_path, &context_path),
655            e => self.execute_lifecycle_effect_b(e, ctx),
656        }
657    }
658
659    fn execute_lifecycle_effect_b(
660        &mut self,
661        effect: Effect,
662        ctx: &mut PhaseContext<'_>,
663    ) -> Result<EffectResult> {
664        match effect {
665            Effect::TriggerLoopRecovery {
666                detected_loop,
667                loop_count,
668            } => Ok(Self::trigger_loop_recovery(ctx, &detected_loop, loop_count)),
669            Effect::EmitRecoveryReset {
670                reset_type,
671                target_phase,
672            } => Ok(self.emit_recovery_reset(ctx, &reset_type, target_phase)),
673            e => self.execute_lifecycle_effect_recovery_or_c(e, ctx),
674        }
675    }
676
677    fn execute_lifecycle_effect_recovery_or_c(
678        &mut self,
679        effect: Effect,
680        ctx: &mut PhaseContext<'_>,
681    ) -> Result<EffectResult> {
682        match effect {
683            Effect::AttemptRecovery {
684                level,
685                attempt_count,
686            } => Ok(self.attempt_recovery(ctx, level, attempt_count)),
687            Effect::EmitRecoverySuccess {
688                level,
689                total_attempts,
690            } => Ok(Self::emit_recovery_success(ctx, level, total_attempts)),
691            e => self.execute_lifecycle_effect_c(e, ctx),
692        }
693    }
694
695    fn execute_lifecycle_effect_c(
696        &mut self,
697        effect: Effect,
698        ctx: &mut PhaseContext<'_>,
699    ) -> Result<EffectResult> {
700        match effect {
701            Effect::TriggerDevFixFlow {
702                failed_phase,
703                failed_role,
704                retry_cycle,
705            } => Ok(self.trigger_dev_fix_flow(ctx, failed_phase, failed_role, retry_cycle)),
706            Effect::EmitCompletionMarkerAndTerminate { is_failure, reason } => Ok(
707                Self::emit_completion_marker_and_terminate(ctx, is_failure, reason),
708            ),
709            Effect::ConfigureGitAuth { auth_method } => {
710                Ok(Self::handle_configure_git_auth(ctx, &auth_method))
711            }
712            e => Self::execute_lifecycle_git_effect(ctx, e),
713        }
714    }
715
716    fn execute_lifecycle_git_effect(
717        ctx: &mut PhaseContext<'_>,
718        effect: Effect,
719    ) -> Result<EffectResult> {
720        match effect {
721            Effect::PushToRemote {
722                remote,
723                branch,
724                force,
725                commit_sha,
726            } => Ok(Self::handle_push_to_remote(
727                ctx, remote, branch, force, commit_sha,
728            )),
729            Effect::CreatePullRequest {
730                base_branch,
731                head_branch,
732                title,
733                body,
734            } => Ok(Self::handle_create_pull_request(
735                ctx,
736                &base_branch,
737                &head_branch,
738                &title,
739                &body,
740            )),
741            _ => unreachable!("execute_lifecycle_effect called with unexpected effect"),
742        }
743    }
744}