use std::path::PathBuf;
use anyhow::Result;
use crate::config::{CliArgs, Config};
use crate::error::{ConfigError, XCheckerError};
use crate::receipt::ReceiptManager;
use crate::spec_id::sanitize_spec_id;
use crate::status::artifact::ArtifactManager;
use crate::types::{PhaseId, StatusOutput};
use super::{ExecutionResult, OrchestratorConfig, PhaseOrchestrator};
pub struct OrchestratorHandle {
orchestrator: PhaseOrchestrator,
config: OrchestratorConfig,
spec_id: String,
}
impl OrchestratorHandle {
pub fn new(spec_id: &str) -> Result<Self, XCheckerError> {
let config = Config::discover(&CliArgs::default())?;
Self::from_config_internal(spec_id, config, false)
}
pub fn from_config(spec_id: &str, config: Config) -> Result<Self, XCheckerError> {
Self::from_config_internal(spec_id, config, false)
}
fn from_config_internal(
spec_id: &str,
config: Config,
force: bool,
) -> Result<Self, XCheckerError> {
let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
XCheckerError::Config(ConfigError::InvalidValue {
key: "spec_id".to_string(),
value: e.to_string(),
})
})?;
let redactor = crate::redaction::SecretRedactor::from_config(&config).map_err(
|e: anyhow::Error| {
XCheckerError::Config(ConfigError::InvalidValue {
key: "security".to_string(),
value: e.to_string(),
})
},
)?;
let orchestrator = if force {
PhaseOrchestrator::new_with_force(&sanitized_id, true)
} else {
PhaseOrchestrator::new(&sanitized_id)
}
.map_err(|e| {
XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
reason: e.to_string(),
})
})?;
let mut orch_config = OrchestratorConfig {
redactor: std::sync::Arc::new(redactor),
full_config: Some(config.clone()),
hooks: Some(config.hooks.clone()),
..Default::default()
};
if let Some(packet_max_bytes) = config.defaults.packet_max_bytes {
orch_config
.config
.insert("packet_max_bytes".to_string(), packet_max_bytes.to_string());
}
if let Some(packet_max_lines) = config.defaults.packet_max_lines {
orch_config
.config
.insert("packet_max_lines".to_string(), packet_max_lines.to_string());
}
if let Some(max_turns) = config.defaults.max_turns {
orch_config
.config
.insert("max_turns".to_string(), max_turns.to_string());
}
if let Some(model) = &config.defaults.model {
orch_config
.config
.insert("model".to_string(), model.clone());
}
if let Some(output_format) = &config.defaults.output_format {
orch_config
.config
.insert("output_format".to_string(), output_format.clone());
}
if let Some(timeout) = config.defaults.phase_timeout {
orch_config
.config
.insert("phase_timeout".to_string(), timeout.to_string());
}
if let Some(stdout_cap_bytes) = config.defaults.stdout_cap_bytes {
orch_config
.config
.insert("stdout_cap_bytes".to_string(), stdout_cap_bytes.to_string());
}
if let Some(stderr_cap_bytes) = config.defaults.stderr_cap_bytes {
orch_config
.config
.insert("stderr_cap_bytes".to_string(), stderr_cap_bytes.to_string());
}
if let Some(lock_ttl_seconds) = config.defaults.lock_ttl_seconds {
orch_config
.config
.insert("lock_ttl_seconds".to_string(), lock_ttl_seconds.to_string());
}
if let Some(debug_packet) = config.defaults.debug_packet
&& debug_packet
{
orch_config
.config
.insert("debug_packet".to_string(), "true".to_string());
}
if let Some(allow_links) = config.defaults.allow_links
&& allow_links
{
orch_config
.config
.insert("allow_links".to_string(), "true".to_string());
}
if let Some(runner_mode) = &config.runner.mode {
orch_config
.config
.insert("runner_mode".to_string(), runner_mode.clone());
}
if let Some(runner_distro) = &config.runner.distro {
orch_config
.config
.insert("runner_distro".to_string(), runner_distro.clone());
}
if let Some(claude_path) = &config.runner.claude_path {
orch_config
.config
.insert("claude_path".to_string(), claude_path.clone());
}
if let Some(provider) = &config.llm.provider {
orch_config
.config
.insert("llm_provider".to_string(), provider.clone());
}
if let Some(fallback_provider) = &config.llm.fallback_provider {
orch_config.config.insert(
"llm_fallback_provider".to_string(),
fallback_provider.clone(),
);
}
if let Some(execution_strategy) = &config.llm.execution_strategy {
orch_config
.config
.insert("execution_strategy".to_string(), execution_strategy.clone());
}
if let Some(prompt_template) = &config.llm.prompt_template {
orch_config
.config
.insert("prompt_template".to_string(), prompt_template.clone());
}
if let Some(claude_config) = &config.llm.claude
&& let Some(binary) = &claude_config.binary
{
orch_config
.config
.insert("llm_claude_binary".to_string(), binary.clone());
}
if let Some(gemini_config) = &config.llm.gemini {
if let Some(binary) = &gemini_config.binary {
orch_config
.config
.insert("llm_gemini_binary".to_string(), binary.clone());
}
if let Some(default_model) = &gemini_config.default_model {
orch_config.config.insert(
"llm_gemini_default_model".to_string(),
default_model.clone(),
);
}
}
orch_config.strict_validation = config.strict_validation();
orch_config.selectors = Some(config.selectors.clone());
Ok(Self {
orchestrator,
config: orch_config,
spec_id: sanitized_id,
})
}
pub fn with_force(spec_id: &str, force: bool) -> Result<Self, XCheckerError> {
let config = Config::discover(&CliArgs::default())?;
Self::from_config_internal(spec_id, config, force)
}
pub fn with_config_and_force(
spec_id: &str,
config: OrchestratorConfig,
force: bool,
) -> Result<Self, XCheckerError> {
let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
XCheckerError::Config(ConfigError::InvalidValue {
key: "spec_id".to_string(),
value: e.to_string(),
})
})?;
let orchestrator = if force {
PhaseOrchestrator::new_with_force(&sanitized_id, true)
} else {
PhaseOrchestrator::new(&sanitized_id)
}
.map_err(|e| {
XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
reason: e.to_string(),
})
})?;
Ok(Self {
orchestrator,
config,
spec_id: sanitized_id,
})
}
pub fn readonly(spec_id: &str) -> Result<Self, XCheckerError> {
let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
XCheckerError::Config(ConfigError::InvalidValue {
key: "spec_id".to_string(),
value: e.to_string(),
})
})?;
let orchestrator = PhaseOrchestrator::new_readonly(&sanitized_id).map_err(|e| {
XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
reason: e.to_string(),
})
})?;
let config = OrchestratorConfig::default();
Ok(Self {
orchestrator,
config,
spec_id: sanitized_id,
})
}
pub async fn run_phase(&mut self, phase: PhaseId) -> Result<ExecutionResult> {
self.orchestrator
.resume_from_phase(phase, &self.config)
.await
}
pub async fn run_all(&mut self) -> Result<ExecutionResult> {
let phases = [PhaseId::Requirements, PhaseId::Design, PhaseId::Tasks];
let mut last_result = None;
for phase in phases {
let result = self
.orchestrator
.resume_from_phase(phase, &self.config)
.await?;
if !result.success {
return Ok(result);
}
last_result = Some(result);
}
last_result.ok_or_else(|| anyhow::anyhow!("No phases executed"))
}
pub fn status(&self) -> Result<StatusOutput, XCheckerError> {
use std::collections::BTreeMap;
let mut effective_config: BTreeMap<String, (String, String)> = self
.config
.full_config
.as_ref()
.map(|config| config.effective_config().into_iter().collect())
.unwrap_or_default();
for (key, value) in &self.config.config {
let override_needed = match effective_config.get(key) {
Some((existing_value, _)) => existing_value != value,
None => true,
};
if override_needed {
effective_config.insert(key.clone(), (value.clone(), "programmatic".to_string()));
}
}
crate::status::status::StatusManager::generate_status_internal(
self.orchestrator.artifact_manager(),
self.orchestrator.receipt_manager(),
effective_config,
None,
None,
Some(&self.config.redactor),
)
.map_err(|e| {
XCheckerError::Config(ConfigError::DiscoveryFailed {
reason: format!("Failed to generate status: {e}"),
})
})
}
#[must_use]
pub fn last_receipt_path(&self) -> Option<PathBuf> {
let phases = [
PhaseId::Final,
PhaseId::Fixup,
PhaseId::Review,
PhaseId::Tasks,
PhaseId::Design,
PhaseId::Requirements,
];
for phase in &phases {
if let Ok(Some(_receipt)) = self
.orchestrator
.receipt_manager()
.read_latest_receipt(*phase)
{
let base_path = self.orchestrator.artifact_manager().base_path();
let receipts_dir = base_path.join("receipts");
if let Ok(entries) = std::fs::read_dir(&receipts_dir) {
let phase_prefix = format!("{}-", phase.as_str());
let mut receipt_files: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(&phase_prefix))
.collect();
receipt_files.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
if let Some(entry) = receipt_files.first() {
return Some(entry.path());
}
}
}
}
None
}
#[must_use]
pub fn spec_id(&self) -> &str {
&self.spec_id
}
pub fn can_run_phase(&self, phase: PhaseId) -> Result<bool> {
self.orchestrator.can_resume_from_phase_public(phase)
}
pub fn current_phase(&self) -> Result<Option<PhaseId>> {
self.orchestrator.get_current_phase_state()
}
pub fn legal_next_phases(&self) -> Result<Vec<PhaseId>> {
let current = self.current_phase()?;
Ok(match current {
None => vec![PhaseId::Requirements],
Some(PhaseId::Requirements) => vec![PhaseId::Requirements, PhaseId::Design],
Some(PhaseId::Design) => vec![PhaseId::Design, PhaseId::Tasks],
Some(PhaseId::Tasks) => vec![PhaseId::Tasks, PhaseId::Review, PhaseId::Final],
Some(PhaseId::Review) => vec![PhaseId::Review, PhaseId::Fixup, PhaseId::Final],
Some(PhaseId::Fixup) => vec![PhaseId::Fixup, PhaseId::Final],
Some(PhaseId::Final) => vec![PhaseId::Final],
})
}
pub fn set_config(&mut self, key: &str, value: &str) {
self.config
.config
.insert(key.to_string(), value.to_string());
}
#[must_use]
pub fn get_config(&self, key: &str) -> Option<&String> {
self.config.config.get(key)
}
pub fn set_dry_run(&mut self, dry_run: bool) {
self.config.dry_run = dry_run;
}
#[must_use]
pub fn orchestrator_config(&self) -> &OrchestratorConfig {
&self.config
}
#[must_use]
#[doc(hidden)]
pub fn artifact_manager(&self) -> &ArtifactManager {
self.orchestrator.artifact_manager()
}
#[must_use]
#[doc(hidden)]
pub fn receipt_manager(&self) -> &ReceiptManager {
self.orchestrator.receipt_manager()
}
#[must_use]
#[doc(hidden)]
pub fn as_orchestrator(&self) -> &PhaseOrchestrator {
&self.orchestrator
}
}
impl xchecker_gate::SpecDataProvider for &OrchestratorHandle {
fn base_path(&self) -> &std::path::Path {
self.orchestrator
.artifact_manager()
.base_path()
.as_std_path()
}
fn spec_id(&self) -> &str {
&self.spec_id
}
fn receipt_manager(&self) -> &xchecker_receipt::ReceiptManager {
self.orchestrator.receipt_manager()
}
fn phase_completed(&self, phase: xchecker_utils::types::PhaseId) -> bool {
self.orchestrator.artifact_manager().phase_completed(phase)
}
fn pending_fixups_result(&self) -> xchecker_gate::PendingFixupsResult {
use crate::fixup::pending_fixups_result_from_handle;
pending_fixups_result_from_handle(self)
}
}