1#[derive(Debug)]
4pub struct CommitMessageResult {
5 pub message: String,
7 pub success: bool,
9 pub _log_path: String,
11 pub generated_prompts: HashMap<String, String>,
13}
14
15pub struct CommitAttemptResult {
17 pub had_error: bool,
18 pub output_valid: bool,
19 pub message: Option<String>,
20 pub validation_detail: String,
21 pub auth_failure: bool,
22}
23
24pub fn run_commit_attempt(
35 ctx: &mut PhaseContext<'_>,
36 attempt: u32,
37 model_safe_diff: &str,
38 commit_agent: &str,
39) -> anyhow::Result<CommitAttemptResult> {
40 let prompt_key = format!("commit_message_attempt_{attempt}");
45 let (prompt, was_replayed, substitution_log) = build_commit_prompt(
46 &prompt_key,
47 ctx.template_context,
48 model_safe_diff,
49 ctx.workspace,
50 &ctx.prompt_history,
51 );
52
53 if let Some(log) = substitution_log {
56 if !log.is_complete() {
57 return Err(anyhow::anyhow!(
58 "Commit prompt has unresolved placeholders: {:?}",
59 log.unsubstituted
60 ));
61 }
62 }
63
64 if !was_replayed {
65 ctx.capture_prompt(&prompt_key, &prompt);
66 }
67
68 let mut runtime = PipelineRuntime {
69 timer: ctx.timer,
70 logger: ctx.logger,
71 colors: ctx.colors,
72 config: ctx.config,
73 executor: ctx.executor,
74 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
75 workspace: ctx.workspace,
76 };
77
78 let log_dir = Path::new(".agent/logs/commit_generation");
79 let mut session = CommitLogSession::new(
80 log_dir
81 .to_str()
82 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8"),
83 ctx.workspace,
84 )
85 .unwrap_or_else(|_| CommitLogSession::noop());
86 let mut attempt_log = session.new_attempt(commit_agent, "single");
87 attempt_log.set_prompt_size(prompt.len());
88 let diff_was_truncated =
92 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
93 attempt_log.set_diff_info(model_safe_diff.len(), diff_was_truncated);
94
95 let agent_config = ctx
96 .registry
97 .resolve_config(commit_agent)
98 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
99 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
100
101 let base_log_path = ctx.run_log_context.agent_log("commit", attempt, None);
103 let log_attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
104 &base_log_path,
105 ctx.workspace,
106 );
107 let logfile = if log_attempt == 0 {
108 base_log_path
109 .to_str()
110 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
111 .to_string()
112 } else {
113 ctx.run_log_context
114 .agent_log("commit", attempt, Some(log_attempt))
115 .to_str()
116 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
117 .to_string()
118 };
119
120 let log_header = format!(
123 "# Ralph Agent Invocation Log\n\
124 # Role: Commit\n\
125 # Agent: {}\n\
126 # Model Index: 0\n\
127 # Attempt: {}\n\
128 # Phase: CommitMessage\n\
129 # Timestamp: {}\n\n",
130 commit_agent,
131 log_attempt,
132 chrono::Utc::now().to_rfc3339()
133 );
134 ctx.workspace
135 .append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
136 .context("Failed to write agent log header - log would be incomplete without metadata")?;
137
138 let log_prefix = format!("commit_{attempt}"); let model_index = 0usize; let prompt_cmd = PromptCommand {
141 label: commit_agent,
142 display_name: commit_agent,
143 cmd_str: &cmd_str,
144 prompt: &prompt,
145 log_prefix: &log_prefix,
146 model_index: Some(model_index),
147 attempt: Some(log_attempt),
148 logfile: &logfile,
149 parser_type: agent_config.json_parser,
150 env_vars: &agent_config.env_vars,
151 };
152
153 let result = run_with_prompt(&prompt_cmd, &mut runtime)?;
154 let had_error = result.exit_code != 0;
155 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
156 attempt_log.set_raw_output(&result.stderr);
157
158 if auth_failure {
159 attempt_log.set_outcome(AttemptOutcome::ExtractionFailed(
160 "Authentication error detected".to_string(),
161 ));
162 if !session.is_noop() {
163 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
164 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
165 }
166 return Ok(CommitAttemptResult {
167 had_error,
168 output_valid: false,
169 message: None,
170 validation_detail: "Authentication error detected".to_string(),
171 auth_failure: true,
172 });
173 }
174
175 let extraction = extract_commit_message_from_file_with_workspace(ctx.workspace);
176 let (outcome, detail, extraction_result) = match extraction {
177 CommitExtractionOutcome::Valid(result) => (
178 AttemptOutcome::Success(result.clone().into_message()),
179 "Valid commit message extracted".to_string(),
180 Some(result),
181 ),
182 CommitExtractionOutcome::InvalidXml(detail) => (
183 AttemptOutcome::XsdValidationFailed(detail.clone()),
184 detail,
185 None,
186 ),
187 CommitExtractionOutcome::MissingFile(detail) => (
188 AttemptOutcome::ExtractionFailed(detail.clone()),
189 detail,
190 None,
191 ),
192 };
193 attempt_log.add_extraction_attempt(match &extraction_result {
194 Some(_) => ExtractionAttempt::success("XML", detail.clone()),
195 None => ExtractionAttempt::failure("XML", detail.clone()),
196 });
197 attempt_log.set_outcome(outcome.clone());
198
199 if !session.is_noop() {
200 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
201 let final_outcome = format!("{outcome}");
202 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
203 }
204
205 if let Some(result) = extraction_result {
206 let message = result.into_message();
207 return Ok(CommitAttemptResult {
208 had_error,
209 output_valid: true,
210 message: Some(message),
211 validation_detail: detail,
212 auth_failure: false,
213 });
214 }
215
216 Ok(CommitAttemptResult {
217 had_error,
218 output_valid: false,
219 message: None,
220 validation_detail: detail,
221 auth_failure: false,
222 })
223}
224
225pub fn generate_commit_message(
249 diff: &str,
250 registry: &AgentRegistry,
251 runtime: &mut PipelineRuntime,
252 commit_agent: &str,
253 template_context: &TemplateContext,
254 workspace: &dyn Workspace,
255 prompt_history: &HashMap<String, String>,
256) -> anyhow::Result<CommitMessageResult> {
257 let model_budget = model_budget_bytes_for_agent_name(commit_agent);
260 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
261 if truncated {
262 runtime.logger.warn(&format!(
263 "Diff size ({} KB) exceeds agent limit ({} KB). Truncated to {} KB.",
264 diff.len() / 1024,
265 model_budget / 1024,
266 model_safe_diff.len() / 1024
267 ));
268 }
269
270 let prompt_key = "commit_message_attempt_1";
271 let (prompt, was_replayed, substitution_log) = build_commit_prompt(
272 prompt_key,
273 template_context,
274 &model_safe_diff,
275 workspace,
276 prompt_history,
277 );
278 if let Some(log) = substitution_log {
279 if !log.is_complete() {
280 return Err(anyhow::anyhow!(
281 "Commit prompt has unresolved placeholders: {:?}",
282 log.unsubstituted
283 ));
284 }
285 }
286
287 let mut generated_prompts = HashMap::new();
288 if !was_replayed {
289 generated_prompts.insert(prompt_key.to_string(), prompt.clone());
290 }
291
292 let agent_config = registry
293 .resolve_config(commit_agent)
294 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
295 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
296
297 let log_prefix = ".agent/logs/commit_generation/commit_generation";
298 let model_index = 0usize;
299 let attempt = 1u32;
300 let agent_for_log = commit_agent.to_lowercase();
301 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
302 log_prefix,
303 &agent_for_log,
304 model_index,
305 attempt,
306 );
307 let prompt_cmd = PromptCommand {
308 label: commit_agent,
309 display_name: commit_agent,
310 cmd_str: &cmd_str,
311 prompt: &prompt,
312 log_prefix,
313 model_index: Some(model_index),
314 attempt: Some(attempt),
315 logfile: &logfile,
316 parser_type: agent_config.json_parser,
317 env_vars: &agent_config.env_vars,
318 };
319
320 let result = run_with_prompt(&prompt_cmd, runtime)?;
321 let had_error = result.exit_code != 0;
322 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
323 if auth_failure {
324 anyhow::bail!("Authentication error detected");
325 }
326
327 let extraction = extract_commit_message_from_file_with_workspace(workspace);
328 let result = match extraction {
329 CommitExtractionOutcome::Valid(result) => result,
330 CommitExtractionOutcome::InvalidXml(detail)
331 | CommitExtractionOutcome::MissingFile(detail) => anyhow::bail!(detail),
332 };
333
334 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
335
336 Ok(CommitMessageResult {
337 message: result.into_message(),
338 success: true,
339 _log_path: String::new(),
340 generated_prompts,
341 })
342}
343
344pub fn generate_commit_message_with_chain(
363 diff: &str,
364 registry: &AgentRegistry,
365 runtime: &mut PipelineRuntime,
366 agents: &[String],
367 template_context: &TemplateContext,
368 workspace: &dyn Workspace,
369 prompt_history: &HashMap<String, String>,
370) -> anyhow::Result<CommitMessageResult> {
371 if agents.is_empty() {
372 anyhow::bail!("No agents provided in commit chain");
373 }
374
375 let model_budget = effective_model_budget_bytes(agents);
377 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
378 if truncated {
379 runtime.logger.warn(&format!(
380 "Diff size ({} KB) exceeds chain limit ({} KB). Truncated to {} KB.",
381 diff.len() / 1024,
382 model_budget / 1024,
383 model_safe_diff.len() / 1024
384 ));
385 }
386
387 let mut last_error: Option<anyhow::Error> = None;
388 let mut generated_prompts = HashMap::new();
389
390 for (agent_index, commit_agent) in agents.iter().enumerate() {
391 let prompt_key = format!("commit_message_chain_attempt_{}", agent_index + 1);
392 let (prompt, was_replayed, substitution_log) = build_commit_prompt(
393 &prompt_key,
394 template_context,
395 &model_safe_diff,
396 workspace,
397 prompt_history,
398 );
399 if let Some(log) = substitution_log {
400 if !log.is_complete() {
401 return Err(anyhow::anyhow!(
402 "Commit prompt has unresolved placeholders: {:?}",
403 log.unsubstituted
404 ));
405 }
406 }
407
408 if !was_replayed {
409 generated_prompts.insert(prompt_key.clone(), prompt.clone());
410 }
411
412 let agent_config = match registry.resolve_config(commit_agent) {
413 Some(config) => config,
414 None => {
415 last_error = Some(anyhow::anyhow!("Agent not found: {}", commit_agent));
416 continue;
417 }
418 };
419 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
420
421 let log_prefix = ".agent/logs/commit_generation/commit_generation";
422 let model_index = agent_index;
423 let attempt = 1u32;
424 let agent_for_log = commit_agent.to_lowercase();
425 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
426 log_prefix,
427 &agent_for_log,
428 model_index,
429 attempt,
430 );
431 let prompt_cmd = PromptCommand {
432 label: commit_agent,
433 display_name: commit_agent,
434 cmd_str: &cmd_str,
435 prompt: &prompt,
436 log_prefix,
437 model_index: Some(model_index),
438 attempt: Some(attempt),
439 logfile: &logfile,
440 parser_type: agent_config.json_parser,
441 env_vars: &agent_config.env_vars,
442 };
443
444 let result = match run_with_prompt(&prompt_cmd, runtime) {
445 Ok(r) => r,
446 Err(e) => {
447 last_error = Some(e.into());
448 continue;
449 }
450 };
451
452 let had_error = result.exit_code != 0;
453 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
454
455 if auth_failure {
456 last_error = Some(anyhow::anyhow!("Authentication error detected"));
457 continue;
458 }
459
460 if had_error {
461 last_error = Some(anyhow::anyhow!(
462 "Agent {} failed with exit code {}",
463 commit_agent,
464 result.exit_code
465 ));
466 continue;
467 }
468
469 let extraction = extract_commit_message_from_file_with_workspace(workspace);
470 match extraction {
471 CommitExtractionOutcome::Valid(extracted) => {
472 archive_xml_file_with_workspace(
473 workspace,
474 Path::new(xml_paths::COMMIT_MESSAGE_XML),
475 );
476 return Ok(CommitMessageResult {
477 message: extracted.into_message(),
478 success: true,
479 _log_path: String::new(),
480 generated_prompts,
481 });
482 }
483 CommitExtractionOutcome::InvalidXml(detail)
484 | CommitExtractionOutcome::MissingFile(detail) => {
485 last_error = Some(anyhow::anyhow!(detail));
486 continue;
487 }
488 }
489 }
490
491 Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All agents in commit chain failed")))
493}