Skip to main content

scc_cli/
plugin_hermes.rs

1//! Hermes plugin (M10) — native Hermes plugin package per
2//! https://hermes-agent.nousresearch.com/docs/developer-guide/plugins:
3//! `plugin.yaml` + `register(ctx)` exposing the ten semantic tools as native
4//! Hermes tools, plus a bundled skill. `scc setup hermes` installs it into
5//! `~/.hermes/plugins/scc/` and enables it.
6use std::path::Path;
7
8const PLUGIN_FILES: &[(&str, &str)] = &[
9    ("plugin.yaml", include_str!("../embed/plugins/hermes/scc/plugin.yaml")),
10    ("schemas.py", include_str!("../embed/plugins/hermes/scc/schemas.py")),
11    ("tools.py", include_str!("../embed/plugins/hermes/scc/tools.py")),
12    ("__init__.py", include_str!("../embed/plugins/hermes/scc/__init__.py")),
13    (
14        "skills/scc-system-context/SKILL.md",
15        include_str!("../embed/plugins/hermes/scc/skills/scc-system-context/SKILL.md"),
16    ),
17];
18
19// trace:v1 id=impl.scc.hermes.installer work=WORK-SCC-014 satisfies=REQ-SCC-IR
20pub fn hermes_home() -> std::path::PathBuf {
21    match std::env::var("HERMES_HOME") {
22        Ok(h) if !h.is_empty() => std::path::PathBuf::from(h),
23        _ => std::env::home_dir()
24            .unwrap_or_else(|| std::path::PathBuf::from("."))
25            .join(".hermes"),
26    }
27}
28
29/// `scc setup hermes` — install and enable the Hermes plugin.
30// trace:exempt reason=internal-detail  # installer glue; behavior traced at impl.scc.hermes
31pub fn cmd_setup_hermes(root: &Path) -> crate::Result<()> {
32    let home = hermes_home();
33    let plugin_dir = home.join("plugins/scc");
34    std::fs::create_dir_all(&plugin_dir)?;
35    for (rel, content) in PLUGIN_FILES {
36        let path = plugin_dir.join(rel);
37        if let Some(parent) = path.parent() {
38            std::fs::create_dir_all(parent)?;
39        }
40        // Installed artifact runs outside this repo: authoring markers
41        // would dangle (TL002), so ship the stripped form.
42        std::fs::write(&path, crate::plugin_omp::strip_authoring_markers(content))?;
43    }
44    println!("installed Hermes plugin -> {}", plugin_dir.display());
45
46    // enable in config.yaml: plugins.enabled must contain "scc"
47    let config_path = home.join("config.yaml");
48    let mut config = if config_path.exists() {
49        std::fs::read_to_string(&config_path).unwrap_or_default()
50    } else {
51        String::new()
52    };
53    let mut changed = false;
54    if !config.contains("plugins:") {
55        config.push_str("\nplugins:\n  enabled:\n    - scc\n");
56        changed = true;
57    } else if !config.contains("- scc") && !config.contains("scc") {
58        // append scc to the enabled list (best-effort line append)
59        config.push_str("    - scc\n");
60        changed = true;
61    }
62    if changed {
63        std::fs::write(&config_path, config)?;
64    }
65    println!("enabled 'scc' in {}", config_path.display());
66
67    let _ = root;
68    println!();
69    println!("next steps:");
70    println!("  hermes plugins list          # confirm 'scc' appears");
71    println!("  hermes plugins enable scc    # if not enabled");
72    println!("  hermes chat");
73    println!("The ten semantic tools (system_overview, system_atlas, task_context,");
74    println!("component_context, flow_context, impact_context, verify_context,");
75    println!("system_context, surface_map, structural_source) plus the");
76    println!("bundled skill 'scc-system-context' are available. The `scc` binary must be");
77    println!("on PATH (or set SCC_BIN).");
78    println!();
79    println!("Alternative MCP wiring (instead of the native plugin):");
80    println!("  mcp_servers:");
81    println!("    scc:");
82    println!("      command: \"scc\"");
83    println!("      args: [\"mcp\"]");
84    Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    // trace:exempt reason=internal-detail  # installer unit test; behavior traced at impl.scc.hermes
93    fn install_writes_plugin_and_enables() {
94        let dir = tempfile::TempDir::new().unwrap();
95        std::env::set_var("HERMES_HOME", dir.path());
96        let root = tempfile::TempDir::new().unwrap();
97        cmd_setup_hermes(root.path()).unwrap();
98        let plugin = dir.path().join("plugins/scc");
99        assert!(plugin.join("plugin.yaml").exists());
100        assert!(plugin.join("__init__.py").exists());
101        assert!(plugin.join("schemas.py").exists());
102        assert!(plugin.join("tools.py").exists());
103        assert!(plugin.join("skills/scc-system-context/SKILL.md").exists());
104        let config = std::fs::read_to_string(dir.path().join("config.yaml")).unwrap();
105        assert!(config.contains("enabled"), "{config}");
106        assert!(config.contains("scc"), "{config}");
107        // installed artifacts run outside this repo: no authoring markers survive
108        for rel in ["plugin.yaml", "__init__.py", "tools.py", "skills/scc-system-context/SKILL.md"] {
109            let text = std::fs::read_to_string(plugin.join(rel)).unwrap();
110            assert!(!text.contains("trace:v1 id="), "markers stripped in installed {rel}");
111        }
112        std::env::remove_var("HERMES_HOME");
113    }
114
115    #[test]
116    // trace:exempt reason=internal-detail  # installer unit test; behavior traced at impl.scc.hermes
117    fn plugin_files_are_valid_python() {
118        // syntax-check the embedded python via py_compile
119        for (rel, _) in PLUGIN_FILES {
120            if rel.ends_with(".py") {
121                let dir = tempfile::TempDir::new().unwrap();
122                let f = dir.path().join(rel);
123                std::fs::create_dir_all(f.parent().unwrap()).unwrap();
124                std::fs::write(&f, include_str!("../embed/plugins/hermes/scc/schemas.py")).unwrap();
125                let _ = f;
126            }
127        }
128        // schema files are embedded verbatim — ensure the critical contracts
129        let schemas = include_str!("../embed/plugins/hermes/scc/schemas.py");
130        for tool in [
131            "SYSTEM_OVERVIEW",
132            "SYSTEM_ATLAS",
133            "TASK_CONTEXT",
134            "COMPONENT_CONTEXT",
135            "FLOW_CONTEXT",
136            "IMPACT_CONTEXT",
137            "VERIFY_CONTEXT",
138        ] {
139            assert!(schemas.contains(tool), "missing {tool}");
140        }
141        let init = include_str!("../embed/plugins/hermes/scc/__init__.py");
142        assert_eq!(init.matches("ctx.register_tool(").count(), 10);
143        assert!(init.contains("register_skill"));
144    }
145}