Skip to main content

oneiros_engine/
skill.rs

1//! Skill inventory — command documentation and package assets.
2//!
3//! `SkillInventory` collects command skill documents from every domain
4//! and provides the complete skill package for installation.
5//!
6//! `SkillPackage` represents the installable artifact — everything Claude Code
7//! needs to use oneiros as a skill: the SKILL.md, plugin metadata, hooks,
8//! agent definitions, resources, and command documentation.
9use crate::*;
10
11/// The version of the package, stamped at compile time.
12const VERSION: &str = env!("CARGO_PKG_VERSION");
13
14const SKILL_MD: &str = include_str!("../templates/skills/oneiros/SKILL.md");
15const PLUGIN_JSON: &str = include_str!("../templates/skills/oneiros/plugin.json");
16const HOOKS_JSON: &str = include_str!("../templates/skills/oneiros/hooks.json");
17const MARKETPLACE_JSON: &str = include_str!("../templates/skills/oneiros/marketplace.json");
18const AGENTS_MD: &str = include_str!("../templates/skills/oneiros/agents-md.md");
19const MORNING_PAGES_MD: &str = include_str!("../templates/skills/oneiros-morning-pages/SKILL.md");
20const EVENING_PAGES_MD: &str = include_str!("../templates/skills/oneiros-evening-pages/SKILL.md");
21
22/// A file in the skill package — name and content, ready to write.
23pub struct SkillAsset {
24    /// Relative path within the install target (e.g., "commands/dream.md").
25    pub path: &'static str,
26    /// File content, already version-stamped if applicable.
27    pub content: String,
28}
29
30/// The complete skill inventory across all domains.
31pub(crate) struct SkillInventory;
32
33impl SkillInventory {
34    /// All command skill documents from every domain.
35    pub(crate) fn all() -> Vec<Skill> {
36        let mut skills = Vec::new();
37
38        skills.extend(ActorSkills::all());
39        skills.extend(AgentSkills::all());
40        skills.extend(BookmarkSkills::all());
41        skills.extend(BrainSkills::all());
42        skills.extend(CognitionSkills::all());
43        skills.extend(ConnectionSkills::all());
44        skills.extend(ContinuitySkills::all());
45        skills.extend(DoctorSkills::all());
46        skills.extend(ExperienceSkills::all());
47        skills.extend(FollowSkills::all());
48        skills.extend(LevelSkills::all());
49        skills.extend(McpConfigSkills::all());
50        skills.extend(MemorySkills::all());
51        skills.extend(NatureSkills::all());
52        skills.extend(PeerSkills::all());
53        skills.extend(PersonaSkills::all());
54        skills.extend(PressureSkills::all());
55        skills.extend(ProjectSkills::all());
56        skills.extend(SearchSkills::all());
57        skills.extend(SeedSkills::all());
58        skills.extend(SensationSkills::all());
59        skills.extend(ServiceSkills::all());
60        skills.extend(SetupSkills::all());
61        skills.extend(StorageSkills::all());
62        skills.extend(SystemSkills::all());
63        skills.extend(TenantSkills::all());
64        skills.extend(TextureSkills::all());
65        skills.extend(TicketSkills::all());
66        skills.extend(UrgeSkills::all());
67
68        skills
69    }
70}
71
72/// The installable skill package — everything Claude Code needs.
73pub struct SkillPackage;
74
75impl SkillPackage {
76    /// All files in the package, ready to write to disk.
77    ///
78    /// Version placeholders (`{{VERSION}}`) are stamped at call time.
79    pub fn assets() -> Vec<SkillAsset> {
80        let mut assets = vec![
81            SkillAsset {
82                path: "skills/oneiros/SKILL.md",
83                content: Self::stamp(SKILL_MD),
84            },
85            SkillAsset {
86                path: ".claude-plugin/plugin.json",
87                content: Self::stamp(PLUGIN_JSON),
88            },
89            SkillAsset {
90                path: ".claude-plugin/marketplace.json",
91                content: Self::stamp(MARKETPLACE_JSON),
92            },
93            SkillAsset {
94                path: "hooks/hooks.json",
95                content: HOOKS_JSON.to_string(),
96            },
97            SkillAsset {
98                path: "agents-md.md",
99                content: AGENTS_MD.to_string(),
100            },
101            SkillAsset {
102                path: "skills/oneiros-morning-pages/SKILL.md",
103                content: Self::stamp(MORNING_PAGES_MD),
104            },
105            SkillAsset {
106                path: "skills/oneiros-evening-pages/SKILL.md",
107                content: Self::stamp(EVENING_PAGES_MD),
108            },
109        ];
110
111        // Agent definitions
112        for (name, content) in Self::agents() {
113            assets.push(SkillAsset {
114                path: name,
115                content: content.to_string(),
116            });
117        }
118
119        // Resources
120        for (name, content) in Self::resources() {
121            assets.push(SkillAsset {
122                path: name,
123                content: content.to_string(),
124            });
125        }
126
127        // Command documentation (from the skill inventory)
128        for skill in SkillInventory::all() {
129            assets.push(SkillAsset {
130                path: leak_path(&format!("commands/{}.md", skill.name)),
131                content: skill.content.to_string(),
132            });
133        }
134
135        assets
136    }
137
138    /// Agent definition files.
139    fn agents() -> Vec<(&'static str, &'static str)> {
140        vec![
141            (
142                "agents/activity.scribe.md",
143                include_str!("../templates/skills/oneiros/agents/activity.scribe.md"),
144            ),
145            (
146                "agents/oneiroi.process.md",
147                include_str!("../templates/skills/oneiros/agents/oneiroi.process.md"),
148            ),
149        ]
150    }
151
152    /// Resource files.
153    fn resources() -> Vec<(&'static str, &'static str)> {
154        vec![
155            (
156                "skills/oneiros/resources/cognitive-model.md",
157                include_str!("../templates/skills/oneiros/resources/cognitive-model.md"),
158            ),
159            (
160                "skills/oneiros/resources/getting-started.md",
161                include_str!("../templates/skills/oneiros/resources/getting-started.md"),
162            ),
163        ]
164    }
165
166    /// Replace `{{VERSION}}` with the current package version.
167    fn stamp(content: &str) -> String {
168        content.replace("{{VERSION}}", VERSION)
169    }
170}
171
172/// Leak a String into a &'static str for SkillAsset paths.
173///
174/// This is fine for the skill package — assets are built once per install
175/// and the leaked strings live for the program's lifetime.
176fn leak_path(s: &str) -> &'static str {
177    Box::leak(s.to_string().into_boxed_str())
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn inventory_is_not_empty() {
186        let skills = SkillInventory::all();
187        assert!(!skills.is_empty(), "skill inventory should not be empty");
188    }
189
190    #[test]
191    fn all_skills_have_content() {
192        for skill in SkillInventory::all() {
193            assert!(
194                !skill.content.trim().is_empty(),
195                "skill '{}' has empty content",
196                skill.name
197            );
198        }
199    }
200
201    #[test]
202    fn all_skill_names_are_unique() {
203        let skills = SkillInventory::all();
204        let mut names: Vec<&str> = skills.iter().map(|s| s.name).collect();
205        names.sort();
206        names.dedup();
207        assert_eq!(names.len(), skills.len(), "duplicate skill names found");
208    }
209
210    #[test]
211    fn level_skills_are_present() {
212        let skills = SkillInventory::all();
213        let level_skills: Vec<_> = skills
214            .iter()
215            .filter(|s| s.name.starts_with("level-"))
216            .collect();
217        assert_eq!(
218            level_skills.len(),
219            4,
220            "expected 4 level skills (set, show, list, remove), got {}",
221            level_skills.len()
222        );
223    }
224
225    #[test]
226    fn continuity_skills_are_present() {
227        let skills = SkillInventory::all();
228        let continuity_skills: Vec<_> = skills
229            .iter()
230            .filter(|s| {
231                matches!(
232                    s.name,
233                    "wake"
234                        | "dream"
235                        | "introspect"
236                        | "reflect"
237                        | "sense"
238                        | "sleep"
239                        | "guidebook"
240                        | "emerge"
241                        | "recede"
242                        | "status"
243                )
244            })
245            .collect();
246        assert_eq!(
247            continuity_skills.len(),
248            10,
249            "expected 10 continuity skills, got {}",
250            continuity_skills.len()
251        );
252    }
253
254    #[test]
255    fn vocabulary_domains_are_complete() {
256        let skills = SkillInventory::all();
257        for domain in &["texture", "sensation", "nature", "persona", "urge"] {
258            let domain_skills: Vec<_> = skills
259                .iter()
260                .filter(|s| s.name.starts_with(domain))
261                .collect();
262            assert_eq!(
263                domain_skills.len(),
264                4,
265                "expected 4 skills for domain '{domain}', got {}",
266                domain_skills.len()
267            );
268        }
269    }
270
271    #[test]
272    fn package_assets_include_skill_md() {
273        let assets = SkillPackage::assets();
274        let skill_md = assets.iter().find(|a| a.path == "skills/oneiros/SKILL.md");
275        assert!(skill_md.is_some(), "package should include SKILL.md");
276        assert!(
277            !skill_md.unwrap().content.contains("{{VERSION}}"),
278            "SKILL.md should have version stamped"
279        );
280    }
281
282    #[test]
283    fn package_assets_include_commands() {
284        let assets = SkillPackage::assets();
285        let commands: Vec<_> = assets
286            .iter()
287            .filter(|a| a.path.starts_with("commands/"))
288            .collect();
289        assert!(
290            !commands.is_empty(),
291            "package should include command documentation"
292        );
293        // Should match the skill inventory count
294        assert_eq!(
295            commands.len(),
296            SkillInventory::all().len(),
297            "command count should match skill inventory"
298        );
299    }
300
301    #[test]
302    fn package_version_is_stamped() {
303        let assets = SkillPackage::assets();
304        let plugin = assets
305            .iter()
306            .find(|a| a.path == ".claude-plugin/plugin.json")
307            .expect("package should include plugin.json");
308        assert!(
309            plugin.content.contains(VERSION),
310            "plugin.json should contain the current version"
311        );
312    }
313}