use super::super::step_validation::StepValidationSpec;
use super::super::validation::{ValidationConfig, ValidationResult};
use super::{pure, StepResult, WorkflowContext, WorkflowExecutor, WorkflowStep};
use crate::cook::execution::ExecutionContext;
use crate::cook::expression::{ExpressionEvaluator, VariableContext};
use crate::cook::orchestrator::ExecutionEnvironment;
use anyhow::{anyhow, Context, Result};
use std::collections::HashMap;
use std::sync::Arc;
pub(super) fn should_continue_retry(attempts: u32, max_attempts: u32, is_complete: bool) -> bool {
attempts < max_attempts && !is_complete
}
#[derive(Debug, Clone, PartialEq)]
pub(super) enum HandlerType {
MultiCommand,
SingleCommand,
NoHandler,
}
pub(super) fn determine_handler_type(
on_incomplete: &crate::cook::workflow::validation::OnIncompleteConfig,
) -> HandlerType {
if on_incomplete.commands.is_some() {
HandlerType::MultiCommand
} else if on_incomplete.claude.is_some() || on_incomplete.shell.is_some() {
HandlerType::SingleCommand
} else {
HandlerType::NoHandler
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub(super) struct RetryProgress {
pub(super) attempts: u32,
pub(super) max_attempts: u32,
pub(super) completion_percentage: f64,
}
#[allow(dead_code)]
pub(super) fn calculate_retry_progress(
attempts: u32,
max_attempts: u32,
completion: f64,
) -> RetryProgress {
RetryProgress {
attempts,
max_attempts,
completion_percentage: completion,
}
}
pub(super) fn should_fail_workflow(
is_complete: bool,
fail_workflow_flag: bool,
_attempts: u32,
) -> bool {
!is_complete && fail_workflow_flag
}
#[derive(Debug, Clone, PartialEq)]
pub(super) enum ValidationExecutionMode {
CommandsArray,
Claude,
Shell,
NoCommand,
}
pub(super) fn determine_validation_execution_mode(
config: &super::super::validation::ValidationConfig,
) -> ValidationExecutionMode {
if config.commands.is_some() {
ValidationExecutionMode::CommandsArray
} else if config.claude.is_some() {
ValidationExecutionMode::Claude
} else if config.shell.is_some() || config.command.is_some() {
ValidationExecutionMode::Shell
} else {
ValidationExecutionMode::NoCommand
}
}
pub(super) fn should_read_result_file_after_commands(
config: &super::super::validation::ValidationConfig,
) -> bool {
config.commands.is_some() && config.result_file.is_some()
}
pub(super) fn should_use_result_file(config: &super::super::validation::ValidationConfig) -> bool {
config.commands.is_none() && config.result_file.is_some()
}
pub(super) fn parse_validation_result_with_fallback(
json_content: &str,
command_success: bool,
) -> super::super::validation::ValidationResult {
use super::super::validation::ValidationResult;
match ValidationResult::from_json(json_content) {
Ok(validation) => validation,
Err(_) => {
if command_success {
ValidationResult::complete()
} else {
ValidationResult::failed("Validation failed (non-JSON output)".to_string())
}
}
}
}
pub(super) fn create_command_step_failure_result(
step_idx: usize,
stdout: &str,
) -> super::super::validation::ValidationResult {
super::super::validation::ValidationResult::failed(format!(
"Validation step {} failed: {}",
step_idx + 1,
stdout
))
}
pub(super) fn create_file_read_error_result(
file_path: &str,
error: &str,
) -> super::super::validation::ValidationResult {
super::super::validation::ValidationResult::failed(format!(
"Failed to read validation result from {}: {}",
file_path, error
))
}
pub(super) fn create_command_execution_failure_result(
exit_code: i32,
) -> super::super::validation::ValidationResult {
super::super::validation::ValidationResult::failed(format!(
"Validation command failed with exit code: {}",
exit_code
))
}
pub(super) fn parse_result_file_content(
content: &str,
) -> super::super::validation::ValidationResult {
use super::super::validation::ValidationResult;
match ValidationResult::from_json(content) {
Ok(validation) => validation,
Err(_) => ValidationResult::complete(),
}
}
pub(super) fn format_validation_passed_message(results_count: usize, attempts: u32) -> String {
format!(
"Step validation passed ({} validation{}, {} attempt{})",
results_count,
if results_count == 1 { "" } else { "s" },
attempts,
if attempts == 1 { "" } else { "s" }
)
}
pub(super) fn format_validation_failed_message(results_count: usize, attempts: u32) -> String {
format!(
"Step validation failed ({} validation{}, {} attempt{})",
results_count,
if results_count == 1 { "" } else { "s" },
attempts,
if attempts == 1 { "" } else { "s" }
)
}
pub(super) fn format_failed_validation_detail(idx: usize, message: &str, exit_code: i32) -> String {
format!(
" Validation {}: {} (exit code: {})",
idx + 1,
message,
exit_code
)
}
pub(super) fn determine_step_name(step: &WorkflowStep) -> &str {
step.name.as_deref().unwrap_or_else(|| {
if step.claude.is_some() {
"claude command"
} else if step.shell.is_some() {
"shell command"
} else {
"workflow step"
}
})
}
pub(super) fn create_validation_execution_context(
working_directory: std::path::PathBuf,
timeout_seconds: Option<u64>,
) -> ExecutionContext {
ExecutionContext {
working_directory,
env_vars: std::collections::HashMap::new(),
capture_output: true,
timeout_seconds,
stdin: None,
capture_streaming: false,
streaming_config: None,
}
}
pub(super) fn create_validation_timeout_result(
timeout_secs: u64,
) -> super::super::step_validation::StepValidationResult {
super::super::step_validation::StepValidationResult {
passed: false,
results: vec![],
duration: std::time::Duration::from_secs(timeout_secs),
attempts: 1,
}
}
impl WorkflowExecutor {
pub(super) async fn handle_validation(
&mut self,
validation_config: &ValidationConfig,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<()> {
if self.dry_run {
if let Some(claude_cmd) = &validation_config.claude {
let validation_desc = format!("validation: claude {}", claude_cmd);
println!("[DRY RUN] Would run validation (Claude): {}", claude_cmd);
self.dry_run_validations.push(validation_desc);
} else if let Some(shell_cmd) = validation_config
.shell
.as_ref()
.or(validation_config.command.as_ref())
{
let validation_desc = format!("validation: shell {}", shell_cmd);
println!("[DRY RUN] Would run validation (shell): {}", shell_cmd);
self.dry_run_validations.push(validation_desc);
}
if let Some(on_incomplete) = &validation_config.on_incomplete {
let handler_desc = if let Some(commands) = &on_incomplete.commands {
format!("on_incomplete: {} commands", commands.len())
} else if let Some(claude) = &on_incomplete.claude {
format!("on_incomplete: claude {}", claude)
} else if let Some(shell) = &on_incomplete.shell {
format!("on_incomplete: shell {}", shell)
} else {
"on_incomplete: unknown".to_string()
};
self.dry_run_potential_handlers.push(format!(
"{} (max {} attempts)",
handler_desc, on_incomplete.max_attempts
));
}
println!(
"[DRY RUN] Validation threshold: {:.1}%",
validation_config.threshold
);
println!("[DRY RUN] Assuming validation would pass");
return Ok(());
}
let validation_result = self.execute_validation(validation_config, env, ctx).await?;
ctx.validation_results
.insert("validation".to_string(), validation_result.clone());
let percentage = validation_result.completion_percentage;
let threshold = validation_config.threshold;
if validation_config.is_complete(&validation_result) {
self.user_interaction.display_success(&format!(
"Validation passed: {:.1}% complete (threshold: {:.1}%)",
percentage, threshold
));
} else {
self.user_interaction.display_warning(&format!(
"Validation incomplete: {:.1}% complete (threshold: {:.1}%)",
percentage, threshold
));
if let Some(on_incomplete) = &validation_config.on_incomplete {
self.handle_incomplete_validation(
validation_config,
on_incomplete,
validation_result,
env,
ctx,
)
.await?;
}
}
Ok(())
}
async fn handle_incomplete_validation(
&mut self,
validation_config: &ValidationConfig,
on_incomplete: &crate::cook::workflow::validation::OnIncompleteConfig,
initial_result: ValidationResult,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<()> {
let mut attempts = 0;
let mut current_result = initial_result;
while should_continue_retry(
attempts,
on_incomplete.max_attempts,
validation_config.is_complete(¤t_result),
) {
attempts += 1;
self.user_interaction.display_info(&format!(
"Attempting to complete implementation (attempt {}/{})",
attempts, on_incomplete.max_attempts
));
let handler_success = match determine_handler_type(on_incomplete) {
HandlerType::MultiCommand => {
let commands = on_incomplete.commands.as_ref().unwrap();
self.user_interaction
.display_progress(&format!("Running {} recovery commands", commands.len()));
let mut all_success = true;
for (idx, cmd) in commands.iter().enumerate() {
let step = self.convert_workflow_command_to_step(cmd, ctx)?;
let step_display = self.get_interpolated_step_display_name(&step, ctx);
self.user_interaction.display_progress(&format!(
" Recovery step {}/{}: {}",
idx + 1,
commands.len(),
step_display
));
let handler_result = Box::pin(self.execute_step(&step, env, ctx)).await?;
if !handler_result.success {
self.user_interaction
.display_error(&format!("Recovery step {} failed", idx + 1));
all_success = false;
break;
}
}
all_success
}
HandlerType::SingleCommand => {
let handler_step = self.create_validation_handler(on_incomplete, ctx).unwrap();
let step_display = self.get_interpolated_step_display_name(&handler_step, ctx);
self.user_interaction
.display_progress(&format!("Running recovery step: {}", step_display));
let handler_result =
Box::pin(self.execute_step(&handler_step, env, ctx)).await?;
handler_result.success
}
HandlerType::NoHandler => {
self.user_interaction
.display_error("No recovery commands configured");
false
}
};
if !handler_success {
break;
}
current_result = self.execute_validation(validation_config, env, ctx).await?;
let percentage = current_result.completion_percentage;
let threshold = validation_config.threshold;
if validation_config.is_complete(¤t_result) {
self.user_interaction.display_success(&format!(
"Validation passed: {:.1}% complete (threshold: {:.1}%)",
percentage, threshold
));
} else {
self.user_interaction.display_info(&format!(
"Validation still incomplete: {:.1}% complete (threshold: {:.1}%)",
percentage, threshold
));
}
ctx.validation_results
.insert("validation".to_string(), current_result.clone());
}
if !validation_config.is_complete(¤t_result) {
if let Some(on_incomplete_cfg) = &validation_config.on_incomplete {
if let Some(ref prompt) = on_incomplete_cfg.prompt {
let _should_continue =
self.user_interaction.prompt_confirmation(prompt).await?;
}
}
}
if should_fail_workflow(
validation_config.is_complete(¤t_result),
on_incomplete.fail_workflow,
attempts,
) {
return Err(anyhow!(
"Validation failed after {} attempts. Completion: {:.1}%",
attempts,
current_result.completion_percentage
));
}
Ok(())
}
pub(super) async fn handle_step_validation(
&mut self,
validation_spec: &StepValidationSpec,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
step: &WorkflowStep,
) -> Result<super::super::step_validation::StepValidationResult> {
if self.dry_run {
match validation_spec {
StepValidationSpec::Single(cmd) => {
println!("[DRY RUN] Would run step validation: {}", cmd);
}
StepValidationSpec::Multiple(cmds) => {
println!("[DRY RUN] Would run step validation commands:");
for cmd in cmds {
println!("[DRY RUN] - {}", cmd);
}
}
StepValidationSpec::Detailed(config) => {
println!("[DRY RUN] Would run detailed step validation commands:");
for cmd in &config.commands {
println!("[DRY RUN] - {}", cmd.command);
}
}
}
println!("[DRY RUN] Assuming step validation would pass");
return Ok(super::super::step_validation::StepValidationResult {
passed: true,
results: vec![],
duration: std::time::Duration::from_secs(0),
attempts: 0,
});
}
let validation_executor = super::super::step_validation::StepValidationExecutor::new(
Arc::new(super::StepValidationCommandExecutor {
workflow_executor: self as *mut WorkflowExecutor,
env: env.clone(),
ctx: ctx.clone(),
}) as Arc<dyn crate::cook::execution::CommandExecutor>,
);
let exec_context = create_validation_execution_context(
env.working_dir.to_path_buf(),
step.validation_timeout,
);
let step_name = determine_step_name(step);
let validation_future =
validation_executor.validate_step(validation_spec, &exec_context, step_name);
let validation_result = if let Some(timeout_secs) = step.validation_timeout {
let timeout = tokio::time::Duration::from_secs(timeout_secs);
match tokio::time::timeout(timeout, validation_future).await {
Ok(result) => result?,
Err(_) => {
self.user_interaction.display_error(&format!(
"Step validation timed out after {} seconds",
timeout_secs
));
create_validation_timeout_result(timeout_secs)
}
}
} else {
validation_future.await?
};
if validation_result.passed {
let message = format_validation_passed_message(
validation_result.results.len(),
validation_result.attempts,
);
self.user_interaction.display_success(&message);
} else {
let message = format_validation_failed_message(
validation_result.results.len(),
validation_result.attempts,
);
self.user_interaction.display_warning(&message);
for (idx, result) in validation_result.results.iter().enumerate() {
if !result.passed {
let detail =
format_failed_validation_detail(idx, &result.message, result.exit_code);
self.user_interaction.display_info(&detail);
}
}
}
Ok(validation_result)
}
pub(super) async fn execute_step_validation(
&mut self,
step: &WorkflowStep,
result: &mut StepResult,
actual_env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<()> {
if !result.success || self.dry_run {
return Ok(());
}
if let Some(validation_config) = &step.validate {
self.handle_validation(validation_config, actual_env, ctx)
.await?;
}
if let Some(step_validation) = &step.step_validate {
if !step.skip_validation {
let validation_result = self
.handle_step_validation(step_validation, actual_env, ctx, step)
.await?;
if !validation_result.passed && !step.ignore_validation_failure {
result.success = false;
result.stdout.push_str(&format!(
"\n[Validation Failed: {} validation(s) executed, {} attempt(s) made]",
validation_result.results.len(),
validation_result.attempts
));
if result.exit_code == Some(0) {
result.exit_code = Some(1); }
}
}
}
Ok(())
}
pub(super) async fn execute_validation(
&mut self,
validation_config: &ValidationConfig,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<ValidationResult> {
use crate::cook::workflow::validation::ValidationResult;
match determine_validation_execution_mode(validation_config) {
ValidationExecutionMode::CommandsArray => {
self.execute_validation_commands_array(validation_config, env, ctx)
.await
}
ValidationExecutionMode::Claude | ValidationExecutionMode::Shell => {
self.execute_validation_single_command(validation_config, env, ctx)
.await
}
ValidationExecutionMode::NoCommand => Ok(ValidationResult::failed(
"No validation command specified".to_string(),
)),
}
}
async fn execute_validation_commands_array(
&mut self,
validation_config: &ValidationConfig,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<ValidationResult> {
use crate::cook::workflow::validation::ValidationResult;
let commands = validation_config.commands.as_ref().unwrap();
self.user_interaction.display_progress(&format!(
"Running validation with {} commands",
commands.len()
));
for (idx, cmd) in commands.iter().enumerate() {
self.user_interaction.display_progress(&format!(
" Validation step {}/{}",
idx + 1,
commands.len()
));
let step = self.convert_workflow_command_to_step(cmd, ctx)?;
let step_result = Box::pin(self.execute_step(&step, env, ctx)).await?;
if !step_result.success {
return Ok(create_command_step_failure_result(idx, &step_result.stdout));
}
}
if should_read_result_file_after_commands(validation_config) {
let result_file = validation_config.result_file.as_ref().unwrap();
let (interpolated_file, _) = ctx.interpolate_with_tracking(result_file);
let file_path = env.working_dir.join(&interpolated_file);
match tokio::fs::read_to_string(&file_path).await {
Ok(content) => return Ok(parse_result_file_content(&content)),
Err(e) => {
return Ok(create_file_read_error_result(
&interpolated_file,
&e.to_string(),
));
}
}
}
Ok(ValidationResult::complete())
}
async fn execute_validation_single_command(
&mut self,
validation_config: &ValidationConfig,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<ValidationResult> {
use crate::cook::workflow::validation::ValidationResult;
let result = if let Some(claude_cmd) = &validation_config.claude {
let (command, resolutions) = ctx.interpolate_with_tracking(claude_cmd);
self.log_variable_resolutions(&resolutions);
self.user_interaction
.display_progress(&format!("Running validation (Claude): {}", command));
let dummy_step = WorkflowStep::default();
let env_vars = self.prepare_env_vars(&dummy_step, env, ctx);
self.execute_claude_command(&command, env, env_vars).await?
} else if let Some(shell_cmd) = validation_config
.shell
.as_ref()
.or(validation_config.command.as_ref())
{
let (command, resolutions) = ctx.interpolate_with_tracking(shell_cmd);
self.log_variable_resolutions(&resolutions);
self.user_interaction
.display_progress(&format!("Running validation (shell): {}", command));
let mut env_vars = HashMap::new();
env_vars.insert("PRODIGY_VALIDATION".to_string(), "true".to_string());
self.execute_shell_command(&command, env, env_vars, validation_config.timeout)
.await?
} else {
return Ok(ValidationResult::failed(
"No validation command specified".to_string(),
));
};
if !result.success {
return Ok(create_command_execution_failure_result(
result.exit_code.unwrap_or(-1),
));
}
let json_content = if should_use_result_file(validation_config) {
let result_file = validation_config.result_file.as_ref().unwrap();
let (interpolated_file, _resolutions) = ctx.interpolate_with_tracking(result_file);
let file_path = env.working_dir.join(&interpolated_file);
match tokio::fs::read_to_string(&file_path).await {
Ok(content) => content,
Err(e) => {
return Ok(create_file_read_error_result(
&interpolated_file,
&e.to_string(),
));
}
}
} else {
result.stdout.clone()
};
let mut validation = parse_validation_result_with_fallback(&json_content, result.success);
validation.raw_output = Some(result.stdout);
Ok(validation)
}
pub(super) async fn handle_conditional_execution(
&mut self,
step: &WorkflowStep,
mut result: StepResult,
env: &ExecutionEnvironment,
ctx: &mut WorkflowContext,
) -> Result<StepResult> {
if !result.success {
if let Some(on_failure_config) = &step.on_failure {
result = self
.handle_on_failure(step, result, on_failure_config, env, ctx)
.await?;
}
} else if let Some(on_success) = &step.on_success {
self.user_interaction
.display_info("Executing on_success step...");
let success_result = Box::pin(self.execute_step(on_success, env, ctx)).await?;
result.stdout.push_str("\n--- on_success output ---\n");
result.stdout.push_str(&success_result.stdout);
}
if let Some(exit_code) = result.exit_code {
if let Some(exit_step) = step.on_exit_code.get(&exit_code) {
self.user_interaction
.display_info(&format!("Executing on_exit_code[{exit_code}] step..."));
let exit_result = Box::pin(self.execute_step(exit_step, env, ctx)).await?;
result
.stdout
.push_str(&format!("\n--- on_exit_code[{exit_code}] output ---\n"));
result.stdout.push_str(&exit_result.stdout);
}
}
Ok(result)
}
pub(crate) fn evaluate_when_condition(
&self,
when_expr: &str,
context: &WorkflowContext,
) -> Result<bool> {
let evaluator = ExpressionEvaluator::new();
let mut variable_context = VariableContext::new();
for (key, value) in &context.variables {
variable_context.set_string(key.clone(), value.clone());
}
for (key, value) in &context.captured_outputs {
variable_context.set_string(key.clone(), value.clone());
}
evaluator
.evaluate(when_expr, &variable_context)
.with_context(|| format!("Failed to evaluate when condition: {}", when_expr))
}
pub(super) fn should_fail_workflow_for_step(
step_result: &StepResult,
step: &WorkflowStep,
) -> bool {
pure::should_fail_workflow_for_step(step_result, step)
}
pub(super) async fn should_continue_iterations(
&self,
_env: &ExecutionEnvironment,
) -> Result<bool> {
Ok(true)
}
pub(crate) fn is_focus_tracking_test(&self) -> bool {
self.test_config.as_ref().is_some_and(|c| c.track_focus)
}
pub fn should_stop_early_in_test_mode(&self) -> bool {
self.test_config.as_ref().is_some_and(|c| {
c.no_changes_commands
.iter()
.any(|cmd| cmd.trim() == "prodigy-code-review" || cmd.trim() == "prodigy-lint")
})
}
pub fn is_test_mode_no_changes_command(&self, command: &str) -> bool {
if let Some(config) = &self.test_config {
let command_name = command.trim_start_matches('/');
let command_name = command_name
.split_whitespace()
.next()
.unwrap_or(command_name);
return config
.no_changes_commands
.iter()
.any(|cmd| cmd.trim() == command_name);
}
false
}
}