Skip to main content

claudectl_core/agent/
mod.rs

1//! Agent families — what claudectl knows about coding agents other than
2//! Claude Code.
3//!
4//! The hive's substrate is agent-neutral: gossip, merge, trust and
5//! membership move `KnowledgeUnit`s and know nothing about sessions. Its two
6//! *ends* are not — capture runs through a Claude Code hook and application
7//! lands in the brain's own prompt. This module holds the neutral pieces
8//! that let a foreign agent's experience reach the same substrate.
9//!
10//! See `docs/cross-agent-hive.md` (epic #521) for the design, and §9 of that
11//! document for the measurements the tables here are built from.
12
13pub mod codex;
14pub mod vocab;
15
16use std::path::PathBuf;
17
18use crate::helpers::dirs_home;
19
20/// Every agent family claudectl knows how to reason about.
21pub const KNOWN_FAMILIES: &[&str] = &[vocab::CLAUDE_CODE, vocab::CODEX];
22
23/// The home directory an agent family keeps its configuration, skills and
24/// transcripts in — `~/.claude` or `~/.codex`.
25///
26/// `None` for a family this build knows nothing about, which is how an
27/// artifact requiring a future agent stays correctly unsatisfiable rather
28/// than accidentally matching.
29pub fn home_for(family: &str) -> Option<PathBuf> {
30    match family {
31        vocab::CLAUDE_CODE => Some(dirs_home().join(".claude")),
32        vocab::CODEX => Some(dirs_home().join(".codex")),
33        _ => None,
34    }
35}
36
37/// Whether an agent family looks present on this machine.
38///
39/// Presence means *its state directory exists* — `~/.claude`, `~/.codex` —
40/// not that its binary is on PATH. The directory is the better signal for
41/// what claudectl does with the answer: a skill installs into that directory
42/// and is read from it on the agent's next run, so an agent whose config
43/// exists but whose binary lives somewhere unusual (a version manager, a
44/// wrapper script, a GUI app bundle) should still receive artifacts.
45pub fn is_family_present(family: &str) -> bool {
46    home_for(family).is_some_and(|p| p.is_dir())
47}
48
49/// Which known agent families look present on this machine.
50pub fn present_families() -> Vec<&'static str> {
51    KNOWN_FAMILIES
52        .iter()
53        .copied()
54        .filter(|f| is_family_present(f))
55        .collect()
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn known_families_all_resolve_to_a_home() {
64        for family in KNOWN_FAMILIES {
65            assert!(
66                home_for(family).is_some(),
67                "{family} is listed as known but has no home directory"
68            );
69        }
70    }
71
72    /// An unknown family must not resolve, so an artifact requiring a future
73    /// agent stays unsatisfiable rather than matching by accident.
74    #[test]
75    fn unknown_family_has_no_home_and_is_absent() {
76        assert!(home_for("some-future-agent").is_none());
77        assert!(!is_family_present("some-future-agent"));
78        assert!(!is_family_present(""));
79    }
80
81    #[test]
82    fn homes_are_distinct_and_dotted() {
83        let claude = home_for(vocab::CLAUDE_CODE).unwrap();
84        let codex = home_for(vocab::CODEX).unwrap();
85        assert_ne!(claude, codex);
86        assert!(claude.ends_with(".claude"));
87        assert!(codex.ends_with(".codex"));
88    }
89
90    #[test]
91    fn present_families_is_a_subset_of_known() {
92        for family in present_families() {
93            assert!(KNOWN_FAMILIES.contains(&family));
94        }
95    }
96}