ralph_workflow/phases/
development.rs

1//! Development phase execution.
2//!
3//! This module handles the development phase of the Ralph pipeline, which consists
4//! of iterative planning and execution cycles. Each iteration:
5//! 1. Creates a PLAN.md from PROMPT.md
6//! 2. Executes the plan
7//! 3. Deletes PLAN.md
8//! 4. Optionally runs fast checks
9
10use 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_with_fallback, PipelineRuntime};
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
40/// Result of the development phase.
41pub struct DevelopmentResult {
42    /// Whether any errors occurred during the phase.
43    pub had_errors: bool,
44}
45
46/// Run the development phase.
47///
48/// This phase runs `developer_iters` iterations, each consisting of:
49/// 1. Planning: Create PLAN.md from PROMPT.md
50/// 2. Execution: Execute the plan
51/// 3. Cleanup: Delete PLAN.md
52///
53/// # Arguments
54///
55/// * `ctx` - The phase context containing shared state
56/// * `start_iter` - The iteration to start from (for resume support)
57/// * `resume_context` - Optional resume context for resumed sessions
58///
59/// # Returns
60///
61/// Returns `Ok(DevelopmentResult)` on success, or an error if a critical failure occurs.
62pub 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        // Step 1: Create PLAN from PROMPT (skip if resuming into development)
81        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        // Verify PLAN.md was created (required)
89        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        // Save checkpoint at start of development phase (if enabled)
96        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        // Record this iteration as completed
117        ctx.record_developer_iteration();
118
119        // Step 2: Execute the PLAN
120        ctx.logger.info("Executing plan...");
121        update_status("Starting development iteration", ctx.config.isolation_mode)?;
122
123        // Run development iteration with XML extraction and XSD validation
124        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        // Record stats
140        ctx.stats.developer_runs_completed += 1;
141
142        // Record execution history
143        {
144            let dev_start_time = Instant::now(); // Note: this is after the iteration runs
145            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        // Log the development result
162        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        // Run fast check if configured
189        if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
190            run_fast_check(ctx, fast_cmd, i)?;
191        }
192
193        // Periodic restoration check - ensure PROMPT.md still exists
194        // This catches agent deletions and restores from backup
195        ensure_prompt_integrity(ctx.logger, "development", i);
196
197        // Step 3: Delete the PLAN
198        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        // Save checkpoint after iteration completes (if enabled)
205        // This checkpoint captures the completed iteration so resume won't re-run it
206        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
235/// Result of a single development iteration.
236struct DevIterationResult {
237    /// Whether an error occurred during the iteration.
238    had_error: bool,
239    /// Optional summary of what was done.
240    summary: Option<String>,
241    /// Optional list of files changed.
242    files_changed: Option<Vec<String>>,
243}
244
245/// Run a single development iteration with XML extraction and XSD validation retry loop.
246///
247/// This function implements a nested loop structure:
248/// - **Outer loop (continuation)**: Continue while status != "completed" (max 100)
249/// - **Inner loop (XSD retry)**: Retry XSD validation with error feedback (max 10)
250///
251/// The continuation logic ignores non-XSD errors and only looks for valid XML.
252/// If XML passes XSD validation with status="completed", we're done for this iteration.
253/// If XML passes XSD validation with status="partial", we continue the outer loop.
254/// If XML passes XSD validation with status="failed", we continue the outer loop.
255///
256/// The development iteration produces side effects (file changes) as its primary output.
257/// The XML status is secondary - we use it for logging/tracking but don't fail the
258/// entire iteration if XML is missing or invalid.
259fn 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 = 10;
271    let max_continuations = 100; // Safety limit to prevent infinite loops
272    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    // Outer loop: Continue until agent returns status="completed" or we hit the limit
277    '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
288        // Inner loop: XSD validation retry with error feedback
289        for retry_num in 0..max_xsd_retries {
290            let is_retry = retry_num > 0;
291            let total_attempts = continuation_num * max_xsd_retries + retry_num + 1;
292
293            // For initial attempt, use XML prompt
294            // For retries, use XSD retry prompt with error feedback
295            let dev_prompt = if !is_retry && !is_continuation {
296                // First attempt ever - use initial XML prompt
297                let prompt_key = format!("development_{}", iteration);
298                let (prompt, was_replayed) =
299                    get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
300                        prompt_developer_iteration_xml_with_context(
301                            ctx.template_context,
302                            &prompt_md,
303                            &plan_md,
304                        )
305                    });
306
307                if !was_replayed {
308                    ctx.capture_prompt(&prompt_key, &prompt);
309                } else {
310                    ctx.logger.info(&format!(
311                        "Using stored prompt from checkpoint for determinism: {}",
312                        prompt_key
313                    ));
314                }
315
316                prompt
317            } else if !is_continuation {
318                // XSD retry only (no continuation yet)
319                ctx.logger.info(&format!(
320                    "  In-session retry {}/{} for XSD validation (total attempt: {})",
321                    retry_num,
322                    max_xsd_retries - 1,
323                    total_attempts
324                ));
325                if let Some(ref error) = xsd_error {
326                    ctx.logger.info(&format!("  XSD error: {}", error));
327                }
328
329                let last_output = read_last_development_output(Path::new(&log_dir));
330
331                prompt_developer_iteration_xsd_retry_with_context(
332                    ctx.template_context,
333                    &prompt_md,
334                    &plan_md,
335                    xsd_error.as_deref().unwrap_or("Unknown error"),
336                    &last_output,
337                )
338            } else if !is_retry {
339                // Continuation only (first XSD attempt after continuation)
340                ctx.logger.info(&format!(
341                    "  Continuation attempt {} (XSD validation attempt {}/{})",
342                    total_attempts, 1, max_xsd_retries
343                ));
344
345                prompt_developer_iteration_xml_with_context(
346                    ctx.template_context,
347                    &prompt_md,
348                    &plan_md,
349                )
350            } else {
351                // Both continuation and XSD retry
352                ctx.logger.info(&format!(
353                    "  Continuation retry {}/{} for XSD validation (total attempt: {})",
354                    retry_num,
355                    max_xsd_retries - 1,
356                    total_attempts
357                ));
358                if let Some(ref error) = xsd_error {
359                    ctx.logger.info(&format!("  XSD error: {}", error));
360                }
361
362                let last_output = read_last_development_output(Path::new(&log_dir));
363
364                prompt_developer_iteration_xsd_retry_with_context(
365                    ctx.template_context,
366                    &prompt_md,
367                    &plan_md,
368                    xsd_error.as_deref().unwrap_or("Unknown error"),
369                    &last_output,
370                )
371            };
372
373            // Run the agent
374            let exit_code = {
375                let mut runtime = PipelineRuntime {
376                    timer: ctx.timer,
377                    logger: ctx.logger,
378                    colors: ctx.colors,
379                    config: ctx.config,
380                    #[cfg(any(test, feature = "test-utils"))]
381                    agent_executor: None,
382                };
383                run_with_fallback(
384                    AgentRole::Developer,
385                    &format!(
386                        "run #{}{}",
387                        iteration,
388                        if is_continuation {
389                            format!(" (continuation {})", continuation_num)
390                        } else {
391                            String::new()
392                        }
393                    ),
394                    &dev_prompt,
395                    &log_dir,
396                    &mut runtime,
397                    ctx.registry,
398                    ctx.developer_agent,
399                )?
400            };
401
402            // Track if any agent run had an error (for final result)
403            if exit_code != 0 {
404                had_any_error = true;
405            }
406
407            // Extract and validate the development result XML
408            let log_dir_path = Path::new(&log_dir);
409            let dev_content = read_last_development_output(log_dir_path);
410
411            // Try to extract XML - if extraction fails, assume entire output is XML
412            // and validate it to get specific XSD errors for retry
413            let xml_to_validate =
414                if let Some(xml_content) = extract_development_result_xml(&dev_content) {
415                    xml_content
416                } else {
417                    // No XML tags found - assume the entire content is XML for validation
418                    // This allows us to get specific XSD errors to send back to the agent
419                    dev_content.clone()
420                };
421
422            // Try to validate against XSD
423            match validate_development_result_xml(&xml_to_validate) {
424                Ok(result_elements) => {
425                    // XSD validation passed - format and log the result
426                    let formatted_xml = format_xml_for_display(&xml_to_validate);
427
428                    if is_retry {
429                        ctx.logger
430                            .success(&format!("Status validated after {} retries", retry_num));
431                    } else {
432                        ctx.logger.success("Status extracted and validated (XML)");
433                    }
434
435                    // Display the formatted status
436                    ctx.logger.info(&format!("\n{}", formatted_xml));
437
438                    // Store the results
439                    final_summary = Some(result_elements.summary.clone());
440                    final_files_changed = result_elements
441                        .files_changed
442                        .as_ref()
443                        .map(|f| f.lines().map(|s| s.to_string()).collect());
444
445                    // Check the status to determine if we should continue
446                    if result_elements.is_completed() {
447                        // Status is "completed" - we're done with this iteration
448                        return Ok(DevIterationResult {
449                            had_error: had_any_error,
450                            summary: final_summary,
451                            files_changed: final_files_changed,
452                        });
453                    } else if result_elements.is_partial() {
454                        // Status is "partial" - continue the outer loop
455                        ctx.logger
456                            .info("Status is 'partial' - continuing with same iteration");
457                        continue 'continuation;
458                    } else if result_elements.is_failed() {
459                        // Status is "failed" - continue the outer loop
460                        ctx.logger
461                            .warn("Status is 'failed' - continuing with same iteration");
462                        continue 'continuation;
463                    }
464                }
465                Err(xsd_err) => {
466                    // XSD validation failed - check if we can retry
467                    let error_msg = format_xsd_error(&xsd_err);
468                    ctx.logger
469                        .warn(&format!("  XSD validation failed: {}", error_msg));
470
471                    if retry_num < max_xsd_retries - 1 {
472                        // Store error for next retry attempt
473                        xsd_error = Some(error_msg);
474                        // Continue to next XSD retry iteration
475                        continue;
476                    } else {
477                        ctx.logger
478                            .warn("  No more in-session XSD retries remaining");
479                        // Fall through to return what we have
480                        break 'continuation;
481                    }
482                }
483            }
484        }
485
486        // If we've exhausted XSD retries, break the continuation loop
487        ctx.logger
488            .warn("XSD retry loop exhausted - stopping continuation");
489        break;
490    }
491
492    // If we get here, we exhausted the continuation limit or XSD retries
493    Ok(DevIterationResult {
494        had_error: had_any_error,
495        summary: final_summary.or_else(|| {
496            Some(format!(
497                "Continuation stopped after {} attempts",
498                max_continuations * max_xsd_retries
499            ))
500        }),
501        files_changed: final_files_changed,
502    })
503}
504
505/// Run the planning step to create PLAN.md.
506///
507/// The orchestrator ALWAYS extracts and writes PLAN.md from agent XML output.
508/// Uses XSD validation with retry loop to ensure valid XML format.
509fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
510    let start_time = Instant::now();
511    // Save checkpoint at start of planning phase (if enabled)
512    if ctx.config.features.checkpoint_enabled {
513        let builder = CheckpointBuilder::new()
514            .phase(
515                PipelinePhase::Planning,
516                iteration,
517                ctx.config.developer_iters,
518            )
519            .reviewer_pass(0, ctx.config.reviewer_reviews)
520            .capture_from_context(
521                ctx.config,
522                ctx.registry,
523                ctx.developer_agent,
524                ctx.reviewer_agent,
525                ctx.logger,
526                &ctx.run_context,
527            )
528            .with_execution_history(ctx.execution_history.clone())
529            .with_prompt_history(ctx.clone_prompt_history());
530
531        if let Some(checkpoint) = builder.build() {
532            let _ = save_checkpoint(&checkpoint);
533        }
534    }
535
536    ctx.logger.info("Creating plan from PROMPT.md...");
537    update_status("Starting planning phase", ctx.config.isolation_mode)?;
538
539    // Read PROMPT.md content to include directly in the planning prompt
540    // This prevents agents from discovering PROMPT.md through file exploration,
541    // which reduces the risk of accidental deletion.
542    let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
543
544    // Note: We don't set is_resume for planning since planning runs on each iteration.
545    // The resume context is set during the development execution step.
546    let prompt_key = format!("planning_{}", iteration);
547    let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
548
549    // Use prompt replay if available, otherwise generate new prompt
550    let (plan_prompt, was_replayed) =
551        get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
552            prompt_planning_xml_with_context(ctx.template_context, Some(prompt_md_str))
553        });
554
555    // Capture the planning prompt for checkpoint/resume (only if newly generated)
556    if !was_replayed {
557        ctx.capture_prompt(&prompt_key, &plan_prompt);
558    } else {
559        ctx.logger.info(&format!(
560            "Using stored prompt from checkpoint for determinism: {}",
561            prompt_key
562        ));
563    }
564
565    let log_dir = format!(".agent/logs/planning_{iteration}");
566    let plan_path = Path::new(".agent/PLAN.md");
567
568    // Ensure .agent directory exists
569    if let Some(parent) = plan_path.parent() {
570        fs::create_dir_all(parent)?;
571    }
572
573    // In-session retry loop with XSD validation feedback
574    let max_retries = 10;
575    let mut xsd_error: Option<String> = None;
576
577    for retry_num in 0..max_retries {
578        // For initial attempt, use XML prompt
579        // For retries, use XSD retry prompt with error feedback
580        let plan_prompt = if retry_num == 0 {
581            plan_prompt.clone()
582        } else {
583            ctx.logger.info(&format!(
584                "  In-session retry {}/{} for XSD validation",
585                retry_num,
586                max_retries - 1
587            ));
588            if let Some(ref error) = xsd_error {
589                ctx.logger.info(&format!("  XSD error: {}", error));
590            }
591
592            // Read the last output for retry context
593            let last_output = read_last_planning_output(Path::new(&log_dir));
594
595            prompt_planning_xsd_retry_with_context(
596                ctx.template_context,
597                prompt_md_str,
598                xsd_error.as_deref().unwrap_or("Unknown error"),
599                &last_output,
600            )
601        };
602
603        let mut runtime = PipelineRuntime {
604            timer: ctx.timer,
605            logger: ctx.logger,
606            colors: ctx.colors,
607            config: ctx.config,
608            #[cfg(any(test, feature = "test-utils"))]
609            agent_executor: None,
610        };
611
612        let _exit_code = run_with_fallback(
613            AgentRole::Developer,
614            &format!("planning #{}", iteration),
615            &plan_prompt,
616            &log_dir,
617            &mut runtime,
618            ctx.registry,
619            ctx.developer_agent,
620        )?;
621
622        // Extract and validate the plan XML
623        let log_dir_path = Path::new(&log_dir);
624        let plan_content = read_last_planning_output(log_dir_path);
625
626        // Try to extract XML - if extraction fails, assume entire output is XML
627        // and validate it to get specific XSD errors for retry
628        let xml_to_validate = if let Some(xml_content) = extract_plan_xml(&plan_content) {
629            xml_content
630        } else {
631            // No XML tags found - assume the entire content is XML for validation
632            // This allows us to get specific XSD errors to send back to the agent
633            plan_content.clone()
634        };
635
636        // Try to validate against XSD
637        match validate_plan_xml(&xml_to_validate) {
638            Ok(plan_elements) => {
639                // XSD validation passed - format and write the plan
640                let formatted_xml = format_xml_for_display(&xml_to_validate);
641
642                // Convert XML to markdown format for PLAN.md
643                let markdown = format_plan_as_markdown(&plan_elements);
644                fs::write(plan_path, &markdown)?;
645
646                if retry_num > 0 {
647                    ctx.logger
648                        .success(&format!("Plan validated after {} retries", retry_num));
649                } else {
650                    ctx.logger.success("Plan extracted and validated (XML)");
651                }
652
653                // Display the formatted plan
654                ctx.logger.info(&format!("\n{}", formatted_xml));
655
656                // Record execution history before returning
657                {
658                    let duration = start_time.elapsed().as_secs();
659                    let step = ExecutionStep::new(
660                        "Planning",
661                        iteration,
662                        "plan_generation",
663                        StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
664                    )
665                    .with_agent(ctx.developer_agent)
666                    .with_duration(duration);
667                    ctx.execution_history.add_step(step);
668                }
669
670                return Ok(());
671            }
672            Err(xsd_err) => {
673                // XSD validation failed - check if we can retry
674                let error_msg = format_xsd_error(&xsd_err);
675                ctx.logger
676                    .warn(&format!("  XSD validation failed: {}", error_msg));
677
678                if retry_num < max_retries - 1 {
679                    // Store error for next retry attempt
680                    xsd_error = Some(error_msg);
681                    // Continue to next retry iteration
682                    continue;
683                } else {
684                    ctx.logger
685                        .error("  No more in-session XSD retries remaining");
686                    // Write placeholder and fail
687                    let placeholder = "# Plan\n\nAgent produced no valid XML output. Only XML format is accepted.\n";
688                    fs::write(plan_path, placeholder)?;
689                    anyhow::bail!(
690                        "Planning agent did not produce valid XML output after {} attempts",
691                        max_retries
692                    );
693                }
694            }
695        }
696    }
697
698    // Record execution history for failed planning (should never be reached since we always return above)
699    {
700        let duration = start_time.elapsed().as_secs();
701        let step = ExecutionStep::new(
702            "Planning",
703            iteration,
704            "plan_generation",
705            StepOutcome::failure("No valid XML output produced".to_string(), false),
706        )
707        .with_agent(ctx.developer_agent)
708        .with_duration(duration);
709        ctx.execution_history.add_step(step);
710    }
711
712    anyhow::bail!("Planning failed after {} XSD retry attempts", max_retries)
713}
714
715/// Read the last planning output from logs.
716///
717/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/planning_1`.
718/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
719/// `.agent/logs/planning_1_ccs-glm_0.log`
720fn read_last_planning_output(log_prefix: &Path) -> String {
721    // The log_prefix is a prefix like ".agent/logs/planning_1"
722    // Actual files are "{prefix}_{agent}_{model}.log"
723    // We need to find files that match this prefix pattern in the parent directory
724
725    let parent = log_prefix.parent().unwrap_or(Path::new("."));
726    let prefix_str = log_prefix
727        .file_name()
728        .and_then(|s| s.to_str())
729        .unwrap_or("");
730
731    // Find all log files matching the prefix pattern and get the most recently modified one
732    let mut best_file: Option<(std::path::PathBuf, std::time::SystemTime)> = None;
733
734    if let Ok(entries) = fs::read_dir(parent) {
735        for entry in entries.flatten() {
736            let path = entry.path();
737            if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
738                // Match files that start with our prefix and end with .log
739                if filename.starts_with(prefix_str)
740                    && filename.len() > prefix_str.len()
741                    && filename.ends_with(".log")
742                {
743                    // Get modification time for this file
744                    if let Ok(metadata) = fs::metadata(&path) {
745                        if let Ok(modified) = metadata.modified() {
746                            match &best_file {
747                                None => best_file = Some((path.clone(), modified)),
748                                Some((_, best_time)) if modified > *best_time => {
749                                    best_file = Some((path.clone(), modified));
750                                }
751                                _ => {}
752                            }
753                        }
754                    }
755                }
756            }
757        }
758    }
759
760    // Read the most recently modified matching log file
761    if let Some((path, _)) = best_file {
762        if let Ok(content) = fs::read_to_string(&path) {
763            return content;
764        }
765    }
766
767    String::new()
768}
769
770/// Read the last development output from logs.
771///
772/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/development_1`.
773/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
774/// `.agent/logs/development_1_ccs-glm_0.log`
775fn read_last_development_output(log_prefix: &Path) -> String {
776    // The log_prefix is a prefix like ".agent/logs/development_1"
777    // Actual files are "{prefix}_{agent}_{model}.log"
778    // We need to find files that match this prefix pattern in the parent directory
779
780    let parent = log_prefix.parent().unwrap_or(Path::new("."));
781    let prefix_str = log_prefix
782        .file_name()
783        .and_then(|s| s.to_str())
784        .unwrap_or("");
785
786    // Find all log files matching the prefix pattern and get the most recently modified one
787    let mut best_file: Option<(std::path::PathBuf, std::time::SystemTime)> = None;
788
789    if let Ok(entries) = fs::read_dir(parent) {
790        for entry in entries.flatten() {
791            let path = entry.path();
792            if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
793                // Match files that start with our prefix and end with .log
794                if filename.starts_with(prefix_str)
795                    && filename.len() > prefix_str.len()
796                    && filename.ends_with(".log")
797                {
798                    // Get modification time for this file
799                    if let Ok(metadata) = fs::metadata(&path) {
800                        if let Ok(modified) = metadata.modified() {
801                            match &best_file {
802                                None => best_file = Some((path.clone(), modified)),
803                                Some((_, best_time)) if modified > *best_time => {
804                                    best_file = Some((path.clone(), modified));
805                                }
806                                _ => {}
807                            }
808                        }
809                    }
810                }
811            }
812        }
813    }
814
815    // Read the most recently modified matching log file
816    if let Some((path, _)) = best_file {
817        if let Ok(content) = fs::read_to_string(&path) {
818            return content;
819        }
820    }
821
822    String::new()
823}
824
825/// Format XSD error for display.
826fn format_xsd_error(error: &XsdValidationError) -> String {
827    format!(
828        "{} - expected: {}, found: {}",
829        error.element_path, error.expected, error.found
830    )
831}
832
833/// Format plan elements as markdown for PLAN.md.
834fn format_plan_as_markdown(elements: &PlanElements) -> String {
835    let mut result = String::new();
836
837    result.push_str("## Summary\n\n");
838    result.push_str(&elements.summary);
839    result.push_str("\n\n");
840
841    result.push_str("## Implementation Steps\n\n");
842    result.push_str(&elements.implementation_steps);
843    result.push_str("\n\n");
844
845    if let Some(ref critical_files) = elements.critical_files {
846        result.push_str("## Critical Files for Implementation\n\n");
847        result.push_str(critical_files);
848        result.push_str("\n\n");
849    }
850
851    if let Some(ref risks) = elements.risks_mitigations {
852        result.push_str("## Risks & Mitigations\n\n");
853        result.push_str(risks);
854        result.push_str("\n\n");
855    }
856
857    if let Some(ref verification) = elements.verification_strategy {
858        result.push_str("## Verification Strategy\n\n");
859        result.push_str(verification);
860        result.push_str("\n\n");
861    }
862
863    result
864}
865
866/// Verify that PLAN.md exists and is non-empty.
867///
868/// With orchestrator-controlled file I/O, `run_planning_step` always writes
869/// PLAN.md (even if just a placeholder). This function checks if the file
870/// exists and has meaningful content. If resuming and plan is missing,
871/// re-run planning.
872fn verify_plan_exists(
873    ctx: &mut PhaseContext<'_>,
874    iteration: u32,
875    resuming_into_development: bool,
876) -> anyhow::Result<bool> {
877    let plan_path = Path::new(".agent/PLAN.md");
878
879    let plan_ok = plan_path
880        .exists()
881        .then(|| fs::read_to_string(plan_path).ok())
882        .flatten()
883        .is_some_and(|s| !s.trim().is_empty());
884
885    // If resuming and plan is missing, re-run planning to recover
886    if !plan_ok && resuming_into_development {
887        ctx.logger
888            .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
889        run_planning_step(ctx, iteration)?;
890
891        // Check again after rerunning - orchestrator guarantees file exists
892        let plan_ok = plan_path
893            .exists()
894            .then(|| fs::read_to_string(plan_path).ok())
895            .flatten()
896            .is_some_and(|s| !s.trim().is_empty());
897
898        return Ok(plan_ok);
899    }
900
901    Ok(plan_ok)
902}
903
904/// Run fast check command.
905fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
906    let argv = crate::common::split_command(fast_cmd)
907        .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
908    if argv.is_empty() {
909        ctx.logger
910            .warn("FAST_CHECK_CMD is empty; skipping fast check");
911        return Ok(());
912    }
913
914    let display_cmd = crate::common::format_argv_for_log(&argv);
915    ctx.logger.info(&format!(
916        "Running fast check: {}{}{}",
917        ctx.colors.dim(),
918        display_cmd,
919        ctx.colors.reset()
920    ));
921
922    let Some((program, cmd_args)) = argv.split_first() else {
923        ctx.logger
924            .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
925        return Ok(());
926    };
927    let status = Command::new(program).args(cmd_args).status()?;
928
929    if status.success() {
930        ctx.logger.success("Fast check passed");
931    } else {
932        ctx.logger.warn("Fast check had issues (non-blocking)");
933    }
934
935    Ok(())
936}
937
938/// Handle commit creation after development changes are detected.
939///
940/// Creates a commit with an auto-generated message using the primary commit agent.
941/// This is done by the orchestrator, not the agent, using fallback-aware commit
942/// generation which tries multiple agents if needed.
943fn handle_commit_after_development(
944    ctx: &mut PhaseContext<'_>,
945    iteration: u32,
946) -> anyhow::Result<()> {
947    let start_time = Instant::now();
948    // Get the primary commit agent from the registry
949    let commit_agent = get_primary_commit_agent(ctx);
950
951    if let Some(agent) = commit_agent {
952        ctx.logger.info(&format!(
953            "Creating commit with auto-generated message (agent: {agent})..."
954        ));
955
956        // Get the diff for commit message generation
957        let diff = match crate::git_helpers::git_diff() {
958            Ok(d) => d,
959            Err(e) => {
960                ctx.logger
961                    .error(&format!("Failed to get diff for commit: {e}"));
962                return Err(anyhow::anyhow!(e));
963            }
964        };
965
966        // Get git identity from config
967        let git_name = ctx.config.git_user_name.as_deref();
968        let git_email = ctx.config.git_user_email.as_deref();
969
970        let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
971
972        match result {
973            CommitResultFallback::Success(oid) => {
974                ctx.logger
975                    .success(&format!("Commit created successfully: {oid}"));
976                ctx.stats.commits_created += 1;
977
978                {
979                    let duration = start_time.elapsed().as_secs();
980                    let step = ExecutionStep::new(
981                        "Development",
982                        iteration,
983                        "commit",
984                        StepOutcome::success(Some(oid.to_string()), vec![]),
985                    )
986                    .with_agent(&agent)
987                    .with_duration(duration);
988                    ctx.execution_history.add_step(step);
989                }
990            }
991            CommitResultFallback::NoChanges => {
992                // No meaningful changes to commit (already handled by has_meaningful_changes)
993                ctx.logger.info("No commit created (no meaningful changes)");
994
995                {
996                    let duration = start_time.elapsed().as_secs();
997                    let step = ExecutionStep::new(
998                        "Development",
999                        iteration,
1000                        "commit",
1001                        StepOutcome::skipped("No meaningful changes to commit".to_string()),
1002                    )
1003                    .with_duration(duration);
1004                    ctx.execution_history.add_step(step);
1005                }
1006            }
1007            CommitResultFallback::Failed(err) => {
1008                // Actual git operation failed - this is critical
1009                ctx.logger.error(&format!(
1010                    "Failed to create commit (git operation failed): {err}"
1011                ));
1012
1013                {
1014                    let duration = start_time.elapsed().as_secs();
1015                    let step = ExecutionStep::new(
1016                        "Development",
1017                        iteration,
1018                        "commit",
1019                        StepOutcome::failure(err.to_string(), false),
1020                    )
1021                    .with_duration(duration);
1022                    ctx.execution_history.add_step(step);
1023                }
1024
1025                // Don't continue - this is a real error that needs attention
1026                return Err(anyhow::anyhow!(err));
1027            }
1028        }
1029    } else {
1030        ctx.logger
1031            .warn("Unable to get primary commit agent for commit");
1032
1033        {
1034            let duration = start_time.elapsed().as_secs();
1035            let step = ExecutionStep::new(
1036                "Development",
1037                iteration,
1038                "commit",
1039                StepOutcome::failure("No commit agent available".to_string(), true),
1040            )
1041            .with_duration(duration);
1042            ctx.execution_history.add_step(step);
1043        }
1044    }
1045
1046    Ok(())
1047}