1use anyhow::{Context, Result};
6use colored::Colorize;
7use std::fs;
8use std::path::PathBuf;
9
10use crate::config::Config;
11use crate::paths::LOGS_DIR;
12use crate::pid;
13use crate::spec::{self, SpecStatus};
14use crate::worktree;
15
16pub struct TakeoverResult {
18 pub spec_id: String,
19 pub analysis: String,
20 pub log_tail: String,
21 pub suggestion: String,
22 pub worktree_path: Option<String>,
23}
24
25pub fn cmd_takeover(id: &str, force: bool) -> Result<TakeoverResult> {
27 let specs_dir = PathBuf::from(crate::paths::SPECS_DIR);
28 if !specs_dir.exists() {
29 anyhow::bail!("Not a chant project (no .chant/ directory found)");
30 }
31
32 let mut spec = spec::resolve_spec(&specs_dir, id)?;
34 let spec_id = spec.id.clone();
35 let spec_path = specs_dir.join(format!("{}.md", spec_id));
36
37 println!("{} Taking over spec {}", "→".cyan(), spec_id.cyan());
38
39 let pid = pid::read_pid_file(&spec_id)?;
41 let was_running = if let Some(pid) = pid {
42 if pid::is_process_running(pid) {
43 println!(" {} Stopping running process (PID: {})", "•".cyan(), pid);
44 pid::stop_process(pid)?;
45 pid::remove_pid_file(&spec_id)?;
46 println!(" {} Process stopped", "✓".green());
47 true
48 } else {
49 println!(" {} Cleaning up stale PID file", "•".cyan());
50 pid::remove_pid_file(&spec_id)?;
51 false
52 }
53 } else {
54 if !force {
55 anyhow::bail!(
56 "Spec {} is not currently running. Use --force to analyze anyway.",
57 spec_id
58 );
59 }
60 false
61 };
62
63 let log_path = PathBuf::from(LOGS_DIR).join(format!("{}.log", spec_id));
65 let (log_tail, analysis) = if log_path.exists() {
66 let log_content = fs::read_to_string(&log_path)
67 .with_context(|| format!("Failed to read log file: {}", log_path.display()))?;
68
69 let tail = get_log_tail(&log_content, 50);
70 let analysis = analyze_log(&log_content);
71
72 (tail, analysis)
73 } else {
74 (
75 "No log file found".to_string(),
76 "No execution log available for analysis".to_string(),
77 )
78 };
79
80 let suggestion = generate_suggestion(&spec, &analysis);
82
83 let config = Config::load().ok();
85 let project_name = config.as_ref().map(|c| c.project.name.as_str());
86 let worktree_path = worktree::get_active_worktree(&spec_id, project_name);
87 let worktree_exists = worktree_path.is_some();
88 let worktree_path_str = worktree_path
89 .as_ref()
90 .map(|p| p.to_string_lossy().to_string());
91
92 if spec.frontmatter.status == SpecStatus::InProgress {
94 let _ = spec.set_status(SpecStatus::Paused);
95 }
96
97 let worktree_info = if let Some(ref path) = worktree_path_str {
99 format!(
100 "\n\n### Worktree Location\n\nWork should be done in the isolated worktree:\n```\ncd {}\n```\n",
101 path
102 )
103 } else {
104 "\n\n### Worktree Location\n\nWorktree no longer exists (agent may have cleaned up). If you need to continue working, recreate the worktree with `chant work <spec-id>`.\n".to_string()
105 };
106
107 let takeover_section = format!(
108 "\n\n## Takeover Analysis\n\n{}\n\n### Recent Log Activity\n\n```\n{}\n```\n{}\n### Recommendation\n\n{}\n",
109 analysis,
110 log_tail,
111 worktree_info,
112 suggestion
113 );
114
115 spec.body.push_str(&takeover_section);
116 spec.save(&spec_path)?;
117
118 println!("{} Updated spec with takeover analysis", "✓".green());
119 if was_running {
120 println!(" {} Status set to: paused", "•".cyan());
121 }
122 println!(" {} Analysis appended to spec body", "•".cyan());
123 if worktree_exists {
124 println!(
125 " {} Worktree at: {}",
126 "•".cyan(),
127 worktree_path_str.as_ref().unwrap()
128 );
129 } else {
130 println!(" {} Worktree no longer exists", "⚠".yellow());
131 }
132
133 Ok(TakeoverResult {
134 spec_id,
135 analysis,
136 log_tail,
137 suggestion,
138 worktree_path: worktree_path_str,
139 })
140}
141
142fn get_log_tail(log_content: &str, lines: usize) -> String {
144 log_content
145 .lines()
146 .rev()
147 .take(lines)
148 .collect::<Vec<_>>()
149 .into_iter()
150 .rev()
151 .collect::<Vec<_>>()
152 .join("\n")
153}
154
155fn analyze_log(log_content: &str) -> String {
157 let lines: Vec<&str> = log_content.lines().collect();
158
159 if lines.is_empty() {
160 return "Log is empty - no execution activity recorded.".to_string();
161 }
162
163 let mut analysis = Vec::new();
164
165 let error_indicators = ["error:", "failed:", "ERROR", "FAIL", "exception", "panic"];
167 let errors: Vec<&str> = lines
168 .iter()
169 .filter(|line| {
170 error_indicators
171 .iter()
172 .any(|indicator| line.to_lowercase().contains(indicator))
173 })
174 .copied()
175 .collect();
176
177 if !errors.is_empty() {
178 analysis.push(format!("Found {} error indicator(s) in log:", errors.len()));
179 for error in errors.iter().take(3) {
180 analysis.push(format!(" - {}", error.trim()));
181 }
182 if errors.len() > 3 {
183 analysis.push(format!(" ... and {} more", errors.len() - 3));
184 }
185 }
186
187 let tool_indicators = [
189 "<function_calls>",
190 "tool_name",
191 "Bash",
192 "Read",
193 "Edit",
194 "Write",
195 ];
196 let tool_uses: Vec<&str> = lines
197 .iter()
198 .filter(|line| {
199 tool_indicators
200 .iter()
201 .any(|indicator| line.contains(indicator))
202 })
203 .copied()
204 .collect();
205
206 if !tool_uses.is_empty() {
207 analysis.push(format!("\nAgent made {} tool call(s)", tool_uses.len()));
208 }
209
210 let completion_indicators = ["completed", "finished", "done", "success"];
212 let has_completion = lines.iter().any(|line| {
213 completion_indicators
214 .iter()
215 .any(|indicator| line.to_lowercase().contains(indicator))
216 });
217
218 if has_completion {
219 analysis.push("\nLog contains completion indicators.".to_string());
220 }
221
222 let total_lines = lines.len();
224 analysis.push(format!("\nLog contains {} lines of output.", total_lines));
225
226 if errors.is_empty() && !has_completion {
227 analysis.push("\nNo errors detected, but work appears incomplete.".to_string());
228 }
229
230 if analysis.is_empty() {
231 "Agent execution started but no significant activity detected.".to_string()
232 } else {
233 analysis.join("\n")
234 }
235}
236
237fn generate_suggestion(spec: &spec::Spec, analysis: &str) -> String {
239 let mut suggestions: Vec<String> = Vec::new();
240
241 if analysis.to_lowercase().contains("error") {
243 suggestions
244 .push("Review the errors in the log and address them before resuming.".to_string());
245 }
246
247 let unchecked = spec.count_unchecked_checkboxes();
249 if unchecked > 0 {
250 suggestions.push(format!(
251 "{} acceptance criteria remain unchecked.",
252 unchecked
253 ));
254 }
255
256 suggestions.push("Continue working on this spec manually or adjust the approach.".to_string());
258 suggestions
259 .push("When ready to resume automated work, use `chant work <spec-id>`.".to_string());
260
261 suggestions.join("\n")
262}