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 connectivity;
120mod context;
121mod development;
122mod development_prompt;
123mod io_agent;
124mod io_commit;
125mod lifecycle;
126mod planning;
127mod rebase;
128pub(crate) mod retry_guidance;
129mod run_fix;
130mod run_review;
131mod run_review_prompt;
132
133#[cfg(test)]
134mod tests;
135
136use crate::phases::PhaseContext;
137use crate::prompts::{PromptHistoryEntry, PromptScopeKey};
138use crate::reducer::effect::{Effect, EffectHandler, EffectResult};
139use crate::reducer::event::{PipelineEvent, PipelinePhase};
140use crate::reducer::state::PipelineState;
141use crate::reducer::ui_event::UIEvent;
142use anyhow::Result;
143use std::hash::BuildHasher;
144
145fn execute_backoff_wait(
146    ctx: &mut PhaseContext<'_>,
147    role: crate::agents::AgentRole,
148    cycle: u32,
149    duration_ms: u64,
150) -> Result<EffectResult> {
151    ctx.registry
152        .retry_timer()
153        .sleep(std::time::Duration::from_millis(duration_ms));
154    Ok(EffectResult::event(
155        PipelineEvent::agent_retry_cycle_started(role, cycle),
156    ))
157}
158
159fn execute_write_continuation_context(
160    ctx: &mut PhaseContext<'_>,
161    data: &crate::reducer::effect::ContinuationContextData,
162) -> Result<EffectResult> {
163    development::write_continuation_context_to_workspace(ctx.workspace, ctx.logger, data)?;
164    Ok(EffectResult::event(
165        PipelineEvent::development_continuation_context_written(data.iteration, data.attempt),
166    ))
167}
168
169fn ensure_completion_marker_dir(ctx: &PhaseContext<'_>) {
170    if let Err(err) = ctx
171        .workspace
172        .create_dir_all(std::path::Path::new(".agent/tmp"))
173    {
174        ctx.logger.warn(&format!(
175            "Failed to create completion marker directory: {err}"
176        ));
177    }
178}
179
180fn write_completion_marker_content(
181    ctx: &PhaseContext<'_>,
182    content: &str,
183    is_failure: bool,
184) -> std::result::Result<(), String> {
185    let marker_path = std::path::Path::new(".agent/tmp/completion_marker");
186    match ctx.workspace.write(marker_path, content) {
187        Ok(()) => {
188            ctx.logger.info(&format!(
189                "Completion marker written: {}",
190                if is_failure { "failure" } else { "success" }
191            ));
192            Ok(())
193        }
194        Err(err) => {
195            ctx.logger
196                .warn(&format!("Failed to write completion marker: {err}"));
197            Err(err.to_string())
198        }
199    }
200}
201
202fn get_stored_or_generate_prompt_with_validation<F, S: BuildHasher>(
203    scope_key: &PromptScopeKey,
204    prompt_history: &std::collections::HashMap<String, PromptHistoryEntry, S>,
205    current_content_id: Option<&str>,
206    generator: F,
207) -> (String, bool, bool)
208where
209    F: FnOnce() -> (String, bool),
210{
211    let key = scope_key.to_string();
212    match prompt_history.get(&key) {
213        Some(entry) if !content_id_mismatch(entry, current_content_id) => {
214            (entry.content.clone(), true, false)
215        }
216        _ => {
217            let (prompt, should_validate) = generator();
218            (prompt, false, should_validate)
219        }
220    }
221}
222
223fn content_id_mismatch(entry: &PromptHistoryEntry, current_content_id: Option<&str>) -> bool {
224    current_content_id.is_some_and(|current_id| entry.content_id.as_deref() != Some(current_id))
225}
226
227/// Main effect handler implementation.
228///
229/// This handler executes effects by calling pipeline subsystems and emitting reducer events.
230pub struct MainEffectHandler {
231    /// Current pipeline state
232    pub state: PipelineState,
233    /// Event log for replay/debugging
234    pub event_log: Vec<PipelineEvent>,
235}
236
237impl MainEffectHandler {
238    /// Create a new effect handler.
239    #[must_use]
240    pub const fn new(state: PipelineState) -> Self {
241        Self {
242            state,
243            event_log: Vec::new(),
244        }
245    }
246}
247
248impl EffectHandler<'_> for MainEffectHandler {
249    fn execute(&mut self, effect: Effect, ctx: &mut PhaseContext<'_>) -> Result<EffectResult> {
250        let result = self.execute_effect(effect, ctx)?;
251        self.event_log.push(result.event.clone());
252        self.event_log
253            .extend(result.additional_events.iter().cloned());
254        Ok(result)
255    }
256}
257
258impl crate::app::event_loop::StatefulHandler for MainEffectHandler {
259    fn update_state(&mut self, state: PipelineState) {
260        self.state = state;
261    }
262}
263
264impl MainEffectHandler {
265    /// Helper to create phase transition UI event.
266    const fn phase_transition_ui(&self, to: PipelinePhase) -> UIEvent {
267        UIEvent::PhaseTransition {
268            from: Some(self.state.phase),
269            to,
270        }
271    }
272
273    fn write_completion_marker(
274        ctx: &PhaseContext<'_>,
275        content: &str,
276        is_failure: bool,
277    ) -> std::result::Result<(), String> {
278        ensure_completion_marker_dir(ctx);
279        write_completion_marker_content(ctx, content, is_failure)
280    }
281
282    fn execute_effect(
283        &mut self,
284        effect: Effect,
285        ctx: &mut PhaseContext<'_>,
286    ) -> Result<EffectResult> {
287        match effect {
288            Effect::AgentInvocation {
289                role,
290                agent,
291                model,
292                prompt,
293            } => self.execute_agent_invocation_effect(ctx, role, agent, model, prompt),
294            Effect::InitializeAgentChain { drain, .. } => {
295                Ok(self.initialize_agent_chain(ctx, drain))
296            }
297            e => self.execute_non_agent_effect(e, ctx),
298        }
299    }
300
301    fn execute_agent_invocation_effect(
302        &mut self,
303        ctx: &mut PhaseContext<'_>,
304        role: crate::agents::AgentRole,
305        agent: String,
306        model: Option<String>,
307        prompt: String,
308    ) -> Result<EffectResult> {
309        self.invoke_agent(
310            ctx,
311            crate::agents::AgentDrain::from(role),
312            role,
313            &agent,
314            model.as_deref(),
315            prompt,
316        )
317    }
318
319    fn execute_non_agent_effect(
320        &mut self,
321        effect: Effect,
322        ctx: &mut PhaseContext<'_>,
323    ) -> Result<EffectResult> {
324        match effect {
325            Effect::BackoffWait {
326                role,
327                cycle,
328                duration_ms,
329            } => execute_backoff_wait(ctx, role, cycle, duration_ms),
330            Effect::ReportAgentChainExhausted { role, phase, cycle } => Err(
331                crate::reducer::event::ErrorEvent::AgentChainExhausted { role, phase, cycle }
332                    .into(),
333            ),
334            e => self.execute_phase_effect(e, ctx),
335        }
336    }
337
338    fn execute_phase_effect(
339        &mut self,
340        effect: Effect,
341        ctx: &mut PhaseContext<'_>,
342    ) -> Result<EffectResult> {
343        match effect {
344            e @ (Effect::PreparePlanningPrompt { .. }
345            | Effect::MaterializePlanningInputs { .. }
346            | Effect::CleanupRequiredFiles { .. }
347            | Effect::InvokePlanningAgent { .. }
348            | Effect::ExtractPlanningXml { .. }
349            | Effect::ValidatePlanningXml { .. }
350            | Effect::WritePlanningMarkdown { .. }
351            | Effect::ArchivePlanningXml { .. }
352            | Effect::ApplyPlanningOutcome { .. }) => self.execute_planning_effect(e, ctx),
353            e @ (Effect::PrepareDevelopmentContext { .. }
354            | Effect::MaterializeDevelopmentInputs { .. }
355            | Effect::PrepareDevelopmentPrompt { .. }
356            | Effect::InvokeDevelopmentAgent { .. }
357            | Effect::InvokeAnalysisAgent { .. }
358            | Effect::ExtractDevelopmentXml { .. }
359            | Effect::ValidateDevelopmentXml { .. }
360            | Effect::ApplyDevelopmentOutcome { .. }
361            | Effect::ArchiveDevelopmentXml { .. }) => self.execute_development_effect(e, ctx),
362            e => self.execute_phase_effect_b(e, ctx),
363        }
364    }
365
366    fn execute_phase_effect_b(
367        &mut self,
368        effect: Effect,
369        ctx: &mut PhaseContext<'_>,
370    ) -> Result<EffectResult> {
371        match effect {
372            e @ (Effect::PrepareReviewContext { .. }
373            | Effect::MaterializeReviewInputs { .. }
374            | Effect::PrepareReviewPrompt { .. }
375            | Effect::InvokeReviewAgent { .. }
376            | Effect::ExtractReviewIssuesXml { .. }
377            | Effect::ValidateReviewIssuesXml { .. }
378            | Effect::WriteIssuesMarkdown { .. }
379            | Effect::ExtractReviewIssueSnippets { .. }
380            | Effect::ArchiveReviewIssuesXml { .. }
381            | Effect::ApplyReviewOutcome { .. }
382            | Effect::PrepareFixPrompt { .. }
383            | Effect::InvokeFixAgent { .. }
384            | Effect::InvokeFixAnalysisAgent { .. }
385            | Effect::ExtractFixResultXml { .. }
386            | Effect::ValidateFixResultXml { .. }
387            | Effect::ApplyFixOutcome { .. }
388            | Effect::ArchiveFixResultXml { .. }) => self.execute_review_effect(e, ctx),
389            e @ (Effect::PrepareCommitPrompt { .. }
390            | Effect::CheckCommitDiff
391            | Effect::MaterializeCommitInputs { .. }
392            | Effect::InvokeCommitAgent
393            | Effect::ExtractCommitXml
394            | Effect::ValidateCommitXml
395            | Effect::ApplyCommitMessageOutcome
396            | Effect::ArchiveCommitXml
397            | Effect::CreateCommit { .. }
398            | Effect::SkipCommit { .. }
399            | Effect::CheckResidualFiles { .. }
400            | Effect::CheckUncommittedChangesBeforeTermination) => {
401                self.execute_commit_effect(e, ctx)
402            }
403            e @ (Effect::RunRebase { .. } | Effect::ResolveRebaseConflicts { .. }) => {
404                self.execute_rebase_effect(e, ctx)
405            }
406            e => self.execute_lifecycle_effect(e, ctx),
407        }
408    }
409
410    fn execute_planning_effect(
411        &mut self,
412        effect: Effect,
413        ctx: &mut PhaseContext<'_>,
414    ) -> Result<EffectResult> {
415        match effect {
416            Effect::PreparePlanningPrompt {
417                iteration,
418                prompt_mode,
419            } => self.prepare_planning_prompt(ctx, iteration, prompt_mode),
420            Effect::MaterializePlanningInputs { iteration } => {
421                self.materialize_planning_inputs(ctx, iteration)
422            }
423            Effect::CleanupRequiredFiles { files } => Ok(self.cleanup_required_files(ctx, &files)),
424            Effect::InvokePlanningAgent { iteration } => self.invoke_planning_agent(ctx, iteration),
425            e => self.execute_planning_effect_b(e, ctx),
426        }
427    }
428
429    fn execute_planning_effect_b(
430        &mut self,
431        effect: Effect,
432        ctx: &mut PhaseContext<'_>,
433    ) -> Result<EffectResult> {
434        match effect {
435            Effect::ExtractPlanningXml { iteration } => {
436                Ok(self.extract_planning_xml(ctx, iteration))
437            }
438            Effect::ValidatePlanningXml { iteration } => self.validate_planning_xml(ctx, iteration),
439            Effect::WritePlanningMarkdown { iteration } => {
440                self.write_planning_markdown(ctx, iteration)
441            }
442            Effect::ArchivePlanningXml { iteration } => {
443                Ok(Self::archive_planning_xml(ctx, iteration))
444            }
445            Effect::ApplyPlanningOutcome { iteration, valid } => {
446                Ok(self.apply_planning_outcome(ctx, iteration, valid))
447            }
448            _ => unreachable!("execute_planning_effect called with non-planning effect"),
449        }
450    }
451
452    fn execute_development_effect(
453        &mut self,
454        effect: Effect,
455        ctx: &mut PhaseContext<'_>,
456    ) -> Result<EffectResult> {
457        match effect {
458            Effect::PrepareDevelopmentContext { iteration } => {
459                Ok(Self::prepare_development_context(ctx, iteration))
460            }
461            Effect::MaterializeDevelopmentInputs { iteration } => {
462                self.materialize_development_inputs(ctx, iteration)
463            }
464            Effect::PrepareDevelopmentPrompt {
465                iteration,
466                prompt_mode,
467            } => self.prepare_development_prompt(ctx, iteration, prompt_mode),
468            Effect::InvokeDevelopmentAgent { iteration } => {
469                self.invoke_development_agent(ctx, iteration)
470            }
471            e => self.execute_development_effect_b(e, ctx),
472        }
473    }
474
475    fn execute_development_effect_b(
476        &mut self,
477        effect: Effect,
478        ctx: &mut PhaseContext<'_>,
479    ) -> Result<EffectResult> {
480        match effect {
481            Effect::InvokeAnalysisAgent { iteration } => self.invoke_analysis_agent(ctx, iteration),
482            Effect::ExtractDevelopmentXml { iteration } => {
483                Ok(self.extract_development_xml(ctx, iteration))
484            }
485            Effect::ValidateDevelopmentXml { iteration } => {
486                Ok(self.validate_development_xml(ctx, iteration))
487            }
488            Effect::ApplyDevelopmentOutcome { iteration } => {
489                self.apply_development_outcome(ctx, iteration)
490            }
491            Effect::ArchiveDevelopmentXml { iteration } => {
492                Ok(Self::archive_development_xml(ctx, iteration))
493            }
494            _ => unreachable!("execute_development_effect called with non-development effect"),
495        }
496    }
497
498    fn execute_review_effect(
499        &mut self,
500        effect: Effect,
501        ctx: &mut PhaseContext<'_>,
502    ) -> Result<EffectResult> {
503        match effect {
504            Effect::PrepareReviewContext { pass } => Ok(self.prepare_review_context(ctx, pass)),
505            Effect::MaterializeReviewInputs { pass } => self.materialize_review_inputs(ctx, pass),
506            Effect::PrepareReviewPrompt { pass, prompt_mode } => {
507                self.prepare_review_prompt(ctx, pass, prompt_mode)
508            }
509            Effect::InvokeReviewAgent { pass } => self.invoke_review_agent(ctx, pass),
510            Effect::ExtractReviewIssuesXml { pass } => {
511                Ok(self.extract_review_issues_xml(ctx, pass))
512            }
513            Effect::ValidateReviewIssuesXml { pass } => {
514                Ok(self.validate_review_issues_xml(ctx, pass))
515            }
516            e => self.execute_fix_effect(e, ctx),
517        }
518    }
519
520    fn execute_fix_effect(
521        &mut self,
522        effect: Effect,
523        ctx: &mut PhaseContext<'_>,
524    ) -> Result<EffectResult> {
525        match effect {
526            Effect::WriteIssuesMarkdown { pass } => self.write_issues_markdown(ctx, pass),
527            Effect::ExtractReviewIssueSnippets { pass } => {
528                self.extract_review_issue_snippets(ctx, pass)
529            }
530            Effect::ArchiveReviewIssuesXml { pass } => {
531                Ok(Self::archive_review_issues_xml(ctx, pass))
532            }
533            e => self.execute_fix_outcome_or_agent_effect(e, ctx),
534        }
535    }
536
537    fn execute_fix_outcome_or_agent_effect(
538        &mut self,
539        effect: Effect,
540        ctx: &mut PhaseContext<'_>,
541    ) -> Result<EffectResult> {
542        match effect {
543            Effect::ApplyReviewOutcome {
544                pass,
545                issues_found,
546                clean_no_issues,
547            } => Ok(Self::apply_review_outcome(
548                ctx,
549                pass,
550                issues_found,
551                clean_no_issues,
552            )),
553            e => self.execute_fix_agent_effect(e, ctx),
554        }
555    }
556
557    fn execute_fix_agent_effect(
558        &mut self,
559        effect: Effect,
560        ctx: &mut PhaseContext<'_>,
561    ) -> Result<EffectResult> {
562        match effect {
563            Effect::PrepareFixPrompt { pass, prompt_mode } => {
564                self.prepare_fix_prompt(ctx, pass, prompt_mode)
565            }
566            Effect::InvokeFixAgent { pass } => self.invoke_fix_agent(ctx, pass),
567            Effect::InvokeFixAnalysisAgent { pass } => self.invoke_fix_analysis_agent(ctx, pass),
568            Effect::ExtractFixResultXml { pass } => Ok(self.extract_fix_result_xml(ctx, pass)),
569            Effect::ValidateFixResultXml { pass } => Ok(self.validate_fix_result_xml(ctx, pass)),
570            Effect::ApplyFixOutcome { pass } => self.apply_fix_outcome(ctx, pass),
571            Effect::ArchiveFixResultXml { pass } => Ok(self.archive_fix_result_xml(ctx, pass)),
572            _ => unreachable!("execute_fix_effect called with non-fix effect"),
573        }
574    }
575
576    fn execute_commit_effect(
577        &mut self,
578        effect: Effect,
579        ctx: &mut PhaseContext<'_>,
580    ) -> Result<EffectResult> {
581        match effect {
582            Effect::PrepareCommitPrompt { prompt_mode } => {
583                self.prepare_commit_prompt(ctx, prompt_mode)
584            }
585            Effect::CheckCommitDiff => Self::check_commit_diff(ctx),
586            Effect::MaterializeCommitInputs { attempt } => {
587                self.materialize_commit_inputs(ctx, attempt)
588            }
589            Effect::InvokeCommitAgent => self.invoke_commit_agent(ctx),
590            Effect::ExtractCommitXml => Ok(self.extract_commit_xml(ctx)),
591            Effect::ValidateCommitXml => Ok(self.validate_commit_xml(ctx)),
592            e => self.execute_commit_finalization_effect(e, ctx),
593        }
594    }
595
596    fn execute_commit_finalization_effect(
597        &mut self,
598        effect: Effect,
599        ctx: &mut PhaseContext<'_>,
600    ) -> Result<EffectResult> {
601        match effect {
602            Effect::ApplyCommitMessageOutcome => self.apply_commit_message_outcome(ctx),
603            Effect::ArchiveCommitXml => Ok(self.archive_commit_xml(ctx)),
604            Effect::CreateCommit {
605                message,
606                files,
607                excluded_files,
608            } => Self::create_commit(ctx, message, &files, &excluded_files),
609            Effect::SkipCommit { reason } => Ok(Self::skip_commit(ctx, reason)),
610            Effect::CheckResidualFiles { pass } => Self::check_residual_files(ctx, pass),
611            Effect::CheckUncommittedChangesBeforeTermination => {
612                Self::check_uncommitted_changes_before_termination(ctx)
613            }
614            _ => unreachable!("execute_commit_effect called with non-commit effect"),
615        }
616    }
617
618    fn execute_rebase_effect(
619        &mut self,
620        effect: Effect,
621        ctx: &mut PhaseContext<'_>,
622    ) -> Result<EffectResult> {
623        match effect {
624            Effect::RunRebase {
625                phase,
626                target_branch,
627            } => self.run_rebase(ctx, phase, &target_branch),
628            Effect::ResolveRebaseConflicts { strategy } => {
629                Ok(Self::resolve_rebase_conflicts(ctx, strategy))
630            }
631            _ => unreachable!("execute_rebase_effect called with non-rebase effect"),
632        }
633    }
634
635    fn execute_lifecycle_effect(
636        &mut self,
637        effect: Effect,
638        ctx: &mut PhaseContext<'_>,
639    ) -> Result<EffectResult> {
640        match effect {
641            Effect::ValidateFinalState => Ok(self.validate_final_state(ctx)),
642            Effect::SaveCheckpoint { trigger } => Ok(self.save_checkpoint(ctx, trigger)),
643            Effect::EnsureGitignoreEntries => Ok(Self::ensure_gitignore_entries(ctx)),
644            Effect::CleanupContext => Self::cleanup_context(ctx),
645            Effect::LockPromptPermissions => Ok(Self::lock_prompt_permissions(ctx)),
646            Effect::RestorePromptPermissions => Ok(self.restore_prompt_permissions(ctx)),
647            Effect::WriteContinuationContext(ref data) => {
648                execute_write_continuation_context(ctx, data)
649            }
650            Effect::CleanupContinuationContext => Self::cleanup_continuation_context(ctx),
651            Effect::WriteTimeoutContext {
652                role,
653                logfile_path,
654                context_path,
655            } => Self::write_timeout_context(ctx, role, &logfile_path, &context_path),
656            e => self.execute_lifecycle_effect_b(e, ctx),
657        }
658    }
659
660    fn execute_lifecycle_effect_b(
661        &mut self,
662        effect: Effect,
663        ctx: &mut PhaseContext<'_>,
664    ) -> Result<EffectResult> {
665        match effect {
666            Effect::TriggerLoopRecovery {
667                detected_loop,
668                loop_count,
669            } => Ok(Self::trigger_loop_recovery(ctx, &detected_loop, loop_count)),
670            Effect::EmitRecoveryReset {
671                reset_type,
672                target_phase,
673            } => Ok(self.emit_recovery_reset(ctx, &reset_type, target_phase)),
674            Effect::CheckNetworkConnectivity => Ok(connectivity::check_network_connectivity(
675                &self.state.connectivity,
676            )),
677            Effect::PollForConnectivity { interval_ms } => Ok(connectivity::poll_for_connectivity(
678                interval_ms,
679                &self.state.connectivity,
680            )),
681            e => self.execute_lifecycle_effect_recovery_or_c(e, ctx),
682        }
683    }
684
685    fn execute_lifecycle_effect_recovery_or_c(
686        &mut self,
687        effect: Effect,
688        ctx: &mut PhaseContext<'_>,
689    ) -> Result<EffectResult> {
690        match effect {
691            Effect::AttemptRecovery {
692                level,
693                attempt_count,
694            } => Ok(self.attempt_recovery(ctx, level, attempt_count)),
695            Effect::EmitRecoverySuccess {
696                level,
697                total_attempts,
698            } => Ok(Self::emit_recovery_success(ctx, level, total_attempts)),
699            e => self.execute_lifecycle_effect_c(e, ctx),
700        }
701    }
702
703    fn execute_lifecycle_effect_c(
704        &mut self,
705        effect: Effect,
706        ctx: &mut PhaseContext<'_>,
707    ) -> Result<EffectResult> {
708        match effect {
709            Effect::TriggerDevFixFlow {
710                failed_phase,
711                failed_role,
712                retry_cycle,
713            } => Ok(self.trigger_dev_fix_flow(ctx, failed_phase, failed_role, retry_cycle)),
714            Effect::EmitCompletionMarkerAndTerminate { is_failure, reason } => Ok(
715                Self::emit_completion_marker_and_terminate(ctx, is_failure, reason),
716            ),
717            Effect::ConfigureGitAuth { auth_method } => {
718                Ok(Self::handle_configure_git_auth(ctx, &auth_method))
719            }
720            e => Self::execute_lifecycle_git_effect(ctx, e),
721        }
722    }
723
724    fn execute_lifecycle_git_effect(
725        ctx: &mut PhaseContext<'_>,
726        effect: Effect,
727    ) -> Result<EffectResult> {
728        match effect {
729            Effect::PushToRemote {
730                remote,
731                branch,
732                force,
733                commit_sha,
734            } => Ok(Self::handle_push_to_remote(
735                ctx, remote, branch, force, commit_sha,
736            )),
737            Effect::CreatePullRequest {
738                base_branch,
739                head_branch,
740                title,
741                body,
742            } => Ok(Self::handle_create_pull_request(
743                ctx,
744                &base_branch,
745                &head_branch,
746                &title,
747                &body,
748            )),
749            _ => unreachable!("execute_lifecycle_effect called with unexpected effect"),
750        }
751    }
752}