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}