use anyhow::{Context, Result};
use crate::error::{PhaseError, XCheckerError};
use crate::fixup::{FixupMode, FixupPhase};
use crate::phase::Phase;
use crate::phases::{DesignPhase, RequirementsPhase, ReviewPhase, TasksPhase};
use crate::types::{FileType, PhaseId, PipelineInfo};
use super::{OrchestratorConfig, PhaseOrchestrator};
#[allow(dead_code)] #[derive(Debug)]
pub(crate) struct WorkflowResult {
pub success: bool,
pub completed_phases: Vec<PhaseExecution>,
pub total_rewinds: usize,
pub final_error: Option<String>,
}
#[allow(dead_code)] #[derive(Debug)]
pub(crate) struct PhaseExecution {
pub phase: PhaseId,
pub success: bool,
pub rewind_triggered: bool,
pub rewind_target: Option<PhaseId>,
pub error: Option<String>,
}
#[allow(dead_code)] #[derive(Debug)]
pub(crate) struct PhaseExecutionResult {
pub success: bool,
pub rewind_triggered: bool,
pub rewind_target: Option<PhaseId>,
pub error: Option<String>,
}
impl PhaseOrchestrator {
#[allow(dead_code)] pub(crate) async fn execute_complete_workflow(
&self,
config: &OrchestratorConfig,
) -> Result<WorkflowResult> {
let mut rewind_count = 0;
const MAX_REWIND_COUNT: usize = 2;
let mut execution_history = Vec::new();
let standard_phases = [
PhaseId::Requirements,
PhaseId::Design,
PhaseId::Tasks,
PhaseId::Review,
PhaseId::Fixup,
PhaseId::Final,
];
let mut current_phase_index = 0;
while current_phase_index < standard_phases.len() {
let phase_id = standard_phases[current_phase_index];
if phase_id == PhaseId::Final {
break;
}
println!("Executing phase: {}", phase_id.as_str());
let result = match self
.execute_single_phase_with_rewind_support(phase_id, config)
.await
{
Ok(result) => result,
Err(e) => {
execution_history.push(PhaseExecution {
phase: phase_id,
success: false,
rewind_triggered: false,
rewind_target: None,
error: Some(e.to_string()),
});
return Ok(WorkflowResult {
success: false,
completed_phases: execution_history,
total_rewinds: rewind_count,
final_error: Some(e.to_string()),
});
}
};
execution_history.push(PhaseExecution {
phase: phase_id,
success: result.success,
rewind_triggered: result.rewind_triggered,
rewind_target: result.rewind_target,
error: result.error.clone(),
});
if !result.success {
return Ok(WorkflowResult {
success: false,
completed_phases: execution_history,
total_rewinds: rewind_count,
final_error: result.error,
});
}
if result.rewind_triggered {
if rewind_count >= MAX_REWIND_COUNT {
return Ok(WorkflowResult {
success: false,
completed_phases: execution_history,
total_rewinds: rewind_count,
final_error: Some(format!(
"Maximum rewind count ({MAX_REWIND_COUNT}) exceeded"
)),
});
}
rewind_count += 1;
if let Some(target_phase) = result.rewind_target {
if let Some(target_index) =
standard_phases.iter().position(|&p| p == target_phase)
{
current_phase_index = target_index;
println!(
"Rewinding to phase: {} (rewind #{}/{})",
target_phase.as_str(),
rewind_count,
MAX_REWIND_COUNT
);
continue;
}
return Ok(WorkflowResult {
success: false,
completed_phases: execution_history,
total_rewinds: rewind_count,
final_error: Some(format!(
"Invalid rewind target: {}",
target_phase.as_str()
)),
});
}
}
current_phase_index += 1;
}
Ok(WorkflowResult {
success: true,
completed_phases: execution_history,
total_rewinds: rewind_count,
final_error: None,
})
}
#[allow(dead_code)] async fn execute_single_phase_with_rewind_support(
&self,
phase_id: PhaseId,
config: &OrchestratorConfig,
) -> Result<PhaseExecutionResult> {
if !self.can_resume_from_phase(phase_id)? {
return Err(XCheckerError::Phase(PhaseError::DependencyNotSatisfied {
phase: phase_id.as_str().to_string(),
dependency: "required previous phases".to_string(),
})
.into());
}
let execution_result = match phase_id {
PhaseId::Requirements => {
let phase = RequirementsPhase::new();
self.execute_phase_with_next_step_handling(&phase, config)
.await?
}
PhaseId::Design => {
let phase = DesignPhase::new();
self.execute_phase_with_next_step_handling(&phase, config)
.await?
}
PhaseId::Tasks => {
let phase = TasksPhase::new();
self.execute_phase_with_next_step_handling(&phase, config)
.await?
}
PhaseId::Review => {
let phase = ReviewPhase::new();
self.execute_phase_with_next_step_handling(&phase, config)
.await?
}
PhaseId::Fixup => {
let apply_fixups = config
.config
.get("apply_fixups")
.is_some_and(|s| s == "true");
let fixup_mode = if apply_fixups {
FixupMode::Apply
} else {
FixupMode::Preview
};
let phase = FixupPhase::new_with_mode(fixup_mode);
self.execute_phase_with_next_step_handling(&phase, config)
.await?
}
PhaseId::Final => {
return Err(XCheckerError::Phase(PhaseError::InvalidTransition {
from: "current state".to_string(),
to: "Final phase not yet implemented".to_string(),
})
.into());
}
};
Ok(execution_result)
}
#[allow(dead_code)] async fn execute_phase_with_next_step_handling<P: Phase>(
&self,
phase: &P,
config: &OrchestratorConfig,
) -> Result<PhaseExecutionResult> {
let phase_id = phase.id();
let core = match self.execute_phase_core(phase, config).await {
Ok(core_output) => core_output,
Err(e) => {
if let Some(xchecker_err) = e.downcast_ref::<XCheckerError>()
&& let XCheckerError::Phase(PhaseError::ExecutionFailed { phase: _, code }) =
xchecker_err
&& *code == crate::exit_codes::codes::SECRET_DETECTED
{
return Ok(PhaseExecutionResult {
success: false,
rewind_triggered: false,
rewind_target: None,
error: Some("Secret detected in packet".to_string()),
});
}
return Err(e);
}
};
if core.claude_exit_code != 0 {
return Ok(PhaseExecutionResult {
success: false,
rewind_triggered: false,
rewind_target: None,
error: Some(format!(
"Claude CLI failed with exit code: {}",
core.claude_exit_code
)),
});
}
let mut output_hashes = Vec::new();
for artifact in &core.phase_result.artifacts {
let _artifact_result = self
.artifact_manager()
.store_artifact(artifact)
.with_context(|| format!("Failed to store artifact: {}", artifact.name))?;
let file_type = if let Some(ext) = std::path::Path::new(&artifact.name).extension() {
FileType::from_extension(ext.to_str().unwrap_or(""))
} else {
match artifact.artifact_type {
crate::status::artifact::ArtifactType::Markdown => FileType::Markdown,
crate::status::artifact::ArtifactType::CoreYaml => FileType::Yaml,
_ => FileType::Text,
}
};
let file_hash = self
.receipt_manager()
.create_file_hash(
&format!("artifacts/{}", artifact.name),
&artifact.content,
file_type,
phase_id.as_str(),
)
.map_err(|e| {
XCheckerError::Phase(PhaseError::OutputValidationFailed {
phase: phase_id.as_str().to_string(),
reason: e.to_string(),
})
})?;
output_hashes.push(file_hash);
}
let mut flags = std::collections::HashMap::new();
flags.insert("phase".to_string(), phase_id.as_str().to_string());
let (rewind_triggered, rewind_target) = match &core.phase_result.next_step {
xchecker_phase_api::NextStep::Rewind { to } => {
flags.insert("rewind_triggered".to_string(), "true".to_string());
flags.insert("rewind_target".to_string(), to.as_str().to_string());
(true, Some(*to))
}
xchecker_phase_api::NextStep::Continue => (false, None),
xchecker_phase_api::NextStep::Complete => (false, None),
};
let (model_alias, model_full_name) = if let Some(metadata) = &core.claude_metadata {
(
metadata.model_alias.clone(),
metadata.model_full_name.clone(),
)
} else {
(None, "haiku".to_string())
};
let mut warnings = Vec::new();
if let Some(warning) = &core.llm_fallback_warning {
warnings.push(warning.clone());
}
let mut receipt = self.receipt_manager().create_receipt_with_redactor(
config.redactor.as_ref(),
self.spec_id(),
phase_id,
core.claude_exit_code,
output_hashes,
env!("CARGO_PKG_VERSION"),
core.claude_metadata
.as_ref()
.map_or("0.8.1", |m| m.claude_cli_version.as_str()),
&model_full_name,
model_alias,
flags,
core.packet_evidence.clone(),
None, None, warnings,
core.claude_metadata.as_ref().map(|m| m.fallback_used),
core.claude_metadata
.as_ref()
.map_or("native", |m| m.runner.as_str()),
core.claude_metadata
.as_ref()
.and_then(|m| m.runner_distro.clone()),
None, None, None, Some(PipelineInfo {
execution_strategy: Some("controlled".to_string()),
}),
);
receipt.llm = core.llm_result.map(|r| r.into_llm_info());
let _receipt_path = self
.receipt_manager()
.write_receipt(&receipt)
.with_context(|| format!("Failed to write receipt for phase: {}", phase_id.as_str()))?;
Ok(PhaseExecutionResult {
success: true,
rewind_triggered,
rewind_target,
error: None,
})
}
}