Extract a one-line commit message from the agent's transcript file. Reads the transcript JSONL, condenses it into structured entries, then looks for assistant text that appears **after the last tool call**. This is the wrap-up summary ("I've set up the project..."), not the opening analysis ("This appears to be a minimal repository..."). If there is no assistant text after the last tool call, re
(transcript_path: &std::path::Path)
| 74 | /// |
| 75 | /// This is purely a file read. No subprocess, no API call, no network. |
| 76 | pub(crate) fn summarize_from_transcript(transcript_path: &std::path::Path) -> Option<String> { |
| 77 | // Read the transcript file (best-effort, non-fatal) |
| 78 | let raw = match std::fs::read(transcript_path) { |
| 79 | Ok(data) if !data.is_empty() => data, |
| 80 | Ok(_) => { |
| 81 | log::debug!("Transcript file is empty"); |
| 82 | return None; |
| 83 | } |
| 84 | Err(e) => { |
| 85 | log::debug!("Could not read transcript for summarization: {}", e); |
| 86 | return None; |
| 87 | } |
| 88 | }; |
| 89 | |
| 90 | // Parse into condensed entries using the existing transcript parser |
| 91 | let entries = transcript::condense_claude_transcript(&raw); |
| 92 | if entries.is_empty() { |
| 93 | return None; |
| 94 | } |
| 95 | |
| 96 | // Find the index of the last tool call in the transcript. |
| 97 | // We only want assistant text that comes AFTER this point — that's |
| 98 | // the wrap-up summary. Text before tool calls is Claude planning, |
| 99 | // analyzing, or describing what it's about to do (not what it did). |
| 100 | let last_tool_idx = entries.iter().rposition(|e: &CondensedEntry| e.is_tool())?; // If no tool calls, no useful summary |
| 101 | |
| 102 | // Find the first assistant text entry AFTER the last tool call. |
| 103 | // Claude's wrap-up message typically comes right after the final tool use. |
| 104 | let wrap_up = entries[last_tool_idx + 1..] |
| 105 | .iter() |
| 106 | .find(|e: &&CondensedEntry| e.is_assistant() && e.content.is_some())?; |
| 107 | |
| 108 | let text = wrap_up.content.as_deref()?; |
| 109 | let text = text.trim(); |
| 110 | |
| 111 | if text.is_empty() { |
| 112 | return None; |
| 113 | } |
| 114 | |
| 115 | // Extract the first sentence. Claude's summaries often start with: |
| 116 | // "I've set up the project with TypeScript, Express, and tests." |
| 117 | // "The authentication bug has been fixed in login.rs." |
| 118 | // "Here's what I did:\n\n1. Created..." |
| 119 | // |
| 120 | // We want just that first sentence as a commit message. |
| 121 | let summary = extract_first_sentence(text); |
| 122 | |
| 123 | // Skip if the extracted sentence is too short to be useful, |
| 124 | // or if it looks like a question/filler rather than a summary. |
| 125 | if summary.len() <= 10 { |
| 126 | return None; |
| 127 | } |
| 128 | |
| 129 | Some(summary) |
| 130 | } |
| 131 | |
| 132 | /// Extract the first sentence from a block of text. |
| 133 | /// |