zeroclaw_runtime/skillforge/
mod.rs1pub mod evaluate;
8pub mod integrate;
9pub mod scout;
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13
14use self::evaluate::{EvalResult, Evaluator, Recommendation};
15use self::integrate::Integrator;
16use self::scout::{GitHubScout, Scout, ScoutResult, ScoutSource};
17
18#[derive(Clone, Serialize, Deserialize)]
23pub struct SkillForgeConfig {
24 #[serde(default)]
25 pub enabled: bool,
26 #[serde(default = "default_auto_integrate")]
27 pub auto_integrate: bool,
28 #[serde(default = "default_sources")]
29 pub sources: Vec<String>,
30 #[serde(default = "default_scan_interval")]
31 pub scan_interval_hours: u64,
32 #[serde(default = "default_min_score")]
33 pub min_score: f64,
34 #[serde(default)]
36 pub github_token: Option<String>,
37 #[serde(default = "default_output_dir")]
39 pub output_dir: String,
40}
41
42fn default_auto_integrate() -> bool {
43 true
44}
45fn default_sources() -> Vec<String> {
46 vec!["github".into(), "clawhub".into()]
47}
48fn default_scan_interval() -> u64 {
49 24
50}
51fn default_min_score() -> f64 {
52 0.7
53}
54fn default_output_dir() -> String {
55 "./skills".into()
56}
57
58impl Default for SkillForgeConfig {
59 fn default() -> Self {
60 Self {
61 enabled: false,
62 auto_integrate: default_auto_integrate(),
63 sources: default_sources(),
64 scan_interval_hours: default_scan_interval(),
65 min_score: default_min_score(),
66 github_token: None,
67 output_dir: default_output_dir(),
68 }
69 }
70}
71
72impl std::fmt::Debug for SkillForgeConfig {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("SkillForgeConfig")
75 .field("enabled", &self.enabled)
76 .field("auto_integrate", &self.auto_integrate)
77 .field("sources", &self.sources)
78 .field("scan_interval_hours", &self.scan_interval_hours)
79 .field("min_score", &self.min_score)
80 .field("github_token", &self.github_token.as_ref().map(|_| "***"))
81 .field("output_dir", &self.output_dir)
82 .finish()
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ForgeReport {
92 pub discovered: usize,
93 pub evaluated: usize,
94 pub auto_integrated: usize,
95 pub manual_review: usize,
96 pub skipped: usize,
97 pub results: Vec<EvalResult>,
98}
99
100pub struct SkillForge {
105 config: SkillForgeConfig,
106 evaluator: Evaluator,
107 integrator: Integrator,
108}
109
110impl SkillForge {
111 pub fn new(config: SkillForgeConfig) -> Self {
112 let evaluator = Evaluator::new(config.min_score);
113 let integrator = Integrator::new(config.output_dir.clone());
114 Self {
115 config,
116 evaluator,
117 integrator,
118 }
119 }
120
121 pub async fn forge(&self) -> Result<ForgeReport> {
123 if !self.config.enabled {
124 ::zeroclaw_log::record!(
125 WARN,
126 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
127 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
128 "SkillForge is disabled — skipping"
129 );
130 return Ok(ForgeReport {
131 discovered: 0,
132 evaluated: 0,
133 auto_integrated: 0,
134 manual_review: 0,
135 skipped: 0,
136 results: vec![],
137 });
138 }
139
140 let mut candidates: Vec<ScoutResult> = Vec::new();
142
143 for src in &self.config.sources {
144 let source: ScoutSource = src.parse().unwrap(); match source {
146 ScoutSource::GitHub => {
147 let scout = GitHubScout::new(self.config.github_token.clone());
148 match scout.discover().await {
149 Ok(mut found) => {
150 ::zeroclaw_log::record!(
151 INFO,
152 ::zeroclaw_log::Event::new(
153 module_path!(),
154 ::zeroclaw_log::Action::Note
155 )
156 .with_attrs(::serde_json::json!({"count": found.len()})),
157 "GitHub scout returned candidates"
158 );
159 candidates.append(&mut found);
160 }
161 Err(e) => {
162 ::zeroclaw_log::record!(
163 WARN,
164 ::zeroclaw_log::Event::new(
165 module_path!(),
166 ::zeroclaw_log::Action::Note
167 )
168 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
169 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
170 "GitHub scout failed, continuing with other sources"
171 );
172 }
173 }
174 }
175 ScoutSource::ClawHub | ScoutSource::HuggingFace => {
176 ::zeroclaw_log::record!(
177 INFO,
178 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
179 .with_attrs(::serde_json::json!({"source": src.as_str()})),
180 "Source not yet implemented — skipping"
181 );
182 }
183 }
184 }
185
186 scout::dedup(&mut candidates);
188 let discovered = candidates.len();
189 ::zeroclaw_log::record!(
190 INFO,
191 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
192 .with_attrs(::serde_json::json!({"discovered": discovered})),
193 "Total unique candidates after dedup"
194 );
195
196 let results: Vec<EvalResult> = candidates
198 .into_iter()
199 .map(|c| self.evaluator.evaluate(c))
200 .collect();
201 let evaluated = results.len();
202
203 let mut auto_integrated = 0usize;
205 let mut manual_review = 0usize;
206 let mut skipped = 0usize;
207
208 for res in &results {
209 match res.recommendation {
210 Recommendation::Auto => {
211 if self.config.auto_integrate {
212 match self.integrator.integrate(&res.candidate) {
213 Ok(_) => {
214 auto_integrated += 1;
215 }
216 Err(e) => {
217 ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Unknown).with_attrs(::serde_json::json!({"skill": res.candidate.name.as_str(), "error": format!("{}", e)})), "Integration failed for candidate, continuing");
218 }
219 }
220 } else {
221 manual_review += 1;
223 }
224 }
225 Recommendation::Manual => {
226 manual_review += 1;
227 }
228 Recommendation::Skip => {
229 skipped += 1;
230 }
231 }
232 }
233
234 ::zeroclaw_log::record!(INFO, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({"auto_integrated": auto_integrated, "manual_review": manual_review, "skipped": skipped})), "Forge pipeline complete");
235
236 Ok(ForgeReport {
237 discovered,
238 evaluated,
239 auto_integrated,
240 manual_review,
241 skipped,
242 results,
243 })
244 }
245}
246
247#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[tokio::test]
256 async fn disabled_forge_returns_empty_report() {
257 let cfg = SkillForgeConfig {
258 enabled: false,
259 ..Default::default()
260 };
261 let forge = SkillForge::new(cfg);
262 let report = forge.forge().await.unwrap();
263 assert_eq!(report.discovered, 0);
264 assert_eq!(report.auto_integrated, 0);
265 }
266
267 #[test]
268 fn default_config_values() {
269 let cfg = SkillForgeConfig::default();
270 assert!(!cfg.enabled);
271 assert!(cfg.auto_integrate);
272 assert_eq!(cfg.scan_interval_hours, 24);
273 assert!((cfg.min_score - 0.7).abs() < f64::EPSILON);
274 assert_eq!(cfg.sources, vec!["github", "clawhub"]);
275 }
276}