1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Process-wide skill and persona registries.
//!
//! Discovery walks every skill directory and reads a `SKILL.md` plus an optional `customize.toml`
//! per skill — blocking I/O, and the same result every time within a run. Doing it per tool call
//! meant party mode paid for it once per persona, on the async runtime's worker threads. Here it
//! happens once, off the runtime, and every caller shares the outcome.
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::OnceCell;
use crate::personas::PersonaRegistry;
use crate::skills::SkillRegistry;
struct Registries {
skills: Arc<SkillRegistry>,
personas: Arc<PersonaRegistry>,
}
static REGISTRIES: OnceCell<Registries> = OnceCell::const_new();
async fn get() -> &'static Registries {
REGISTRIES
.get_or_init(|| async {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
// Both registries are built inside the one blocking task: personas borrow the skill
// registry, and splitting them would either move it across the boundary or read
// `customize.toml` back on the runtime.
let built = tokio::task::spawn_blocking(move || {
let (skills, mut warnings) = SkillRegistry::discover(&cwd);
let (personas, persona_warnings) = PersonaRegistry::discover(&skills);
warnings.extend(persona_warnings);
(skills, personas, warnings)
})
.await;
match built {
Ok((skills, personas, warnings)) => {
// A skill that failed to load is invisible in the registry; recording why is
// the only thing that distinguishes it from one that was never installed.
for warning in warnings {
crate::diag::warn(warning);
}
Registries {
skills: Arc::new(skills),
personas: Arc::new(personas),
}
}
// A panicked or cancelled discovery must not poison every later lookup, but an
// empty registry looks exactly like "no skills installed" unless it is recorded.
Err(e) => {
crate::diag::warn(format!("Skill discovery failed: {}", e));
Registries {
skills: Arc::new(SkillRegistry::new()),
personas: Arc::new(PersonaRegistry::new()),
}
}
}
})
.await
}
/// Returns the shared skill registry, discovering on first use.
pub async fn skills() -> Arc<SkillRegistry> {
Arc::clone(&get().await.skills)
}
/// Returns the shared persona registry, discovering on first use.
pub async fn personas() -> Arc<PersonaRegistry> {
Arc::clone(&get().await.personas)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn discovery_happens_once_and_is_shared() {
let first = skills().await;
let second = skills().await;
assert!(
Arc::ptr_eq(&first, &second),
"the registry must be shared, not rebuilt per call"
);
let personas_first = personas().await;
let personas_second = personas().await;
assert!(Arc::ptr_eq(&personas_first, &personas_second));
}
// No longer every persona: the six built-in specialists (`personas::builtin`) ship in the
// binary and have no `SKILL.md` behind them, precisely so they exist without an install step.
// What still has to hold is that every persona *discovered from disk* — the ones a customize.toml
// added — points at a skill that is actually there.
#[tokio::test]
async fn every_disk_persona_comes_from_a_discovered_skill() {
let builtin_names: std::collections::HashSet<String> = crate::personas::builtin::personas()
.into_iter()
.map(|p| p.skill_name)
.collect();
let skills = skills().await;
for persona in personas().await.all() {
if builtin_names.contains(&persona.skill_name) {
continue;
}
assert!(
skills.get(&persona.skill_name).is_some(),
"persona {} has no backing skill",
persona.name
);
}
}
}