mod commit;
mod development;
mod planning;
mod review;
use crate::reducer::effect::Effect;
use crate::reducer::event::PipelinePhase;
use crate::reducer::state::PipelineState;
pub fn determine_next_effect_for_phase(state: &PipelineState) -> Effect {
match state.phase {
PipelinePhase::Planning => planning::determine_planning_effect(state),
PipelinePhase::Development => development::determine_development_effect(state),
PipelinePhase::Review => review::determine_review_effect(state),
PipelinePhase::CommitMessage => commit::determine_commit_effect(state),
PipelinePhase::FinalValidation => {
if !state.pre_termination_commit_checked {
return Effect::CheckUncommittedChangesBeforeTermination;
}
Effect::ValidateFinalState
}
PipelinePhase::Finalizing => Effect::RestorePromptPermissions,
PipelinePhase::AwaitingDevFix => {
if state.completion_marker_pending {
if !state.interrupted_by_user && !state.pre_termination_commit_checked {
return Effect::CheckUncommittedChangesBeforeTermination;
}
return Effect::EmitCompletionMarkerAndTerminate {
is_failure: state.completion_marker_is_failure,
reason: state.completion_marker_reason.clone(),
};
}
if state.dev_fix_triggered && state.recovery_escalation_level > 0 {
if state.recovery_escalation_level == 1 {
Effect::AttemptRecovery {
level: state.recovery_escalation_level,
attempt_count: state.dev_fix_attempt_count,
}
} else {
use crate::reducer::effect::RecoveryResetType;
let (reset_type, target_phase) = match state.recovery_escalation_level {
2 => (
RecoveryResetType::PhaseStart,
state
.failed_phase_for_recovery
.unwrap_or(PipelinePhase::Development),
),
3 => (RecoveryResetType::IterationReset, PipelinePhase::Planning),
_ => (RecoveryResetType::CompleteReset, PipelinePhase::Planning),
};
Effect::EmitRecoveryReset {
reset_type,
target_phase,
}
}
} else {
let failed_phase = state
.failed_phase_for_recovery
.or(state.previous_phase)
.unwrap_or(PipelinePhase::Development);
let failed_phase = if failed_phase == PipelinePhase::AwaitingDevFix {
PipelinePhase::Development
} else {
failed_phase
};
Effect::TriggerDevFixFlow {
failed_phase,
failed_role: state.agent_chain.current_drain.role(),
retry_cycle: state.agent_chain.retry_cycle,
}
}
}
PipelinePhase::Complete | PipelinePhase::Interrupted => {
use crate::reducer::event::CheckpointTrigger;
if state.phase == PipelinePhase::Interrupted && state.interrupted_by_user {
if !state.prompt_permissions.restored {
return Effect::RestorePromptPermissions;
}
return Effect::SaveCheckpoint {
trigger: CheckpointTrigger::Interrupt,
};
}
if !state.pre_termination_commit_checked {
return Effect::CheckUncommittedChangesBeforeTermination;
}
if state.phase == PipelinePhase::Interrupted && !state.prompt_permissions.restored {
return Effect::RestorePromptPermissions;
}
Effect::SaveCheckpoint {
trigger: CheckpointTrigger::PhaseTransition,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agents::AgentRole;
use crate::reducer::state::{AgentChainState, PromptPermissionsState};
#[test]
fn trigger_dev_fix_flow_prefers_failed_phase_for_recovery_over_previous_phase() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::AwaitingDevFix,
previous_phase: Some(PipelinePhase::AwaitingDevFix),
failed_phase_for_recovery: Some(PipelinePhase::CommitMessage),
dev_fix_triggered: false,
recovery_escalation_level: 0,
agent_chain: AgentChainState {
current_role: AgentRole::Developer,
retry_cycle: 7,
..AgentChainState::default()
},
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::TriggerDevFixFlow {
failed_phase,
failed_role,
retry_cycle,
} => {
assert_eq!(failed_phase, PipelinePhase::CommitMessage);
assert_eq!(failed_role, AgentRole::Developer);
assert_eq!(retry_cycle, 7);
}
other => panic!("expected TriggerDevFixFlow, got: {other:?}"),
}
}
#[test]
fn trigger_dev_fix_flow_never_reports_awaiting_dev_fix_as_failed_phase() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::AwaitingDevFix,
previous_phase: Some(PipelinePhase::AwaitingDevFix),
failed_phase_for_recovery: None,
dev_fix_triggered: false,
recovery_escalation_level: 0,
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::TriggerDevFixFlow { failed_phase, .. } => {
assert_ne!(failed_phase, PipelinePhase::AwaitingDevFix);
}
other => panic!("expected TriggerDevFixFlow, got: {other:?}"),
}
}
#[test]
fn awaiting_dev_fix_completion_marker_pending_requires_safety_check_first() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::AwaitingDevFix,
completion_marker_pending: true,
completion_marker_is_failure: true,
completion_marker_reason: Some("safety_valve".to_string()),
interrupted_by_user: false,
pre_termination_commit_checked: false,
..state
};
let effect = determine_next_effect_for_phase(&state);
assert!(
matches!(effect, Effect::CheckUncommittedChangesBeforeTermination),
"expected safety check to preempt completion marker emission, got: {effect:?}"
);
}
#[test]
fn awaiting_dev_fix_completion_marker_pending_emits_after_safety_check() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::AwaitingDevFix,
completion_marker_pending: true,
completion_marker_is_failure: true,
completion_marker_reason: Some("safety_valve".to_string()),
interrupted_by_user: false,
pre_termination_commit_checked: true,
..state
};
let effect = determine_next_effect_for_phase(&state);
assert!(
matches!(
effect,
Effect::EmitCompletionMarkerAndTerminate {
is_failure: true,
ref reason
} if reason.as_deref() == Some("safety_valve")
),
"expected EmitCompletionMarkerAndTerminate after safety check, got: {effect:?}"
);
}
#[test]
fn user_interrupt_skips_pre_termination_safety_check() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Interrupted,
interrupted_by_user: true,
pre_termination_commit_checked: false,
prompt_permissions: PromptPermissionsState {
restored: true,
..Default::default()
},
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::SaveCheckpoint { trigger } => {
assert_eq!(trigger, crate::reducer::event::CheckpointTrigger::Interrupt);
}
other => panic!(
"Expected SaveCheckpoint effect when interrupted_by_user=true, got {other:?}. \
User interrupt should skip pre-termination safety check."
),
}
}
#[test]
fn programmatic_interrupt_requires_pre_termination_safety_check() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Interrupted,
interrupted_by_user: false,
pre_termination_commit_checked: false,
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::CheckUncommittedChangesBeforeTermination => {
}
other => panic!(
"Expected CheckUncommittedChangesBeforeTermination when interrupted_by_user=false, got {other:?}. \
Programmatic interrupts must commit uncommitted work before terminating."
),
}
}
#[test]
fn complete_phase_requires_pre_termination_safety_check() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Complete,
interrupted_by_user: false,
pre_termination_commit_checked: false,
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::CheckUncommittedChangesBeforeTermination => {
}
other => panic!(
"Expected CheckUncommittedChangesBeforeTermination before Complete, got {other:?}"
),
}
}
#[test]
fn final_validation_requires_pre_termination_safety_check() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::FinalValidation,
pre_termination_commit_checked: false,
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::CheckUncommittedChangesBeforeTermination => {
}
other => panic!(
"Expected CheckUncommittedChangesBeforeTermination before FinalValidation, got {other:?}"
),
}
}
#[test]
fn safety_check_allows_proceed_after_checked() {
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Interrupted,
interrupted_by_user: false,
pre_termination_commit_checked: true,
prompt_permissions: PromptPermissionsState {
restored: true,
..Default::default()
},
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::SaveCheckpoint { .. } => {
}
other => panic!(
"Expected SaveCheckpoint after safety check and restoration complete, got {other:?}"
),
}
}
#[test]
fn complete_saves_checkpoint_with_phase_transition_trigger_after_safety_check() {
use crate::reducer::event::CheckpointTrigger;
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Complete,
pre_termination_commit_checked: true,
interrupted_by_user: false,
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::SaveCheckpoint { trigger } => {
assert_eq!(trigger, CheckpointTrigger::PhaseTransition);
}
other => panic!("expected SaveCheckpoint, got: {other:?}"),
}
}
#[test]
fn programmatic_interrupt_saves_checkpoint_with_phase_transition_trigger_after_safety_check() {
use crate::reducer::event::CheckpointTrigger;
let state = PipelineState::initial(1, 0);
let state = PipelineState {
phase: PipelinePhase::Interrupted,
pre_termination_commit_checked: true,
interrupted_by_user: false,
prompt_permissions: PromptPermissionsState {
restored: true,
..Default::default()
},
..state
};
let effect = determine_next_effect_for_phase(&state);
match effect {
Effect::SaveCheckpoint { trigger } => {
assert_eq!(trigger, CheckpointTrigger::PhaseTransition);
}
other => panic!("expected SaveCheckpoint, got: {other:?}"),
}
}
}