use std::path::PathBuf;
use clap::Subcommand;
use swarm_engine_core::config::PathResolver;
use swarm_engine_core::learn::{
ProfileState, ProfileStore, ScenarioProfile, ScenarioProfileId, ScenarioSource,
};
#[derive(Subcommand)]
pub enum ProfileAction {
Add {
scenario: PathBuf,
#[arg(long)]
id: Option<String>,
},
List {
#[arg(short, long)]
verbose: bool,
#[arg(long)]
state: Option<String>,
},
Show {
profile_id: String,
},
Bootstrap {
profile_id: String,
#[arg(short = 'n', long, default_value = "10")]
runs: usize,
#[arg(long, default_value = "with_graph")]
variant: String,
#[arg(short, long)]
verbose: bool,
},
Delete {
profile_id: String,
#[arg(short, long)]
force: bool,
},
SetState {
profile_id: String,
state: String,
},
Validate {
profile_id: String,
#[arg(short = 'n', long, default_value = "5")]
runs: usize,
#[arg(long, default_value = "no_regression")]
strategy: String,
#[arg(long)]
threshold: Option<f64>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
variant: Option<String>,
},
Retry {
profile_id: String,
},
SkipValidation {
profile_id: String,
},
}
pub fn cmd_profile(action: ProfileAction) {
let profiles_dir = PathResolver::user_data_dir().join("profiles");
match action {
ProfileAction::Add { scenario, id } => {
cmd_profile_add(&scenario, id.as_deref(), &profiles_dir);
}
ProfileAction::List { verbose, state } => {
cmd_profile_list(verbose, state.as_deref(), &profiles_dir);
}
ProfileAction::Show { profile_id } => {
cmd_profile_show(&profile_id, &profiles_dir);
}
ProfileAction::Bootstrap {
profile_id,
runs,
variant,
verbose,
} => {
cmd_profile_bootstrap(&profile_id, runs, &variant, verbose, &profiles_dir);
}
ProfileAction::Delete { profile_id, force } => {
cmd_profile_delete(&profile_id, force, &profiles_dir);
}
ProfileAction::SetState { profile_id, state } => {
cmd_profile_set_state(&profile_id, &state, &profiles_dir);
}
ProfileAction::Validate {
profile_id,
runs,
strategy,
threshold,
verbose,
variant,
} => {
cmd_profile_validate(
&profile_id,
runs,
&strategy,
threshold,
verbose,
variant.as_deref(),
&profiles_dir,
);
}
ProfileAction::Retry { profile_id } => {
cmd_profile_retry(&profile_id, &profiles_dir);
}
ProfileAction::SkipValidation { profile_id } => {
cmd_profile_skip_validation(&profile_id, &profiles_dir);
}
}
}
fn cmd_profile_add(scenario_path: &PathBuf, custom_id: Option<&str>, profiles_dir: &PathBuf) {
use swarm_engine_eval::scenario::EvalScenario;
println!("=== Profile Add ===");
println!("Scenario: {}", scenario_path.display());
if !scenario_path.exists() {
eprintln!(
"Error: Scenario file not found: {}",
scenario_path.display()
);
std::process::exit(1);
}
let content = match std::fs::read_to_string(scenario_path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: Failed to read scenario file: {}", e);
std::process::exit(1);
}
};
let scenario: EvalScenario = match toml::from_str(&content) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Failed to parse scenario TOML: {}", e);
std::process::exit(1);
}
};
let profile_id = custom_id
.map(|s| s.to_string())
.unwrap_or_else(|| scenario.meta.id.learning_key().to_string());
println!("Profile ID: {}", profile_id);
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(&profile_id);
if store.exists(&id) {
eprintln!("Error: Profile '{}' already exists", profile_id);
eprintln!(
"Use 'swarm profile delete {}' to remove it first",
profile_id
);
std::process::exit(1);
}
let canonical_path = scenario_path
.canonicalize()
.unwrap_or_else(|_| scenario_path.clone());
let profile = ScenarioProfile::new(&profile_id, ScenarioSource::from_path(canonical_path));
match store.save(&profile) {
Ok(()) => {
println!();
println!("\x1b[32m✓ Profile '{}' registered\x1b[0m", profile_id);
println!();
println!("Next steps:");
println!(
" 1. Bootstrap: swarm profile bootstrap {} -n 10",
profile_id
);
println!(" 2. Use: swarm run --profile {} \"task\"", profile_id);
}
Err(e) => {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
}
}
fn cmd_profile_list(verbose: bool, state_filter: Option<&str>, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let profiles = match store.load_all() {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profiles: {}", e);
std::process::exit(1);
}
};
if profiles.is_empty() {
println!("No profiles registered.");
println!();
println!("Register a profile:");
println!(" swarm profile add scenarios/troubleshooting.toml");
return;
}
let profiles: Vec<_> = if let Some(state_str) = state_filter {
let state = match state_str.to_lowercase().as_str() {
"draft" => ProfileState::Draft,
"bootstrapping" => ProfileState::Bootstrapping,
"validating" => ProfileState::Validating,
"active" => ProfileState::Active,
"optimizing" => ProfileState::Optimizing,
"failed" => ProfileState::Failed,
_ => {
eprintln!("Error: Invalid state: {}", state_str);
eprintln!(
"Valid states: draft, bootstrapping, validating, active, optimizing, failed"
);
std::process::exit(1);
}
};
profiles.into_iter().filter(|p| p.state == state).collect()
} else {
profiles
};
println!("=== Profiles ({}) ===", profiles.len());
println!();
if verbose {
for profile in &profiles {
print_profile_detail(profile);
println!();
}
} else {
println!(
"{:<20} {:<15} {:<10} {:<10}",
"ID", "State", "Confidence", "Runs"
);
println!("{}", "-".repeat(60));
for profile in &profiles {
let confidence = if profile.avg_confidence() > 0.0 {
format!("{:.0}%", profile.avg_confidence() * 100.0)
} else {
"-".to_string()
};
println!(
"{:<20} {:<15} {:<10} {:<10}",
profile.id.0,
profile.state.to_string(),
confidence,
profile.stats.total_runs
);
}
}
}
fn cmd_profile_show(profile_id: &str, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
print_profile_detail(&profile);
}
fn print_profile_detail(profile: &ScenarioProfile) {
println!("=== Profile: {} ===", profile.id.0);
println!();
println!("State: {}", profile.state);
match &profile.scenario_source {
ScenarioSource::File { path } => {
println!("Source: {}", path.display());
}
ScenarioSource::Inline { .. } => {
println!("Source: (inline)");
}
}
println!("Created: {}", format_timestamp(profile.created_at));
println!("Updated: {}", format_timestamp(profile.updated_at));
println!();
println!("--- Statistics ---");
println!("Total runs: {}", profile.stats.total_runs);
if profile.stats.total_runs > 0 {
println!("Success rate: {:.1}%", profile.stats.success_rate * 100.0);
println!("Avg duration: {}ms", profile.stats.avg_duration_ms);
}
if let Some(bootstrap) = &profile.bootstrap {
println!();
println!("--- Bootstrap ---");
println!("Sessions: {}", bootstrap.session_count);
println!("Success rate: {:.1}%", bootstrap.success_rate * 100.0);
println!("Variant: {}", bootstrap.source_variant);
println!("Completed: {}", format_timestamp(bootstrap.completed_at));
}
if let Some(validation) = &profile.validation {
println!();
println!("--- Validation ---");
println!("Strategy: {}", validation.strategy);
println!("Baseline: {:.1}%", validation.baseline * 100.0);
println!("Current: {:.1}%", validation.current * 100.0);
println!("Improvement: {:.1}%", validation.improvement());
println!("Samples: {}", validation.sample_count);
if validation.passed {
println!("Result: \x1b[32mPASSED\x1b[0m");
} else {
println!("Result: \x1b[31mFAILED\x1b[0m");
if let Some(reason) = &validation.failure_reason {
println!("Reason: {}", reason);
}
}
}
println!();
println!("--- Learned Components ---");
if let Some(exploration) = &profile.exploration {
println!(
"Exploration: ucb1_c={:.3}, learning_weight={:.3} (confidence: {:.0}%)",
exploration.ucb1_c,
exploration.learning_weight,
exploration.confidence * 100.0
);
} else {
println!("Exploration: (not learned)");
}
if let Some(strategy) = &profile.strategy {
println!(
"Strategy: {} (maturity={}, confidence: {:.0}%)",
strategy.initial_strategy,
strategy.maturity_threshold,
strategy.confidence * 100.0
);
} else {
println!("Strategy: (not learned)");
}
if let Some(dep_graph) = &profile.dep_graph {
println!(
"DepGraph: {} actions (confidence: {:.0}%)",
dep_graph.action_order.len(),
dep_graph.confidence * 100.0
);
if !dep_graph.action_order.is_empty() {
println!(" Order: {:?}", dep_graph.action_order);
}
} else {
println!("DepGraph: (not learned)");
}
}
fn format_timestamp(ts: u64) -> String {
use std::time::{Duration, UNIX_EPOCH};
if ts == 0 {
return "(unknown)".to_string();
}
let datetime = UNIX_EPOCH + Duration::from_secs(ts);
let now = std::time::SystemTime::now();
if let Ok(duration) = now.duration_since(datetime) {
let secs = duration.as_secs();
if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
format!("{}d ago", secs / 86400)
}
} else {
"(future?)".to_string()
}
}
fn cmd_profile_bootstrap(
profile_id: &str,
runs: usize,
variant: &str,
verbose: bool,
profiles_dir: &PathBuf,
) {
use swarm_engine_core::learn::BootstrapData;
use swarm_engine_eval::scenario::EvalScenario;
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let mut profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
println!("=== Profile Bootstrap ===");
println!("Profile: {}", profile_id);
println!("Runs: {}", runs);
println!("Variant: {}", variant);
println!();
let scenario_path = match &profile.scenario_source {
ScenarioSource::File { path } => path.clone(),
ScenarioSource::Inline { .. } => {
eprintln!("Error: Inline scenarios not supported for bootstrap");
std::process::exit(1);
}
};
if !scenario_path.exists() {
eprintln!(
"Error: Scenario file not found: {}",
scenario_path.display()
);
std::process::exit(1);
}
let content = match std::fs::read_to_string(&scenario_path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: Failed to read scenario file: {}", e);
std::process::exit(1);
}
};
let base_scenario: EvalScenario = match toml::from_str(&content) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Failed to parse scenario TOML: {}", e);
std::process::exit(1);
}
};
let variant_names = base_scenario.variant_names();
if !variant_names.contains(&variant) {
eprintln!("Error: Variant '{}' not found in scenario", variant);
eprintln!("Available variants: {:?}", variant_names);
std::process::exit(1);
}
profile.start_bootstrap();
if let Err(e) = store.save(&profile) {
eprintln!("Warning: Failed to save profile state: {}", e);
}
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let handle = rt.handle().clone();
let bootstrap_scenario = base_scenario.with_variant(variant).unwrap();
let learning_path = PathResolver::user_data_dir().join("learning");
let mut success_count = 0;
println!("Running bootstrap...");
println!();
for i in 0..runs {
let seed = 42 + i as u64;
print!(" Run {}/{}: ", i + 1, runs);
std::io::Write::flush(&mut std::io::stdout()).ok();
let runner = build_bootstrap_runner(
bootstrap_scenario.clone(),
handle.clone(),
seed,
verbose,
&learning_path,
);
match runner.run() {
Ok(report) => {
let success = report.aggregated.success_rate >= 1.0;
let ticks = report.aggregated.statistics.total_ticks.mean as u64;
if success {
success_count += 1;
println!("\x1b[32m✓ Success ({} ticks)\x1b[0m", ticks);
} else {
println!("\x1b[31m✗ Failure ({} ticks)\x1b[0m", ticks);
}
}
Err(e) => {
println!("\x1b[31m✗ Error: {}\x1b[0m", e);
}
}
}
println!();
let success_rate = success_count as f64 / runs as f64;
println!(
"Summary: {}/{} success ({:.0}%)",
success_count,
runs,
success_rate * 100.0
);
println!();
println!("Running offline learning...");
let scenario_key = base_scenario.meta.id.learning_key();
use swarm_engine_core::exploration::DependencyGraph;
use swarm_engine_core::learn::daemon::{Processor, ProcessorConfig, ProcessorMode};
use swarm_engine_core::learn::store::InMemoryEpisodeStore;
use swarm_engine_core::learn::{
LearnedDepGraph, LearnedExploration, LearnedStrategy, LearningStore,
};
let learning_store = match LearningStore::new(&learning_path) {
Ok(s) => s,
Err(e) => {
eprintln!(" Warning: Failed to open learning store: {}", e);
eprintln!(" Skipping offline learning.");
let bootstrap_data = BootstrapData::new(runs, success_rate, variant);
profile.complete_bootstrap(bootstrap_data);
if let Err(e) = store.save(&profile) {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
println!();
println!(
"\x1b[32m✓ Bootstrap complete. Profile '{}' is now Validating.\x1b[0m",
profile_id
);
println!();
println!("Next steps:");
println!(" 1. Validate: swarm profile validate {} -n 5", profile_id);
println!(
" 2. Or skip: swarm profile skip-validation {}",
profile_id
);
return;
}
};
let existing_action_order = learning_store
.load_offline_model(&scenario_key)
.ok()
.and_then(|m| m.action_order);
let config = ProcessorConfig::new(&scenario_key)
.mode(ProcessorMode::OfflineOnly)
.max_sessions(runs);
let processor = Processor::new(config).with_learning_store(learning_store);
let episode_store = InMemoryEpisodeStore::new();
let offline_model = match rt.block_on(processor.run(&episode_store)) {
Ok(result) => result.offline_model().cloned(),
Err(e) => {
eprintln!(" Warning: Offline learning failed: {}", e);
None
}
};
if let Some(mut model) = offline_model {
if model.action_order.is_none() {
model.action_order = existing_action_order.clone();
}
println!(" \x1b[32m✓ Offline model generated\x1b[0m");
let exploration = LearnedExploration {
ucb1_c: model.parameters.ucb1_c,
learning_weight: model.parameters.learning_weight,
ngram_weight: model.parameters.ngram_weight,
confidence: success_rate,
session_count: runs,
updated_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
profile.update_exploration(exploration);
println!(
" Exploration: ucb1_c={:.3}, learning_weight={:.3}",
model.parameters.ucb1_c, model.parameters.learning_weight
);
let strategy = LearnedStrategy {
initial_strategy: model.strategy_config.initial_strategy.clone(),
maturity_threshold: model.strategy_config.maturity_threshold as usize,
error_rate_threshold: model.strategy_config.error_rate_threshold,
confidence: success_rate,
session_count: runs,
updated_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
profile.update_strategy(strategy);
println!(
" Strategy: {} (maturity={})",
model.strategy_config.initial_strategy, model.strategy_config.maturity_threshold
);
if let Some(ref action_order) = model.action_order {
let dep_graph = LearnedDepGraph::with_orders(
DependencyGraph::new(),
action_order.discover.clone(),
action_order.not_discover.clone(),
)
.with_confidence(success_rate)
.with_sessions((0..runs).map(|i| format!("bootstrap_{}", i)).collect())
.with_recommended_paths(model.recommended_paths.clone());
profile.update_dep_graph(dep_graph);
println!(
" DepGraph: discover={:?}, not_discover={:?}",
action_order.discover, action_order.not_discover
);
}
} else {
println!(" No offline model generated (not enough data)");
}
let bootstrap_data = BootstrapData::new(runs, success_rate, variant);
profile.complete_bootstrap(bootstrap_data);
if let Err(e) = store.save(&profile) {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
println!();
println!(
"\x1b[32m✓ Bootstrap complete. Profile '{}' is now Validating.\x1b[0m",
profile_id
);
println!();
println!("Next steps:");
println!(" 1. Validate: swarm profile validate {} -n 5", profile_id);
println!(
" 2. Or skip: swarm profile skip-validation {}",
profile_id
);
}
fn build_bootstrap_runner(
scenario: swarm_engine_eval::scenario::EvalScenario,
handle: tokio::runtime::Handle,
seed: u64,
verbose: bool,
learning_path: &PathBuf,
) -> swarm_engine_eval::prelude::EvalRunner {
use swarm_engine_core::agent::{DefaultBatchManagerAgent, ManagerId};
use swarm_engine_core::exploration::{
AdaptiveLlmOperatorProvider, AdaptiveOperatorProvider, ReviewPolicy,
};
use swarm_engine_eval::prelude::EvalRunner;
use swarm_engine_eval::scenario::LlmProvider;
use swarm_engine_llm::{
create_llm_invoker, ChatTemplate, LlamaCppServerConfig, LlamaCppServerDecider,
LlmStrategyAdvisor, OllamaDecider,
};
match scenario.llm.provider {
LlmProvider::Ollama => {
let llm_config = scenario
.llm
.to_ollama_config(scenario.batch_processor.max_concurrency);
let llm_config_for_advisor = llm_config.clone();
let handle_for_advisor = handle.clone();
EvalRunner::new(scenario, handle.clone())
.with_runs(1)
.with_seed(seed)
.with_verbose(verbose)
.with_exploration(true)
.with_learning_store(learning_path)
.with_manager_factory(|| Box::new(DefaultBatchManagerAgent::new(ManagerId(0))))
.with_batch_invoker_factory(move || {
let decider = OllamaDecider::new(llm_config.clone());
Box::new(create_llm_invoker(decider, handle.clone()))
})
.with_operator_provider_factory(move || {
let decider =
std::sync::Arc::new(OllamaDecider::new(llm_config_for_advisor.clone()));
let advisor = LlmStrategyAdvisor::new(decider, handle_for_advisor.clone());
Box::new(
AdaptiveLlmOperatorProvider::new(Box::new(advisor))
.with_adaptive(AdaptiveOperatorProvider::default())
.with_policy(ReviewPolicy::default()),
)
})
}
LlmProvider::LlamaCppServer => {
let endpoint = scenario
.llm
.endpoint
.clone()
.unwrap_or_else(|| "http://localhost:8080".to_string());
let server_config = LlamaCppServerConfig::new(endpoint)
.with_model_name(&scenario.llm.model)
.with_max_tokens(scenario.llm.max_tokens.unwrap_or(256))
.with_temperature(scenario.llm.temperature)
.with_chat_template(ChatTemplate::Lfm2);
let decider = LlamaCppServerDecider::new(server_config.clone())
.expect("Failed to create LlamaCppServerDecider");
let server_config_for_advisor = server_config;
let handle_for_advisor = handle.clone();
EvalRunner::new(scenario, handle.clone())
.with_runs(1)
.with_seed(seed)
.with_verbose(verbose)
.with_exploration(true)
.with_learning_store(learning_path)
.with_manager_factory(|| Box::new(DefaultBatchManagerAgent::new(ManagerId(0))))
.with_batch_invoker_factory(move || {
Box::new(create_llm_invoker(decider.clone(), handle.clone()))
})
.with_operator_provider_factory(move || {
let decider = std::sync::Arc::new(
LlamaCppServerDecider::new(server_config_for_advisor.clone())
.expect("Failed to create LlamaCppServerDecider for advisor"),
);
let advisor = LlmStrategyAdvisor::new(decider, handle_for_advisor.clone());
Box::new(
AdaptiveLlmOperatorProvider::new(Box::new(advisor))
.with_adaptive(AdaptiveOperatorProvider::default())
.with_policy(ReviewPolicy::default()),
)
})
}
other => {
eprintln!("Error: Provider {:?} is not supported for bootstrap", other);
std::process::exit(1);
}
}
}
fn cmd_profile_delete(profile_id: &str, force: bool, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
if !store.exists(&id) {
eprintln!("Error: Profile '{}' not found", profile_id);
std::process::exit(1);
}
if !force {
print!(
"Delete profile '{}'? This cannot be undone. [y/N]: ",
profile_id
);
std::io::Write::flush(&mut std::io::stdout()).ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return;
}
}
match store.delete(&id) {
Ok(()) => {
println!("\x1b[32m✓ Profile '{}' deleted\x1b[0m", profile_id);
}
Err(e) => {
eprintln!("Error: Failed to delete profile: {}", e);
std::process::exit(1);
}
}
}
fn cmd_profile_set_state(profile_id: &str, state_str: &str, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let mut profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
let new_state = match state_str.to_lowercase().as_str() {
"draft" => ProfileState::Draft,
"active" => ProfileState::Active,
_ => {
eprintln!("Error: Invalid state: {}", state_str);
eprintln!("Valid states for manual set: draft, active");
std::process::exit(1);
}
};
let old_state = profile.state;
profile.state = new_state;
match store.save(&profile) {
Ok(()) => {
println!(
"Profile '{}' state changed: {} -> {}",
profile_id, old_state, new_state
);
}
Err(e) => {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
}
}
fn cmd_profile_validate(
profile_id: &str,
runs: usize,
strategy_str: &str,
threshold: Option<f64>,
verbose: bool,
variant_override: Option<&str>,
profiles_dir: &PathBuf,
) {
use swarm_engine_core::validation::{Absolute, Improvement, NoRegression, Validator};
use swarm_engine_eval::scenario::EvalScenario;
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let mut profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
if profile.state != ProfileState::Validating {
eprintln!(
"Error: Profile '{}' is in '{}' state, not 'validating'",
profile_id, profile.state
);
if profile.state == ProfileState::Draft {
eprintln!("Run 'swarm profile bootstrap {}' first.", profile_id);
}
std::process::exit(1);
}
let baseline = profile
.bootstrap
.as_ref()
.map(|b| b.success_rate)
.unwrap_or(0.5);
let strategy: Box<dyn swarm_engine_core::validation::ValidationStrategy> =
match strategy_str.to_lowercase().as_str() {
"no_regression" => Box::new(NoRegression::new()),
"improvement" => {
let t = threshold.unwrap_or(1.1);
Box::new(Improvement::new(t))
}
"absolute" => {
let t = threshold.unwrap_or(0.8);
Box::new(Absolute::new(t))
}
_ => {
eprintln!("Error: Invalid strategy: {}", strategy_str);
eprintln!("Valid strategies: no_regression, improvement, absolute");
std::process::exit(1);
}
};
println!("=== Profile Validation ===");
println!("Profile: {}", profile_id);
println!("Strategy: {}", strategy.name());
println!("Baseline: {:.1}%", baseline * 100.0);
println!("Runs: {}", runs);
println!();
let scenario_path = match &profile.scenario_source {
ScenarioSource::File { path } => path.clone(),
ScenarioSource::Inline { .. } => {
eprintln!("Error: Inline scenarios not supported for validation");
std::process::exit(1);
}
};
let content = match std::fs::read_to_string(&scenario_path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: Failed to read scenario file: {}", e);
std::process::exit(1);
}
};
let base_scenario: EvalScenario = match toml::from_str(&content) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Failed to parse scenario TOML: {}", e);
std::process::exit(1);
}
};
let variant = variant_override
.map(|s| s.to_string())
.or_else(|| profile.bootstrap.as_ref().map(|b| b.source_variant.clone()))
.unwrap_or_else(|| "with_graph".to_string());
let scenario = match base_scenario.with_variant(&variant) {
Some(s) => s,
None => {
eprintln!("Error: Variant '{}' not found", variant);
std::process::exit(1);
}
};
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let handle = rt.handle().clone();
let learning_path = PathResolver::user_data_dir().join("learning");
let mut success_count = 0;
println!("Running validation...");
println!();
for i in 0..runs {
let seed = 1000 + i as u64;
print!(" Run {}/{}: ", i + 1, runs);
std::io::Write::flush(&mut std::io::stdout()).ok();
let runner = build_bootstrap_runner(
scenario.clone(),
handle.clone(),
seed,
verbose,
&learning_path,
);
match runner.run() {
Ok(report) => {
let success = report.aggregated.success_rate >= 1.0;
let ticks = report.aggregated.statistics.total_ticks.mean as u64;
if success {
success_count += 1;
println!("\x1b[32m✓ Success ({} ticks)\x1b[0m", ticks);
} else {
println!("\x1b[31m✗ Failure ({} ticks)\x1b[0m", ticks);
}
}
Err(e) => {
println!("\x1b[31m✗ Error: {}\x1b[0m", e);
}
}
}
let current = success_count as f64 / runs as f64;
println!();
println!(
"Validation result: {}/{} success ({:.1}%)",
success_count,
runs,
current * 100.0
);
let validator: Validator<()> = Validator::new(1.0, strategy); let result = validator.validate_with_baseline(&[], baseline, |_| current);
profile.apply_validation(result.clone());
if let Err(e) = store.save(&profile) {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
println!();
if result.passed {
println!(
"\x1b[32m✓ Validation PASSED. Profile '{}' is now Active.\x1b[0m",
profile_id
);
println!();
println!("Next: Use the profile");
println!(" swarm run --profile {} \"task\"", profile_id);
} else {
println!(
"\x1b[31m✗ Validation FAILED: {}\x1b[0m",
result.failure_reason.as_deref().unwrap_or("unknown")
);
println!();
println!("Options:");
println!(" 1. Retry bootstrap: swarm profile retry {}", profile_id);
println!(
" 2. Use different strategy: swarm profile validate {} --strategy absolute --threshold 0.7",
profile_id
);
}
}
fn cmd_profile_retry(profile_id: &str, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let mut profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
if profile.state != ProfileState::Failed {
eprintln!(
"Error: Profile '{}' is in '{}' state, not 'failed'",
profile_id, profile.state
);
eprintln!("Only failed profiles can be retried.");
std::process::exit(1);
}
profile.retry();
match store.save(&profile) {
Ok(()) => {
println!("\x1b[32m✓ Profile '{}' reset to Draft\x1b[0m", profile_id);
println!();
println!("Next: Run bootstrap again");
println!(" swarm profile bootstrap {} -n 10", profile_id);
}
Err(e) => {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
}
}
fn cmd_profile_skip_validation(profile_id: &str, profiles_dir: &PathBuf) {
let store = ProfileStore::new(profiles_dir);
let id = ScenarioProfileId::new(profile_id);
let mut profile = match store.load(&id) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: Failed to load profile '{}': {}", profile_id, e);
std::process::exit(1);
}
};
if profile.state != ProfileState::Validating {
eprintln!(
"Error: Profile '{}' is in '{}' state, not 'validating'",
profile_id, profile.state
);
std::process::exit(1);
}
profile.skip_validation();
match store.save(&profile) {
Ok(()) => {
println!(
"\x1b[32m✓ Validation skipped. Profile '{}' is now Active.\x1b[0m",
profile_id
);
println!();
println!("Next: Use the profile");
println!(" swarm run --profile {} \"task\"", profile_id);
}
Err(e) => {
eprintln!("Error: Failed to save profile: {}", e);
std::process::exit(1);
}
}
}