1#![cfg_attr(not(test), allow(dead_code))]
7
8use std::path::Path;
9
10use crate::agent::{AgentAdapter, SpawnConfig};
11use crate::prompt::PromptPatterns;
12
13pub struct KiroCliAdapter {
15 program: String,
17}
18
19impl KiroCliAdapter {
20 pub fn new(program: Option<String>) -> Self {
21 Self {
22 program: program.unwrap_or_else(|| "kiro".to_string()),
23 }
24 }
25}
26
27impl AgentAdapter for KiroCliAdapter {
28 fn name(&self) -> &str {
29 "kiro-cli"
30 }
31
32 fn spawn_config(&self, task_description: &str, work_dir: &Path) -> SpawnConfig {
33 SpawnConfig {
34 program: self.program.clone(),
35 args: vec![
36 "chat".to_string(),
37 "--mode".to_string(),
38 "agent".to_string(),
39 task_description.to_string(),
40 ],
41 work_dir: work_dir.to_string_lossy().to_string(),
42 env: vec![],
43 }
44 }
45
46 fn prompt_patterns(&self) -> PromptPatterns {
47 PromptPatterns::kiro_cli()
48 }
49
50 fn instruction_candidates(&self) -> &'static [&'static str] {
51 &["AGENTS.md", "CLAUDE.md"]
52 }
53
54 fn wrap_launch_prompt(&self, prompt: &str) -> String {
55 format!(
56 "You are running as Kiro under Batty supervision.\n\
57 Treat the launch context below as authoritative session context.\n\n\
58 {prompt}"
59 )
60 }
61
62 fn format_input(&self, response: &str) -> String {
63 format!("{response}\n")
64 }
65
66 fn reset_context_keys(&self) -> Vec<(String, bool)> {
67 vec![("C-c".to_string(), false)]
68 }
69
70 fn launch_command(
71 &self,
72 prompt: &str,
73 idle: bool,
74 _resume: bool,
75 _session_id: Option<&str>,
76 ) -> anyhow::Result<String> {
77 let escaped = prompt.replace('\'', "'\\''");
78 let prefix = "exec kiro chat --mode agent";
79 if idle {
80 Ok(prefix.to_string())
81 } else {
82 Ok(format!("{prefix} '{escaped}'"))
83 }
84 }
85
86 fn health_check(&self) -> super::BackendHealth {
87 super::check_binary_available(&self.program)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn default_program_is_kiro() {
97 let adapter = KiroCliAdapter::new(None);
98 let config = adapter.spawn_config("test", Path::new("/tmp"));
99 assert_eq!(config.program, "kiro");
100 }
101
102 #[test]
103 fn spawn_uses_chat_agent_mode() {
104 let adapter = KiroCliAdapter::new(None);
105 let config = adapter.spawn_config("ship the patch", Path::new("/tmp/worktree"));
106 assert_eq!(
107 config.args,
108 vec![
109 "chat".to_string(),
110 "--mode".to_string(),
111 "agent".to_string(),
112 "ship the patch".to_string()
113 ]
114 );
115 assert_eq!(config.work_dir, "/tmp/worktree");
116 }
117
118 #[test]
119 fn kiro_prefers_agents_md_instruction_order() {
120 let adapter = KiroCliAdapter::new(None);
121 assert_eq!(
122 adapter.instruction_candidates(),
123 &["AGENTS.md", "CLAUDE.md"]
124 );
125 }
126
127 #[test]
128 fn kiro_wraps_launch_prompt() {
129 let adapter = KiroCliAdapter::new(None);
130 let wrapped = adapter.wrap_launch_prompt("Launch body");
131 assert!(wrapped.contains("Kiro under Batty supervision"));
132 assert!(wrapped.contains("Launch body"));
133 }
134
135 #[test]
138 fn launch_command_active_includes_prompt() {
139 let adapter = KiroCliAdapter::new(None);
140 let cmd = adapter
141 .launch_command("do the thing", false, false, None)
142 .unwrap();
143 assert_eq!(cmd, "exec kiro chat --mode agent 'do the thing'");
144 }
145
146 #[test]
147 fn launch_command_idle_omits_prompt() {
148 let adapter = KiroCliAdapter::new(None);
149 let cmd = adapter
150 .launch_command("ignored", true, false, None)
151 .unwrap();
152 assert_eq!(cmd, "exec kiro chat --mode agent");
153 }
154
155 #[test]
156 fn launch_command_escapes_single_quotes() {
157 let adapter = KiroCliAdapter::new(None);
158 let cmd = adapter
159 .launch_command("fix user's bug", false, false, None)
160 .unwrap();
161 assert!(cmd.contains("user'\\''s"));
162 }
163
164 #[test]
165 fn new_session_id_returns_none() {
166 let adapter = KiroCliAdapter::new(None);
167 assert!(adapter.new_session_id().is_none());
168 }
169
170 #[test]
171 fn supports_resume_is_false() {
172 let adapter = KiroCliAdapter::new(None);
173 assert!(!adapter.supports_resume());
174 }
175}