//! SkillForge — Skill auto-discovery, evaluation, and integration engine. //! //! Pipeline: Scout → Evaluate → Integrate //! Discovers skills from external sources, scores them, and generates //! ZeroClaw-compatible manifests for qualified candidates. pub mod evaluate; pub mod integrate; pub mod scout; use anyhow::Result; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; use self::evaluate::{EvalResult, Evaluator, Recommendation}; use self::integrate::Integrator; use self::scout::{GitHubScout, Scout, ScoutResult, ScoutSource}; use crate::config::Config; #[derive(Debug, Clone, clap::Subcommand)] pub enum SkillforgeCommands { Run { #[arg(long, default_value = "0.7")] min_score: f64, #[arg(long)] dry_run: bool, #[arg(long)] source: Option, }, Scout { #[arg(long, default_value = "zeroclaw skill")] query: String, #[arg(long, default_value = "20")] limit: usize, }, Status, } pub async fn handle_command(command: SkillforgeCommands, config: &Config) -> Result<()> { let _forge_config = config.skillforge.clone(); match command { SkillforgeCommands::Run { min_score, dry_run, source } => { run_forge(config, min_score, dry_run, source).await } SkillforgeCommands::Scout { query, limit } => { run_scout(config, query, limit).await } SkillforgeCommands::Status => { show_status(config) } } } async fn run_forge(config: &Config, min_score: f64, dry_run: bool, source: Option) -> Result<()> { println!("🔮 SkillForge Pipeline"); println!(" Min Score: {}", min_score); println!(" Dry Run: {}", dry_run); println!(" Source: {}", source.as_deref().unwrap_or("all")); println!(); let mut forge_config = config.skillforge.clone(); forge_config.enabled = true; forge_config.min_score = min_score; forge_config.auto_integrate = !dry_run; if let Some(src) = source { forge_config.sources = vec![src]; } let forge = SkillForge::new(forge_config); let report = forge.forge().await?; println!("📊 Results:"); println!(" Discovered: {}", report.discovered); println!(" Evaluated: {}", report.evaluated); println!(" Auto-integrated: {}", report.auto_integrated); println!(" Manual review: {}", report.manual_review); println!(" Skipped: {}", report.skipped); println!(); if !report.results.is_empty() { println!("📋 Top Candidates:"); for (i, res) in report.results.iter().take(10).enumerate() { let status = match res.recommendation { Recommendation::Auto => "✅ AUTO", Recommendation::Manual => "⚠️ REVIEW", Recommendation::Skip => "❌ SKIP", }; println!(" {}. {} [{:.2}] {}", i + 1, res.candidate.name, res.total_score, status); println!(" {}", res.candidate.description.chars().take(60).collect::()); } } Ok(()) } async fn run_scout(config: &Config, query: String, limit: usize) -> Result<()> { println!("🔍 SkillForge Scout"); println!(" Query: {}", query); println!(" Limit: {}", limit); println!(); let github_token = config.skillforge.github_token.clone(); let scout = GitHubScout::new_with_query(github_token, query.clone()); let results = scout.discover().await?; let limited: Vec<_> = results.into_iter().take(limit).collect(); println!("📋 Found {} candidates:", limited.len()); for (i, candidate) in limited.iter().enumerate() { println!(" {}. {}", i + 1, candidate.name); println!(" URL: {}", candidate.url); println!(" ⭐ {} stars", candidate.stars); let desc: String = candidate.description.chars().take(60).collect(); if !desc.is_empty() { println!(" {}", desc); } } Ok(()) } fn show_status(config: &Config) -> Result<()> { println!("🔮 SkillForge Status"); println!(); println!("Configuration:"); println!(" Enabled: {}", config.skillforge.enabled); println!(" Auto-integrate: {}", config.skillforge.auto_integrate); println!(" Sources: {:?}", config.skillforge.sources); println!(" Scan interval: {}h", config.skillforge.scan_interval_hours); println!(" Min score: {:.2}", config.skillforge.min_score); println!(" Output directory: {}", config.skillforge.output_dir); println!(" GitHub token: {}", if config.skillforge.github_token.is_some() { "configured" } else { "not set" }); let skills_dir = std::path::Path::new(&config.skillforge.output_dir); if skills_dir.exists() { let count = std::fs::read_dir(skills_dir) .map(|entries| entries.filter_map(|e| e.ok()).count()) .unwrap_or(0); println!(); println!("Integrated skills: {}", count); } Ok(()) } // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- #[derive(Clone, Serialize, Deserialize)] pub struct SkillForgeConfig { #[serde(default)] pub enabled: bool, #[serde(default = "default_auto_integrate")] pub auto_integrate: bool, #[serde(default = "default_sources")] pub sources: Vec, #[serde(default = "default_scan_interval")] pub scan_interval_hours: u64, #[serde(default = "default_min_score")] pub min_score: f64, /// Optional GitHub personal-access token for higher rate limits. #[serde(default)] pub github_token: Option, /// Directory where integrated skills are written. #[serde(default = "default_output_dir")] pub output_dir: String, } fn default_auto_integrate() -> bool { true } fn default_sources() -> Vec { vec!["github".into(), "clawhub".into()] } fn default_scan_interval() -> u64 { 24 } fn default_min_score() -> f64 { 0.7 } fn default_output_dir() -> String { "./skills".into() } impl Default for SkillForgeConfig { fn default() -> Self { Self { enabled: false, auto_integrate: default_auto_integrate(), sources: default_sources(), scan_interval_hours: default_scan_interval(), min_score: default_min_score(), github_token: None, output_dir: default_output_dir(), } } } impl std::fmt::Debug for SkillForgeConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SkillForgeConfig") .field("enabled", &self.enabled) .field("auto_integrate", &self.auto_integrate) .field("sources", &self.sources) .field("scan_interval_hours", &self.scan_interval_hours) .field("min_score", &self.min_score) .field("github_token", &self.github_token.as_ref().map(|_| "***")) .field("output_dir", &self.output_dir) .finish() } } // --------------------------------------------------------------------------- // ForgeReport — summary of a single pipeline run // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ForgeReport { pub discovered: usize, pub evaluated: usize, pub auto_integrated: usize, pub manual_review: usize, pub skipped: usize, pub results: Vec, } // --------------------------------------------------------------------------- // SkillForge // --------------------------------------------------------------------------- pub struct SkillForge { config: SkillForgeConfig, evaluator: Evaluator, integrator: Integrator, } impl SkillForge { pub fn new(config: SkillForgeConfig) -> Self { let evaluator = Evaluator::new(config.min_score); let integrator = Integrator::new(config.output_dir.clone()); Self { config, evaluator, integrator, } } /// Run the full pipeline: Scout → Evaluate → Integrate. pub async fn forge(&self) -> Result { if !self.config.enabled { warn!("SkillForge is disabled — skipping"); return Ok(ForgeReport { discovered: 0, evaluated: 0, auto_integrated: 0, manual_review: 0, skipped: 0, results: vec![], }); } // --- Scout ---------------------------------------------------------- let mut candidates: Vec = Vec::new(); for src in &self.config.sources { let source: ScoutSource = src.parse().unwrap(); // Infallible match source { ScoutSource::GitHub => { let scout = GitHubScout::new(self.config.github_token.clone()); match scout.discover().await { Ok(mut found) => { info!(count = found.len(), "GitHub scout returned candidates"); candidates.append(&mut found); } Err(e) => { warn!(error = %e, "GitHub scout failed, continuing with other sources"); } } } ScoutSource::ClawHub | ScoutSource::HuggingFace => { info!( source = src.as_str(), "Source not yet implemented — skipping" ); } } } // Deduplicate by URL scout::dedup(&mut candidates); let discovered = candidates.len(); info!(discovered, "Total unique candidates after dedup"); // --- Evaluate ------------------------------------------------------- let results: Vec = candidates .into_iter() .map(|c| self.evaluator.evaluate(c)) .collect(); let evaluated = results.len(); // --- Integrate ------------------------------------------------------ let mut auto_integrated = 0usize; let mut manual_review = 0usize; let mut skipped = 0usize; for res in &results { match res.recommendation { Recommendation::Auto => { if self.config.auto_integrate { match self.integrator.integrate(&res.candidate) { Ok(_) => { auto_integrated += 1; } Err(e) => { warn!( skill = res.candidate.name.as_str(), error = %e, "Integration failed for candidate, continuing" ); } } } else { // Count as would-be auto but not actually integrated manual_review += 1; } } Recommendation::Manual => { manual_review += 1; } Recommendation::Skip => { skipped += 1; } } } info!( auto_integrated, manual_review, skipped, "Forge pipeline complete" ); Ok(ForgeReport { discovered, evaluated, auto_integrated, manual_review, skipped, results, }) } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn disabled_forge_returns_empty_report() { let cfg = SkillForgeConfig { enabled: false, ..Default::default() }; let forge = SkillForge::new(cfg); let report = forge.forge().await.unwrap(); assert_eq!(report.discovered, 0); assert_eq!(report.auto_integrated, 0); } #[test] fn default_config_values() { let cfg = SkillForgeConfig::default(); assert!(!cfg.enabled); assert!(cfg.auto_integrate); assert_eq!(cfg.scan_interval_hours, 24); assert!((cfg.min_score - 0.7).abs() < f64::EPSILON); assert_eq!(cfg.sources, vec!["github", "clawhub"]); } }