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    read_last_output_from_prefix(log_prefix)
722}
723
724/// Read the last development output from logs.
725///
726/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/development_1`.
727/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
728/// `.agent/logs/development_1_ccs-glm_0.log`
729fn read_last_development_output(log_prefix: &Path) -> String {
730    read_last_output_from_prefix(log_prefix)
731}
732
733/// Read the most recent log file matching a prefix pattern.
734///
735/// This is a shared helper for reading log output. Truncation of large prompts
736/// is handled centrally in `build_agent_command` to prevent E2BIG errors.
737fn read_last_output_from_prefix(log_prefix: &Path) -> String {
738    let parent = log_prefix.parent().unwrap_or(Path::new("."));
739    let prefix_str = log_prefix
740        .file_name()
741        .and_then(|s| s.to_str())
742        .unwrap_or("");
743
744    // Find all log files matching the prefix pattern and get the most recently modified one
745    let mut best_file: Option<(std::path::PathBuf, std::time::SystemTime)> = None;
746
747    if let Ok(entries) = fs::read_dir(parent) {
748        for entry in entries.flatten() {
749            let path = entry.path();
750            if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
751                // Match files that start with our prefix and end with .log
752                if filename.starts_with(prefix_str)
753                    && filename.len() > prefix_str.len()
754                    && filename.ends_with(".log")
755                {
756                    // Get modification time for this file
757                    if let Ok(metadata) = fs::metadata(&path) {
758                        if let Ok(modified) = metadata.modified() {
759                            match &best_file {
760                                None => best_file = Some((path.clone(), modified)),
761                                Some((_, best_time)) if modified > *best_time => {
762                                    best_file = Some((path.clone(), modified));
763                                }
764                                _ => {}
765                            }
766                        }
767                    }
768                }
769            }
770        }
771    }
772
773    // Read the most recently modified matching log file
774    if let Some((path, _)) = best_file {
775        if let Ok(content) = fs::read_to_string(&path) {
776            return content;
777        }
778    }
779
780    String::new()
781}
782
783/// Format XSD error for display.
784fn format_xsd_error(error: &XsdValidationError) -> String {
785    format!(
786        "{} - expected: {}, found: {}",
787        error.element_path, error.expected, error.found
788    )
789}
790
791/// Format plan elements as markdown for PLAN.md.
792fn format_plan_as_markdown(elements: &PlanElements) -> String {
793    let mut result = String::new();
794
795    result.push_str("## Summary\n\n");
796    result.push_str(&elements.summary);
797    result.push_str("\n\n");
798
799    result.push_str("## Implementation Steps\n\n");
800    result.push_str(&elements.implementation_steps);
801    result.push_str("\n\n");
802
803    if let Some(ref critical_files) = elements.critical_files {
804        result.push_str("## Critical Files for Implementation\n\n");
805        result.push_str(critical_files);
806        result.push_str("\n\n");
807    }
808
809    if let Some(ref risks) = elements.risks_mitigations {
810        result.push_str("## Risks & Mitigations\n\n");
811        result.push_str(risks);
812        result.push_str("\n\n");
813    }
814
815    if let Some(ref verification) = elements.verification_strategy {
816        result.push_str("## Verification Strategy\n\n");
817        result.push_str(verification);
818        result.push_str("\n\n");
819    }
820
821    result
822}
823
824/// Verify that PLAN.md exists and is non-empty.
825///
826/// With orchestrator-controlled file I/O, `run_planning_step` always writes
827/// PLAN.md (even if just a placeholder). This function checks if the file
828/// exists and has meaningful content. If resuming and plan is missing,
829/// re-run planning.
830fn verify_plan_exists(
831    ctx: &mut PhaseContext<'_>,
832    iteration: u32,
833    resuming_into_development: bool,
834) -> anyhow::Result<bool> {
835    let plan_path = Path::new(".agent/PLAN.md");
836
837    let plan_ok = plan_path
838        .exists()
839        .then(|| fs::read_to_string(plan_path).ok())
840        .flatten()
841        .is_some_and(|s| !s.trim().is_empty());
842
843    // If resuming and plan is missing, re-run planning to recover
844    if !plan_ok && resuming_into_development {
845        ctx.logger
846            .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
847        run_planning_step(ctx, iteration)?;
848
849        // Check again after rerunning - orchestrator guarantees file exists
850        let plan_ok = plan_path
851            .exists()
852            .then(|| fs::read_to_string(plan_path).ok())
853            .flatten()
854            .is_some_and(|s| !s.trim().is_empty());
855
856        return Ok(plan_ok);
857    }
858
859    Ok(plan_ok)
860}
861
862/// Run fast check command.
863fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
864    let argv = crate::common::split_command(fast_cmd)
865        .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
866    if argv.is_empty() {
867        ctx.logger
868            .warn("FAST_CHECK_CMD is empty; skipping fast check");
869        return Ok(());
870    }
871
872    let display_cmd = crate::common::format_argv_for_log(&argv);
873    ctx.logger.info(&format!(
874        "Running fast check: {}{}{}",
875        ctx.colors.dim(),
876        display_cmd,
877        ctx.colors.reset()
878    ));
879
880    let Some((program, cmd_args)) = argv.split_first() else {
881        ctx.logger
882            .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
883        return Ok(());
884    };
885    let status = Command::new(program).args(cmd_args).status()?;
886
887    if status.success() {
888        ctx.logger.success("Fast check passed");
889    } else {
890        ctx.logger.warn("Fast check had issues (non-blocking)");
891    }
892
893    Ok(())
894}
895
896/// Handle commit creation after development changes are detected.
897///
898/// Creates a commit with an auto-generated message using the primary commit agent.
899/// This is done by the orchestrator, not the agent, using fallback-aware commit
900/// generation which tries multiple agents if needed.
901fn handle_commit_after_development(
902    ctx: &mut PhaseContext<'_>,
903    iteration: u32,
904) -> anyhow::Result<()> {
905    let start_time = Instant::now();
906    // Get the primary commit agent from the registry
907    let commit_agent = get_primary_commit_agent(ctx);
908
909    if let Some(agent) = commit_agent {
910        ctx.logger.info(&format!(
911            "Creating commit with auto-generated message (agent: {agent})..."
912        ));
913
914        // Get the diff for commit message generation
915        let diff = match crate::git_helpers::git_diff() {
916            Ok(d) => d,
917            Err(e) => {
918                ctx.logger
919                    .error(&format!("Failed to get diff for commit: {e}"));
920                return Err(anyhow::anyhow!(e));
921            }
922        };
923
924        // Get git identity from config
925        let git_name = ctx.config.git_user_name.as_deref();
926        let git_email = ctx.config.git_user_email.as_deref();
927
928        let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
929
930        match result {
931            CommitResultFallback::Success(oid) => {
932                ctx.logger
933                    .success(&format!("Commit created successfully: {oid}"));
934                ctx.stats.commits_created += 1;
935
936                {
937                    let duration = start_time.elapsed().as_secs();
938                    let step = ExecutionStep::new(
939                        "Development",
940                        iteration,
941                        "commit",
942                        StepOutcome::success(Some(oid.to_string()), vec![]),
943                    )
944                    .with_agent(&agent)
945                    .with_duration(duration);
946                    ctx.execution_history.add_step(step);
947                }
948            }
949            CommitResultFallback::NoChanges => {
950                // No meaningful changes to commit (already handled by has_meaningful_changes)
951                ctx.logger.info("No commit created (no meaningful changes)");
952
953                {
954                    let duration = start_time.elapsed().as_secs();
955                    let step = ExecutionStep::new(
956                        "Development",
957                        iteration,
958                        "commit",
959                        StepOutcome::skipped("No meaningful changes to commit".to_string()),
960                    )
961                    .with_duration(duration);
962                    ctx.execution_history.add_step(step);
963                }
964            }
965            CommitResultFallback::Failed(err) => {
966                // Actual git operation failed - this is critical
967                ctx.logger.error(&format!(
968                    "Failed to create commit (git operation failed): {err}"
969                ));
970
971                {
972                    let duration = start_time.elapsed().as_secs();
973                    let step = ExecutionStep::new(
974                        "Development",
975                        iteration,
976                        "commit",
977                        StepOutcome::failure(err.to_string(), false),
978                    )
979                    .with_duration(duration);
980                    ctx.execution_history.add_step(step);
981                }
982
983                // Don't continue - this is a real error that needs attention
984                return Err(anyhow::anyhow!(err));
985            }
986        }
987    } else {
988        ctx.logger
989            .warn("Unable to get primary commit agent for commit");
990
991        {
992            let duration = start_time.elapsed().as_secs();
993            let step = ExecutionStep::new(
994                "Development",
995                iteration,
996                "commit",
997                StepOutcome::failure("No commit agent available".to_string(), true),
998            )
999            .with_duration(duration);
1000            ctx.execution_history.add_step(step);
1001        }
1002    }
1003
1004    Ok(())
1005}