use super::super::checkpoint::{
create_checkpoint_with_total_steps, CheckpointManager,
CompletedStep as CheckpointCompletedStep, ResumeContext, RetryState,
};
use super::super::error_recovery::ErrorRecoveryState;
use super::super::normalized;
use super::super::normalized::NormalizedWorkflow;
use super::super::validation::OnIncompleteConfig;
use super::{
CaptureOutput, ExtendedWorkflowConfig, SensitivePatternConfig, StepResult, WorkflowContext,
WorkflowExecutor, WorkflowStep,
};
use crate::abstractions::git::RealGitOperations;
use crate::commands::CommandRegistry;
use crate::cook::execution::ClaudeExecutor;
use crate::cook::interaction::UserInteraction;
use crate::cook::retry_state::RetryStateManager;
use crate::cook::session::SessionManager;
use crate::testing::config::TestConfiguration;
use crate::unified_session::TimingTracker;
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
impl WorkflowExecutor {
pub fn new(
claude_executor: Arc<dyn ClaudeExecutor>,
session_manager: Arc<dyn SessionManager>,
user_interaction: Arc<dyn UserInteraction>,
) -> Self {
use std::sync::atomic::AtomicBool;
Self {
claude_executor,
session_manager,
user_interaction,
timing_tracker: TimingTracker::new(),
test_config: None,
command_registry: None,
subprocess: crate::subprocess::SubprocessManager::production(),
sensitive_config: SensitivePatternConfig::default(),
completed_steps: Vec::new(),
checkpoint_manager: None,
workflow_id: None,
checkpoint_completed_steps: Vec::new(),
environment_manager: None,
global_environment_config: None,
current_workflow: None,
current_step_index: None,
git_operations: Arc::new(RealGitOperations::new()),
resume_context: None,
retry_state_manager: Arc::new(RetryStateManager::new()),
workflow_path: None,
dry_run: false,
assumed_commits: Vec::new(),
dry_run_commands: Vec::new(),
dry_run_validations: Vec::new(),
dry_run_potential_handlers: Vec::new(),
positional_args: None,
shutdown_signal: Arc::new(AtomicBool::new(false)),
}
}
pub async fn with_command_registry(mut self) -> Self {
self.command_registry = Some(CommandRegistry::with_defaults().await);
self
}
pub fn with_resume_context(mut self, context: ResumeContext) -> Self {
if let Some(ref checkpoint) = context.checkpoint {
if let Some(retry_checkpoint_state) = checkpoint.retry_checkpoint_state.clone() {
let retry_manager = self.retry_state_manager.clone();
tokio::spawn(async move {
if let Err(e) = retry_manager
.restore_from_checkpoint(&retry_checkpoint_state)
.await
{
tracing::warn!("Failed to restore retry state from checkpoint: {}", e);
} else {
tracing::info!("Successfully restored retry state from checkpoint");
}
});
}
}
self.resume_context = Some(context);
self
}
pub fn with_workflow_path(mut self, path: PathBuf) -> Self {
self.workflow_path = Some(path);
self
}
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn with_environment_config(
mut self,
config: crate::cook::environment::EnvironmentConfig,
) -> Result<Self> {
let current_dir = std::env::current_dir()?;
self.environment_manager = Some(crate::cook::environment::EnvironmentManager::new(
current_dir,
)?);
self.global_environment_config = Some(config);
Ok(self)
}
pub fn with_positional_args(mut self, args: Vec<String>) -> Self {
self.positional_args = Some(args);
self
}
pub fn setup_signal_handler(&self) {
use std::sync::atomic::Ordering;
let shutdown_flag = Arc::clone(&self.shutdown_signal);
#[cfg(unix)]
{
use signal_hook::consts::{SIGINT, SIGTERM};
use signal_hook::iterator::Signals;
use std::thread;
thread::spawn(move || {
let mut signals =
Signals::new([SIGINT, SIGTERM]).expect("Failed to create signal handler");
for sig in signals.forever() {
match sig {
SIGINT | SIGTERM => {
tracing::info!(
"Received shutdown signal, initiating graceful shutdown"
);
shutdown_flag.store(true, Ordering::Release);
break;
}
_ => {}
}
}
});
}
#[cfg(not(unix))]
{
std::thread::spawn(move || {
use std::sync::mpsc::channel;
let (tx, rx) = channel();
ctrlc::set_handler(move || {
let _ = tx.send(());
})
.expect("Failed to set Ctrl+C handler");
let _ = rx.recv();
tracing::info!("Received Ctrl+C, initiating graceful shutdown");
shutdown_flag.store(true, Ordering::Release);
});
}
}
pub fn is_shutdown_requested(&self) -> bool {
use std::sync::atomic::Ordering;
self.shutdown_signal.load(Ordering::Acquire)
}
pub fn with_checkpoint_manager(
mut self,
manager: Arc<CheckpointManager>,
workflow_id: String,
) -> Self {
self.checkpoint_manager = Some(manager);
self.workflow_id = Some(workflow_id);
self
}
pub fn with_sensitive_patterns(mut self, config: SensitivePatternConfig) -> Self {
self.sensitive_config = config;
self
}
pub fn with_test_config(
claude_executor: Arc<dyn ClaudeExecutor>,
session_manager: Arc<dyn SessionManager>,
user_interaction: Arc<dyn UserInteraction>,
test_config: Arc<TestConfiguration>,
) -> Self {
use std::sync::atomic::AtomicBool;
Self {
claude_executor,
session_manager,
user_interaction,
timing_tracker: TimingTracker::new(),
test_config: Some(test_config),
command_registry: None,
subprocess: crate::subprocess::SubprocessManager::production(),
sensitive_config: SensitivePatternConfig::default(),
completed_steps: Vec::new(),
checkpoint_manager: None,
workflow_id: None,
checkpoint_completed_steps: Vec::new(),
environment_manager: None,
global_environment_config: None,
current_workflow: None,
current_step_index: None,
git_operations: Arc::new(RealGitOperations::new()),
resume_context: None,
retry_state_manager: Arc::new(RetryStateManager::new()),
workflow_path: None,
dry_run: false,
assumed_commits: Vec::new(),
dry_run_commands: Vec::new(),
dry_run_validations: Vec::new(),
dry_run_potential_handlers: Vec::new(),
positional_args: None,
shutdown_signal: Arc::new(AtomicBool::new(false)),
}
}
#[cfg(test)]
pub fn with_test_config_and_git(
claude_executor: Arc<dyn ClaudeExecutor>,
session_manager: Arc<dyn SessionManager>,
user_interaction: Arc<dyn UserInteraction>,
test_config: Arc<TestConfiguration>,
git_operations: Arc<dyn crate::abstractions::git::GitOperations>,
) -> Self {
use std::sync::atomic::AtomicBool;
Self {
claude_executor,
session_manager,
user_interaction,
timing_tracker: TimingTracker::new(),
test_config: Some(test_config),
command_registry: None,
subprocess: crate::subprocess::SubprocessManager::production(),
sensitive_config: SensitivePatternConfig::default(),
completed_steps: Vec::new(),
checkpoint_manager: None,
workflow_id: None,
checkpoint_completed_steps: Vec::new(),
environment_manager: None,
global_environment_config: None,
current_workflow: None,
current_step_index: None,
git_operations,
resume_context: None,
retry_state_manager: Arc::new(RetryStateManager::new()),
workflow_path: None,
dry_run: false,
assumed_commits: Vec::new(),
dry_run_commands: Vec::new(),
dry_run_validations: Vec::new(),
dry_run_potential_handlers: Vec::new(),
positional_args: None,
shutdown_signal: Arc::new(AtomicBool::new(false)),
}
}
pub(super) fn create_validation_handler(
&self,
on_incomplete: &OnIncompleteConfig,
_ctx: &WorkflowContext,
) -> Option<WorkflowStep> {
if on_incomplete.claude.is_some() || on_incomplete.shell.is_some() {
Some(WorkflowStep {
name: None,
claude: on_incomplete.claude.clone(),
shell: on_incomplete.shell.clone(),
test: None,
foreach: None,
write_file: None,
command: None,
handler: None,
capture: None,
capture_format: None,
capture_streams: Default::default(),
output_file: None,
timeout: None,
capture_output: CaptureOutput::Disabled,
on_failure: None,
retry: None,
on_success: None,
on_exit_code: Default::default(),
commit_required: on_incomplete.commit_required,
auto_commit: false,
commit_config: None,
working_dir: None,
env: Default::default(),
validate: None,
step_validate: None,
skip_validation: false,
validation_timeout: None,
ignore_validation_failure: false,
when: None,
})
} else {
None
}
}
pub(super) fn restore_error_recovery_state(
&self,
step_index: usize,
workflow_context: &mut WorkflowContext,
) {
if let Some(ref resume_ctx) = self.resume_context {
if let Some(recovery_state_value) =
resume_ctx.variable_state.get("__error_recovery_state")
{
if let Ok(error_recovery_state) =
serde_json::from_value::<ErrorRecoveryState>(recovery_state_value.clone())
{
if !error_recovery_state.active_handlers.is_empty() {
tracing::info!(
"Restored {} error handlers for step {}",
error_recovery_state.active_handlers.len(),
step_index
);
for (key, value) in error_recovery_state.error_context {
workflow_context
.variables
.insert(format!("error.{}", key), value.to_string());
}
}
}
}
}
}
pub(super) async fn save_retry_checkpoint(
&self,
workflow: &NormalizedWorkflow,
current_step_index: usize,
retry_state: Option<RetryState>,
ctx: &WorkflowContext,
) {
if let Some(ref checkpoint_manager) = self.checkpoint_manager {
if let Some(ref workflow_id) = self.workflow_id {
let workflow_hash = format!("{:?}", workflow.steps.len());
let mut checkpoint_steps = self.checkpoint_completed_steps.clone();
if let Some(retry_state) = retry_state {
let step_name = if current_step_index < workflow.steps.len() {
match &workflow.steps[current_step_index].command {
normalized::StepCommand::Claude(cmd) => format!("claude: {}", cmd),
normalized::StepCommand::Shell(cmd) => format!("shell: {}", cmd),
normalized::StepCommand::Test { command, .. } => {
format!("test: {}", command)
}
normalized::StepCommand::Simple(cmd) => cmd.to_string(),
_ => "complex command".to_string(),
}
} else {
"unknown step".to_string()
};
let retry_step = CheckpointCompletedStep {
step_index: current_step_index,
command: step_name,
success: false,
output: None,
captured_variables: HashMap::new(),
duration: Duration::from_secs(0),
completed_at: chrono::Utc::now(),
retry_state: Some(retry_state),
};
checkpoint_steps.retain(|s| s.step_index != current_step_index);
checkpoint_steps.push(retry_step);
}
let mut checkpoint = create_checkpoint_with_total_steps(
workflow_id.clone(),
workflow,
ctx,
checkpoint_steps,
current_step_index,
workflow_hash,
workflow.steps.len(),
);
if let Some(ref path) = self.workflow_path {
checkpoint.workflow_path = Some(path.clone());
}
if let Ok(retry_checkpoint_state) =
self.retry_state_manager.create_checkpoint_state().await
{
checkpoint.retry_checkpoint_state = Some(retry_checkpoint_state);
}
if let Err(e) = checkpoint_manager.save_checkpoint(&checkpoint).await {
tracing::warn!("Failed to save retry checkpoint: {}", e);
} else {
tracing::debug!(
"Saved retry checkpoint at step {} attempt",
current_step_index
);
}
}
}
}
pub fn display_dry_run_info(&self, workflow: &ExtendedWorkflowConfig) {
if self.dry_run {
println!("[DRY RUN] Workflow execution simulation mode");
println!("[DRY RUN] No commands will be executed");
if workflow.max_iterations > 1 {
println!("[DRY RUN] Would run {} iterations", workflow.max_iterations);
}
}
}
pub fn display_dry_run_summary(&self) {
if !self.dry_run {
return;
}
println!("\n[DRY RUN] Summary:");
println!("==================");
println!(
"Main commands that would execute: {}",
self.dry_run_commands.len()
);
if !self.dry_run_commands.is_empty() {
for cmd in &self.dry_run_commands {
println!(" - {}", cmd);
}
}
if !self.dry_run_validations.is_empty() {
println!(
"\nValidation commands that would execute: {}",
self.dry_run_validations.len()
);
for val in &self.dry_run_validations {
println!(" - {}", val);
}
}
if !self.dry_run_potential_handlers.is_empty() {
println!("\nPotential failure handlers (if needed):");
for handler in &self.dry_run_potential_handlers {
println!(" - {}", handler);
}
}
if !self.assumed_commits.is_empty() {
println!("\nAssumed commits: {}", self.assumed_commits.len());
for commit in &self.assumed_commits {
println!(" - From: {}", commit);
}
}
println!("\nNo actual commands executed or files changed.");
println!("To run for real, remove the --dry-run flag.");
}
pub(super) fn build_step_error_message(step: &WorkflowStep, result: &StepResult) -> String {
super::pure::build_step_error_message(step, result)
}
pub fn generate_commit_message(
&self,
step: &WorkflowStep,
context: &WorkflowContext,
) -> String {
super::commit_handler::generate_commit_message(
step,
context,
&self.get_step_display_name(step),
)
}
}