ralph_workflow/phases/commit/runner/
attempt.rs1#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum CommitMessageOutcome {
7 Message(String),
9 Skipped { reason: String },
11}
12
13#[derive(Debug)]
15pub struct CommitMessageResult {
16 pub outcome: CommitMessageOutcome,
17}
18
19pub struct CommitAttemptResult {
21 pub had_error: bool,
22 pub output_valid: bool,
23 pub message: Option<String>,
24 pub skip_reason: Option<String>,
25 pub files: Vec<String>,
26 pub excluded_files: Vec<crate::reducer::state::pipeline::ExcludedFile>,
27 pub validation_detail: String,
28 pub auth_failure: bool,
29}
30
31pub fn run_commit_attempt(
50 ctx: &mut PhaseContext<'_>,
51 attempt: u32,
52 model_safe_diff: &str,
53 commit_agent: &str,
54) -> anyhow::Result<CommitAttemptResult> {
55 let (prompt, substitution_log) =
60 build_commit_prompt(ctx.template_context, model_safe_diff, ctx.workspace);
61
62 if !substitution_log.is_complete() {
65 return Err(anyhow::anyhow!(
66 "Commit prompt has unresolved placeholders: {:?}",
67 substitution_log.unsubstituted
68 ));
69 }
70
71 let mut runtime = PipelineRuntime {
72 timer: ctx.timer,
73 logger: ctx.logger,
74 colors: ctx.colors,
75 config: ctx.config,
76 executor: ctx.executor,
77 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
78 workspace: ctx.workspace,
79 workspace_arc: std::sync::Arc::clone(&ctx.workspace_arc),
80 };
81
82 let log_dir = ctx
83 .run_log_context
84 .run_dir()
85 .join("debug")
86 .join("commit_generation");
87 let mut session = CommitLogSession::new(
88 log_dir
89 .to_str()
90 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8"),
91 ctx.workspace,
92 )
93 .unwrap_or_else(|_| CommitLogSession::noop());
94 let mut attempt_log = session.new_attempt(commit_agent, "single");
95 attempt_log.set_prompt_size(prompt.len());
96 let diff_was_truncated =
100 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
101 attempt_log.set_diff_info(model_safe_diff.len(), diff_was_truncated);
102
103 let agent_config = ctx
104 .registry
105 .resolve_config(commit_agent)
106 .ok_or_else(|| anyhow::anyhow!("Agent not found: {commit_agent}"))?;
107 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
108
109 let base_log_path = ctx.run_log_context.agent_log("commit", attempt, None);
111 let log_attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
112 &base_log_path,
113 ctx.workspace,
114 );
115 let logfile = if log_attempt == 0 {
116 base_log_path
117 .to_str()
118 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
119 .to_string()
120 } else {
121 ctx.run_log_context
122 .agent_log("commit", attempt, Some(log_attempt))
123 .to_str()
124 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
125 .to_string()
126 };
127
128 let log_header = format!(
131 "# Ralph Agent Invocation Log\n\
132 # Role: Commit\n\
133 # Agent: {}\n\
134 # Model Index: 0\n\
135 # Attempt: {}\n\
136 # Phase: CommitMessage\n\
137 # Timestamp: {}\n\n",
138 commit_agent,
139 log_attempt,
140 chrono::Utc::now().to_rfc3339()
141 );
142 ctx.workspace
143 .append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
144 .context("Failed to write agent log header - log would be incomplete without metadata")?;
145
146 let log_prefix = format!("commit_{attempt}"); let model_index = 0usize; let prompt_cmd = PromptCommand {
149 label: commit_agent,
150 display_name: commit_agent,
151 cmd_str: &cmd_str,
152 prompt: &prompt,
153 log_prefix: &log_prefix,
154 model_index: Some(model_index),
155 attempt: Some(log_attempt),
156 logfile: &logfile,
157 parser_type: agent_config.json_parser,
158 env_vars: &agent_config.env_vars,
159 };
160
161 let result = run_with_prompt(&prompt_cmd, &mut runtime)?;
162 let had_error = result.exit_code != 0;
163 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
164 attempt_log.set_raw_output(&result.stderr);
165
166 if auth_failure {
167 attempt_log.set_outcome(AttemptOutcome::ExtractionFailed(
168 "Authentication error detected".to_string(),
169 ));
170 if !session.is_noop() {
171 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
172 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
173 }
174 return Ok(CommitAttemptResult {
175 had_error,
176 output_valid: false,
177 message: None,
178 skip_reason: None,
179 files: vec![],
180 excluded_files: vec![],
181 validation_detail: "Authentication error detected".to_string(),
182 auth_failure: true,
183 });
184 }
185
186 let extraction = extract_commit_message_from_file_with_workspace(ctx.workspace);
187 let (outcome, detail, extraction_result, extraction_succeeded, skip_reason, files, excluded) =
188 match extraction {
189 CommitExtractionOutcome::Valid {
190 extracted: result,
191 files,
192 excluded_files,
193 } => (
194 AttemptOutcome::Success(result.clone().into_message()),
195 "Valid commit message extracted".to_string(),
196 Some(result),
197 true,
198 None,
199 files,
200 excluded_files,
201 ),
202 CommitExtractionOutcome::InvalidXml(detail) => (
203 AttemptOutcome::XsdValidationFailed(detail.clone()),
204 detail,
205 None,
206 false,
207 None,
208 vec![],
209 vec![],
210 ),
211 CommitExtractionOutcome::MissingFile(detail) => (
212 AttemptOutcome::ExtractionFailed(detail.clone()),
213 detail,
214 None,
215 false,
216 None,
217 vec![],
218 vec![],
219 ),
220 CommitExtractionOutcome::Skipped(reason) => (
221 AttemptOutcome::Success(format!("SKIPPED: {reason}")),
222 format!("Commit skipped: {reason}"),
223 None,
224 true,
225 Some(reason),
226 vec![],
227 vec![],
228 ),
229 };
230 attempt_log.add_extraction_attempt(if extraction_succeeded {
231 ExtractionAttempt::success("XML", detail.clone())
232 } else {
233 ExtractionAttempt::failure("XML", detail.clone())
234 });
235 attempt_log.set_outcome(outcome.clone());
236
237 if !session.is_noop() {
238 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
239 let final_outcome = format!("{outcome}");
240 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
241 }
242
243 if let Some(result) = extraction_result {
244 let message = result.into_message();
245 return Ok(CommitAttemptResult {
246 had_error,
247 output_valid: true,
248 message: Some(message),
249 skip_reason: None,
250 files,
251 excluded_files: excluded,
252 validation_detail: detail,
253 auth_failure: false,
254 });
255 }
256
257 Ok(CommitAttemptResult {
258 had_error,
259 output_valid: extraction_succeeded,
260 message: None,
261 skip_reason,
262 files,
263 excluded_files: excluded,
264 validation_detail: detail,
265 auth_failure: false,
266 })
267}