Skip to main content

autom8/claude/
commit.rs

1//! Commit message generation.
2//!
3//! Handles running Claude to create semantic commit messages.
4
5use std::io::{BufRead, BufReader, Write};
6use std::process::{Command, Stdio};
7
8use crate::error::{Autom8Error, Result};
9use crate::git;
10use crate::prompts::COMMIT_PROMPT;
11use crate::spec::Spec;
12
13use super::stream::{extract_text_from_stream_line, extract_usage_from_result_line};
14use super::types::{ClaudeErrorInfo, ClaudeUsage};
15
16/// Result from running Claude for commit.
17#[derive(Debug, Clone)]
18pub struct CommitResult {
19    pub outcome: CommitOutcome,
20    /// Token usage data from the Claude API response
21    pub usage: Option<ClaudeUsage>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum CommitOutcome {
26    /// Commit succeeded, with short commit hash
27    Success(String),
28    NothingToCommit,
29    Error(ClaudeErrorInfo),
30}
31
32/// Run Claude to commit changes after all stories are complete
33pub fn run_for_commit<F>(spec: &Spec, mut on_output: F) -> Result<CommitResult>
34where
35    F: FnMut(&str),
36{
37    // Build stories summary for context
38    let stories_summary = spec
39        .user_stories
40        .iter()
41        .map(|s| format!("- {}: {}", s.id, s.title))
42        .collect::<Vec<_>>()
43        .join("\n");
44
45    let prompt = COMMIT_PROMPT
46        .replace("{project}", &spec.project)
47        .replace("{feature_description}", &spec.description)
48        .replace("{stories_summary}", &stories_summary);
49
50    let mut child = Command::new("claude")
51        .args([
52            "--dangerously-skip-permissions",
53            "--print",
54            "--output-format",
55            "stream-json",
56            "--verbose",
57        ])
58        .stdin(Stdio::piped())
59        .stdout(Stdio::piped())
60        .stderr(Stdio::piped())
61        .spawn()
62        .map_err(|e| Autom8Error::ClaudeError(format!("Failed to spawn claude: {}", e)))?;
63
64    // Write prompt to stdin
65    if let Some(mut stdin) = child.stdin.take() {
66        stdin
67            .write_all(prompt.as_bytes())
68            .map_err(|e| Autom8Error::ClaudeError(format!("Failed to write to stdin: {}", e)))?;
69    }
70
71    // Take stderr handle before consuming stdout
72    let stderr = child.stderr.take();
73
74    // Stream stdout and check for "nothing to commit"
75    let stdout = child
76        .stdout
77        .take()
78        .ok_or_else(|| Autom8Error::ClaudeError("Failed to capture stdout".into()))?;
79
80    let reader = BufReader::new(stdout);
81    let mut nothing_to_commit = false;
82    let mut accumulated_text = String::new();
83    let mut usage: Option<ClaudeUsage> = None;
84
85    for line in reader.lines() {
86        let line = line.map_err(|e| Autom8Error::ClaudeError(format!("Read error: {}", e)))?;
87
88        // Parse stream-json output and extract text content
89        if let Some(text) = extract_text_from_stream_line(&line) {
90            on_output(&text);
91            accumulated_text.push_str(&text);
92
93            if text.to_lowercase().contains("nothing to commit")
94                || accumulated_text
95                    .to_lowercase()
96                    .contains("nothing to commit")
97            {
98                nothing_to_commit = true;
99            }
100        }
101
102        // Try to extract usage from result events
103        if let Some(line_usage) = extract_usage_from_result_line(&line) {
104            usage = Some(line_usage);
105        }
106    }
107
108    // Wait for process to complete
109    let status = child
110        .wait()
111        .map_err(|e| Autom8Error::ClaudeError(format!("Wait error: {}", e)))?;
112
113    if !status.success() {
114        let stderr_content = stderr
115            .map(|s| std::io::read_to_string(s).unwrap_or_default())
116            .unwrap_or_default();
117        let error_info = ClaudeErrorInfo::from_process_failure(
118            status,
119            if stderr_content.is_empty() {
120                None
121            } else {
122                Some(stderr_content)
123            },
124        );
125        return Ok(CommitResult {
126            outcome: CommitOutcome::Error(error_info),
127            usage,
128        });
129    }
130
131    let outcome = if nothing_to_commit {
132        CommitOutcome::NothingToCommit
133    } else {
134        // Get the short commit hash after successful commit
135        let commit_hash = git::latest_commit_short().unwrap_or_else(|_| "unknown".to_string());
136        CommitOutcome::Success(commit_hash)
137    };
138
139    Ok(CommitResult { outcome, usage })
140}