1use std::io::{BufRead, BufReader, Write};
6use std::path::Path;
7use std::process::{Command, Stdio};
8
9use crate::error::{Autom8Error, Result};
10use crate::prompts::{SPEC_JSON_CORRECTION_PROMPT, SPEC_JSON_PROMPT};
11use crate::spec::Spec;
12
13use super::stream::{extract_text_from_stream_line, extract_usage_from_result_line};
14use super::types::{ClaudeErrorInfo, ClaudeUsage};
15use super::utils::{extract_json, fix_json_syntax, truncate_json_preview};
16
17const MAX_JSON_RETRY_ATTEMPTS: u32 = 3;
18
19#[derive(Debug, Clone)]
21pub struct SpecGenerationResult {
22 pub spec: Spec,
23 pub usage: Option<ClaudeUsage>,
25}
26
27struct ClaudeCallResult {
29 output: String,
30 usage: Option<ClaudeUsage>,
31}
32
33pub fn run_for_spec_generation<F>(
36 spec_content: &str,
37 output_path: &Path,
38 mut on_output: F,
39) -> Result<SpecGenerationResult>
40where
41 F: FnMut(&str),
42{
43 let mut total_usage: Option<ClaudeUsage> = None;
44
45 let mut accumulate_usage = |call_usage: Option<ClaudeUsage>| {
47 if let Some(usage) = call_usage {
48 match &mut total_usage {
49 Some(existing) => existing.add(&usage),
50 None => total_usage = Some(usage),
51 }
52 }
53 };
54
55 let initial_prompt = SPEC_JSON_PROMPT.replace("{spec_content}", spec_content);
57 let call_result = run_claude_with_prompt(&initial_prompt, &mut on_output)?;
58 let mut full_output = call_result.output;
59 accumulate_usage(call_result.usage);
60
61 let mut json_str = if let Some(json) = extract_json(&full_output) {
63 json
64 } else if output_path.exists() {
65 std::fs::read_to_string(output_path).map_err(|e| {
67 Autom8Error::InvalidGeneratedSpec(format!("Failed to read generated file: {}", e))
68 })?
69 } else {
70 let preview = if full_output.len() > 200 {
71 format!("{}...", &full_output[..200])
72 } else {
73 full_output.clone()
74 };
75 return Err(Autom8Error::InvalidGeneratedSpec(format!(
76 "No valid JSON found in response. Response preview: {:?}",
77 preview
78 )));
79 };
80
81 let mut last_error: Option<serde_json::Error> = None;
83
84 for attempt in 1..=MAX_JSON_RETRY_ATTEMPTS {
85 match serde_json::from_str::<Spec>(&json_str) {
86 Ok(spec) => {
87 spec.save(output_path)?;
88 return Ok(SpecGenerationResult {
89 spec,
90 usage: total_usage,
91 });
92 }
93 Err(e) => {
94 last_error = Some(e);
95
96 if attempt == MAX_JSON_RETRY_ATTEMPTS {
97 break;
98 }
99
100 let retry_msg = format!(
101 "\nJSON malformed, retrying (attempt {}/{})...\n",
102 attempt + 1,
103 MAX_JSON_RETRY_ATTEMPTS
104 );
105 on_output(&retry_msg);
106
107 let correction_prompt = SPEC_JSON_CORRECTION_PROMPT
108 .replace("{spec_content}", spec_content)
109 .replace("{malformed_json}", &json_str)
110 .replace("{error_message}", &last_error.as_ref().unwrap().to_string())
111 .replace("{attempt}", &(attempt + 1).to_string())
112 .replace("{max_attempts}", &MAX_JSON_RETRY_ATTEMPTS.to_string());
113
114 let call_result = run_claude_with_prompt(&correction_prompt, &mut on_output)?;
115 full_output = call_result.output;
116 accumulate_usage(call_result.usage);
117
118 if let Some(json) = extract_json(&full_output) {
119 json_str = json;
120 } else {
121 json_str = full_output.clone();
122 }
123 }
124 }
125 }
126
127 on_output("\nAttempting programmatic JSON fix...\n");
129
130 let fixed_json = fix_json_syntax(&json_str);
131
132 match serde_json::from_str::<Spec>(&fixed_json) {
133 Ok(spec) => {
134 on_output("Programmatic fix succeeded!\n");
135 spec.save(output_path)?;
136 Ok(SpecGenerationResult {
137 spec,
138 usage: total_usage,
139 })
140 }
141 Err(fallback_err) => {
142 let agentic_error = last_error
143 .map(|e| e.to_string())
144 .unwrap_or_else(|| "Unknown error".to_string());
145 let fallback_error = fallback_err.to_string();
146
147 let json_preview = truncate_json_preview(&json_str, 500);
148
149 Err(Autom8Error::InvalidGeneratedSpec(format!(
150 "JSON generation failed after {} agentic attempts and programmatic fallback.\n\n\
151 Agent error: {}\n\n\
152 Fallback error: {}\n\n\
153 Malformed JSON preview:\n{}",
154 MAX_JSON_RETRY_ATTEMPTS, agentic_error, fallback_error, json_preview
155 )))
156 }
157 }
158}
159
160fn run_claude_with_prompt<F>(prompt: &str, mut on_output: F) -> Result<ClaudeCallResult>
162where
163 F: FnMut(&str),
164{
165 let mut child = Command::new("claude")
166 .args([
167 "--dangerously-skip-permissions",
168 "--print",
169 "--output-format",
170 "stream-json",
171 "--verbose",
172 ])
173 .stdin(Stdio::piped())
174 .stdout(Stdio::piped())
175 .stderr(Stdio::piped())
176 .spawn()
177 .map_err(|e| Autom8Error::ClaudeError(format!("Failed to spawn claude: {}", e)))?;
178
179 if let Some(mut stdin) = child.stdin.take() {
180 stdin
181 .write_all(prompt.as_bytes())
182 .map_err(|e| Autom8Error::ClaudeError(format!("Failed to write to stdin: {}", e)))?;
183 }
184
185 let stderr = child.stderr.take();
186
187 let stdout = child
188 .stdout
189 .take()
190 .ok_or_else(|| Autom8Error::ClaudeError("Failed to capture stdout".into()))?;
191
192 let reader = BufReader::new(stdout);
193 let mut full_output = String::new();
194 let mut usage: Option<ClaudeUsage> = None;
195
196 for line in reader.lines() {
197 let line = line.map_err(|e| Autom8Error::ClaudeError(format!("Read error: {}", e)))?;
198
199 if let Some(text) = extract_text_from_stream_line(&line) {
200 on_output(&text);
201 full_output.push_str(&text);
202 }
203
204 if let Some(line_usage) = extract_usage_from_result_line(&line) {
206 usage = Some(line_usage);
207 }
208 }
209
210 let status = child
211 .wait()
212 .map_err(|e| Autom8Error::ClaudeError(format!("Wait error: {}", e)))?;
213
214 if !status.success() {
215 let stderr_content = stderr
216 .map(|s| std::io::read_to_string(s).unwrap_or_default())
217 .unwrap_or_default();
218 let error_info = ClaudeErrorInfo::from_process_failure(
219 status,
220 if stderr_content.is_empty() {
221 None
222 } else {
223 Some(stderr_content)
224 },
225 );
226 return Err(Autom8Error::SpecGenerationFailed(error_info.message));
227 }
228
229 Ok(ClaudeCallResult {
230 output: full_output,
231 usage,
232 })
233}