Skip to main content

batty_cli/agent/
kiro.rs

1//! Kiro CLI adapter.
2//!
3//! The locally installed Kiro CLI exposes `kiro chat [prompt]` with window and
4//! mode flags, but not stable session-management flags. Batty therefore treats
5//! Kiro as a launchable interactive backend without explicit resume support.
6#![cfg_attr(not(test), allow(dead_code))]
7
8use std::path::Path;
9
10use crate::agent::{AgentAdapter, SpawnConfig};
11use crate::prompt::PromptPatterns;
12
13/// Adapter for the Kiro CLI.
14pub struct KiroCliAdapter {
15    /// Override the kiro binary name/path (default: "kiro").
16    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    // --- Backend trait method tests ---
136
137    #[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}