ralph_workflow/phases/commit/runner/
attempt.rs1use crate::phases::commit_logging::CommitAttemptLog;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum CommitMessageOutcome {
9 Message(String),
11 Skipped { reason: String },
13}
14
15#[derive(Debug)]
17pub struct CommitMessageResult {
18 pub outcome: CommitMessageOutcome,
19}
20
21pub struct CommitAttemptResult {
23 pub had_error: bool,
24 pub output_valid: bool,
25 pub message: Option<String>,
26 pub skip_reason: Option<String>,
27 pub files: Vec<String>,
28 pub excluded_files: Vec<crate::reducer::state::pipeline::ExcludedFile>,
29 pub validation_detail: String,
30 pub auth_failure: bool,
31}
32
33pub fn run_commit_attempt(
52 ctx: &mut PhaseContext<'_>,
53 attempt: u32,
54 model_safe_diff: &str,
55 commit_agent: &str,
56) -> anyhow::Result<CommitAttemptResult> {
57 let (prompt, substitution_log) =
62 build_commit_prompt(ctx.template_context, model_safe_diff, ctx.workspace);
63
64 if !substitution_log.is_complete() {
67 return Err(anyhow::anyhow!(
68 "Commit prompt has unresolved placeholders: {:?}",
69 substitution_log.unsubstituted
70 ));
71 }
72
73 let log_dir = ctx
74 .run_log_context
75 .run_dir()
76 .join("debug")
77 .join("commit_generation");
78 let (session, attempt_number) =
79 crate::phases::commit::create_session_and_get_attempt_number(&log_dir, ctx.workspace);
80 let diff_was_truncated =
81 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
82 let attempt_log = CommitAttemptLog::with_basics(
83 attempt_number,
84 commit_agent,
85 "single",
86 prompt.len(),
87 model_safe_diff.len(),
88 diff_was_truncated,
89 );
90
91 let agent_config = ctx
92 .registry
93 .resolve_config(commit_agent)
94 .ok_or_else(|| anyhow::anyhow!("Agent not found: {commit_agent}"))?;
95 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
96
97 let base_log_path = ctx.run_log_context.agent_log("commit", attempt, None);
99 let log_attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
100 &base_log_path,
101 ctx.workspace,
102 );
103 let logfile = if log_attempt == 0 {
104 base_log_path
105 .to_str()
106 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
107 .to_string()
108 } else {
109 ctx.run_log_context
110 .agent_log("commit", attempt, Some(log_attempt))
111 .to_str()
112 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
113 .to_string()
114 };
115
116 let log_header = format!(
119 "# Ralph Agent Invocation Log\n\
120 # Role: Commit\n\
121 # Agent: {}\n\
122 # Model Index: 0\n\
123 # Attempt: {}\n\
124 # Phase: CommitMessage\n\
125 # Timestamp: {}\n\n",
126 commit_agent,
127 log_attempt,
128 chrono::Utc::now().to_rfc3339()
129 );
130 ctx.workspace
131 .append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
132 .context("Failed to write agent log header - log would be incomplete without metadata")?;
133
134 let log_prefix = format!("commit_{attempt}"); let model_index = 0usize; let prompt_cmd = PromptCommand {
137 label: commit_agent,
138 display_name: commit_agent,
139 cmd_str: &cmd_str,
140 prompt: &prompt,
141 log_prefix: &log_prefix,
142 model_index: Some(model_index),
143 attempt: Some(log_attempt),
144 logfile: &logfile,
145 parser_type: agent_config.json_parser,
146 env_vars: &agent_config.env_vars,
147 completion_output_path: Some(Path::new(xml_paths::COMMIT_MESSAGE_XML)),
148 };
149
150 let result = run_with_prompt(
151 &prompt_cmd,
152 &mut PipelineRuntime {
153 timer: ctx.timer,
154 logger: ctx.logger,
155 colors: ctx.colors,
156 config: ctx.config,
157 executor: ctx.executor,
158 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
159 workspace: ctx.workspace,
160 workspace_arc: std::sync::Arc::clone(&ctx.workspace_arc),
161 },
162 )?;
163 let had_error = result.exit_code != 0;
164 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
165
166 if auth_failure {
167 let attempt_log = attempt_log.with_raw_output(&result.stderr);
168 if !session.is_noop() {
169 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
170 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
171 }
172 return Ok(CommitAttemptResult {
173 had_error,
174 output_valid: false,
175 message: None,
176 skip_reason: None,
177 files: vec![],
178 excluded_files: vec![],
179 validation_detail: "Authentication error detected".to_string(),
180 auth_failure: true,
181 });
182 }
183
184 let attempt_log = attempt_log.with_raw_output(&result.stderr);
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
231 let attempt_log = attempt_log
232 .add_extraction_attempt(if extraction_succeeded {
233 ExtractionAttempt::success("XML", detail.clone())
234 } else {
235 ExtractionAttempt::failure("XML", detail.clone())
236 })
237 .with_outcome(outcome.clone());
238
239 if !session.is_noop() {
240 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
241 let final_outcome = format!("{outcome}");
242 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
243 }
244
245 if let Some(result) = extraction_result {
246 let message = result.into_message();
247 return Ok(CommitAttemptResult {
248 had_error,
249 output_valid: true,
250 message: Some(message),
251 skip_reason: None,
252 files,
253 excluded_files: excluded,
254 validation_detail: detail,
255 auth_failure: false,
256 });
257 }
258
259 Ok(CommitAttemptResult {
260 had_error,
261 output_valid: extraction_succeeded,
262 message: None,
263 skip_reason,
264 files,
265 excluded_files: excluded,
266 validation_detail: detail,
267 auth_failure: false,
268 })
269}