1pub mod claude;
9pub mod codex;
10pub mod grok;
11mod pack;
12
13pub use claude::ClaudePack;
14pub use codex::CodexPack;
15pub use grok::GrokPack;
16pub use pack::{Pack, PackContext};
17
18static GROK: GrokPack = GrokPack;
20static CLAUDE: ClaudePack = ClaudePack;
22static CODEX: CodexPack = CodexPack;
24
25const PACKS: &[&(dyn Pack + 'static)] = &[&GROK, &CLAUDE, &CODEX];
27
28#[must_use]
30pub fn available() -> Vec<&'static str> {
31 PACKS.iter().map(|pack| pack.name()).collect()
32}
33
34pub fn resolve(name: &str) -> Result<&'static dyn Pack, UnknownHarness> {
40 PACKS
41 .iter()
42 .copied()
43 .find(|pack| pack.name() == name)
44 .ok_or_else(|| UnknownHarness {
45 requested: name.to_owned(),
46 available: available(),
47 })
48}
49
50#[derive(Debug, thiserror::Error)]
52#[error("unknown harness `{requested}`; available: {}", .available.join(", "))]
53pub struct UnknownHarness {
54 pub requested: String,
56 pub available: Vec<&'static str>,
58}
59
60#[cfg(test)]
61mod tests {
62 #![allow(clippy::unnecessary_literal_bound)]
64
65 use super::*;
66 use async_trait::async_trait;
67 use locode_host::{Host, HostConfig};
68 use locode_protocol::{ContentBlock, Message, Role};
69 use locode_tools::{Registry, Tool, ToolCtx, ToolError, ToolKind, ToolOutput};
70 use serde::Serialize;
71 use serde_json::{Value, json};
72 use std::path::PathBuf;
73 use std::sync::Arc;
74 use tokio_util::sync::CancellationToken;
75
76 fn test_host() -> (tempfile::TempDir, Arc<Host>) {
78 let dir = tempfile::tempdir().unwrap();
79 let host = Arc::new(Host::new(HostConfig::new(dir.path())).unwrap());
80 (dir, host)
81 }
82
83 #[derive(Serialize)]
87 struct EchoOut {
88 echoed: String,
89 }
90 impl ToolOutput for EchoOut {
91 fn to_prompt_text(&self) -> String {
92 self.echoed.clone()
93 }
94 }
95
96 struct Echo;
97 #[async_trait]
98 impl Tool for Echo {
99 type Args = Value;
100 type Output = EchoOut;
101 fn kind(&self) -> ToolKind {
102 ToolKind::Shell
103 }
104 fn description(&self) -> &str {
105 "echo"
106 }
107 async fn run(&self, _ctx: &ToolCtx, args: Value) -> Result<EchoOut, ToolError> {
108 Ok(EchoOut {
109 echoed: args.to_string(),
110 })
111 }
112 }
113
114 struct FakePack;
115 impl Pack for FakePack {
116 fn name(&self) -> &'static str {
117 "fake"
118 }
119 fn register(&self, _host: &Arc<Host>, registry: &mut Registry) {
120 registry.register("alpha", Echo);
121 registry.register("beta", Echo);
122 }
123 fn preamble(&self, _ctx: &PackContext) -> Vec<Message> {
124 vec![Message {
125 role: Role::System,
126 content: vec![ContentBlock::Text {
127 text: "fake pack".into(),
128 }],
129 }]
130 }
131 }
132
133 struct DupPack;
134 impl Pack for DupPack {
135 fn name(&self) -> &'static str {
136 "dup"
137 }
138 fn register(&self, _host: &Arc<Host>, registry: &mut Registry) {
139 registry.register("alpha", Echo);
140 registry.register("alpha", Echo); }
142 fn preamble(&self, _ctx: &PackContext) -> Vec<Message> {
143 Vec::new()
144 }
145 }
146
147 fn ctx(headless: bool) -> PackContext {
148 PackContext {
149 cwd: PathBuf::from("/repo"),
150 os: "linux".into(),
151 shell: "/bin/bash".into(),
152 date: "2026-07-18".into(),
153 headless,
154 is_git_repo: false,
155 model: None,
156 os_version: None,
157 timezone: None,
158 strip_identity: false,
159 }
160 }
161
162 #[test]
165 fn resolve_grok_returns_grok_pack() {
166 assert_eq!(resolve("grok").unwrap().name(), "grok");
167 }
168
169 #[test]
170 fn available_lists_wired_packs() {
171 assert_eq!(available(), vec!["grok", "claude", "codex"]);
172 }
173
174 #[test]
175 fn resolve_claude_returns_claude_pack() {
176 assert_eq!(resolve("claude").unwrap().name(), "claude");
177 }
178
179 #[test]
180 fn resolve_codex_returns_codex_pack() {
181 assert_eq!(resolve("codex").unwrap().name(), "codex");
182 }
183
184 #[test]
185 fn unknown_harness_errors_clearly() {
186 let err = resolve("gpt").err().expect("unknown harness");
187 let msg = err.to_string();
188 assert!(msg.contains("gpt"), "{msg}");
189 assert!(msg.contains("grok"), "names the available packs: {msg}");
190 assert_eq!(err.requested, "gpt");
191 }
192
193 #[test]
196 fn pack_builds_expected_specs() {
197 let (_dir, host) = test_host();
198 let registry = FakePack.build_registry(&host);
199 assert!(registry.contains("alpha"));
200 assert!(registry.contains("beta"));
201 let specs = registry.specs();
202 let mut names: Vec<&str> = specs.iter().map(|s| s.name.as_str()).collect();
203 names.sort_unstable();
204 assert_eq!(names, vec!["alpha", "beta"]);
205 let alpha = specs.iter().find(|s| s.name == "alpha").unwrap();
207 assert_eq!(alpha.description, "echo");
208 }
209
210 #[tokio::test]
211 async fn pack_routes_to_impl() {
212 let (_dir, host) = test_host();
213 let registry = FakePack.build_registry(&host);
214 let tool_ctx = ToolCtx::new(
215 PathBuf::from("/repo"),
216 "c1".into(),
217 PathBuf::from("/repo"),
218 CancellationToken::new(),
219 );
220 let dispatched = registry
221 .dispatch("alpha", json!({ "x": 1 }), &tool_ctx)
222 .await;
223 assert!(dispatched.record.ok, "routes to the registered impl");
224 assert_eq!(dispatched.record.name, "alpha");
225 assert_eq!(dispatched.record.output["echoed"], json!(r#"{"x":1}"#));
227 }
228
229 #[test]
230 #[should_panic(expected = "duplicate tool registration")]
231 fn duplicate_registration_panics() {
232 let (_dir, host) = test_host();
233 let _ = DupPack.build_registry(&host);
234 }
235
236 #[test]
241 fn grok_tools_expose_kinds_for_the_approval_seam() {
242 use locode_tools::ToolKind;
243 let (_dir, host) = test_host();
244 let registry = resolve("grok").unwrap().build_registry(&host);
245 for (tool, kind) in [
246 ("run_terminal_cmd", ToolKind::Shell),
247 ("read_file", ToolKind::Read),
248 ("search_replace", ToolKind::Edit),
249 ("grep", ToolKind::Grep),
250 ("list_dir", ToolKind::Glob),
251 ] {
252 assert_eq!(registry.kind_of(tool), Some(kind), "kind_of({tool})");
253 }
254 assert_eq!(registry.kind_of("no_such_tool"), None);
255 }
256
257 #[test]
258 fn grok_registers_real_tools_and_preamble() {
259 let (_dir, host) = test_host();
260 let pack = resolve("grok").unwrap();
261 let registry = pack.build_registry(&host);
262 for tool in [
263 "run_terminal_cmd",
264 "read_file",
265 "search_replace",
266 "grep",
267 "list_dir",
268 ] {
269 assert!(registry.contains(tool), "grok pack registers {tool}");
270 }
271
272 let headless = pack.preamble(&ctx(true));
274 assert_eq!(headless.len(), 2);
275 assert_eq!(headless[0].role, Role::System);
276 assert_eq!(headless[1].role, Role::User);
277 let interactive = pack.preamble(&ctx(false));
278 assert_ne!(
279 headless, interactive,
280 "headless branch changes the identity"
281 );
282 }
283}