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, format_xml_for_display,
17 validate_development_result_xml, validate_plan_xml, xml_paths, PlanElements,
18};
19use crate::files::{delete_plan_file_with_workspace, update_status_with_workspace};
20use crate::git_helpers::{git_snapshot, CommitResultFallback};
21use crate::logger::print_progress;
22use crate::phases::commit::commit_with_generated_message;
23use crate::phases::get_primary_commit_agent;
24use crate::phases::integrity::ensure_prompt_integrity;
25use crate::pipeline::{run_xsd_retry_with_session, PipelineRuntime, XsdRetryConfig};
26use crate::prompts::{
27 get_stored_or_generate_prompt, prompt_developer_iteration_xml_with_context,
28 prompt_developer_iteration_xsd_retry_with_context, prompt_planning_xml_with_context,
29 prompt_planning_xsd_retry_with_context, ContextLevel,
30};
31use std::path::Path;
32
33use super::context::PhaseContext;
34
35use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
36
37use std::time::Instant;
38
39pub struct DevelopmentResult {
41 pub had_errors: bool,
43}
44
45pub fn run_development_phase(
62 ctx: &mut PhaseContext<'_>,
63 start_iter: u32,
64 resume_context: Option<&ResumeContext>,
65) -> anyhow::Result<DevelopmentResult> {
66 let mut had_errors = false;
67 let mut prev_snap = git_snapshot()?;
68 let developer_context = ContextLevel::from(ctx.config.developer_context);
69
70 for i in start_iter..=ctx.config.developer_iters {
71 ctx.logger.subheader(&format!(
72 "Iteration {} of {}",
73 i, ctx.config.developer_iters
74 ));
75 print_progress(i, ctx.config.developer_iters, "Overall");
76
77 let resuming_into_development = resume_context.is_some() && i == start_iter;
78
79 if resuming_into_development {
81 ctx.logger
82 .info("Resuming at development step; skipping plan generation");
83 } else {
84 run_planning_step(ctx, i)?;
85 }
86
87 let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
89 if !plan_ok {
90 anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
91 }
92 ctx.logger.success("PLAN.md created");
93
94 if ctx.config.features.checkpoint_enabled {
96 let builder = CheckpointBuilder::new()
97 .phase(PipelinePhase::Development, i, ctx.config.developer_iters)
98 .reviewer_pass(0, ctx.config.reviewer_reviews)
99 .capture_from_context(
100 ctx.config,
101 ctx.registry,
102 ctx.developer_agent,
103 ctx.reviewer_agent,
104 ctx.logger,
105 &ctx.run_context,
106 )
107 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
108 .with_execution_history(ctx.execution_history.clone())
109 .with_prompt_history(ctx.clone_prompt_history());
110
111 if let Some(checkpoint) = builder.build() {
112 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
113 }
114 }
115
116 ctx.record_developer_iteration();
118
119 ctx.logger.info("Executing plan...");
121 update_status_with_workspace(
122 ctx.workspace,
123 "Starting development iteration",
124 ctx.config.isolation_mode,
125 )?;
126
127 let dev_result = run_development_iteration_with_xml_retry(
129 ctx,
130 i,
131 developer_context,
132 resuming_into_development,
133 resume_context,
134 None,
135 )?;
136
137 if dev_result.had_error {
138 ctx.logger.error(&format!(
139 "Iteration {i} encountered an error but continuing"
140 ));
141 had_errors = true;
142 }
143
144 ctx.stats.developer_runs_completed += 1;
146
147 {
149 let dev_start_time = Instant::now(); let duration = dev_start_time.elapsed().as_secs();
151 let outcome = if dev_result.had_error {
152 StepOutcome::failure("Agent exited with non-zero code".to_string(), true)
153 } else {
154 StepOutcome::success(
155 dev_result.summary.clone(),
156 dev_result.files_changed.clone().unwrap_or_default(),
157 )
158 };
159 let step = ExecutionStep::new("Development", i, "dev_run", outcome)
160 .with_agent(ctx.developer_agent)
161 .with_duration(duration);
162 ctx.execution_history.add_step(step);
163 }
164 update_status_with_workspace(
165 ctx.workspace,
166 "Completed progress step",
167 ctx.config.isolation_mode,
168 )?;
169
170 if let Some(ref summary) = dev_result.summary {
172 ctx.logger
173 .info(&format!("Development summary: {}", summary));
174 }
175
176 let snap = git_snapshot()?;
177 if snap == prev_snap {
178 if snap.is_empty() {
179 ctx.logger
180 .warn("No git-status change detected (repository is clean)");
181 } else {
182 ctx.logger.warn(&format!(
183 "No git-status change detected (existing changes: {})",
184 snap.lines().count()
185 ));
186 }
187 } else {
188 ctx.logger.success(&format!(
189 "Repository modified ({} file(s) changed)",
190 snap.lines().count()
191 ));
192 ctx.stats.changes_detected += 1;
193 handle_commit_after_development(ctx, i)?;
194 }
195 prev_snap = snap;
196
197 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
199 run_fast_check(ctx, fast_cmd, i)?;
200 }
201
202 ensure_prompt_integrity(ctx.workspace, ctx.logger, "development", i);
205
206 ctx.logger.info("Deleting PLAN.md...");
208 if let Err(err) = delete_plan_file_with_workspace(ctx.workspace) {
209 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
210 }
211 ctx.logger.success("PLAN.md deleted");
212
213 if ctx.config.features.checkpoint_enabled {
216 let next_iteration = i + 1;
217 let builder = CheckpointBuilder::new()
218 .phase(
219 PipelinePhase::Development,
220 next_iteration,
221 ctx.config.developer_iters,
222 )
223 .reviewer_pass(0, ctx.config.reviewer_reviews)
224 .capture_from_context(
225 ctx.config,
226 ctx.registry,
227 ctx.developer_agent,
228 ctx.reviewer_agent,
229 ctx.logger,
230 &ctx.run_context,
231 )
232 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
233 .with_execution_history(ctx.execution_history.clone())
234 .with_prompt_history(ctx.clone_prompt_history());
235
236 if let Some(checkpoint) = builder.build() {
237 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
238 }
239 }
240 }
241
242 Ok(DevelopmentResult { had_errors })
243}
244
245#[derive(Debug)]
247pub struct DevIterationResult {
248 pub had_error: bool,
250 pub summary: Option<String>,
252 pub files_changed: Option<Vec<String>>,
254}
255
256pub fn run_development_iteration_with_xml_retry(
271 ctx: &mut PhaseContext<'_>,
272 iteration: u32,
273 _developer_context: ContextLevel,
274 _resuming_into_development: bool,
275 _resume_context: Option<&ResumeContext>,
276 _agent: Option<&str>,
277) -> anyhow::Result<DevIterationResult> {
278 let prompt_md = ctx
279 .workspace
280 .read(Path::new("PROMPT.md"))
281 .unwrap_or_default();
282 let plan_md = ctx
283 .workspace
284 .read(Path::new(".agent/PLAN.md"))
285 .unwrap_or_default();
286 let log_dir = format!(".agent/logs/developer_{iteration}");
287
288 let max_xsd_retries = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize;
289 let max_continuations = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize; let mut final_summary: Option<String> = None;
291 let mut final_files_changed: Option<Vec<String>> = None;
292 let mut had_any_error = false;
293
294 'continuation: for continuation_num in 0..max_continuations {
296 let is_continuation = continuation_num > 0;
297 if is_continuation {
298 ctx.logger.info(&format!(
299 "Continuation {} of {} (status was not 'completed')",
300 continuation_num, max_continuations
301 ));
302 }
303
304 let mut xsd_error: Option<String> = None;
305 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
306
307 for retry_num in 0..max_xsd_retries {
310 let is_retry = retry_num > 0;
311 let total_attempts = continuation_num * max_xsd_retries + retry_num + 1;
312
313 if is_retry {
316 use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
317
318 let xml_path = Path::new(
319 crate::files::llm_output_extraction::xml_paths::DEVELOPMENT_RESULT_XML,
320 );
321 let _ = check_and_cleanup_xml_before_retry_with_workspace(
322 ctx.workspace,
323 xml_path,
324 ctx.logger,
325 );
326 }
327
328 let dev_prompt = if !is_retry && !is_continuation {
331 let prompt_key = format!("development_{}", iteration);
333 let (prompt, was_replayed) =
334 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
335 prompt_developer_iteration_xml_with_context(
336 ctx.template_context,
337 &prompt_md,
338 &plan_md,
339 )
340 });
341
342 if !was_replayed {
343 ctx.capture_prompt(&prompt_key, &prompt);
344 } else {
345 ctx.logger.info(&format!(
346 "Using stored prompt from checkpoint for determinism: {}",
347 prompt_key
348 ));
349 }
350
351 prompt
352 } else if !is_continuation {
353 ctx.logger.info(&format!(
355 " In-session retry {}/{} for XSD validation (total attempt: {})",
356 retry_num,
357 max_xsd_retries - 1,
358 total_attempts
359 ));
360 if let Some(ref error) = xsd_error {
361 ctx.logger.info(&format!(" XSD error: {}", error));
362 }
363
364 let last_output = read_last_development_output(Path::new(&log_dir), ctx.workspace);
365
366 prompt_developer_iteration_xsd_retry_with_context(
367 ctx.template_context,
368 &prompt_md,
369 &plan_md,
370 xsd_error.as_deref().unwrap_or("Unknown error"),
371 &last_output,
372 ctx.workspace,
373 )
374 } else if !is_retry {
375 ctx.logger.info(&format!(
377 " Continuation attempt {} (XSD validation attempt {}/{})",
378 total_attempts, 1, max_xsd_retries
379 ));
380
381 prompt_developer_iteration_xml_with_context(
382 ctx.template_context,
383 &prompt_md,
384 &plan_md,
385 )
386 } else {
387 ctx.logger.info(&format!(
389 " Continuation 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 };
409
410 let exit_code = {
414 let mut runtime = PipelineRuntime {
415 timer: ctx.timer,
416 logger: ctx.logger,
417 colors: ctx.colors,
418 config: ctx.config,
419 executor: ctx.executor,
420 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
421 workspace: ctx.workspace,
422 };
423 let base_label = format!(
424 "run #{}{}",
425 iteration,
426 if is_continuation {
427 format!(" (continuation {})", continuation_num)
428 } else {
429 String::new()
430 }
431 );
432 let mut xsd_retry_config = XsdRetryConfig {
433 role: AgentRole::Developer,
434 base_label: &base_label,
435 prompt: &dev_prompt,
436 logfile_prefix: &log_dir,
437 runtime: &mut runtime,
438 registry: ctx.registry,
439 primary_agent: _agent.unwrap_or(ctx.developer_agent),
440 session_info: session_info.as_ref(),
441 retry_num,
442 output_validator: None,
443 workspace: ctx.workspace,
444 };
445 run_xsd_retry_with_session(&mut xsd_retry_config)?
446 };
447
448 if exit_code != 0 {
450 had_any_error = true;
451 }
452
453 let log_dir_path = Path::new(&log_dir);
455 let dev_content = read_last_development_output(log_dir_path, ctx.workspace);
456
457 if session_info.is_none() {
460 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
461 ctx.logger.info(&format!(
462 " [dev] Extracting session from {:?} with parser {:?}",
463 log_dir_path, agent_config.json_parser
464 ));
465 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
466 log_dir_path,
467 agent_config.json_parser,
468 Some(ctx.developer_agent),
469 ctx.workspace,
470 );
471 if let Some(ref info) = session_info {
472 ctx.logger.info(&format!(
473 " [dev] Extracted session: agent={}, session_id={}...",
474 info.agent_name,
475 &info.session_id[..8.min(info.session_id.len())]
476 ));
477 } else {
478 ctx.logger
479 .warn(" [dev] Failed to extract session info from log");
480 }
481 }
482 }
483
484 let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
486 ctx.workspace,
487 Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
488 &dev_content,
489 extract_development_result_xml,
490 )
491 .unwrap_or_else(|| {
492 dev_content.clone()
495 });
496
497 match validate_development_result_xml(&xml_to_validate) {
499 Ok(result_elements) => {
500 let formatted_xml = format_xml_for_display(&xml_to_validate);
502
503 archive_xml_file_with_workspace(
505 ctx.workspace,
506 Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
507 );
508
509 if is_retry {
510 ctx.logger
511 .success(&format!("Status validated after {} retries", retry_num));
512 } else {
513 ctx.logger.success("Status extracted and validated (XML)");
514 }
515
516 ctx.logger.info(&format!("\n{}", formatted_xml));
518
519 final_summary = Some(result_elements.summary.clone());
521 final_files_changed = result_elements
522 .files_changed
523 .as_ref()
524 .map(|f| f.lines().map(|s| s.to_string()).collect());
525
526 if result_elements.is_completed() {
528 return Ok(DevIterationResult {
530 had_error: had_any_error,
531 summary: final_summary,
532 files_changed: final_files_changed,
533 });
534 } else if result_elements.is_partial() {
535 ctx.logger
537 .info("Status is 'partial' - continuing with same iteration");
538 continue 'continuation;
539 } else if result_elements.is_failed() {
540 ctx.logger
542 .warn("Status is 'failed' - continuing with same iteration");
543 continue 'continuation;
544 }
545 }
546 Err(xsd_err) => {
547 let error_msg = format_xsd_error(&xsd_err);
549 ctx.logger
550 .warn(&format!(" XSD validation failed: {}", error_msg));
551
552 if retry_num < max_xsd_retries - 1 {
553 xsd_error = Some(error_msg);
555 continue;
557 } else {
558 ctx.logger
559 .warn(" No more in-session XSD retries remaining");
560 break 'continuation;
562 }
563 }
564 }
565 }
566
567 ctx.logger
569 .warn("XSD retry loop exhausted - stopping continuation");
570 break;
571 }
572
573 Ok(DevIterationResult {
575 had_error: had_any_error,
576 summary: final_summary.or_else(|| {
577 Some(format!(
578 "Continuation stopped after {} attempts",
579 max_continuations * max_xsd_retries
580 ))
581 }),
582 files_changed: final_files_changed,
583 })
584}
585
586pub fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
591 let start_time = Instant::now();
592 if ctx.config.features.checkpoint_enabled {
594 let builder = CheckpointBuilder::new()
595 .phase(
596 PipelinePhase::Planning,
597 iteration,
598 ctx.config.developer_iters,
599 )
600 .reviewer_pass(0, ctx.config.reviewer_reviews)
601 .capture_from_context(
602 ctx.config,
603 ctx.registry,
604 ctx.developer_agent,
605 ctx.reviewer_agent,
606 ctx.logger,
607 &ctx.run_context,
608 )
609 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
610 .with_execution_history(ctx.execution_history.clone())
611 .with_prompt_history(ctx.clone_prompt_history());
612
613 if let Some(checkpoint) = builder.build() {
614 let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
615 }
616 }
617
618 ctx.logger.info("Creating plan from PROMPT.md...");
619 update_status_with_workspace(
620 ctx.workspace,
621 "Starting planning phase",
622 ctx.config.isolation_mode,
623 )?;
624
625 let prompt_md_content = ctx.workspace.read(Path::new("PROMPT.md")).ok();
629
630 let prompt_key = format!("planning_{}", iteration);
633 let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
634
635 let (plan_prompt, was_replayed) =
637 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
638 prompt_planning_xml_with_context(
639 ctx.template_context,
640 Some(prompt_md_str),
641 ctx.workspace,
642 )
643 });
644
645 if !was_replayed {
647 ctx.capture_prompt(&prompt_key, &plan_prompt);
648 } else {
649 ctx.logger.info(&format!(
650 "Using stored prompt from checkpoint for determinism: {}",
651 prompt_key
652 ));
653 }
654
655 let log_dir = format!(".agent/logs/planning_{iteration}");
656 let plan_path = Path::new(".agent/PLAN.md");
657
658 if let Some(parent) = plan_path.parent() {
660 ctx.workspace.create_dir_all(parent)?;
661 }
662
663 let max_retries = crate::reducer::state::MAX_VALIDATION_RETRY_ATTEMPTS as usize;
666 let mut xsd_error: Option<String> = None;
667 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
668
669 for retry_num in 0..max_retries {
670 if retry_num > 0 {
672 use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
673 let xml_path = Path::new(crate::files::llm_output_extraction::xml_paths::PLAN_XML);
674 let _ = check_and_cleanup_xml_before_retry_with_workspace(
675 ctx.workspace,
676 xml_path,
677 ctx.logger,
678 );
679 }
680
681 let plan_prompt = if retry_num == 0 {
684 plan_prompt.clone()
685 } else {
686 ctx.logger.info(&format!(
687 " In-session retry {}/{} for XSD validation",
688 retry_num,
689 max_retries - 1
690 ));
691 if let Some(ref error) = xsd_error {
692 ctx.logger.info(&format!(" XSD error: {}", error));
693 }
694
695 let last_output = read_last_planning_output(Path::new(&log_dir), ctx.workspace);
697
698 prompt_planning_xsd_retry_with_context(
699 ctx.template_context,
700 prompt_md_str,
701 xsd_error.as_deref().unwrap_or("Unknown error"),
702 &last_output,
703 ctx.workspace,
704 )
705 };
706
707 let mut runtime = PipelineRuntime {
708 timer: ctx.timer,
709 logger: ctx.logger,
710 colors: ctx.colors,
711 config: ctx.config,
712 executor: ctx.executor,
713 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
714 workspace: ctx.workspace,
715 };
716
717 let mut xsd_retry_config = XsdRetryConfig {
721 role: AgentRole::Developer,
722 base_label: &format!("planning #{}", iteration),
723 prompt: &plan_prompt,
724 logfile_prefix: &log_dir,
725 runtime: &mut runtime,
726 registry: ctx.registry,
727 primary_agent: ctx.developer_agent,
728 session_info: session_info.as_ref(),
729 retry_num,
730 output_validator: None,
731 workspace: ctx.workspace,
732 };
733
734 let _exit_code = run_xsd_retry_with_session(&mut xsd_retry_config)?;
735
736 let log_dir_path = Path::new(&log_dir);
738 let plan_content = read_last_planning_output(log_dir_path, ctx.workspace);
739
740 if session_info.is_none() {
743 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
744 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
745 log_dir_path,
746 agent_config.json_parser,
747 Some(ctx.developer_agent),
748 ctx.workspace,
749 );
750 }
751 }
752
753 let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
755 ctx.workspace,
756 Path::new(xml_paths::PLAN_XML),
757 &plan_content,
758 extract_plan_xml,
759 )
760 .unwrap_or_else(|| {
761 plan_content.clone()
764 });
765
766 match validate_plan_xml(&xml_to_validate) {
768 Ok(plan_elements) => {
769 let formatted_xml = format_xml_for_display(&xml_to_validate);
771
772 let markdown = format_plan_as_markdown(&plan_elements);
774 ctx.workspace.write(plan_path, &markdown)?;
775
776 archive_xml_file_with_workspace(ctx.workspace, Path::new(xml_paths::PLAN_XML));
778
779 if retry_num > 0 {
780 ctx.logger
781 .success(&format!("Plan validated after {} retries", retry_num));
782 } else {
783 ctx.logger.success("Plan extracted and validated (XML)");
784 }
785
786 ctx.logger.info(&format!("\n{}", formatted_xml));
788
789 {
791 let duration = start_time.elapsed().as_secs();
792 let step = ExecutionStep::new(
793 "Planning",
794 iteration,
795 "plan_generation",
796 StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
797 )
798 .with_agent(ctx.developer_agent)
799 .with_duration(duration);
800 ctx.execution_history.add_step(step);
801 }
802
803 return Ok(());
804 }
805 Err(xsd_err) => {
806 let error_msg = format_xsd_error(&xsd_err);
808 ctx.logger
809 .warn(&format!(" XSD validation failed: {}", error_msg));
810
811 if retry_num < max_retries - 1 {
812 xsd_error = Some(error_msg);
814 continue;
816 } else {
817 ctx.logger
818 .error(" No more in-session XSD retries remaining");
819 let placeholder = "# Plan\n\nAgent produced no valid XML output. Only XML format is accepted.\n";
821 ctx.workspace.write(plan_path, placeholder)?;
822 anyhow::bail!(
823 "Planning agent did not produce valid XML output after {} attempts",
824 max_retries
825 );
826 }
827 }
828 }
829 }
830
831 {
833 let duration = start_time.elapsed().as_secs();
834 let step = ExecutionStep::new(
835 "Planning",
836 iteration,
837 "plan_generation",
838 StepOutcome::failure("No valid XML output produced".to_string(), false),
839 )
840 .with_agent(ctx.developer_agent)
841 .with_duration(duration);
842 ctx.execution_history.add_step(step);
843 }
844
845 anyhow::bail!("Planning failed after {} XSD retry attempts", max_retries)
846}
847
848fn read_last_planning_output(
854 log_prefix: &Path,
855 workspace: &dyn crate::workspace::Workspace,
856) -> String {
857 read_last_output_from_prefix(log_prefix, workspace)
858}
859
860fn read_last_development_output(
866 log_prefix: &Path,
867 workspace: &dyn crate::workspace::Workspace,
868) -> String {
869 read_last_output_from_prefix(log_prefix, workspace)
870}
871
872fn read_last_output_from_prefix(
877 log_prefix: &Path,
878 workspace: &dyn crate::workspace::Workspace,
879) -> String {
880 crate::pipeline::logfile::read_most_recent_logfile(log_prefix, workspace)
881}
882
883fn format_xsd_error(error: &XsdValidationError) -> String {
885 format!(
886 "{} - expected: {}, found: {}",
887 error.element_path, error.expected, error.found
888 )
889}
890
891fn format_plan_as_markdown(elements: &PlanElements) -> String {
893 let mut result = String::new();
894
895 result.push_str("## Summary\n\n");
897 result.push_str(&elements.summary.context);
898 result.push_str("\n\n");
899
900 result.push_str("### Scope\n\n");
902 for item in &elements.summary.scope_items {
903 if let Some(ref count) = item.count {
904 result.push_str(&format!("- **{}** {}", count, item.description));
905 } else {
906 result.push_str(&format!("- {}", item.description));
907 }
908 if let Some(ref category) = item.category {
909 result.push_str(&format!(" ({})", category));
910 }
911 result.push('\n');
912 }
913 result.push('\n');
914
915 result.push_str("## Implementation Steps\n\n");
917 for step in &elements.steps {
918 let step_type_str = match step.step_type {
920 crate::files::llm_output_extraction::xsd_validation_plan::StepType::FileChange => {
921 "file-change"
922 }
923 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Action => "action",
924 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Research => {
925 "research"
926 }
927 };
928 let priority_str = step.priority.map_or(String::new(), |p| {
929 format!(
930 " [{}]",
931 match p {
932 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Critical =>
933 "critical",
934 crate::files::llm_output_extraction::xsd_validation_plan::Priority::High =>
935 "high",
936 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Medium =>
937 "medium",
938 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Low =>
939 "low",
940 }
941 )
942 });
943
944 result.push_str(&format!(
945 "### Step {} ({}){}: {}\n\n",
946 step.number, step_type_str, priority_str, step.title
947 ));
948
949 if !step.target_files.is_empty() {
951 result.push_str("**Target Files:**\n");
952 for tf in &step.target_files {
953 let action_str = match tf.action {
954 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
955 "create"
956 }
957 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
958 "modify"
959 }
960 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
961 "delete"
962 }
963 };
964 result.push_str(&format!("- `{}` ({})\n", tf.path, action_str));
965 }
966 result.push('\n');
967 }
968
969 if let Some(ref location) = step.location {
971 result.push_str(&format!("**Location:** {}\n\n", location));
972 }
973
974 if let Some(ref rationale) = step.rationale {
976 result.push_str(&format!("**Rationale:** {}\n\n", rationale));
977 }
978
979 result.push_str(&format_rich_content(&step.content));
981 result.push('\n');
982
983 if !step.depends_on.is_empty() {
985 result.push_str("**Depends on:** ");
986 let deps: Vec<String> = step
987 .depends_on
988 .iter()
989 .map(|d| format!("Step {}", d))
990 .collect();
991 result.push_str(&deps.join(", "));
992 result.push_str("\n\n");
993 }
994 }
995
996 result.push_str("## Critical Files\n\n");
998 result.push_str("### Primary Files\n\n");
999 for pf in &elements.critical_files.primary_files {
1000 let action_str = match pf.action {
1001 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
1002 "create"
1003 }
1004 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
1005 "modify"
1006 }
1007 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
1008 "delete"
1009 }
1010 };
1011 if let Some(ref est) = pf.estimated_changes {
1012 result.push_str(&format!("- `{}` ({}) - {}\n", pf.path, action_str, est));
1013 } else {
1014 result.push_str(&format!("- `{}` ({})\n", pf.path, action_str));
1015 }
1016 }
1017 result.push('\n');
1018
1019 if !elements.critical_files.reference_files.is_empty() {
1020 result.push_str("### Reference Files\n\n");
1021 for rf in &elements.critical_files.reference_files {
1022 result.push_str(&format!("- `{}` - {}\n", rf.path, rf.purpose));
1023 }
1024 result.push('\n');
1025 }
1026
1027 result.push_str("## Risks & Mitigations\n\n");
1029 for rp in &elements.risks_mitigations {
1030 let severity_str = rp.severity.map_or(String::new(), |s| {
1031 format!(
1032 " [{}]",
1033 match s {
1034 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Low =>
1035 "low",
1036 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Medium =>
1037 "medium",
1038 crate::files::llm_output_extraction::xsd_validation_plan::Severity::High =>
1039 "high",
1040 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Critical =>
1041 "critical",
1042 }
1043 )
1044 });
1045 result.push_str(&format!("**Risk{}:** {}\n", severity_str, rp.risk));
1046 result.push_str(&format!("**Mitigation:** {}\n\n", rp.mitigation));
1047 }
1048
1049 result.push_str("## Verification Strategy\n\n");
1051 for (i, v) in elements.verification_strategy.iter().enumerate() {
1052 result.push_str(&format!("{}. **{}**\n", i + 1, v.method));
1053 result.push_str(&format!(" Expected: {}\n\n", v.expected_outcome));
1054 }
1055
1056 result
1057}
1058
1059fn format_rich_content(
1061 content: &crate::files::llm_output_extraction::xsd_validation_plan::RichContent,
1062) -> String {
1063 use crate::files::llm_output_extraction::xsd_validation_plan::ContentElement;
1064
1065 let mut result = String::new();
1066
1067 for element in &content.elements {
1068 match element {
1069 ContentElement::Paragraph(p) => {
1070 result.push_str(&format_inline_content(&p.content));
1071 result.push_str("\n\n");
1072 }
1073 ContentElement::CodeBlock(cb) => {
1074 let lang = cb.language.as_deref().unwrap_or("");
1075 result.push_str(&format!("```{}\n", lang));
1076 result.push_str(&cb.content);
1077 if !cb.content.ends_with('\n') {
1078 result.push('\n');
1079 }
1080 result.push_str("```\n\n");
1081 }
1082 ContentElement::Table(t) => {
1083 if let Some(ref caption) = t.caption {
1084 result.push_str(&format!("**{}**\n\n", caption));
1085 }
1086 if !t.columns.is_empty() {
1088 result.push_str("| ");
1089 result.push_str(&t.columns.join(" | "));
1090 result.push_str(" |\n");
1091 result.push('|');
1092 for _ in &t.columns {
1093 result.push_str(" --- |");
1094 }
1095 result.push('\n');
1096 } else if let Some(first_row) = t.rows.first() {
1097 result.push('|');
1099 for _ in &first_row.cells {
1100 result.push_str(" --- |");
1101 }
1102 result.push('\n');
1103 }
1104 for row in &t.rows {
1106 result.push_str("| ");
1107 let cells: Vec<String> = row
1108 .cells
1109 .iter()
1110 .map(|c| format_inline_content(&c.content))
1111 .collect();
1112 result.push_str(&cells.join(" | "));
1113 result.push_str(" |\n");
1114 }
1115 result.push('\n');
1116 }
1117 ContentElement::List(l) => {
1118 result.push_str(&format_list(l, 0));
1119 result.push('\n');
1120 }
1121 ContentElement::Heading(h) => {
1122 let prefix = "#".repeat(h.level as usize);
1123 result.push_str(&format!("{} {}\n\n", prefix, h.text));
1124 }
1125 }
1126 }
1127
1128 result
1129}
1130
1131fn format_inline_content(
1133 content: &[crate::files::llm_output_extraction::xsd_validation_plan::InlineElement],
1134) -> String {
1135 use crate::files::llm_output_extraction::xsd_validation_plan::InlineElement;
1136
1137 content
1138 .iter()
1139 .map(|e| match e {
1140 InlineElement::Text(s) => s.clone(),
1141 InlineElement::Emphasis(s) => format!("**{}**", s),
1142 InlineElement::Code(s) => format!("`{}`", s),
1143 InlineElement::Link { href, text } => format!("[{}]({})", text, href),
1144 })
1145 .collect::<Vec<_>>()
1146 .join("")
1147}
1148
1149fn format_list(
1151 list: &crate::files::llm_output_extraction::xsd_validation_plan::List,
1152 indent: usize,
1153) -> String {
1154 use crate::files::llm_output_extraction::xsd_validation_plan::ListType;
1155
1156 let mut result = String::new();
1157 let indent_str = " ".repeat(indent);
1158
1159 for (i, item) in list.items.iter().enumerate() {
1160 let marker = match list.list_type {
1161 ListType::Ordered => format!("{}. ", i + 1),
1162 ListType::Unordered => "- ".to_string(),
1163 };
1164
1165 result.push_str(&indent_str);
1166 result.push_str(&marker);
1167 result.push_str(&format_inline_content(&item.content));
1168 result.push('\n');
1169
1170 if let Some(ref nested) = item.nested_list {
1171 result.push_str(&format_list(nested, indent + 1));
1172 }
1173 }
1174
1175 result
1176}
1177
1178fn verify_plan_exists(
1185 ctx: &mut PhaseContext<'_>,
1186 iteration: u32,
1187 resuming_into_development: bool,
1188) -> anyhow::Result<bool> {
1189 let plan_path = Path::new(".agent/PLAN.md");
1190
1191 let plan_ok = ctx
1192 .workspace
1193 .exists(plan_path)
1194 .then(|| ctx.workspace.read(plan_path).ok())
1195 .flatten()
1196 .is_some_and(|s| !s.trim().is_empty());
1197
1198 if !plan_ok && resuming_into_development {
1200 ctx.logger
1201 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
1202 run_planning_step(ctx, iteration)?;
1203
1204 let plan_ok = ctx
1206 .workspace
1207 .exists(plan_path)
1208 .then(|| ctx.workspace.read(plan_path).ok())
1209 .flatten()
1210 .is_some_and(|s| !s.trim().is_empty());
1211
1212 return Ok(plan_ok);
1213 }
1214
1215 Ok(plan_ok)
1216}
1217
1218fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
1220 let argv = crate::common::split_command(fast_cmd)
1221 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
1222 if argv.is_empty() {
1223 ctx.logger
1224 .warn("FAST_CHECK_CMD is empty; skipping fast check");
1225 return Ok(());
1226 }
1227
1228 let display_cmd = crate::common::format_argv_for_log(&argv);
1229 ctx.logger.info(&format!(
1230 "Running fast check: {}{}{}",
1231 ctx.colors.dim(),
1232 display_cmd,
1233 ctx.colors.reset()
1234 ));
1235
1236 let Some((program, cmd_args)) = argv.split_first() else {
1237 ctx.logger
1238 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
1239 return Ok(());
1240 };
1241 let args_refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect();
1242 let output = ctx.executor.execute(program, &args_refs, &[], None)?;
1243 let status = output.status;
1244
1245 if status.success() {
1246 ctx.logger.success("Fast check passed");
1247 } else {
1248 ctx.logger.warn("Fast check had issues (non-blocking)");
1249 }
1250
1251 Ok(())
1252}
1253
1254fn handle_commit_after_development(
1260 ctx: &mut PhaseContext<'_>,
1261 iteration: u32,
1262) -> anyhow::Result<()> {
1263 let start_time = Instant::now();
1264 let commit_agent = get_primary_commit_agent(ctx);
1266
1267 if let Some(agent) = commit_agent {
1268 ctx.logger.info(&format!(
1269 "Creating commit with auto-generated message (agent: {agent})..."
1270 ));
1271
1272 let diff = match crate::git_helpers::git_diff() {
1274 Ok(d) => d,
1275 Err(e) => {
1276 ctx.logger
1277 .error(&format!("Failed to get diff for commit: {e}"));
1278 return Err(anyhow::anyhow!(e));
1279 }
1280 };
1281
1282 let git_name = ctx.config.git_user_name.as_deref();
1284 let git_email = ctx.config.git_user_email.as_deref();
1285
1286 let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
1287
1288 match result {
1289 CommitResultFallback::Success(oid) => {
1290 ctx.logger
1291 .success(&format!("Commit created successfully: {oid}"));
1292 ctx.stats.commits_created += 1;
1293
1294 {
1295 let duration = start_time.elapsed().as_secs();
1296 let step = ExecutionStep::new(
1297 "Development",
1298 iteration,
1299 "commit",
1300 StepOutcome::success(Some(oid.to_string()), vec![]),
1301 )
1302 .with_agent(&agent)
1303 .with_duration(duration);
1304 ctx.execution_history.add_step(step);
1305 }
1306 }
1307 CommitResultFallback::NoChanges => {
1308 ctx.logger.info("No commit created (no meaningful changes)");
1310
1311 {
1312 let duration = start_time.elapsed().as_secs();
1313 let step = ExecutionStep::new(
1314 "Development",
1315 iteration,
1316 "commit",
1317 StepOutcome::skipped("No meaningful changes to commit".to_string()),
1318 )
1319 .with_duration(duration);
1320 ctx.execution_history.add_step(step);
1321 }
1322 }
1323 CommitResultFallback::Failed(err) => {
1324 ctx.logger.error(&format!(
1326 "Failed to create commit (git operation failed): {err}"
1327 ));
1328
1329 {
1330 let duration = start_time.elapsed().as_secs();
1331 let step = ExecutionStep::new(
1332 "Development",
1333 iteration,
1334 "commit",
1335 StepOutcome::failure(err.to_string(), false),
1336 )
1337 .with_duration(duration);
1338 ctx.execution_history.add_step(step);
1339 }
1340
1341 return Err(anyhow::anyhow!(err));
1343 }
1344 }
1345 } else {
1346 ctx.logger
1347 .warn("Unable to get primary commit agent for commit");
1348
1349 {
1350 let duration = start_time.elapsed().as_secs();
1351 let step = ExecutionStep::new(
1352 "Development",
1353 iteration,
1354 "commit",
1355 StepOutcome::failure("No commit agent available".to_string(), true),
1356 )
1357 .with_duration(duration);
1358 ctx.execution_history.add_step(step);
1359 }
1360 }
1361
1362 Ok(())
1363}