use crate::abstractions::git::GitOperations;
use crate::config::WorkflowConfig;
use crate::core::orchestration::{plan_execution, ExecutionMode, ExecutionPlan};
use crate::testing::config::TestConfiguration;
use crate::worktree::WorktreeManager;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::path::PathBuf;
use std::sync::Arc;
use crate::cook::command::CookCommand;
use crate::cook::execution::{ClaudeExecutor, CommandExecutor};
use crate::cook::interaction::UserInteraction;
use crate::cook::session::SessionManager;
pub(crate) use super::workflow_classifier::WorkflowType;
#[derive(Debug, Clone)]
pub struct CookConfig {
pub command: CookCommand,
pub project_path: Arc<PathBuf>,
pub workflow: Arc<WorkflowConfig>,
pub mapreduce_config: Option<Arc<crate::config::MapReduceWorkflowConfig>>,
}
#[async_trait]
pub trait CookOrchestrator: Send + Sync {
async fn run(&self, config: CookConfig) -> Result<()>;
async fn check_prerequisites(&self) -> Result<()>;
async fn setup_environment(&self, config: &CookConfig) -> Result<ExecutionEnvironment>;
async fn execute_workflow(&self, env: &ExecutionEnvironment, config: &CookConfig)
-> Result<()>;
async fn cleanup(&self, env: &ExecutionEnvironment, config: &CookConfig) -> Result<()>;
}
#[derive(Debug)]
pub struct ExecutionEnvironment {
pub working_dir: Arc<PathBuf>,
pub project_dir: Arc<PathBuf>,
pub worktree_name: Option<Arc<str>>,
pub session_id: Arc<str>,
}
impl Clone for ExecutionEnvironment {
fn clone(&self) -> Self {
Self {
working_dir: Arc::clone(&self.working_dir),
project_dir: Arc::clone(&self.project_dir),
worktree_name: self.worktree_name.as_ref().map(Arc::clone),
session_id: Arc::clone(&self.session_id),
}
}
}
pub struct DefaultCookOrchestrator {
session_manager: Arc<dyn SessionManager>,
#[allow(dead_code)]
command_executor: Arc<dyn CommandExecutor>,
claude_executor: Arc<dyn ClaudeExecutor>,
user_interaction: Arc<dyn UserInteraction>,
#[allow(dead_code)]
git_operations: Arc<dyn GitOperations>,
subprocess: crate::subprocess::SubprocessManager,
#[allow(dead_code)]
test_config: Option<Arc<TestConfiguration>>,
session_ops: super::session_ops::SessionOperations,
#[allow(dead_code)]
workflow_executor: super::workflow_execution::WorkflowExecutor,
argument_processor: super::argument_processing::ArgumentProcessor,
execution_pipeline: super::execution_pipeline::ExecutionPipeline,
}
impl DefaultCookOrchestrator {
#[allow(clippy::too_many_arguments)]
pub fn new(
session_manager: Arc<dyn SessionManager>,
command_executor: Arc<dyn CommandExecutor>,
claude_executor: Arc<dyn ClaudeExecutor>,
user_interaction: Arc<dyn UserInteraction>,
git_operations: Arc<dyn GitOperations>,
subprocess: crate::subprocess::SubprocessManager,
) -> Self {
Self::from_builder(
session_manager,
command_executor,
claude_executor,
user_interaction,
git_operations,
subprocess,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn from_builder(
session_manager: Arc<dyn SessionManager>,
command_executor: Arc<dyn CommandExecutor>,
claude_executor: Arc<dyn ClaudeExecutor>,
user_interaction: Arc<dyn UserInteraction>,
git_operations: Arc<dyn GitOperations>,
subprocess: crate::subprocess::SubprocessManager,
test_config: Option<Arc<TestConfiguration>>,
) -> Self {
let session_ops = super::session_ops::SessionOperations::new(
Arc::clone(&session_manager),
Arc::clone(&claude_executor),
Arc::clone(&user_interaction),
Arc::clone(&git_operations),
subprocess.clone(),
);
let workflow_executor = super::workflow_execution::WorkflowExecutor::new(
Arc::clone(&session_manager),
Arc::clone(&claude_executor),
Arc::clone(&user_interaction),
subprocess.clone(),
);
let argument_processor = super::argument_processing::ArgumentProcessor::new(
Arc::clone(&claude_executor),
Arc::clone(&session_manager),
Arc::clone(&user_interaction),
test_config.clone(),
);
let execution_pipeline = super::execution_pipeline::ExecutionPipeline::new(
Arc::clone(&session_manager),
Arc::clone(&user_interaction),
Arc::clone(&claude_executor),
Arc::clone(&git_operations),
subprocess.clone(),
session_ops.clone(),
workflow_executor.clone(),
);
Self {
session_manager,
command_executor,
claude_executor,
user_interaction,
git_operations,
subprocess,
test_config,
session_ops,
workflow_executor,
argument_processor,
execution_pipeline,
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_test_config(
session_manager: Arc<dyn SessionManager>,
command_executor: Arc<dyn CommandExecutor>,
claude_executor: Arc<dyn ClaudeExecutor>,
user_interaction: Arc<dyn UserInteraction>,
git_operations: Arc<dyn GitOperations>,
subprocess: crate::subprocess::SubprocessManager,
test_config: Arc<TestConfiguration>,
) -> Self {
Self::from_builder(
session_manager,
command_executor,
claude_executor,
user_interaction,
git_operations,
subprocess,
Some(test_config),
)
}
pub(super) fn create_workflow_executor_internal(
&self,
config: &CookConfig,
) -> crate::cook::workflow::WorkflowExecutorImpl {
super::construction::create_workflow_executor(
Arc::clone(&self.claude_executor),
Arc::clone(&self.session_manager),
Arc::clone(&self.user_interaction),
config.command.playbook.clone(),
)
}
pub(crate) fn classify_workflow_type(config: &CookConfig) -> WorkflowType {
super::workflow_classifier::classify_workflow_type(config)
}
async fn create_worktree(
&self,
config: &CookConfig,
session_id: &str,
) -> Result<(Arc<PathBuf>, Option<Arc<str>>)> {
let manager = WorktreeManager::with_config(
config.project_path.to_path_buf(),
self.subprocess.clone(),
config.command.verbosity,
super::construction::extract_merge_config(&config.workflow, &config.mapreduce_config),
super::construction::extract_workflow_env(&config.workflow),
)?;
let session = manager.create_session_with_id(session_id).await?;
self.user_interaction
.display_info(&format!("Created worktree at: {}", session.path.display()));
Ok((
Arc::new(session.path.clone()),
Some(Arc::from(session.name.as_ref())),
))
}
async fn cleanup_worktree(
&self,
env: &ExecutionEnvironment,
config: &CookConfig,
worktree: &str,
) -> Result<()> {
let test_mode = std::env::var("PRODIGY_TEST_MODE").unwrap_or_default() == "true";
let merge_config =
super::construction::extract_merge_config(&config.workflow, &config.mapreduce_config);
let workflow_env = super::construction::extract_workflow_env(&config.workflow);
let manager = WorktreeManager::with_config(
env.project_dir.to_path_buf(),
self.subprocess.clone(),
config.command.verbosity,
merge_config,
workflow_env,
)?;
let should_merge =
match super::construction::should_merge_worktree(test_mode, config.command.auto_accept)
{
Some(decision) => decision,
None => {
let target = manager
.get_merge_target(worktree)
.await
.unwrap_or_else(|_| "master".to_string());
self.user_interaction
.prompt_yes_no(&format!("Merge {} to {}", worktree, target))
.await?
}
};
if should_merge {
manager.merge_session(worktree).await?;
self.user_interaction
.display_success("Worktree changes merged successfully!");
}
Ok(())
}
async fn execute_by_mode(
&self,
env: &ExecutionEnvironment,
config: &CookConfig,
plan: &ExecutionPlan,
) -> Result<()> {
let is_mapreduce = config.mapreduce_config.is_some();
let effective_mode = if plan.mode == ExecutionMode::DryRun && is_mapreduce {
ExecutionMode::MapReduce
} else {
plan.mode
};
match effective_mode {
ExecutionMode::MapReduce => {
let mr_config = config.mapreduce_config.as_ref().ok_or_else(|| {
anyhow!("MapReduce workflow requires mapreduce configuration")
})?;
self.execution_pipeline
.execute_mapreduce_workflow_with_executor(
env,
config,
mr_config,
self.create_workflow_executor_internal(config)
.with_dry_run(config.command.dry_run),
)
.await
}
ExecutionMode::Iterative => {
self.user_interaction
.display_info("Processing workflow with arguments or file patterns");
self.argument_processor
.execute_workflow_with_args(env, config)
.await
}
ExecutionMode::DryRun | ExecutionMode::Standard => {
self.execute_standard_workflow(env, config).await
}
}
}
async fn execute_standard_workflow(
&self,
env: &ExecutionEnvironment,
config: &CookConfig,
) -> Result<()> {
if Self::classify_workflow_type(config) == WorkflowType::StructuredWithOutputs {
return self
.execution_pipeline
.execute_structured_workflow(env, config)
.await;
}
let extended = super::workflow_execution::build_standard_workflow_config(
&config.workflow.commands,
config.command.max_iterations,
);
let checkpoint_mgr = Arc::new(crate::cook::workflow::CheckpointManager::with_storage(
crate::cook::workflow::CheckpointStorage::Session {
session_id: env.session_id.to_string(),
},
));
let mut executor = self
.create_workflow_executor_internal(config)
.with_checkpoint_manager(
checkpoint_mgr,
format!("workflow-{}", chrono::Utc::now().timestamp_millis()),
)
.with_dry_run(config.command.dry_run);
if !config.command.args.is_empty() {
executor = executor.with_positional_args(config.command.args.clone());
}
if super::workflow_execution::has_env_config(&config.workflow) {
executor = executor.with_environment_config(super::construction::create_env_config(
&config.workflow,
))?;
}
executor.execute(&extended, env).await
}
}
#[async_trait]
impl CookOrchestrator for DefaultCookOrchestrator {
async fn run(&self, config: CookConfig) -> Result<()> {
if let Some(session_id) = config.command.resume.clone() {
return self
.execution_pipeline
.resume_workflow(&session_id, config)
.await;
}
let plan = plan_execution(&config);
log::debug!(
"Execution plan: mode={:?}, phases={}",
plan.mode,
plan.phase_count()
);
self.session_ops
.check_prerequisites_with_config(&config)
.await?;
let env = self.setup_environment(&config).await?;
self.execution_pipeline
.initialize_session_metadata(&env.session_id, &config)
.await?;
let interrupt_handler = self.execution_pipeline.setup_signal_handlers(
&config,
&env.session_id,
env.worktree_name.as_ref().map(Arc::clone),
)?;
let execution_result = self.execute_by_mode(&env, &config, &plan).await;
interrupt_handler.abort();
self.session_ops
.update_unified_session_status(&env.session_id, execution_result.is_ok())
.await;
self.execution_pipeline
.finalize_session(&env, &config, execution_result, self.cleanup(&env, &config))
.await
}
async fn check_prerequisites(&self) -> Result<()> {
self.session_ops.check_prerequisites().await
}
async fn setup_environment(&self, config: &CookConfig) -> Result<ExecutionEnvironment> {
let mut session_id = Arc::from(self.session_ops.generate_session_id().as_str());
if super::construction::should_create_unified_session(
config.mapreduce_config.is_some(),
config.command.dry_run,
) {
session_id = Arc::from(
self.session_ops
.create_unified_session(config)
.await?
.as_str(),
);
}
let (working_dir, worktree_name) = if !config.command.dry_run {
self.create_worktree(config, &session_id).await?
} else {
self.user_interaction
.display_info("[DRY RUN] Would create worktree for isolated execution");
(Arc::clone(&config.project_path), None)
};
Ok(ExecutionEnvironment {
working_dir,
project_dir: Arc::clone(&config.project_path),
worktree_name,
session_id,
})
}
async fn execute_workflow(
&self,
env: &ExecutionEnvironment,
config: &CookConfig,
) -> Result<()> {
let plan = plan_execution(config);
self.execute_by_mode(env, config, &plan).await
}
async fn cleanup(&self, env: &ExecutionEnvironment, config: &CookConfig) -> Result<()> {
let session_state_path = env.working_dir.join(".prodigy/session_state.json");
self.session_manager.save_state(&session_state_path).await?;
if let Some(ref worktree_name) = env.worktree_name {
self.cleanup_worktree(env, config, worktree_name).await?;
}
Ok(())
}
}