1use crate::agents::AgentRole;
11use crate::checkpoint::restore::ResumeContext;
12use crate::checkpoint::{save_checkpoint, CheckpointBuilder, PipelinePhase};
13use crate::files::llm_output_extraction::xsd_validation::XsdValidationError;
14use crate::files::llm_output_extraction::{
15 extract_development_result_xml, extract_plan_xml, format_xml_for_display,
16 validate_development_result_xml, validate_plan_xml, PlanElements,
17};
18use crate::files::{delete_plan_file, update_status};
19use crate::git_helpers::{git_snapshot, CommitResultFallback};
20use crate::logger::print_progress;
21use crate::phases::commit::commit_with_generated_message;
22use crate::phases::get_primary_commit_agent;
23use crate::phases::integrity::ensure_prompt_integrity;
24use crate::pipeline::{run_xsd_retry_with_session, PipelineRuntime, XsdRetryConfig};
25use crate::prompts::{
26 get_stored_or_generate_prompt, prompt_developer_iteration_xml_with_context,
27 prompt_developer_iteration_xsd_retry_with_context, prompt_planning_xml_with_context,
28 prompt_planning_xsd_retry_with_context, ContextLevel,
29};
30use std::fs;
31use std::path::Path;
32use std::process::Command;
33
34use super::context::PhaseContext;
35
36use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
37
38use std::time::Instant;
39
40pub struct DevelopmentResult {
42 pub had_errors: bool,
44}
45
46pub fn run_development_phase(
63 ctx: &mut PhaseContext<'_>,
64 start_iter: u32,
65 resume_context: Option<&ResumeContext>,
66) -> anyhow::Result<DevelopmentResult> {
67 let mut had_errors = false;
68 let mut prev_snap = git_snapshot()?;
69 let developer_context = ContextLevel::from(ctx.config.developer_context);
70
71 for i in start_iter..=ctx.config.developer_iters {
72 ctx.logger.subheader(&format!(
73 "Iteration {} of {}",
74 i, ctx.config.developer_iters
75 ));
76 print_progress(i, ctx.config.developer_iters, "Overall");
77
78 let resuming_into_development = resume_context.is_some() && i == start_iter;
79
80 if resuming_into_development {
82 ctx.logger
83 .info("Resuming at development step; skipping plan generation");
84 } else {
85 run_planning_step(ctx, i)?;
86 }
87
88 let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
90 if !plan_ok {
91 anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
92 }
93 ctx.logger.success("PLAN.md created");
94
95 if ctx.config.features.checkpoint_enabled {
97 let builder = CheckpointBuilder::new()
98 .phase(PipelinePhase::Development, i, ctx.config.developer_iters)
99 .reviewer_pass(0, ctx.config.reviewer_reviews)
100 .capture_from_context(
101 ctx.config,
102 ctx.registry,
103 ctx.developer_agent,
104 ctx.reviewer_agent,
105 ctx.logger,
106 &ctx.run_context,
107 )
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(&checkpoint);
113 }
114 }
115
116 ctx.record_developer_iteration();
118
119 ctx.logger.info("Executing plan...");
121 update_status("Starting development iteration", ctx.config.isolation_mode)?;
122
123 let dev_result = run_development_iteration_with_xml_retry(
125 ctx,
126 i,
127 developer_context,
128 resuming_into_development,
129 resume_context,
130 )?;
131
132 if dev_result.had_error {
133 ctx.logger.error(&format!(
134 "Iteration {i} encountered an error but continuing"
135 ));
136 had_errors = true;
137 }
138
139 ctx.stats.developer_runs_completed += 1;
141
142 {
144 let dev_start_time = Instant::now(); let duration = dev_start_time.elapsed().as_secs();
146 let outcome = if dev_result.had_error {
147 StepOutcome::failure("Agent exited with non-zero code".to_string(), true)
148 } else {
149 StepOutcome::success(
150 dev_result.summary.clone(),
151 dev_result.files_changed.clone().unwrap_or_default(),
152 )
153 };
154 let step = ExecutionStep::new("Development", i, "dev_run", outcome)
155 .with_agent(ctx.developer_agent)
156 .with_duration(duration);
157 ctx.execution_history.add_step(step);
158 }
159 update_status("Completed progress step", ctx.config.isolation_mode)?;
160
161 if let Some(ref summary) = dev_result.summary {
163 ctx.logger
164 .info(&format!("Development summary: {}", summary));
165 }
166
167 let snap = git_snapshot()?;
168 if snap == prev_snap {
169 if snap.is_empty() {
170 ctx.logger
171 .warn("No git-status change detected (repository is clean)");
172 } else {
173 ctx.logger.warn(&format!(
174 "No git-status change detected (existing changes: {})",
175 snap.lines().count()
176 ));
177 }
178 } else {
179 ctx.logger.success(&format!(
180 "Repository modified ({} file(s) changed)",
181 snap.lines().count()
182 ));
183 ctx.stats.changes_detected += 1;
184 handle_commit_after_development(ctx, i)?;
185 }
186 prev_snap = snap;
187
188 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
190 run_fast_check(ctx, fast_cmd, i)?;
191 }
192
193 ensure_prompt_integrity(ctx.logger, "development", i);
196
197 ctx.logger.info("Deleting PLAN.md...");
199 if let Err(err) = delete_plan_file() {
200 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
201 }
202 ctx.logger.success("PLAN.md deleted");
203
204 if ctx.config.features.checkpoint_enabled {
207 let next_iteration = i + 1;
208 let builder = CheckpointBuilder::new()
209 .phase(
210 PipelinePhase::Development,
211 next_iteration,
212 ctx.config.developer_iters,
213 )
214 .reviewer_pass(0, ctx.config.reviewer_reviews)
215 .capture_from_context(
216 ctx.config,
217 ctx.registry,
218 ctx.developer_agent,
219 ctx.reviewer_agent,
220 ctx.logger,
221 &ctx.run_context,
222 )
223 .with_execution_history(ctx.execution_history.clone())
224 .with_prompt_history(ctx.clone_prompt_history());
225
226 if let Some(checkpoint) = builder.build() {
227 let _ = save_checkpoint(&checkpoint);
228 }
229 }
230 }
231
232 Ok(DevelopmentResult { had_errors })
233}
234
235struct DevIterationResult {
237 had_error: bool,
239 summary: Option<String>,
241 files_changed: Option<Vec<String>>,
243}
244
245fn run_development_iteration_with_xml_retry(
260 ctx: &mut PhaseContext<'_>,
261 iteration: u32,
262 _developer_context: ContextLevel,
263 _resuming_into_development: bool,
264 _resume_context: Option<&ResumeContext>,
265) -> anyhow::Result<DevIterationResult> {
266 let prompt_md = fs::read_to_string("PROMPT.md").unwrap_or_default();
267 let plan_md = fs::read_to_string(".agent/PLAN.md").unwrap_or_default();
268 let log_dir = format!(".agent/logs/developer_{iteration}");
269
270 let max_xsd_retries = 100;
271 let max_continuations = 100; let mut final_summary: Option<String> = None;
273 let mut final_files_changed: Option<Vec<String>> = None;
274 let mut had_any_error = false;
275
276 'continuation: for continuation_num in 0..max_continuations {
278 let is_continuation = continuation_num > 0;
279 if is_continuation {
280 ctx.logger.info(&format!(
281 "Continuation {} of {} (status was not 'completed')",
282 continuation_num, max_continuations
283 ));
284 }
285
286 let mut xsd_error: Option<String> = None;
287 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
288
289 for retry_num in 0..max_xsd_retries {
292 let is_retry = retry_num > 0;
293 let total_attempts = continuation_num * max_xsd_retries + retry_num + 1;
294
295 let dev_prompt = if !is_retry && !is_continuation {
298 let prompt_key = format!("development_{}", iteration);
300 let (prompt, was_replayed) =
301 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
302 prompt_developer_iteration_xml_with_context(
303 ctx.template_context,
304 &prompt_md,
305 &plan_md,
306 )
307 });
308
309 if !was_replayed {
310 ctx.capture_prompt(&prompt_key, &prompt);
311 } else {
312 ctx.logger.info(&format!(
313 "Using stored prompt from checkpoint for determinism: {}",
314 prompt_key
315 ));
316 }
317
318 prompt
319 } else if !is_continuation {
320 ctx.logger.info(&format!(
322 " In-session retry {}/{} for XSD validation (total attempt: {})",
323 retry_num,
324 max_xsd_retries - 1,
325 total_attempts
326 ));
327 if let Some(ref error) = xsd_error {
328 ctx.logger.info(&format!(" XSD error: {}", error));
329 }
330
331 let last_output = read_last_development_output(Path::new(&log_dir));
332
333 prompt_developer_iteration_xsd_retry_with_context(
334 ctx.template_context,
335 &prompt_md,
336 &plan_md,
337 xsd_error.as_deref().unwrap_or("Unknown error"),
338 &last_output,
339 )
340 } else if !is_retry {
341 ctx.logger.info(&format!(
343 " Continuation attempt {} (XSD validation attempt {}/{})",
344 total_attempts, 1, max_xsd_retries
345 ));
346
347 prompt_developer_iteration_xml_with_context(
348 ctx.template_context,
349 &prompt_md,
350 &plan_md,
351 )
352 } else {
353 ctx.logger.info(&format!(
355 " Continuation 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));
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 )
373 };
374
375 let exit_code = {
379 let mut runtime = PipelineRuntime {
380 timer: ctx.timer,
381 logger: ctx.logger,
382 colors: ctx.colors,
383 config: ctx.config,
384 #[cfg(any(test, feature = "test-utils"))]
385 agent_executor: None,
386 };
387 let base_label = format!(
388 "run #{}{}",
389 iteration,
390 if is_continuation {
391 format!(" (continuation {})", continuation_num)
392 } else {
393 String::new()
394 }
395 );
396 let mut xsd_retry_config = XsdRetryConfig {
397 role: AgentRole::Developer,
398 base_label: &base_label,
399 prompt: &dev_prompt,
400 logfile_prefix: &log_dir,
401 runtime: &mut runtime,
402 registry: ctx.registry,
403 primary_agent: ctx.developer_agent,
404 session_info: session_info.as_ref(),
405 retry_num,
406 output_validator: None,
407 };
408 run_xsd_retry_with_session(&mut xsd_retry_config)?
409 };
410
411 if exit_code != 0 {
413 had_any_error = true;
414 }
415
416 let log_dir_path = Path::new(&log_dir);
418 let dev_content = read_last_development_output(log_dir_path);
419
420 if session_info.is_none() {
423 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
424 ctx.logger.info(&format!(
425 " [dev] Extracting session from {:?} with parser {:?}",
426 log_dir_path, agent_config.json_parser
427 ));
428 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
429 log_dir_path,
430 agent_config.json_parser,
431 Some(ctx.developer_agent),
432 );
433 if let Some(ref info) = session_info {
434 ctx.logger.info(&format!(
435 " [dev] Extracted session: agent={}, session_id={}...",
436 info.agent_name,
437 &info.session_id[..8.min(info.session_id.len())]
438 ));
439 } else {
440 ctx.logger
441 .warn(" [dev] Failed to extract session info from log");
442 }
443 }
444 }
445
446 let xml_to_validate =
449 if let Some(xml_content) = extract_development_result_xml(&dev_content) {
450 xml_content
451 } else {
452 dev_content.clone()
455 };
456
457 match validate_development_result_xml(&xml_to_validate) {
459 Ok(result_elements) => {
460 let formatted_xml = format_xml_for_display(&xml_to_validate);
462
463 if is_retry {
464 ctx.logger
465 .success(&format!("Status validated after {} retries", retry_num));
466 } else {
467 ctx.logger.success("Status extracted and validated (XML)");
468 }
469
470 ctx.logger.info(&format!("\n{}", formatted_xml));
472
473 final_summary = Some(result_elements.summary.clone());
475 final_files_changed = result_elements
476 .files_changed
477 .as_ref()
478 .map(|f| f.lines().map(|s| s.to_string()).collect());
479
480 if result_elements.is_completed() {
482 return Ok(DevIterationResult {
484 had_error: had_any_error,
485 summary: final_summary,
486 files_changed: final_files_changed,
487 });
488 } else if result_elements.is_partial() {
489 ctx.logger
491 .info("Status is 'partial' - continuing with same iteration");
492 continue 'continuation;
493 } else if result_elements.is_failed() {
494 ctx.logger
496 .warn("Status is 'failed' - continuing with same iteration");
497 continue 'continuation;
498 }
499 }
500 Err(xsd_err) => {
501 let error_msg = format_xsd_error(&xsd_err);
503 ctx.logger
504 .warn(&format!(" XSD validation failed: {}", error_msg));
505
506 if retry_num < max_xsd_retries - 1 {
507 xsd_error = Some(error_msg);
509 continue;
511 } else {
512 ctx.logger
513 .warn(" No more in-session XSD retries remaining");
514 break 'continuation;
516 }
517 }
518 }
519 }
520
521 ctx.logger
523 .warn("XSD retry loop exhausted - stopping continuation");
524 break;
525 }
526
527 Ok(DevIterationResult {
529 had_error: had_any_error,
530 summary: final_summary.or_else(|| {
531 Some(format!(
532 "Continuation stopped after {} attempts",
533 max_continuations * max_xsd_retries
534 ))
535 }),
536 files_changed: final_files_changed,
537 })
538}
539
540fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
545 let start_time = Instant::now();
546 if ctx.config.features.checkpoint_enabled {
548 let builder = CheckpointBuilder::new()
549 .phase(
550 PipelinePhase::Planning,
551 iteration,
552 ctx.config.developer_iters,
553 )
554 .reviewer_pass(0, ctx.config.reviewer_reviews)
555 .capture_from_context(
556 ctx.config,
557 ctx.registry,
558 ctx.developer_agent,
559 ctx.reviewer_agent,
560 ctx.logger,
561 &ctx.run_context,
562 )
563 .with_execution_history(ctx.execution_history.clone())
564 .with_prompt_history(ctx.clone_prompt_history());
565
566 if let Some(checkpoint) = builder.build() {
567 let _ = save_checkpoint(&checkpoint);
568 }
569 }
570
571 ctx.logger.info("Creating plan from PROMPT.md...");
572 update_status("Starting planning phase", ctx.config.isolation_mode)?;
573
574 let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
578
579 let prompt_key = format!("planning_{}", iteration);
582 let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
583
584 let (plan_prompt, was_replayed) =
586 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
587 prompt_planning_xml_with_context(ctx.template_context, Some(prompt_md_str))
588 });
589
590 if !was_replayed {
592 ctx.capture_prompt(&prompt_key, &plan_prompt);
593 } else {
594 ctx.logger.info(&format!(
595 "Using stored prompt from checkpoint for determinism: {}",
596 prompt_key
597 ));
598 }
599
600 let log_dir = format!(".agent/logs/planning_{iteration}");
601 let plan_path = Path::new(".agent/PLAN.md");
602
603 if let Some(parent) = plan_path.parent() {
605 fs::create_dir_all(parent)?;
606 }
607
608 let max_retries = 100;
611 let mut xsd_error: Option<String> = None;
612 let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
613
614 for retry_num in 0..max_retries {
615 let plan_prompt = if retry_num == 0 {
618 plan_prompt.clone()
619 } else {
620 ctx.logger.info(&format!(
621 " In-session retry {}/{} for XSD validation",
622 retry_num,
623 max_retries - 1
624 ));
625 if let Some(ref error) = xsd_error {
626 ctx.logger.info(&format!(" XSD error: {}", error));
627 }
628
629 let last_output = read_last_planning_output(Path::new(&log_dir));
631
632 prompt_planning_xsd_retry_with_context(
633 ctx.template_context,
634 prompt_md_str,
635 xsd_error.as_deref().unwrap_or("Unknown error"),
636 &last_output,
637 )
638 };
639
640 let mut runtime = PipelineRuntime {
641 timer: ctx.timer,
642 logger: ctx.logger,
643 colors: ctx.colors,
644 config: ctx.config,
645 #[cfg(any(test, feature = "test-utils"))]
646 agent_executor: None,
647 };
648
649 let mut xsd_retry_config = XsdRetryConfig {
653 role: AgentRole::Developer,
654 base_label: &format!("planning #{}", iteration),
655 prompt: &plan_prompt,
656 logfile_prefix: &log_dir,
657 runtime: &mut runtime,
658 registry: ctx.registry,
659 primary_agent: ctx.developer_agent,
660 session_info: session_info.as_ref(),
661 retry_num,
662 output_validator: None,
663 };
664
665 let _exit_code = run_xsd_retry_with_session(&mut xsd_retry_config)?;
666
667 let log_dir_path = Path::new(&log_dir);
669 let plan_content = read_last_planning_output(log_dir_path);
670
671 if session_info.is_none() {
674 if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
675 session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
676 log_dir_path,
677 agent_config.json_parser,
678 Some(ctx.developer_agent),
679 );
680 }
681 }
682
683 let xml_to_validate = if let Some(xml_content) = extract_plan_xml(&plan_content) {
686 xml_content
687 } else {
688 plan_content.clone()
691 };
692
693 match validate_plan_xml(&xml_to_validate) {
695 Ok(plan_elements) => {
696 let formatted_xml = format_xml_for_display(&xml_to_validate);
698
699 let markdown = format_plan_as_markdown(&plan_elements);
701 fs::write(plan_path, &markdown)?;
702
703 if retry_num > 0 {
704 ctx.logger
705 .success(&format!("Plan validated after {} retries", retry_num));
706 } else {
707 ctx.logger.success("Plan extracted and validated (XML)");
708 }
709
710 ctx.logger.info(&format!("\n{}", formatted_xml));
712
713 {
715 let duration = start_time.elapsed().as_secs();
716 let step = ExecutionStep::new(
717 "Planning",
718 iteration,
719 "plan_generation",
720 StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
721 )
722 .with_agent(ctx.developer_agent)
723 .with_duration(duration);
724 ctx.execution_history.add_step(step);
725 }
726
727 return Ok(());
728 }
729 Err(xsd_err) => {
730 let error_msg = format_xsd_error(&xsd_err);
732 ctx.logger
733 .warn(&format!(" XSD validation failed: {}", error_msg));
734
735 if retry_num < max_retries - 1 {
736 xsd_error = Some(error_msg);
738 continue;
740 } else {
741 ctx.logger
742 .error(" No more in-session XSD retries remaining");
743 let placeholder = "# Plan\n\nAgent produced no valid XML output. Only XML format is accepted.\n";
745 fs::write(plan_path, placeholder)?;
746 anyhow::bail!(
747 "Planning agent did not produce valid XML output after {} attempts",
748 max_retries
749 );
750 }
751 }
752 }
753 }
754
755 {
757 let duration = start_time.elapsed().as_secs();
758 let step = ExecutionStep::new(
759 "Planning",
760 iteration,
761 "plan_generation",
762 StepOutcome::failure("No valid XML output produced".to_string(), false),
763 )
764 .with_agent(ctx.developer_agent)
765 .with_duration(duration);
766 ctx.execution_history.add_step(step);
767 }
768
769 anyhow::bail!("Planning failed after {} XSD retry attempts", max_retries)
770}
771
772fn read_last_planning_output(log_prefix: &Path) -> String {
778 read_last_output_from_prefix(log_prefix)
779}
780
781fn read_last_development_output(log_prefix: &Path) -> String {
787 read_last_output_from_prefix(log_prefix)
788}
789
790fn read_last_output_from_prefix(log_prefix: &Path) -> String {
795 crate::pipeline::logfile::read_most_recent_logfile(log_prefix)
796}
797
798fn format_xsd_error(error: &XsdValidationError) -> String {
800 format!(
801 "{} - expected: {}, found: {}",
802 error.element_path, error.expected, error.found
803 )
804}
805
806fn format_plan_as_markdown(elements: &PlanElements) -> String {
808 let mut result = String::new();
809
810 result.push_str("## Summary\n\n");
812 result.push_str(&elements.summary.context);
813 result.push_str("\n\n");
814
815 result.push_str("### Scope\n\n");
817 for item in &elements.summary.scope_items {
818 if let Some(ref count) = item.count {
819 result.push_str(&format!("- **{}** {}", count, item.description));
820 } else {
821 result.push_str(&format!("- {}", item.description));
822 }
823 if let Some(ref category) = item.category {
824 result.push_str(&format!(" ({})", category));
825 }
826 result.push('\n');
827 }
828 result.push('\n');
829
830 result.push_str("## Implementation Steps\n\n");
832 for step in &elements.steps {
833 let step_type_str = match step.step_type {
835 crate::files::llm_output_extraction::xsd_validation_plan::StepType::FileChange => {
836 "file-change"
837 }
838 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Action => "action",
839 crate::files::llm_output_extraction::xsd_validation_plan::StepType::Research => {
840 "research"
841 }
842 };
843 let priority_str = step.priority.map_or(String::new(), |p| {
844 format!(
845 " [{}]",
846 match p {
847 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Critical =>
848 "critical",
849 crate::files::llm_output_extraction::xsd_validation_plan::Priority::High =>
850 "high",
851 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Medium =>
852 "medium",
853 crate::files::llm_output_extraction::xsd_validation_plan::Priority::Low =>
854 "low",
855 }
856 )
857 });
858
859 result.push_str(&format!(
860 "### Step {} ({}){}: {}\n\n",
861 step.number, step_type_str, priority_str, step.title
862 ));
863
864 if !step.target_files.is_empty() {
866 result.push_str("**Target Files:**\n");
867 for tf in &step.target_files {
868 let action_str = match tf.action {
869 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
870 "create"
871 }
872 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
873 "modify"
874 }
875 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
876 "delete"
877 }
878 };
879 result.push_str(&format!("- `{}` ({})\n", tf.path, action_str));
880 }
881 result.push('\n');
882 }
883
884 if let Some(ref location) = step.location {
886 result.push_str(&format!("**Location:** {}\n\n", location));
887 }
888
889 if let Some(ref rationale) = step.rationale {
891 result.push_str(&format!("**Rationale:** {}\n\n", rationale));
892 }
893
894 result.push_str(&format_rich_content(&step.content));
896 result.push('\n');
897
898 if !step.depends_on.is_empty() {
900 result.push_str("**Depends on:** ");
901 let deps: Vec<String> = step
902 .depends_on
903 .iter()
904 .map(|d| format!("Step {}", d))
905 .collect();
906 result.push_str(&deps.join(", "));
907 result.push_str("\n\n");
908 }
909 }
910
911 result.push_str("## Critical Files\n\n");
913 result.push_str("### Primary Files\n\n");
914 for pf in &elements.critical_files.primary_files {
915 let action_str = match pf.action {
916 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
917 "create"
918 }
919 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
920 "modify"
921 }
922 crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
923 "delete"
924 }
925 };
926 if let Some(ref est) = pf.estimated_changes {
927 result.push_str(&format!("- `{}` ({}) - {}\n", pf.path, action_str, est));
928 } else {
929 result.push_str(&format!("- `{}` ({})\n", pf.path, action_str));
930 }
931 }
932 result.push('\n');
933
934 if !elements.critical_files.reference_files.is_empty() {
935 result.push_str("### Reference Files\n\n");
936 for rf in &elements.critical_files.reference_files {
937 result.push_str(&format!("- `{}` - {}\n", rf.path, rf.purpose));
938 }
939 result.push('\n');
940 }
941
942 result.push_str("## Risks & Mitigations\n\n");
944 for rp in &elements.risks_mitigations {
945 let severity_str = rp.severity.map_or(String::new(), |s| {
946 format!(
947 " [{}]",
948 match s {
949 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Low =>
950 "low",
951 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Medium =>
952 "medium",
953 crate::files::llm_output_extraction::xsd_validation_plan::Severity::High =>
954 "high",
955 crate::files::llm_output_extraction::xsd_validation_plan::Severity::Critical =>
956 "critical",
957 }
958 )
959 });
960 result.push_str(&format!("**Risk{}:** {}\n", severity_str, rp.risk));
961 result.push_str(&format!("**Mitigation:** {}\n\n", rp.mitigation));
962 }
963
964 result.push_str("## Verification Strategy\n\n");
966 for (i, v) in elements.verification_strategy.iter().enumerate() {
967 result.push_str(&format!("{}. **{}**\n", i + 1, v.method));
968 result.push_str(&format!(" Expected: {}\n\n", v.expected_outcome));
969 }
970
971 result
972}
973
974fn format_rich_content(
976 content: &crate::files::llm_output_extraction::xsd_validation_plan::RichContent,
977) -> String {
978 use crate::files::llm_output_extraction::xsd_validation_plan::ContentElement;
979
980 let mut result = String::new();
981
982 for element in &content.elements {
983 match element {
984 ContentElement::Paragraph(p) => {
985 result.push_str(&format_inline_content(&p.content));
986 result.push_str("\n\n");
987 }
988 ContentElement::CodeBlock(cb) => {
989 let lang = cb.language.as_deref().unwrap_or("");
990 result.push_str(&format!("```{}\n", lang));
991 result.push_str(&cb.content);
992 if !cb.content.ends_with('\n') {
993 result.push('\n');
994 }
995 result.push_str("```\n\n");
996 }
997 ContentElement::Table(t) => {
998 if let Some(ref caption) = t.caption {
999 result.push_str(&format!("**{}**\n\n", caption));
1000 }
1001 if !t.columns.is_empty() {
1003 result.push_str("| ");
1004 result.push_str(&t.columns.join(" | "));
1005 result.push_str(" |\n");
1006 result.push('|');
1007 for _ in &t.columns {
1008 result.push_str(" --- |");
1009 }
1010 result.push('\n');
1011 } else if let Some(first_row) = t.rows.first() {
1012 result.push('|');
1014 for _ in &first_row.cells {
1015 result.push_str(" --- |");
1016 }
1017 result.push('\n');
1018 }
1019 for row in &t.rows {
1021 result.push_str("| ");
1022 let cells: Vec<String> = row
1023 .cells
1024 .iter()
1025 .map(|c| format_inline_content(&c.content))
1026 .collect();
1027 result.push_str(&cells.join(" | "));
1028 result.push_str(" |\n");
1029 }
1030 result.push('\n');
1031 }
1032 ContentElement::List(l) => {
1033 result.push_str(&format_list(l, 0));
1034 result.push('\n');
1035 }
1036 ContentElement::Heading(h) => {
1037 let prefix = "#".repeat(h.level as usize);
1038 result.push_str(&format!("{} {}\n\n", prefix, h.text));
1039 }
1040 }
1041 }
1042
1043 result
1044}
1045
1046fn format_inline_content(
1048 content: &[crate::files::llm_output_extraction::xsd_validation_plan::InlineElement],
1049) -> String {
1050 use crate::files::llm_output_extraction::xsd_validation_plan::InlineElement;
1051
1052 content
1053 .iter()
1054 .map(|e| match e {
1055 InlineElement::Text(s) => s.clone(),
1056 InlineElement::Emphasis(s) => format!("**{}**", s),
1057 InlineElement::Code(s) => format!("`{}`", s),
1058 InlineElement::Link { href, text } => format!("[{}]({})", text, href),
1059 })
1060 .collect::<Vec<_>>()
1061 .join("")
1062}
1063
1064fn format_list(
1066 list: &crate::files::llm_output_extraction::xsd_validation_plan::List,
1067 indent: usize,
1068) -> String {
1069 use crate::files::llm_output_extraction::xsd_validation_plan::ListType;
1070
1071 let mut result = String::new();
1072 let indent_str = " ".repeat(indent);
1073
1074 for (i, item) in list.items.iter().enumerate() {
1075 let marker = match list.list_type {
1076 ListType::Ordered => format!("{}. ", i + 1),
1077 ListType::Unordered => "- ".to_string(),
1078 };
1079
1080 result.push_str(&indent_str);
1081 result.push_str(&marker);
1082 result.push_str(&format_inline_content(&item.content));
1083 result.push('\n');
1084
1085 if let Some(ref nested) = item.nested_list {
1086 result.push_str(&format_list(nested, indent + 1));
1087 }
1088 }
1089
1090 result
1091}
1092
1093fn verify_plan_exists(
1100 ctx: &mut PhaseContext<'_>,
1101 iteration: u32,
1102 resuming_into_development: bool,
1103) -> anyhow::Result<bool> {
1104 let plan_path = Path::new(".agent/PLAN.md");
1105
1106 let plan_ok = plan_path
1107 .exists()
1108 .then(|| fs::read_to_string(plan_path).ok())
1109 .flatten()
1110 .is_some_and(|s| !s.trim().is_empty());
1111
1112 if !plan_ok && resuming_into_development {
1114 ctx.logger
1115 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
1116 run_planning_step(ctx, iteration)?;
1117
1118 let plan_ok = plan_path
1120 .exists()
1121 .then(|| fs::read_to_string(plan_path).ok())
1122 .flatten()
1123 .is_some_and(|s| !s.trim().is_empty());
1124
1125 return Ok(plan_ok);
1126 }
1127
1128 Ok(plan_ok)
1129}
1130
1131fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
1133 let argv = crate::common::split_command(fast_cmd)
1134 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
1135 if argv.is_empty() {
1136 ctx.logger
1137 .warn("FAST_CHECK_CMD is empty; skipping fast check");
1138 return Ok(());
1139 }
1140
1141 let display_cmd = crate::common::format_argv_for_log(&argv);
1142 ctx.logger.info(&format!(
1143 "Running fast check: {}{}{}",
1144 ctx.colors.dim(),
1145 display_cmd,
1146 ctx.colors.reset()
1147 ));
1148
1149 let Some((program, cmd_args)) = argv.split_first() else {
1150 ctx.logger
1151 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
1152 return Ok(());
1153 };
1154 let status = Command::new(program).args(cmd_args).status()?;
1155
1156 if status.success() {
1157 ctx.logger.success("Fast check passed");
1158 } else {
1159 ctx.logger.warn("Fast check had issues (non-blocking)");
1160 }
1161
1162 Ok(())
1163}
1164
1165fn handle_commit_after_development(
1171 ctx: &mut PhaseContext<'_>,
1172 iteration: u32,
1173) -> anyhow::Result<()> {
1174 let start_time = Instant::now();
1175 let commit_agent = get_primary_commit_agent(ctx);
1177
1178 if let Some(agent) = commit_agent {
1179 ctx.logger.info(&format!(
1180 "Creating commit with auto-generated message (agent: {agent})..."
1181 ));
1182
1183 let diff = match crate::git_helpers::git_diff() {
1185 Ok(d) => d,
1186 Err(e) => {
1187 ctx.logger
1188 .error(&format!("Failed to get diff for commit: {e}"));
1189 return Err(anyhow::anyhow!(e));
1190 }
1191 };
1192
1193 let git_name = ctx.config.git_user_name.as_deref();
1195 let git_email = ctx.config.git_user_email.as_deref();
1196
1197 let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
1198
1199 match result {
1200 CommitResultFallback::Success(oid) => {
1201 ctx.logger
1202 .success(&format!("Commit created successfully: {oid}"));
1203 ctx.stats.commits_created += 1;
1204
1205 {
1206 let duration = start_time.elapsed().as_secs();
1207 let step = ExecutionStep::new(
1208 "Development",
1209 iteration,
1210 "commit",
1211 StepOutcome::success(Some(oid.to_string()), vec![]),
1212 )
1213 .with_agent(&agent)
1214 .with_duration(duration);
1215 ctx.execution_history.add_step(step);
1216 }
1217 }
1218 CommitResultFallback::NoChanges => {
1219 ctx.logger.info("No commit created (no meaningful changes)");
1221
1222 {
1223 let duration = start_time.elapsed().as_secs();
1224 let step = ExecutionStep::new(
1225 "Development",
1226 iteration,
1227 "commit",
1228 StepOutcome::skipped("No meaningful changes to commit".to_string()),
1229 )
1230 .with_duration(duration);
1231 ctx.execution_history.add_step(step);
1232 }
1233 }
1234 CommitResultFallback::Failed(err) => {
1235 ctx.logger.error(&format!(
1237 "Failed to create commit (git operation failed): {err}"
1238 ));
1239
1240 {
1241 let duration = start_time.elapsed().as_secs();
1242 let step = ExecutionStep::new(
1243 "Development",
1244 iteration,
1245 "commit",
1246 StepOutcome::failure(err.to_string(), false),
1247 )
1248 .with_duration(duration);
1249 ctx.execution_history.add_step(step);
1250 }
1251
1252 return Err(anyhow::anyhow!(err));
1254 }
1255 }
1256 } else {
1257 ctx.logger
1258 .warn("Unable to get primary commit agent for commit");
1259
1260 {
1261 let duration = start_time.elapsed().as_secs();
1262 let step = ExecutionStep::new(
1263 "Development",
1264 iteration,
1265 "commit",
1266 StepOutcome::failure("No commit agent available".to_string(), true),
1267 )
1268 .with_duration(duration);
1269 ctx.execution_history.add_step(step);
1270 }
1271 }
1272
1273 Ok(())
1274}