1use crate::agents::AgentRole;
11use crate::checkpoint::restore::ResumeContext;
12use crate::checkpoint::{save_checkpoint_with_workspace, CheckpointBuilder, PipelinePhase};
13use crate::files::llm_output_extraction::xsd_validation::XsdValidationError;
14use crate::files::llm_output_extraction::{
15 archive_xml_file_with_workspace, extract_development_result_xml, extract_plan_xml,
16 extract_xml_with_file_fallback_with_workspace, validate_development_result_xml,
17 validate_plan_xml, xml_paths, PlanElements,
18};
19use crate::files::{delete_plan_file_with_workspace, update_status_with_workspace};
20use crate::format_xml_for_display;
21use crate::git_helpers::{git_snapshot, CommitResultFallback};
22use crate::logger::print_progress;
23use crate::phases::commit::commit_with_generated_message;
24use crate::phases::get_primary_commit_agent;
25use crate::phases::integrity::ensure_prompt_integrity;
26use crate::pipeline::{run_xsd_retry_with_session, PipelineRuntime, XsdRetryConfig};
27use crate::prompts::{
28 get_stored_or_generate_prompt, prompt_developer_iteration_continuation_xml,
29 prompt_developer_iteration_xml_with_context, prompt_developer_iteration_xsd_retry_with_context,
30 prompt_planning_xml_with_context, prompt_planning_xsd_retry_with_context, ContextLevel,
31};
32use crate::reducer::state::{ContinuationState, DevelopmentStatus};
33use std::path::Path;
34
35use super::context::PhaseContext;
36
37use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
38
39use std::time::Instant;
40
41const CONTINUATION_CONTEXT_PATH: &str = ".agent/tmp/continuation_context.md";
42
43pub struct DevelopmentResult {
45 pub had_errors: bool,
47}
48
49pub fn run_development_phase(
66 ctx: &mut PhaseContext<'_>,
67 start_iter: u32,
68 resume_context: Option<&ResumeContext>,
69) -> anyhow::Result<DevelopmentResult> {
70 let mut had_errors = false;
71 let mut prev_snap = git_snapshot()?;
72 let developer_context = ContextLevel::from(ctx.config.developer_context);
73
74 for i in start_iter..=ctx.config.developer_iters {
75 ctx.logger.subheader(&format!(
76 "Iteration {} of {}",
77 i, ctx.config.developer_iters
78 ));
79 print_progress(i, ctx.config.developer_iters, "Overall");
80
81 let resuming_into_development = resume_context.is_some() && i == start_iter;
82
83 if !resuming_into_development {
85 let _ = cleanup_continuation_context_file(ctx);
86 }
87
88 if resuming_into_development {
90 ctx.logger
91 .info("Resuming at development step; skipping plan generation");
92 } else {
93 run_planning_step(ctx, i)?;
94 }
95
96 let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
98 if !plan_ok {
99 anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
100 }
101 ctx.logger.success("PLAN.md created");
102
103 if ctx.config.features.checkpoint_enabled {
105 let builder = CheckpointBuilder::new()
106 .phase(PipelinePhase::Development, i, ctx.config.developer_iters)
107 .reviewer_pass(0, ctx.config.reviewer_reviews)
108 .capture_from_context(
109 ctx.config,
110 ctx.registry,
111 ctx.developer_agent,
112 ctx.reviewer_agent,
113 ctx.logger,
114 &ctx.run_context,
115 )
116 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
117 .with_execution_history(ctx.execution_history.clone())
118 .with_prompt_history(ctx.clone_prompt_history());
119
120 if let Some(checkpoint) = builder.build() {
121 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
122 }
123 }
124
125 ctx.record_developer_iteration();
127
128 ctx.logger.info("Executing plan...");
130 update_status_with_workspace(
131 ctx.workspace,
132 "Starting development iteration",
133 ctx.config.isolation_mode,
134 )?;
135
136 let continuation_state = if resuming_into_development {
140 load_continuation_state_from_context_file(ctx.workspace).unwrap_or_default()
141 } else {
142 ContinuationState::new()
143 };
144 let max_continuations = ctx.config.max_dev_continuations.unwrap_or(2) as usize;
145 let max_total_attempts = 1 + max_continuations;
146 let continuation_config = ContinuationConfig {
147 state: &continuation_state,
148 max_attempts: max_total_attempts,
149 };
150
151 let dev_start_time = Instant::now();
152 let dev_result = run_development_iteration_with_xml_retry(
153 ctx,
154 i,
155 developer_context,
156 resuming_into_development,
157 resume_context,
158 None,
159 continuation_config,
160 )?;
161
162 let _ = cleanup_continuation_context_file(ctx);
164
165 if dev_result.had_error {
166 ctx.logger.error(&format!(
167 "Iteration {i} encountered an error but continuing"
168 ));
169 had_errors = true;
170 }
171
172 ctx.stats.developer_runs_completed += 1;
174
175 {
177 let duration = dev_start_time.elapsed().as_secs();
178 let outcome = if dev_result.had_error {
179 StepOutcome::failure("Agent exited with non-zero code".to_string(), true)
180 } else {
181 StepOutcome::success(
182 dev_result.summary.clone(),
183 dev_result.files_changed.clone().unwrap_or_default(),
184 )
185 };
186 let step = ExecutionStep::new("Development", i, "dev_run", outcome)
187 .with_agent(ctx.developer_agent)
188 .with_duration(duration);
189 ctx.execution_history.add_step(step);
190 }
191 update_status_with_workspace(
192 ctx.workspace,
193 "Completed progress step",
194 ctx.config.isolation_mode,
195 )?;
196
197 if let Some(ref summary) = dev_result.summary {
199 ctx.logger
200 .info(&format!("Development summary: {}", summary));
201 }
202
203 let snap = git_snapshot()?;
204 if snap == prev_snap {
205 if snap.is_empty() {
206 ctx.logger
207 .warn("No git-status change detected (repository is clean)");
208 } else {
209 ctx.logger.warn(&format!(
210 "No git-status change detected (existing changes: {})",
211 snap.lines().count()
212 ));
213 }
214 } else {
215 ctx.logger.success(&format!(
216 "Repository modified ({} file(s) changed)",
217 snap.lines().count()
218 ));
219 ctx.stats.changes_detected += 1;
220 handle_commit_after_development(ctx, i)?;
221 }
222 prev_snap = snap;
223
224 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
226 run_fast_check(ctx, fast_cmd, i)?;
227 }
228
229 ensure_prompt_integrity(ctx.workspace, ctx.logger, "development", i);
232
233 ctx.logger.info("Deleting PLAN.md...");
235 if let Err(err) = delete_plan_file_with_workspace(ctx.workspace) {
236 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
237 }
238 ctx.logger.success("PLAN.md deleted");
239
240 if ctx.config.features.checkpoint_enabled {
243 let next_iteration = i + 1;
244 let builder = CheckpointBuilder::new()
245 .phase(
246 PipelinePhase::Development,
247 next_iteration,
248 ctx.config.developer_iters,
249 )
250 .reviewer_pass(0, ctx.config.reviewer_reviews)
251 .capture_from_context(
252 ctx.config,
253 ctx.registry,
254 ctx.developer_agent,
255 ctx.reviewer_agent,
256 ctx.logger,
257 &ctx.run_context,
258 )
259 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
260 .with_execution_history(ctx.execution_history.clone())
261 .with_prompt_history(ctx.clone_prompt_history());
262
263 if let Some(checkpoint) = builder.build() {
264 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
265 }
266 }
267 }
268
269 Ok(DevelopmentResult { had_errors })
270}
271
272#[derive(Debug)]
274pub struct DevIterationResult {
275 pub had_error: bool,
277 pub summary: Option<String>,
279 pub files_changed: Option<Vec<String>>,
281}
282
283#[derive(Debug, Clone)]
287pub struct ContinuationConfig<'a> {
288 pub state: &'a ContinuationState,
290 pub max_attempts: usize,
292}
293
294#[derive(Debug, Clone)]
296pub struct DevAttemptResult {
297 pub had_error: bool,
299 pub output_valid: bool,
301 pub status: DevelopmentStatus,
303 pub summary: String,
305 pub files_changed: Option<Vec<String>>,
307 pub next_steps: Option<String>,
309}
310
311pub fn run_development_attempt_with_xml_retry(
317 ctx: &mut PhaseContext<'_>,
318 iteration: u32,
319 _developer_context: ContextLevel,
320 _resuming_into_development: bool,
321 _resume_context: Option<&ResumeContext>,
322 _agent: Option<&str>,
323 continuation_state: &ContinuationState,
324) -> anyhow::Result<DevAttemptResult> {
325 let prompt_md = ctx
326 .workspace
327 .read(Path::new("PROMPT.md"))
328 .unwrap_or_default();
329 let plan_md = ctx
330 .workspace
331 .read(Path::new(".agent/PLAN.md"))
332 .unwrap_or_default();
333 let log_dir = format!(".agent/logs/developer_{iteration}");
334
335 let max_xsd_retries = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize;
336 let is_continuation = continuation_state.is_continuation();
337 let mut had_error = false;
338
339 let mut xsd_error: Option<String> = None;
340 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
341
342 for retry_num in 0..max_xsd_retries {
345 let is_retry = retry_num > 0;
346 let total_attempts = retry_num + 1;
347
348 if is_retry {
351 use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
352
353 let xml_path =
354 Path::new(crate::files::llm_output_extraction::xml_paths::DEVELOPMENT_RESULT_XML);
355 let _ = check_and_cleanup_xml_before_retry_with_workspace(
356 ctx.workspace,
357 xml_path,
358 ctx.logger,
359 );
360 }
361
362 let dev_prompt = if !is_retry && !is_continuation {
365 let prompt_key = format!("development_{}", iteration);
367 let (prompt, was_replayed) =
368 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
369 prompt_developer_iteration_xml_with_context(
370 ctx.template_context,
371 &prompt_md,
372 &plan_md,
373 )
374 });
375
376 if !was_replayed {
377 ctx.capture_prompt(&prompt_key, &prompt);
378 } else {
379 ctx.logger.info(&format!(
380 "Using stored prompt from checkpoint for determinism: {}",
381 prompt_key
382 ));
383 }
384
385 prompt
386 } else if !is_continuation {
387 ctx.logger.info(&format!(
389 " In-session retry {}/{} for XSD validation (total attempt: {})",
390 retry_num,
391 max_xsd_retries - 1,
392 total_attempts
393 ));
394 if let Some(ref error) = xsd_error {
395 ctx.logger.info(&format!(" XSD error: {}", error));
396 }
397
398 let last_output = read_last_development_output(Path::new(&log_dir), ctx.workspace);
399
400 prompt_developer_iteration_xsd_retry_with_context(
401 ctx.template_context,
402 &prompt_md,
403 &plan_md,
404 xsd_error.as_deref().unwrap_or("Unknown error"),
405 &last_output,
406 ctx.workspace,
407 )
408 } else if !is_retry {
409 ctx.logger.info(&format!(
411 " Continuation attempt {} (XSD validation attempt {}/{})",
412 continuation_state.continuation_attempt, 1, max_xsd_retries
413 ));
414
415 let prompt_key = format!(
416 "development_{}_continuation_{}",
417 iteration, continuation_state.continuation_attempt
418 );
419 let (prompt, was_replayed) =
420 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
421 prompt_developer_iteration_continuation_xml(
422 ctx.template_context,
423 continuation_state,
424 )
425 });
426
427 if !was_replayed {
428 ctx.capture_prompt(&prompt_key, &prompt);
429 } else {
430 ctx.logger.info(&format!(
431 "Using stored prompt from checkpoint for determinism: {}",
432 prompt_key
433 ));
434 }
435
436 prompt
437 } else {
438 ctx.logger.info(&format!(
440 " Continuation retry {}/{} for XSD validation (total attempt: {})",
441 retry_num,
442 max_xsd_retries - 1,
443 total_attempts
444 ));
445 if let Some(ref error) = xsd_error {
446 ctx.logger.info(&format!(" XSD error: {}", error));
447 }
448
449 let last_output = read_last_development_output(Path::new(&log_dir), ctx.workspace);
450
451 prompt_developer_iteration_xsd_retry_with_context(
452 ctx.template_context,
453 &prompt_md,
454 &plan_md,
455 xsd_error.as_deref().unwrap_or("Unknown error"),
456 &last_output,
457 ctx.workspace,
458 )
459 };
460
461 let exit_code = {
465 let mut runtime = PipelineRuntime {
466 timer: ctx.timer,
467 logger: ctx.logger,
468 colors: ctx.colors,
469 config: ctx.config,
470 executor: ctx.executor,
471 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
472 workspace: ctx.workspace,
473 };
474 let base_label = format!(
475 "run #{}{}",
476 iteration,
477 if is_continuation {
478 format!(
479 " (continuation {})",
480 continuation_state.continuation_attempt
481 )
482 } else {
483 String::new()
484 }
485 );
486 let mut xsd_retry_config = XsdRetryConfig {
487 role: AgentRole::Developer,
488 base_label: &base_label,
489 prompt: &dev_prompt,
490 logfile_prefix: &log_dir,
491 runtime: &mut runtime,
492 registry: ctx.registry,
493 primary_agent: _agent.unwrap_or(ctx.developer_agent),
494 session_info: session_info.as_ref(),
495 retry_num,
496 output_validator: None,
497 workspace: ctx.workspace,
498 };
499 run_xsd_retry_with_session(&mut xsd_retry_config)?
500 };
501
502 if exit_code != 0 {
503 had_error = true;
504 }
505
506 let log_dir_path = Path::new(&log_dir);
508 let dev_content = read_last_development_output(log_dir_path, ctx.workspace);
509
510 if session_info.is_none() {
513 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
514 ctx.logger.info(&format!(
515 " [dev] Extracting session from {:?} with parser {:?}",
516 log_dir_path, agent_config.json_parser
517 ));
518 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
519 log_dir_path,
520 agent_config.json_parser,
521 Some(ctx.developer_agent),
522 ctx.workspace,
523 );
524 if let Some(ref info) = session_info {
525 ctx.logger.info(&format!(
526 " [dev] Extracted session: agent={}, session_id={}...",
527 info.agent_name,
528 &info.session_id[..8.min(info.session_id.len())]
529 ));
530 } else {
531 ctx.logger
532 .warn(" [dev] Failed to extract session info from log");
533 }
534 }
535 }
536
537 let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
539 ctx.workspace,
540 Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
541 &dev_content,
542 extract_development_result_xml,
543 )
544 .unwrap_or_else(|| {
545 dev_content.clone()
548 });
549
550 match validate_development_result_xml(&xml_to_validate) {
551 Ok(result_elements) => {
552 let formatted_xml = format_xml_for_display(&xml_to_validate);
554
555 archive_xml_file_with_workspace(
557 ctx.workspace,
558 Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
559 );
560
561 if is_retry {
562 ctx.logger
563 .success(&format!("Status validated after {} retries", retry_num));
564 } else {
565 ctx.logger.success("Status extracted and validated (XML)");
566 }
567
568 ctx.logger.info(&format!("\n{}", formatted_xml));
569
570 let files_changed = result_elements
571 .files_changed
572 .as_ref()
573 .map(|f| f.lines().map(|s| s.to_string()).collect());
574
575 let status = if result_elements.is_completed() {
576 DevelopmentStatus::Completed
577 } else if result_elements.is_partial() {
578 DevelopmentStatus::Partial
579 } else {
580 DevelopmentStatus::Failed
581 };
582
583 return Ok(DevAttemptResult {
584 had_error,
585 output_valid: true,
586 status,
587 summary: result_elements.summary.clone(),
588 files_changed,
589 next_steps: result_elements.next_steps.clone(),
590 });
591 }
592 Err(xsd_err) => {
593 let error_msg = format_xsd_error(&xsd_err);
594 ctx.logger
595 .warn(&format!(" XSD validation failed: {}", error_msg));
596
597 if retry_num < max_xsd_retries - 1 {
598 xsd_error = Some(error_msg);
599 continue;
600 }
601
602 ctx.logger.warn(&format!(
603 " XSD retries exhausted ({}/{}). Will attempt fresh continuation.",
604 retry_num + 1,
605 max_xsd_retries
606 ));
607 break;
608 }
609 }
610 }
611
612 Ok(DevAttemptResult {
613 had_error,
614 output_valid: false,
615 status: DevelopmentStatus::Failed,
616 summary: "XML output failed validation. Your previous (invalid) output is at .agent/tmp/last_output.xml for reference.".to_string(),
617 files_changed: None,
618 next_steps: Some(
619 "Complete the task and provide valid XML output conforming to the XSD schema."
620 .to_string(),
621 ),
622 })
623}
624
625pub fn run_development_iteration_with_xml_retry(
651 ctx: &mut PhaseContext<'_>,
652 iteration: u32,
653 _developer_context: ContextLevel,
654 _resuming_into_development: bool,
655 _resume_context: Option<&ResumeContext>,
656 _agent: Option<&str>,
657 continuation_config: ContinuationConfig<'_>,
658) -> anyhow::Result<DevIterationResult> {
659 let max_xsd_retries = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize;
660 let max_total_attempts = continuation_config.max_attempts;
661 let max_continuations = max_total_attempts.saturating_sub(1);
662
663 let mut local_continuation = continuation_config.state.clone();
665 let mut had_any_error = false;
666 let mut last_summary: Option<String> = None;
667
668 for _ in 0..max_total_attempts {
671 if local_continuation.is_continuation() {
672 ctx.logger.info(&format!(
673 "Continuation {} of {} (status was not 'completed')",
674 local_continuation.continuation_attempt, max_continuations
675 ));
676 }
677
678 let attempt = run_development_attempt_with_xml_retry(
679 ctx,
680 iteration,
681 _developer_context,
682 _resuming_into_development,
683 _resume_context,
684 _agent,
685 &local_continuation,
686 )?;
687
688 had_any_error |= attempt.had_error;
689 last_summary = Some(attempt.summary.clone());
690
691 if attempt.output_valid && matches!(attempt.status, DevelopmentStatus::Completed) {
692 return Ok(DevIterationResult {
693 had_error: had_any_error,
694 summary: Some(attempt.summary),
695 files_changed: attempt.files_changed,
696 });
697 }
698
699 local_continuation = local_continuation.trigger_continuation(
702 attempt.status,
703 attempt.summary,
704 attempt.files_changed,
705 attempt.next_steps,
706 );
707
708 let _ = write_continuation_context_file(ctx, iteration, &local_continuation);
711 }
712
713 let summary = last_summary.unwrap_or_else(|| {
717 format!(
718 "Continuation stopped after {} attempts",
719 max_total_attempts * max_xsd_retries
720 )
721 });
722 anyhow::bail!(
723 "Development iteration did not reach status='completed' after {} total valid attempts (max_continuations={}, max_xsd_retries={} per attempt). Last summary: {}",
724 max_total_attempts,
725 max_continuations,
726 max_xsd_retries,
727 summary
728 );
729}
730
731fn cleanup_continuation_context_file(ctx: &mut PhaseContext<'_>) -> anyhow::Result<()> {
732 let path = Path::new(CONTINUATION_CONTEXT_PATH);
733 if ctx.workspace.exists(path) {
734 ctx.workspace.remove(path)?;
735 }
736 Ok(())
737}
738
739fn write_continuation_context_file(
740 ctx: &mut PhaseContext<'_>,
741 iteration: u32,
742 continuation_state: &ContinuationState,
743) -> anyhow::Result<()> {
744 let tmp_dir = Path::new(".agent/tmp");
745 if !ctx.workspace.exists(tmp_dir) {
746 ctx.workspace.create_dir_all(tmp_dir)?;
747 }
748
749 let mut content = String::new();
750 content.push_str("# Development Continuation Context\n\n");
751 content.push_str(&format!("- Iteration: {iteration}\n"));
752 content.push_str(&format!(
753 "- Continuation attempt: {}\n",
754 continuation_state.continuation_attempt
755 ));
756 if let Some(ref status) = continuation_state.previous_status {
757 content.push_str(&format!("- Previous status: {status}\n\n"));
758 } else {
759 content.push_str("- Previous status: unknown\n\n");
760 }
761
762 content.push_str("## Previous summary\n\n");
763 if let Some(ref summary) = continuation_state.previous_summary {
764 content.push_str(summary);
765 }
766 content.push('\n');
767
768 if let Some(ref files) = continuation_state.previous_files_changed {
769 content.push_str("\n## Files changed\n\n");
770 for file in files {
771 content.push_str("- ");
772 content.push_str(file);
773 content.push('\n');
774 }
775 }
776
777 if let Some(ref next_steps) = continuation_state.previous_next_steps {
778 content.push_str("\n## Recommended next steps\n\n");
779 content.push_str(next_steps);
780 content.push('\n');
781 }
782
783 content.push_str("\n## Reference files (do not modify)\n\n");
784 content.push_str("- PROMPT.md\n");
785 content.push_str("- .agent/PLAN.md\n");
786
787 ctx.workspace
788 .write(Path::new(CONTINUATION_CONTEXT_PATH), &content)?;
789
790 Ok(())
791}
792
793fn load_continuation_state_from_context_file(
794 workspace: &dyn crate::workspace::Workspace,
795) -> Option<ContinuationState> {
796 let path = Path::new(CONTINUATION_CONTEXT_PATH);
797 if !workspace.exists(path) {
798 return None;
799 }
800 let content = workspace.read(path).ok()?;
801 parse_continuation_context_markdown(&content)
802}
803
804fn parse_continuation_context_markdown(content: &str) -> Option<ContinuationState> {
805 let mut continuation_attempt: Option<u32> = None;
806 let mut previous_status: Option<DevelopmentStatus> = None;
807 let mut previous_summary_lines: Vec<String> = Vec::new();
808 let mut previous_next_steps_lines: Vec<String> = Vec::new();
809 let mut previous_files_changed: Vec<String> = Vec::new();
810
811 enum Section {
812 None,
813 PreviousSummary,
814 FilesChanged,
815 NextSteps,
816 }
817
818 let mut section = Section::None;
819
820 for line in content.lines() {
821 let line = line.trim_end();
822
823 if let Some(rest) = line.strip_prefix("- Continuation attempt:") {
824 continuation_attempt = rest.trim().parse::<u32>().ok();
825 continue;
826 }
827 if let Some(rest) = line.strip_prefix("- Previous status:") {
828 let s = rest.trim().to_ascii_lowercase();
829 previous_status = match s.as_str() {
830 "completed" => Some(DevelopmentStatus::Completed),
831 "partial" => Some(DevelopmentStatus::Partial),
832 "failed" => Some(DevelopmentStatus::Failed),
833 _ => None,
834 };
835 continue;
836 }
837
838 if line == "## Previous summary" {
839 section = Section::PreviousSummary;
840 continue;
841 }
842 if line == "## Files changed" {
843 section = Section::FilesChanged;
844 continue;
845 }
846 if line == "## Recommended next steps" {
847 section = Section::NextSteps;
848 continue;
849 }
850 if line.starts_with("## ") {
851 section = Section::None;
852 continue;
853 }
854
855 match section {
856 Section::PreviousSummary => previous_summary_lines.push(line.to_string()),
857 Section::FilesChanged => {
858 if let Some(item) = line.strip_prefix("- ") {
859 if !item.trim().is_empty() {
860 previous_files_changed.push(item.trim().to_string());
861 }
862 }
863 }
864 Section::NextSteps => previous_next_steps_lines.push(line.to_string()),
865 Section::None => {}
866 }
867 }
868
869 let continuation_attempt = continuation_attempt?;
870 let previous_summary = previous_summary_lines.join("\n").trim().to_string();
871 let previous_next_steps = previous_next_steps_lines.join("\n").trim().to_string();
872
873 Some(ContinuationState {
874 previous_status,
875 previous_summary: if previous_summary.is_empty() {
876 None
877 } else {
878 Some(previous_summary)
879 },
880 previous_files_changed: if previous_files_changed.is_empty() {
881 None
882 } else {
883 Some(previous_files_changed)
884 },
885 previous_next_steps: if previous_next_steps.is_empty() {
886 None
887 } else {
888 Some(previous_next_steps)
889 },
890 continuation_attempt,
891 })
892}
893
894pub fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
899 let start_time = Instant::now();
900 if ctx.config.features.checkpoint_enabled {
902 let builder = CheckpointBuilder::new()
903 .phase(
904 PipelinePhase::Planning,
905 iteration,
906 ctx.config.developer_iters,
907 )
908 .reviewer_pass(0, ctx.config.reviewer_reviews)
909 .capture_from_context(
910 ctx.config,
911 ctx.registry,
912 ctx.developer_agent,
913 ctx.reviewer_agent,
914 ctx.logger,
915 &ctx.run_context,
916 )
917 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
918 .with_execution_history(ctx.execution_history.clone())
919 .with_prompt_history(ctx.clone_prompt_history());
920
921 if let Some(checkpoint) = builder.build() {
922 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
923 }
924 }
925
926 ctx.logger.info("Creating plan from PROMPT.md...");
927 update_status_with_workspace(
928 ctx.workspace,
929 "Starting planning phase",
930 ctx.config.isolation_mode,
931 )?;
932
933 let prompt_md_content = ctx.workspace.read(Path::new("PROMPT.md")).ok();
937
938 let prompt_key = format!("planning_{}", iteration);
941 let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
942
943 let (plan_prompt, was_replayed) =
945 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
946 prompt_planning_xml_with_context(
947 ctx.template_context,
948 Some(prompt_md_str),
949 ctx.workspace,
950 )
951 });
952
953 if !was_replayed {
955 ctx.capture_prompt(&prompt_key, &plan_prompt);
956 } else {
957 ctx.logger.info(&format!(
958 "Using stored prompt from checkpoint for determinism: {}",
959 prompt_key
960 ));
961 }
962
963 let log_dir = format!(".agent/logs/planning_{iteration}");
964 let plan_path = Path::new(".agent/PLAN.md");
965
966 if let Some(parent) = plan_path.parent() {
968 ctx.workspace.create_dir_all(parent)?;
969 }
970
971 let max_retries = crate::reducer::state::MAX_VALIDATION_RETRY_ATTEMPTS as usize;
974 let mut xsd_error: Option<String> = None;
975 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
976
977 for retry_num in 0..max_retries {
978 if retry_num > 0 {
980 use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
981 let xml_path = Path::new(crate::files::llm_output_extraction::xml_paths::PLAN_XML);
982 let _ = check_and_cleanup_xml_before_retry_with_workspace(
983 ctx.workspace,
984 xml_path,
985 ctx.logger,
986 );
987 }
988
989 let plan_prompt = if retry_num == 0 {
992 plan_prompt.clone()
993 } else {
994 ctx.logger.info(&format!(
995 " In-session retry {}/{} for XSD validation",
996 retry_num,
997 max_retries - 1
998 ));
999 if let Some(ref error) = xsd_error {
1000 ctx.logger.info(&format!(" XSD error: {}", error));
1001 }
1002
1003 let last_output = read_last_planning_output(Path::new(&log_dir), ctx.workspace);
1005
1006 prompt_planning_xsd_retry_with_context(
1007 ctx.template_context,
1008 prompt_md_str,
1009 xsd_error.as_deref().unwrap_or("Unknown error"),
1010 &last_output,
1011 ctx.workspace,
1012 )
1013 };
1014
1015 let mut runtime = PipelineRuntime {
1016 timer: ctx.timer,
1017 logger: ctx.logger,
1018 colors: ctx.colors,
1019 config: ctx.config,
1020 executor: ctx.executor,
1021 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
1022 workspace: ctx.workspace,
1023 };
1024
1025 let mut xsd_retry_config = XsdRetryConfig {
1029 role: AgentRole::Developer,
1030 base_label: &format!("planning #{}", iteration),
1031 prompt: &plan_prompt,
1032 logfile_prefix: &log_dir,
1033 runtime: &mut runtime,
1034 registry: ctx.registry,
1035 primary_agent: ctx.developer_agent,
1036 session_info: session_info.as_ref(),
1037 retry_num,
1038 output_validator: None,
1039 workspace: ctx.workspace,
1040 };
1041
1042 let _exit_code = run_xsd_retry_with_session(&mut xsd_retry_config)?;
1043
1044 let log_dir_path = Path::new(&log_dir);
1046 let plan_content = read_last_planning_output(log_dir_path, ctx.workspace);
1047
1048 if session_info.is_none() {
1051 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
1052 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
1053 log_dir_path,
1054 agent_config.json_parser,
1055 Some(ctx.developer_agent),
1056 ctx.workspace,
1057 );
1058 }
1059 }
1060
1061 let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
1063 ctx.workspace,
1064 Path::new(xml_paths::PLAN_XML),
1065 &plan_content,
1066 extract_plan_xml,
1067 )
1068 .unwrap_or_else(|| {
1069 plan_content.clone()
1072 });
1073
1074 match validate_plan_xml(&xml_to_validate) {
1076 Ok(plan_elements) => {
1077 let markdown = format_plan_as_markdown(&plan_elements);
1080 ctx.workspace.write(plan_path, &markdown)?;
1081
1082 archive_xml_file_with_workspace(ctx.workspace, Path::new(xml_paths::PLAN_XML));
1084
1085 if retry_num > 0 {
1086 ctx.logger
1087 .success(&format!("Plan validated after {} retries", retry_num));
1088 } else {
1089 ctx.logger.success("Plan extracted and validated (XML)");
1090 }
1091
1092 {
1094 let duration = start_time.elapsed().as_secs();
1095 let step = ExecutionStep::new(
1096 "Planning",
1097 iteration,
1098 "plan_generation",
1099 StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
1100 )
1101 .with_agent(ctx.developer_agent)
1102 .with_duration(duration);
1103 ctx.execution_history.add_step(step);
1104 }
1105
1106 return Ok(());
1107 }
1108 Err(xsd_err) => {
1109 let error_msg = format_xsd_error(&xsd_err);
1111 ctx.logger
1112 .warn(&format!(" XSD validation failed: {}", error_msg));
1113
1114 if retry_num < max_retries - 1 {
1115 xsd_error = Some(error_msg);
1117 continue;
1119 } else {
1120 ctx.logger
1121 .error(" No more in-session XSD retries remaining");
1122 let placeholder = "# Plan\n\nAgent produced no valid XML output. Only XML format is accepted.\n";
1124 ctx.workspace.write(plan_path, placeholder)?;
1125 anyhow::bail!(
1126 "Planning agent did not produce valid XML output after {} attempts",
1127 max_retries
1128 );
1129 }
1130 }
1131 }
1132 }
1133
1134 {
1136 let duration = start_time.elapsed().as_secs();
1137 let step = ExecutionStep::new(
1138 "Planning",
1139 iteration,
1140 "plan_generation",
1141 StepOutcome::failure("No valid XML output produced".to_string(), false),
1142 )
1143 .with_agent(ctx.developer_agent)
1144 .with_duration(duration);
1145 ctx.execution_history.add_step(step);
1146 }
1147
1148 anyhow::bail!("Planning failed after {} XSD retry attempts", max_retries)
1149}
1150
1151fn read_last_planning_output(
1157 log_prefix: &Path,
1158 workspace: &dyn crate::workspace::Workspace,
1159) -> String {
1160 read_last_output_from_prefix(log_prefix, workspace)
1161}
1162
1163fn read_last_development_output(
1169 log_prefix: &Path,
1170 workspace: &dyn crate::workspace::Workspace,
1171) -> String {
1172 read_last_output_from_prefix(log_prefix, workspace)
1173}
1174
1175fn read_last_output_from_prefix(
1180 log_prefix: &Path,
1181 workspace: &dyn crate::workspace::Workspace,
1182) -> String {
1183 crate::pipeline::logfile::read_most_recent_logfile(log_prefix, workspace)
1184}
1185
1186fn format_xsd_error(error: &XsdValidationError) -> String {
1188 format!(
1189 "{} - expected: {}, found: {}",
1190 error.element_path, error.expected, error.found
1191 )
1192}
1193
1194fn format_plan_as_markdown(elements: &PlanElements) -> String {
1196 let mut result = String::new();
1197
1198 result.push_str("## Summary\n\n");
1200 result.push_str(&elements.summary.context);
1201 result.push_str("\n\n");
1202
1203 result.push_str("### Scope\n\n");
1205 for item in &elements.summary.scope_items {
1206 if let Some(ref count) = item.count {
1207 result.push_str(&format!("- **{}** {}", count, item.description));
1208 } else {
1209 result.push_str(&format!("- {}", item.description));
1210 }
1211 if let Some(ref category) = item.category {
1212 result.push_str(&format!(" ({})", category));
1213 }
1214 result.push('\n');
1215 }
1216 result.push('\n');
1217
1218 result.push_str("## Implementation Steps\n\n");
1220 for step in &elements.steps {
1221 let step_type_str = match step.step_type {
1223 crate::files::llm_output_extraction::xsd_validation_plan::StepType::FileChange => {
1224 "file-change"
1225 }
1226 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Action => "action",
1227 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Research => {
1228 "research"
1229 }
1230 };
1231 let priority_str = step.priority.map_or(String::new(), |p| {
1232 format!(
1233 " [{}]",
1234 match p {
1235 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Critical =>
1236 "critical",
1237 crate::files::llm_output_extraction::xsd_validation_plan::Priority::High =>
1238 "high",
1239 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Medium =>
1240 "medium",
1241 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Low =>
1242 "low",
1243 }
1244 )
1245 });
1246
1247 result.push_str(&format!(
1248 "### Step {} ({}){}: {}\n\n",
1249 step.number, step_type_str, priority_str, step.title
1250 ));
1251
1252 if !step.target_files.is_empty() {
1254 result.push_str("**Target Files:**\n");
1255 for tf in &step.target_files {
1256 let action_str = match tf.action {
1257 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
1258 "create"
1259 }
1260 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
1261 "modify"
1262 }
1263 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
1264 "delete"
1265 }
1266 };
1267 result.push_str(&format!("- `{}` ({})\n", tf.path, action_str));
1268 }
1269 result.push('\n');
1270 }
1271
1272 if let Some(ref location) = step.location {
1274 result.push_str(&format!("**Location:** {}\n\n", location));
1275 }
1276
1277 if let Some(ref rationale) = step.rationale {
1279 result.push_str(&format!("**Rationale:** {}\n\n", rationale));
1280 }
1281
1282 result.push_str(&format_rich_content(&step.content));
1284 result.push('\n');
1285
1286 if !step.depends_on.is_empty() {
1288 result.push_str("**Depends on:** ");
1289 let deps: Vec<String> = step
1290 .depends_on
1291 .iter()
1292 .map(|d| format!("Step {}", d))
1293 .collect();
1294 result.push_str(&deps.join(", "));
1295 result.push_str("\n\n");
1296 }
1297 }
1298
1299 result.push_str("## Critical Files\n\n");
1301 result.push_str("### Primary Files\n\n");
1302 for pf in &elements.critical_files.primary_files {
1303 let action_str = match pf.action {
1304 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
1305 "create"
1306 }
1307 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
1308 "modify"
1309 }
1310 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
1311 "delete"
1312 }
1313 };
1314 if let Some(ref est) = pf.estimated_changes {
1315 result.push_str(&format!("- `{}` ({}) - {}\n", pf.path, action_str, est));
1316 } else {
1317 result.push_str(&format!("- `{}` ({})\n", pf.path, action_str));
1318 }
1319 }
1320 result.push('\n');
1321
1322 if !elements.critical_files.reference_files.is_empty() {
1323 result.push_str("### Reference Files\n\n");
1324 for rf in &elements.critical_files.reference_files {
1325 result.push_str(&format!("- `{}` - {}\n", rf.path, rf.purpose));
1326 }
1327 result.push('\n');
1328 }
1329
1330 result.push_str("## Risks & Mitigations\n\n");
1332 for rp in &elements.risks_mitigations {
1333 let severity_str = rp.severity.map_or(String::new(), |s| {
1334 format!(
1335 " [{}]",
1336 match s {
1337 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Low =>
1338 "low",
1339 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Medium =>
1340 "medium",
1341 crate::files::llm_output_extraction::xsd_validation_plan::Severity::High =>
1342 "high",
1343 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Critical =>
1344 "critical",
1345 }
1346 )
1347 });
1348 result.push_str(&format!("**Risk{}:** {}\n", severity_str, rp.risk));
1349 result.push_str(&format!("**Mitigation:** {}\n\n", rp.mitigation));
1350 }
1351
1352 result.push_str("## Verification Strategy\n\n");
1354 for (i, v) in elements.verification_strategy.iter().enumerate() {
1355 result.push_str(&format!("{}. **{}**\n", i + 1, v.method));
1356 result.push_str(&format!(" Expected: {}\n\n", v.expected_outcome));
1357 }
1358
1359 result
1360}
1361
1362fn format_rich_content(
1364 content: &crate::files::llm_output_extraction::xsd_validation_plan::RichContent,
1365) -> String {
1366 use crate::files::llm_output_extraction::xsd_validation_plan::ContentElement;
1367
1368 let mut result = String::new();
1369
1370 for element in &content.elements {
1371 match element {
1372 ContentElement::Paragraph(p) => {
1373 result.push_str(&format_inline_content(&p.content));
1374 result.push_str("\n\n");
1375 }
1376 ContentElement::CodeBlock(cb) => {
1377 let lang = cb.language.as_deref().unwrap_or("");
1378 result.push_str(&format!("```{}\n", lang));
1379 result.push_str(&cb.content);
1380 if !cb.content.ends_with('\n') {
1381 result.push('\n');
1382 }
1383 result.push_str("```\n\n");
1384 }
1385 ContentElement::Table(t) => {
1386 if let Some(ref caption) = t.caption {
1387 result.push_str(&format!("**{}**\n\n", caption));
1388 }
1389 if !t.columns.is_empty() {
1391 result.push_str("| ");
1392 result.push_str(&t.columns.join(" | "));
1393 result.push_str(" |\n");
1394 result.push('|');
1395 for _ in &t.columns {
1396 result.push_str(" --- |");
1397 }
1398 result.push('\n');
1399 } else if let Some(first_row) = t.rows.first() {
1400 result.push('|');
1402 for _ in &first_row.cells {
1403 result.push_str(" --- |");
1404 }
1405 result.push('\n');
1406 }
1407 for row in &t.rows {
1409 result.push_str("| ");
1410 let cells: Vec<String> = row
1411 .cells
1412 .iter()
1413 .map(|c| format_inline_content(&c.content))
1414 .collect();
1415 result.push_str(&cells.join(" | "));
1416 result.push_str(" |\n");
1417 }
1418 result.push('\n');
1419 }
1420 ContentElement::List(l) => {
1421 result.push_str(&format_list(l, 0));
1422 result.push('\n');
1423 }
1424 ContentElement::Heading(h) => {
1425 let prefix = "#".repeat(h.level as usize);
1426 result.push_str(&format!("{} {}\n\n", prefix, h.text));
1427 }
1428 }
1429 }
1430
1431 result
1432}
1433
1434fn format_inline_content(
1436 content: &[crate::files::llm_output_extraction::xsd_validation_plan::InlineElement],
1437) -> String {
1438 use crate::files::llm_output_extraction::xsd_validation_plan::InlineElement;
1439
1440 content
1441 .iter()
1442 .map(|e| match e {
1443 InlineElement::Text(s) => s.clone(),
1444 InlineElement::Emphasis(s) => format!("**{}**", s),
1445 InlineElement::Code(s) => format!("`{}`", s),
1446 InlineElement::Link { href, text } => format!("[{}]({})", text, href),
1447 })
1448 .collect::<Vec<_>>()
1449 .join("")
1450}
1451
1452fn format_list(
1454 list: &crate::files::llm_output_extraction::xsd_validation_plan::List,
1455 indent: usize,
1456) -> String {
1457 use crate::files::llm_output_extraction::xsd_validation_plan::ListType;
1458
1459 let mut result = String::new();
1460 let indent_str = " ".repeat(indent);
1461
1462 for (i, item) in list.items.iter().enumerate() {
1463 let marker = match list.list_type {
1464 ListType::Ordered => format!("{}. ", i + 1),
1465 ListType::Unordered => "- ".to_string(),
1466 };
1467
1468 result.push_str(&indent_str);
1469 result.push_str(&marker);
1470 result.push_str(&format_inline_content(&item.content));
1471 result.push('\n');
1472
1473 if let Some(ref nested) = item.nested_list {
1474 result.push_str(&format_list(nested, indent + 1));
1475 }
1476 }
1477
1478 result
1479}
1480
1481fn verify_plan_exists(
1488 ctx: &mut PhaseContext<'_>,
1489 iteration: u32,
1490 resuming_into_development: bool,
1491) -> anyhow::Result<bool> {
1492 let plan_path = Path::new(".agent/PLAN.md");
1493
1494 let plan_ok = ctx
1495 .workspace
1496 .exists(plan_path)
1497 .then(|| ctx.workspace.read(plan_path).ok())
1498 .flatten()
1499 .is_some_and(|s| !s.trim().is_empty());
1500
1501 if !plan_ok && resuming_into_development {
1503 ctx.logger
1504 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
1505 run_planning_step(ctx, iteration)?;
1506
1507 let plan_ok = ctx
1509 .workspace
1510 .exists(plan_path)
1511 .then(|| ctx.workspace.read(plan_path).ok())
1512 .flatten()
1513 .is_some_and(|s| !s.trim().is_empty());
1514
1515 return Ok(plan_ok);
1516 }
1517
1518 Ok(plan_ok)
1519}
1520
1521fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
1523 let argv = crate::common::split_command(fast_cmd)
1524 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
1525 if argv.is_empty() {
1526 ctx.logger
1527 .warn("FAST_CHECK_CMD is empty; skipping fast check");
1528 return Ok(());
1529 }
1530
1531 let display_cmd = crate::common::format_argv_for_log(&argv);
1532 ctx.logger.info(&format!(
1533 "Running fast check: {}{}{}",
1534 ctx.colors.dim(),
1535 display_cmd,
1536 ctx.colors.reset()
1537 ));
1538
1539 let Some((program, cmd_args)) = argv.split_first() else {
1540 ctx.logger
1541 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
1542 return Ok(());
1543 };
1544 let args_refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect();
1545 let output = ctx.executor.execute(program, &args_refs, &[], None)?;
1546 let status = output.status;
1547
1548 if status.success() {
1549 ctx.logger.success("Fast check passed");
1550 } else {
1551 ctx.logger.warn("Fast check had issues (non-blocking)");
1552 }
1553
1554 Ok(())
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559 use super::*;
1560 use crate::agents::AgentRegistry;
1561 use crate::checkpoint::execution_history::ExecutionHistory;
1562 use crate::checkpoint::RunContext;
1563 use crate::config::Config;
1564 use crate::executor::MockProcessExecutor;
1565 use crate::logger::{Colors, Logger};
1566 use crate::pipeline::{Stats, Timer};
1567 use crate::prompts::template_context::TemplateContext;
1568 use crate::workspace::MemoryWorkspace;
1569 use crate::workspace::Workspace;
1570 use std::path::{Path, PathBuf};
1571
1572 struct TestFixture {
1573 config: Config,
1574 registry: AgentRegistry,
1575 colors: Colors,
1576 logger: Logger,
1577 timer: Timer,
1578 stats: Stats,
1579 template_context: TemplateContext,
1580 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
1581 repo_root: PathBuf,
1582 workspace: MemoryWorkspace,
1583 }
1584
1585 impl TestFixture {
1586 fn new() -> Self {
1587 let colors = Colors { enabled: false };
1588 let executor = MockProcessExecutor::new();
1589 let executor_arc = std::sync::Arc::new(executor)
1590 as std::sync::Arc<dyn crate::executor::ProcessExecutor>;
1591 let repo_root = PathBuf::from("/test/repo");
1592 let workspace = MemoryWorkspace::new(repo_root.clone());
1593 let registry = AgentRegistry::new().unwrap();
1594
1595 Self {
1596 config: Config::default(),
1597 registry,
1598 colors,
1599 logger: Logger::new(colors),
1600 timer: Timer::new(),
1601 stats: Stats::default(),
1602 template_context: TemplateContext::default(),
1603 executor_arc,
1604 repo_root,
1605 workspace,
1606 }
1607 }
1608 }
1609
1610 #[test]
1611 fn test_run_development_iteration_with_xml_retry_errors_when_continuations_exhausted_without_completion(
1612 ) {
1613 let mut fixture = TestFixture::new();
1614 fixture.config.max_dev_continuations = Some(1);
1615
1616 fixture
1617 .workspace
1618 .write(Path::new("PROMPT.md"), "do the thing")
1619 .unwrap();
1620 fixture
1621 .workspace
1622 .write(Path::new(".agent/PLAN.md"), "plan")
1623 .unwrap();
1624 fixture
1625 .workspace
1626 .create_dir_all(Path::new(".agent/tmp"))
1627 .unwrap();
1628 fixture
1629 .workspace
1630 .write(
1631 Path::new(".agent/tmp/development_result.xml"),
1632 r#"<ralph-development-result>
1633<ralph-status>partial</ralph-status>
1634<ralph-summary>partial work</ralph-summary>
1635</ralph-development-result>"#,
1636 )
1637 .unwrap();
1638
1639 let mut ctx = PhaseContext {
1640 config: &fixture.config,
1641 registry: &fixture.registry,
1642 logger: &fixture.logger,
1643 colors: &fixture.colors,
1644 timer: &mut fixture.timer,
1645 stats: &mut fixture.stats,
1646 developer_agent: "codex",
1647 reviewer_agent: "codex",
1648 review_guidelines: None,
1649 template_context: &fixture.template_context,
1650 run_context: RunContext::new(),
1651 execution_history: ExecutionHistory::new(),
1652 prompt_history: std::collections::HashMap::new(),
1653 executor: &*fixture.executor_arc,
1654 executor_arc: std::sync::Arc::clone(&fixture.executor_arc),
1655 repo_root: &fixture.repo_root,
1656 workspace: &fixture.workspace,
1657 };
1658
1659 let continuation_state = ContinuationState::new();
1660 let continuation_config = ContinuationConfig {
1661 state: &continuation_state,
1662 max_attempts: 1 + fixture.config.max_dev_continuations.unwrap_or(2) as usize,
1665 };
1666
1667 let result = run_development_iteration_with_xml_retry(
1668 &mut ctx,
1669 1,
1670 ContextLevel::Minimal,
1671 false,
1672 None::<&crate::checkpoint::restore::ResumeContext>,
1673 Some("codex"),
1674 continuation_config,
1675 );
1676
1677 assert!(
1678 result.is_err(),
1679 "Expected error when continuations exhausted without status='completed'"
1680 );
1681 }
1682}
1683
1684fn handle_commit_after_development(
1690 ctx: &mut PhaseContext<'_>,
1691 iteration: u32,
1692) -> anyhow::Result<()> {
1693 let start_time = Instant::now();
1694 let commit_agent = get_primary_commit_agent(ctx);
1696
1697 if let Some(agent) = commit_agent {
1698 ctx.logger.info(&format!(
1699 "Creating commit with auto-generated message (agent: {agent})..."
1700 ));
1701
1702 let diff = match crate::git_helpers::git_diff() {
1704 Ok(d) => d,
1705 Err(e) => {
1706 ctx.logger
1707 .error(&format!("Failed to get diff for commit: {e}"));
1708 return Err(anyhow::anyhow!(e));
1709 }
1710 };
1711
1712 let git_name = ctx.config.git_user_name.as_deref();
1714 let git_email = ctx.config.git_user_email.as_deref();
1715
1716 let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
1717
1718 match result {
1719 CommitResultFallback::Success(oid) => {
1720 ctx.logger
1721 .success(&format!("Commit created successfully: {oid}"));
1722 ctx.stats.commits_created += 1;
1723
1724 {
1725 let duration = start_time.elapsed().as_secs();
1726 let step = ExecutionStep::new(
1727 "Development",
1728 iteration,
1729 "commit",
1730 StepOutcome::success(Some(oid.to_string()), vec![]),
1731 )
1732 .with_agent(&agent)
1733 .with_duration(duration);
1734 ctx.execution_history.add_step(step);
1735 }
1736 }
1737 CommitResultFallback::NoChanges => {
1738 ctx.logger.info("No commit created (no meaningful changes)");
1740
1741 {
1742 let duration = start_time.elapsed().as_secs();
1743 let step = ExecutionStep::new(
1744 "Development",
1745 iteration,
1746 "commit",
1747 StepOutcome::skipped("No meaningful changes to commit".to_string()),
1748 )
1749 .with_duration(duration);
1750 ctx.execution_history.add_step(step);
1751 }
1752 }
1753 CommitResultFallback::Failed(err) => {
1754 ctx.logger.error(&format!(
1756 "Failed to create commit (git operation failed): {err}"
1757 ));
1758
1759 {
1760 let duration = start_time.elapsed().as_secs();
1761 let step = ExecutionStep::new(
1762 "Development",
1763 iteration,
1764 "commit",
1765 StepOutcome::failure(err.to_string(), false),
1766 )
1767 .with_duration(duration);
1768 ctx.execution_history.add_step(step);
1769 }
1770
1771 return Err(anyhow::anyhow!(err));
1773 }
1774 }
1775 } else {
1776 ctx.logger
1777 .warn("Unable to get primary commit agent for commit");
1778
1779 {
1780 let duration = start_time.elapsed().as_secs();
1781 let step = ExecutionStep::new(
1782 "Development",
1783 iteration,
1784 "commit",
1785 StepOutcome::failure("No commit agent available".to_string(), true),
1786 )
1787 .with_duration(duration);
1788 ctx.execution_history.add_step(step);
1789 }
1790 }
1791
1792 Ok(())
1793}