use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use crate::error::{Autom8Error, Result};
use crate::prompts::{SPEC_JSON_CORRECTION_PROMPT, SPEC_JSON_PROMPT};
use crate::spec::Spec;
use super::stream::{extract_text_from_stream_line, extract_usage_from_result_line};
use super::types::{ClaudeErrorInfo, ClaudeUsage};
use super::utils::{extract_json, fix_json_syntax, truncate_json_preview};
const MAX_JSON_RETRY_ATTEMPTS: u32 = 3;
#[derive(Debug, Clone)]
pub struct SpecGenerationResult {
pub spec: Spec,
pub usage: Option<ClaudeUsage>,
}
struct ClaudeCallResult {
output: String,
usage: Option<ClaudeUsage>,
}
pub fn run_for_spec_generation<F>(
spec_content: &str,
output_path: &Path,
mut on_output: F,
) -> Result<SpecGenerationResult>
where
F: FnMut(&str),
{
let mut total_usage: Option<ClaudeUsage> = None;
let mut accumulate_usage = |call_usage: Option<ClaudeUsage>| {
if let Some(usage) = call_usage {
match &mut total_usage {
Some(existing) => existing.add(&usage),
None => total_usage = Some(usage),
}
}
};
let initial_prompt = SPEC_JSON_PROMPT.replace("{spec_content}", spec_content);
let call_result = run_claude_with_prompt(&initial_prompt, &mut on_output)?;
let mut full_output = call_result.output;
accumulate_usage(call_result.usage);
let mut json_str = if let Some(json) = extract_json(&full_output) {
json
} else if output_path.exists() {
std::fs::read_to_string(output_path).map_err(|e| {
Autom8Error::InvalidGeneratedSpec(format!("Failed to read generated file: {}", e))
})?
} else {
let preview = if full_output.len() > 200 {
format!("{}...", &full_output[..200])
} else {
full_output.clone()
};
return Err(Autom8Error::InvalidGeneratedSpec(format!(
"No valid JSON found in response. Response preview: {:?}",
preview
)));
};
let mut last_error: Option<serde_json::Error> = None;
for attempt in 1..=MAX_JSON_RETRY_ATTEMPTS {
match serde_json::from_str::<Spec>(&json_str) {
Ok(spec) => {
spec.save(output_path)?;
return Ok(SpecGenerationResult {
spec,
usage: total_usage,
});
}
Err(e) => {
last_error = Some(e);
if attempt == MAX_JSON_RETRY_ATTEMPTS {
break;
}
let retry_msg = format!(
"\nJSON malformed, retrying (attempt {}/{})...\n",
attempt + 1,
MAX_JSON_RETRY_ATTEMPTS
);
on_output(&retry_msg);
let correction_prompt = SPEC_JSON_CORRECTION_PROMPT
.replace("{spec_content}", spec_content)
.replace("{malformed_json}", &json_str)
.replace("{error_message}", &last_error.as_ref().unwrap().to_string())
.replace("{attempt}", &(attempt + 1).to_string())
.replace("{max_attempts}", &MAX_JSON_RETRY_ATTEMPTS.to_string());
let call_result = run_claude_with_prompt(&correction_prompt, &mut on_output)?;
full_output = call_result.output;
accumulate_usage(call_result.usage);
if let Some(json) = extract_json(&full_output) {
json_str = json;
} else {
json_str = full_output.clone();
}
}
}
}
on_output("\nAttempting programmatic JSON fix...\n");
let fixed_json = fix_json_syntax(&json_str);
match serde_json::from_str::<Spec>(&fixed_json) {
Ok(spec) => {
on_output("Programmatic fix succeeded!\n");
spec.save(output_path)?;
Ok(SpecGenerationResult {
spec,
usage: total_usage,
})
}
Err(fallback_err) => {
let agentic_error = last_error
.map(|e| e.to_string())
.unwrap_or_else(|| "Unknown error".to_string());
let fallback_error = fallback_err.to_string();
let json_preview = truncate_json_preview(&json_str, 500);
Err(Autom8Error::InvalidGeneratedSpec(format!(
"JSON generation failed after {} agentic attempts and programmatic fallback.\n\n\
Agent error: {}\n\n\
Fallback error: {}\n\n\
Malformed JSON preview:\n{}",
MAX_JSON_RETRY_ATTEMPTS, agentic_error, fallback_error, json_preview
)))
}
}
}
fn run_claude_with_prompt<F>(prompt: &str, mut on_output: F) -> Result<ClaudeCallResult>
where
F: FnMut(&str),
{
let mut child = Command::new("claude")
.args([
"--dangerously-skip-permissions",
"--print",
"--output-format",
"stream-json",
"--verbose",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| Autom8Error::ClaudeError(format!("Failed to spawn claude: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(prompt.as_bytes())
.map_err(|e| Autom8Error::ClaudeError(format!("Failed to write to stdin: {}", e)))?;
}
let stderr = child.stderr.take();
let stdout = child
.stdout
.take()
.ok_or_else(|| Autom8Error::ClaudeError("Failed to capture stdout".into()))?;
let reader = BufReader::new(stdout);
let mut full_output = String::new();
let mut usage: Option<ClaudeUsage> = None;
for line in reader.lines() {
let line = line.map_err(|e| Autom8Error::ClaudeError(format!("Read error: {}", e)))?;
if let Some(text) = extract_text_from_stream_line(&line) {
on_output(&text);
full_output.push_str(&text);
}
if let Some(line_usage) = extract_usage_from_result_line(&line) {
usage = Some(line_usage);
}
}
let status = child
.wait()
.map_err(|e| Autom8Error::ClaudeError(format!("Wait error: {}", e)))?;
if !status.success() {
let stderr_content = stderr
.map(|s| std::io::read_to_string(s).unwrap_or_default())
.unwrap_or_default();
let error_info = ClaudeErrorInfo::from_process_failure(
status,
if stderr_content.is_empty() {
None
} else {
Some(stderr_content)
},
);
return Err(Autom8Error::SpecGenerationFailed(error_info.message));
}
Ok(ClaudeCallResult {
output: full_output,
usage,
})
}