miyabi_cli/commands/
agent.rs

1//! Agent command - Run agents
2
3use crate::error::{CliError, Result};
4use colored::Colorize;
5use miyabi_agents::base::BaseAgent;
6use miyabi_agents::codegen::CodeGenAgent;
7use miyabi_agents::coordinator::CoordinatorAgent;
8use miyabi_agents::issue::IssueAgent;
9use miyabi_types::{AgentConfig, AgentType, Task};
10use std::collections::HashMap;
11
12pub struct AgentCommand {
13    pub agent_type: String,
14    pub issue: Option<u64>,
15}
16
17impl AgentCommand {
18    pub fn new(agent_type: String, issue: Option<u64>) -> Self {
19        Self { agent_type, issue }
20    }
21
22    pub async fn execute(&self) -> Result<()> {
23        println!(
24            "{}",
25            format!("🤖 Running {} agent...", self.agent_type)
26                .cyan()
27                .bold()
28        );
29
30        // Parse agent type
31        let agent_type = self.parse_agent_type()?;
32
33        // Load configuration
34        let config = self.load_config()?;
35
36        // Create and execute agent
37        match agent_type {
38            AgentType::CoordinatorAgent => {
39                self.run_coordinator_agent(config).await?;
40            }
41            AgentType::CodeGenAgent => {
42                self.run_codegen_agent(config).await?;
43            }
44            AgentType::IssueAgent => {
45                self.run_issue_agent(config).await?;
46            }
47            _ => {
48                println!(
49                    "{}",
50                    format!("Agent type {:?} not yet implemented", agent_type).yellow()
51                );
52            }
53        }
54
55        println!();
56        println!("{}", "✅ Agent completed successfully!".green().bold());
57
58        Ok(())
59    }
60
61    pub fn parse_agent_type(&self) -> Result<AgentType> {
62        match self.agent_type.to_lowercase().as_str() {
63            "coordinator" => Ok(AgentType::CoordinatorAgent),
64            "codegen" | "code-gen" => Ok(AgentType::CodeGenAgent),
65            "review" => Ok(AgentType::ReviewAgent),
66            "issue" => Ok(AgentType::IssueAgent),
67            "pr" => Ok(AgentType::PRAgent),
68            "deployment" | "deploy" => Ok(AgentType::DeploymentAgent),
69            _ => Err(CliError::InvalidAgentType(self.agent_type.clone())),
70        }
71    }
72
73    fn load_config(&self) -> Result<AgentConfig> {
74        // Get GitHub token from environment
75        let github_token =
76            std::env::var("GITHUB_TOKEN").map_err(|_| CliError::MissingGitHubToken)?;
77
78        // Get device identifier (optional)
79        let device_identifier = std::env::var("DEVICE_IDENTIFIER")
80            .unwrap_or_else(|_| hostname::get().unwrap().to_string_lossy().to_string());
81
82        // Parse repository owner and name from git remote
83        let (repo_owner, repo_name) = self.parse_git_remote()?;
84
85        // Load from .miyabi.yml or use defaults
86        Ok(AgentConfig {
87            device_identifier,
88            github_token,
89            repo_owner: Some(repo_owner),
90            repo_name: Some(repo_name),
91            use_task_tool: false,
92            use_worktree: true,
93            worktree_base_path: Some(".worktrees".to_string()),
94            log_directory: "./logs".to_string(),
95            report_directory: "./reports".to_string(),
96            tech_lead_github_username: None,
97            ciso_github_username: None,
98            po_github_username: None,
99            firebase_production_project: None,
100            firebase_staging_project: None,
101            production_url: None,
102            staging_url: None,
103        })
104    }
105
106    /// Parse repository owner and name from git remote URL
107    ///
108    /// Supports formats:
109    /// - https://github.com/owner/repo
110    /// - https://github.com/owner/repo.git
111    /// - git@github.com:owner/repo.git
112    fn parse_git_remote(&self) -> Result<(String, String)> {
113        // Run git remote get-url origin
114        let output = std::process::Command::new("git")
115            .args(["remote", "get-url", "origin"])
116            .output()
117            .map_err(|e| CliError::GitConfig(format!("Failed to run git command: {}", e)))?;
118
119        if !output.status.success() {
120            return Err(CliError::GitConfig(
121                "Failed to get git remote URL. Not a git repository?".to_string(),
122            ));
123        }
124
125        let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
126
127        // Parse HTTPS format: https://github.com/owner/repo(.git)?
128        if remote_url.starts_with("http") && remote_url.contains("github.com/") {
129            let parts: Vec<&str> = remote_url
130                .split("github.com/")
131                .nth(1)
132                .ok_or_else(|| CliError::GitConfig("Invalid GitHub URL".to_string()))?
133                .trim_end_matches(".git")
134                .split('/')
135                .collect();
136
137            if parts.len() >= 2 {
138                return Ok((parts[0].to_string(), parts[1].to_string()));
139            }
140        }
141
142        // Parse SSH format: git@github.com:owner/repo.git
143        if remote_url.starts_with("git@github.com:") {
144            let repo_part = remote_url
145                .strip_prefix("git@github.com:")
146                .ok_or_else(|| CliError::GitConfig("Invalid SSH URL".to_string()))?
147                .trim_end_matches(".git");
148
149            let parts: Vec<&str> = repo_part.split('/').collect();
150            if parts.len() >= 2 {
151                return Ok((parts[0].to_string(), parts[1].to_string()));
152            }
153        }
154
155        Err(CliError::GitConfig(format!(
156            "Could not parse GitHub owner/repo from remote URL: {}",
157            remote_url
158        )))
159    }
160
161    async fn run_coordinator_agent(&self, config: AgentConfig) -> Result<()> {
162        let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
163
164        println!("  Issue: #{}", issue_number);
165        println!("  Type: CoordinatorAgent (Task decomposition & DAG)");
166        println!();
167
168        // Create agent
169        let agent = CoordinatorAgent::new(config);
170
171        // Create task for coordinator
172        let task = Task {
173            id: format!("coordinator-issue-{}", issue_number),
174            title: format!("Coordinate Issue #{}", issue_number),
175            description: format!("Decompose Issue #{} into executable tasks", issue_number),
176            task_type: miyabi_types::task::TaskType::Feature,
177            priority: 1,
178            severity: None,
179            impact: None,
180            assigned_agent: Some(AgentType::CoordinatorAgent),
181            dependencies: vec![],
182            estimated_duration: Some(5),
183            status: None,
184            start_time: None,
185            end_time: None,
186            metadata: Some(HashMap::from([(
187                "issue_number".to_string(),
188                serde_json::json!(issue_number),
189            )])),
190        };
191
192        // Execute agent
193        println!("{}", "  Executing...".dimmed());
194        let result = agent.execute(&task).await?;
195
196        // Display results
197        println!();
198        println!("  Results:");
199        println!("    Status: {:?}", result.status);
200
201        if let Some(metrics) = result.metrics {
202            println!("    Duration: {}ms", metrics.duration_ms);
203        }
204
205        if let Some(data) = result.data {
206            println!("    Data: {}", serde_json::to_string_pretty(&data)?);
207        }
208
209        Ok(())
210    }
211
212    async fn run_codegen_agent(&self, config: AgentConfig) -> Result<()> {
213        let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
214
215        println!("  Issue: #{}", issue_number);
216        println!("  Type: CodeGenAgent (Code generation)");
217        println!();
218
219        // Create agent
220        let agent = CodeGenAgent::new(config);
221
222        // Create task for codegen
223        let task = Task {
224            id: format!("codegen-issue-{}", issue_number),
225            title: format!("Generate code for Issue #{}", issue_number),
226            description: format!("Implement solution for Issue #{}", issue_number),
227            task_type: miyabi_types::task::TaskType::Feature,
228            priority: 1,
229            severity: None,
230            impact: None,
231            assigned_agent: Some(AgentType::CodeGenAgent),
232            dependencies: vec![],
233            estimated_duration: Some(30),
234            status: None,
235            start_time: None,
236            end_time: None,
237            metadata: Some(HashMap::from([(
238                "issue_number".to_string(),
239                serde_json::json!(issue_number),
240            )])),
241        };
242
243        // Execute agent
244        println!("{}", "  Executing...".dimmed());
245        let result = agent.execute(&task).await?;
246
247        // Display results
248        println!();
249        println!("  Results:");
250        println!("    Status: {:?}", result.status);
251
252        if let Some(metrics) = result.metrics {
253            println!("    Duration: {}ms", metrics.duration_ms);
254            if let Some(lines_changed) = metrics.lines_changed {
255                println!("    Lines changed: {}", lines_changed);
256            }
257            if let Some(tests_added) = metrics.tests_added {
258                println!("    Tests added: {}", tests_added);
259            }
260        }
261
262        Ok(())
263    }
264
265    async fn run_issue_agent(&self, config: AgentConfig) -> Result<()> {
266        let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
267
268        println!("  Issue: #{}", issue_number);
269        println!("  Type: IssueAgent (Issue analysis & labeling)");
270        println!();
271
272        // Create agent
273        let agent = IssueAgent::new(config);
274
275        // Create task for issue analysis
276        let task = Task {
277            id: format!("issue-analysis-{}", issue_number),
278            title: format!("Analyze Issue #{}", issue_number),
279            description: format!("Classify Issue #{} and apply labels", issue_number),
280            task_type: miyabi_types::task::TaskType::Feature,
281            priority: 1,
282            severity: None,
283            impact: None,
284            assigned_agent: Some(AgentType::IssueAgent),
285            dependencies: vec![],
286            estimated_duration: Some(5),
287            status: None,
288            start_time: None,
289            end_time: None,
290            metadata: Some(HashMap::from([(
291                "issue_number".to_string(),
292                serde_json::json!(issue_number),
293            )])),
294        };
295
296        // Execute agent
297        println!("{}", "  Executing...".dimmed());
298        let result = agent.execute(&task).await?;
299
300        // Display results
301        println!();
302        println!("  Results:");
303        println!("    Status: {:?}", result.status);
304
305        if let Some(metrics) = result.metrics {
306            println!("    Duration: {}ms", metrics.duration_ms);
307        }
308
309        if let Some(data) = result.data {
310            // Try to parse as IssueAnalysis
311            if let Ok(analysis) = serde_json::from_value::<miyabi_types::IssueAnalysis>(data) {
312                println!("  Analysis:");
313                println!("    Issue Type: {:?}", analysis.issue_type);
314                println!("    Severity: {:?}", analysis.severity);
315                println!("    Impact: {:?}", analysis.impact);
316                println!("    Assigned Agent: {:?}", analysis.assigned_agent);
317                println!("    Estimated Duration: {} minutes", analysis.estimated_duration);
318                println!("    Dependencies: {}", analysis.dependencies.join(", "));
319                println!("    Applied Labels: {}", analysis.labels.join(", "));
320            }
321        }
322
323        Ok(())
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_parse_agent_type() {
333        let cmd = AgentCommand::new("coordinator".to_string(), None);
334        assert!(matches!(
335            cmd.parse_agent_type().unwrap(),
336            AgentType::CoordinatorAgent
337        ));
338
339        let cmd = AgentCommand::new("codegen".to_string(), None);
340        assert!(matches!(
341            cmd.parse_agent_type().unwrap(),
342            AgentType::CodeGenAgent
343        ));
344
345        let cmd = AgentCommand::new("code-gen".to_string(), None);
346        assert!(matches!(
347            cmd.parse_agent_type().unwrap(),
348            AgentType::CodeGenAgent
349        ));
350
351        let cmd = AgentCommand::new("invalid".to_string(), None);
352        assert!(cmd.parse_agent_type().is_err());
353    }
354
355    #[test]
356    fn test_agent_command_creation() {
357        let cmd = AgentCommand::new("coordinator".to_string(), Some(123));
358        assert_eq!(cmd.agent_type, "coordinator");
359        assert_eq!(cmd.issue, Some(123));
360
361        let cmd = AgentCommand::new("codegen".to_string(), None);
362        assert_eq!(cmd.agent_type, "codegen");
363        assert_eq!(cmd.issue, None);
364    }
365}