1#![cfg_attr(not(test), allow(dead_code))]
7
8use std::path::Path;
9
10use uuid::Uuid;
11
12use crate::agent::{AgentAdapter, SpawnConfig};
13use crate::prompt::PromptPatterns;
14
15pub const KIRO_DEFAULT_MODEL: &str = "claude-opus-4.6-1m";
17
18pub struct KiroCliAdapter {
20 program: String,
22}
23
24impl KiroCliAdapter {
25 pub fn new(program: Option<String>) -> Self {
26 Self {
27 program: program.unwrap_or_else(|| "kiro".to_string()),
28 }
29 }
30}
31
32pub fn write_kiro_agent_config(
35 member_name: &str,
36 prompt: &str,
37 work_dir: &Path,
38) -> anyhow::Result<String> {
39 let agent_name = format!("batty-{member_name}");
40 let agents_dir = work_dir.join(".kiro").join("agents");
41 std::fs::create_dir_all(&agents_dir)?;
42 let config_path = agents_dir.join(format!("{agent_name}.json"));
43 let config = serde_json::json!({
44 "name": agent_name,
45 "description": format!("Batty-managed agent for {member_name}"),
46 "prompt": prompt,
47 "tools": ["*"],
48 "allowedTools": ["*"],
49 "model": KIRO_DEFAULT_MODEL,
50 "includeMcpJson": true
51 });
52 std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
53 Ok(agent_name)
54}
55
56impl AgentAdapter for KiroCliAdapter {
57 fn name(&self) -> &str {
58 "kiro-cli"
59 }
60
61 fn spawn_config(&self, task_description: &str, work_dir: &Path) -> SpawnConfig {
62 SpawnConfig {
63 program: self.program.clone(),
64 args: vec![
65 "chat".to_string(),
66 "--trust-all-tools".to_string(),
67 task_description.to_string(),
68 ],
69 work_dir: work_dir.to_string_lossy().to_string(),
70 env: vec![],
71 }
72 }
73
74 fn prompt_patterns(&self) -> PromptPatterns {
75 PromptPatterns::kiro_cli()
76 }
77
78 fn instruction_candidates(&self) -> &'static [&'static str] {
79 &["AGENTS.md", "CLAUDE.md"]
80 }
81
82 fn wrap_launch_prompt(&self, prompt: &str) -> String {
83 format!(
84 "You are running as Kiro under Batty supervision.\n\
85 Treat the launch context below as authoritative session context.\n\n\
86 {prompt}"
87 )
88 }
89
90 fn format_input(&self, response: &str) -> String {
91 format!("{response}\n")
92 }
93
94 fn launch_command(
95 &self,
96 prompt: &str,
97 _idle: bool,
98 _resume: bool,
99 _session_id: Option<&str>,
100 ) -> anyhow::Result<String> {
101 if !prompt.contains(' ') && prompt.starts_with("batty-") {
106 Ok(format!("exec kiro chat --trust-all-tools --agent {prompt}"))
107 } else {
108 let escaped = prompt.replace('\'', "'\\''");
109 Ok(format!(
110 "exec kiro chat --trust-all-tools --model {KIRO_DEFAULT_MODEL} '{escaped}'"
111 ))
112 }
113 }
114
115 fn new_session_id(&self) -> Option<String> {
116 Some(Uuid::new_v4().to_string())
117 }
118
119 fn health_check(&self) -> super::BackendHealth {
120 super::check_binary_available(&self.program)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn default_program_is_kiro() {
130 let adapter = KiroCliAdapter::new(None);
131 let config = adapter.spawn_config("test", Path::new("/tmp"));
132 assert_eq!(config.program, "kiro");
133 }
134
135 #[test]
136 fn spawn_uses_chat_with_trust_all_tools() {
137 let adapter = KiroCliAdapter::new(None);
138 let config = adapter.spawn_config("ship the patch", Path::new("/tmp/worktree"));
139 assert_eq!(
140 config.args,
141 vec![
142 "chat".to_string(),
143 "--trust-all-tools".to_string(),
144 "ship the patch".to_string()
145 ]
146 );
147 assert_eq!(config.work_dir, "/tmp/worktree");
148 }
149
150 #[test]
151 fn kiro_prefers_agents_md_instruction_order() {
152 let adapter = KiroCliAdapter::new(None);
153 assert_eq!(
154 adapter.instruction_candidates(),
155 &["AGENTS.md", "CLAUDE.md"]
156 );
157 }
158
159 #[test]
160 fn kiro_wraps_launch_prompt() {
161 let adapter = KiroCliAdapter::new(None);
162 let wrapped = adapter.wrap_launch_prompt("Launch body");
163 assert!(wrapped.contains("Kiro under Batty supervision"));
164 assert!(wrapped.contains("Launch body"));
165 }
166
167 #[test]
168 fn launch_command_with_agent_name() {
169 let adapter = KiroCliAdapter::new(None);
170 let cmd = adapter
171 .launch_command("batty-architect", false, false, None)
172 .unwrap();
173 assert_eq!(
174 cmd,
175 "exec kiro chat --trust-all-tools --agent batty-architect"
176 );
177 }
178
179 #[test]
180 fn launch_command_with_raw_prompt_fallback() {
181 let adapter = KiroCliAdapter::new(None);
182 let cmd = adapter
183 .launch_command("do the thing", false, false, None)
184 .unwrap();
185 assert!(cmd.contains("--model"));
186 assert!(cmd.contains("'do the thing'"));
187 }
188
189 #[test]
190 fn launch_command_escapes_single_quotes() {
191 let adapter = KiroCliAdapter::new(None);
192 let cmd = adapter
193 .launch_command("fix user's bug", false, false, None)
194 .unwrap();
195 assert!(cmd.contains("user'\\''s"));
196 }
197
198 #[test]
199 fn write_agent_config_creates_json() {
200 let tmp = tempfile::tempdir().unwrap();
201 let name =
202 write_kiro_agent_config("architect", "You are an architect", tmp.path()).unwrap();
203 assert_eq!(name, "batty-architect");
204 let config_path = tmp.path().join(".kiro/agents/batty-architect.json");
205 assert!(config_path.exists());
206 let content: serde_json::Value =
207 serde_json::from_str(&std::fs::read_to_string(&config_path).unwrap()).unwrap();
208 assert_eq!(content["prompt"], "You are an architect");
209 assert_eq!(content["model"], KIRO_DEFAULT_MODEL);
210 assert_eq!(content["tools"], serde_json::json!(["*"]));
211 assert_eq!(content["allowedTools"], serde_json::json!(["*"]));
212 }
213
214 #[test]
215 fn new_session_id_returns_uuid() {
216 let adapter = KiroCliAdapter::new(None);
217 let sid = adapter.new_session_id();
218 assert!(sid.is_some());
219 assert!(!sid.unwrap().is_empty());
220 }
221
222 #[test]
223 fn supports_resume_is_false() {
224 let adapter = KiroCliAdapter::new(None);
225 assert!(!adapter.supports_resume());
226 }
227}