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