Skip to main content

release_kit/
skills.rs

1//! The agent skills: the payload, the user-scope record, and the installer.
2//!
3//! Skills land under the invoking user's home and never into a target
4//! repository. An agent resolves a skill by name across scopes, so a second
5//! copy under one name is a second entry offering the same skill, with no way
6//! for the operator to tell which one runs. One installed binary already
7//! serves every repository, and the skills routing into it belong at the same
8//! scope.
9//!
10//! That places every destination outside the reach of `rk init`, which has a
11//! target directory to compare against and a landing to refuse. Here there is
12//! no target and no manifest, so [`record`] stands in for one: it answers the
13//! single question the installer cannot otherwise answer — are these bytes
14//! ones we wrote?
15
16pub mod installer;
17pub mod record;
18
19pub use crate::digest::Digest;
20use crate::embedded;
21use crate::error::RkError;
22
23/// One embedded skill: its directory name and its `SKILL.md` text.
24#[derive(Debug)]
25pub struct Skill {
26    /// The directory name, which is also the skill's `name` frontmatter.
27    pub name: String,
28    /// The authored `SKILL.md`, byte-identical to the file under `skills/`.
29    pub text: &'static str,
30}
31
32/// Every embedded skill, sorted by name.
33///
34/// # Errors
35///
36/// Returns [`RkError::Other`] when a skill directory carries no readable
37/// UTF-8 `SKILL.md`. That is a defect in the payload this binary was built
38/// from, not something a caller can correct.
39pub fn all() -> Result<Vec<Skill>, RkError> {
40    let mut out = Vec::new();
41    for dir in embedded::SKILLS.dirs() {
42        let name = dir.path().to_string_lossy().into_owned();
43        let text = dir
44            .get_file(format!("{name}/SKILL.md"))
45            .and_then(include_dir::File::contents_utf8)
46            .ok_or_else(|| anyhow::anyhow!("payload skill carries no UTF-8 SKILL.md: {name}"))?;
47        out.push(Skill { name, text });
48    }
49    out.sort_by(|a, b| a.name.cmp(&b.name));
50    Ok(out)
51}
52
53/// One shared artifact: its path under the shared root, and its bytes.
54#[derive(Debug)]
55pub struct SharedArtifact {
56    /// The path relative to the shared root, as it lands.
57    pub path: String,
58    /// The authored bytes, byte-identical to the file under `skill-shared/`.
59    pub bytes: &'static [u8],
60}
61
62/// Every artifact the skills share, sorted by path.
63///
64/// These land once, outside the agent skill roots, because every skill names
65/// the same absolute path for them. A copy per skill would be one file to
66/// correct per agent root per skill; one copy is one.
67#[must_use]
68pub fn shared() -> Vec<SharedArtifact> {
69    embedded::walk(&embedded::SKILL_SHARED)
70        .into_iter()
71        .map(|(path, bytes)| SharedArtifact { path, bytes })
72        .collect()
73}
74
75#[cfg(test)]
76mod tests {
77    #![allow(clippy::expect_used)]
78
79    use super::{all, shared};
80
81    #[test]
82    fn the_payload_carries_the_shared_plan_gate() {
83        let shared = shared();
84        assert!(
85            shared
86                .iter()
87                .any(|artifact| artifact.path == "plan-gate.md"),
88            "the payload carries no shared plan gate"
89        );
90    }
91
92    #[test]
93    fn the_payload_carries_every_authored_skill() {
94        let skills = all().expect("the embedded skills read");
95        assert!(!skills.is_empty(), "the payload carries no skills");
96        for skill in &skills {
97            assert!(
98                skill.text.contains(&format!("name: {}", skill.name)),
99                "{}: the frontmatter name differs from the directory",
100                skill.name
101            );
102        }
103    }
104}