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_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
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 100)
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 = 100;
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        let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
288
289        // Inner loop: XSD validation retry with error feedback
290        // Session continuation allows the AI to retain memory between XSD retries
291        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            // For initial attempt, use XML prompt
296            // For retries, use XSD retry prompt with error feedback
297            let dev_prompt = if !is_retry && !is_continuation {
298                // First attempt ever - use initial XML prompt
299                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                // XSD retry only (no continuation yet)
321                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                // Continuation only (first XSD attempt after continuation)
342                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                // Both continuation and XSD retry
354                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            // Run the agent with session continuation for XSD retries
376            // This is completely fault-tolerant - if session continuation fails for any reason
377            // (including agent crash, segfault, invalid session), it falls back to normal behavior
378            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            // Track if any agent run had an error (for final result)
412            if exit_code != 0 {
413                had_any_error = true;
414            }
415
416            // Extract and validate the development result XML
417            let log_dir_path = Path::new(&log_dir);
418            let dev_content = read_last_development_output(log_dir_path);
419
420            // Extract session info for potential retry (only if we don't have it yet)
421            // This is best-effort - if extraction fails, we just won't use session continuation
422            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            // Try to extract XML - if extraction fails, assume entire output is XML
447            // and validate it to get specific XSD errors for retry
448            let xml_to_validate =
449                if let Some(xml_content) = extract_development_result_xml(&dev_content) {
450                    xml_content
451                } else {
452                    // No XML tags found - assume the entire content is XML for validation
453                    // This allows us to get specific XSD errors to send back to the agent
454                    dev_content.clone()
455                };
456
457            // Try to validate against XSD
458            match validate_development_result_xml(&xml_to_validate) {
459                Ok(result_elements) => {
460                    // XSD validation passed - format and log the result
461                    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                    // Display the formatted status
471                    ctx.logger.info(&format!("\n{}", formatted_xml));
472
473                    // Store the results
474                    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                    // Check the status to determine if we should continue
481                    if result_elements.is_completed() {
482                        // Status is "completed" - we're done with this iteration
483                        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                        // Status is "partial" - continue the outer loop
490                        ctx.logger
491                            .info("Status is 'partial' - continuing with same iteration");
492                        continue 'continuation;
493                    } else if result_elements.is_failed() {
494                        // Status is "failed" - continue the outer loop
495                        ctx.logger
496                            .warn("Status is 'failed' - continuing with same iteration");
497                        continue 'continuation;
498                    }
499                }
500                Err(xsd_err) => {
501                    // XSD validation failed - check if we can retry
502                    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                        // Store error for next retry attempt
508                        xsd_error = Some(error_msg);
509                        // Continue to next XSD retry iteration
510                        continue;
511                    } else {
512                        ctx.logger
513                            .warn("  No more in-session XSD retries remaining");
514                        // Fall through to return what we have
515                        break 'continuation;
516                    }
517                }
518            }
519        }
520
521        // If we've exhausted XSD retries, break the continuation loop
522        ctx.logger
523            .warn("XSD retry loop exhausted - stopping continuation");
524        break;
525    }
526
527    // If we get here, we exhausted the continuation limit or XSD retries
528    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
540/// Run the planning step to create PLAN.md.
541///
542/// The orchestrator ALWAYS extracts and writes PLAN.md from agent XML output.
543/// Uses XSD validation with retry loop to ensure valid XML format.
544fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
545    let start_time = Instant::now();
546    // Save checkpoint at start of planning phase (if enabled)
547    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    // Read PROMPT.md content to include directly in the planning prompt
575    // This prevents agents from discovering PROMPT.md through file exploration,
576    // which reduces the risk of accidental deletion.
577    let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
578
579    // Note: We don't set is_resume for planning since planning runs on each iteration.
580    // The resume context is set during the development execution step.
581    let prompt_key = format!("planning_{}", iteration);
582    let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
583
584    // Use prompt replay if available, otherwise generate new prompt
585    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    // Capture the planning prompt for checkpoint/resume (only if newly generated)
591    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    // Ensure .agent directory exists
604    if let Some(parent) = plan_path.parent() {
605        fs::create_dir_all(parent)?;
606    }
607
608    // In-session retry loop with XSD validation feedback
609    // Session continuation allows the AI to retain memory between XSD retries
610    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        // For initial attempt, use XML prompt
616        // For retries, use XSD retry prompt with error feedback
617        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            // Read the last output for retry context (used as fallback if session continuation fails)
630            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        // Use session continuation for XSD retries (retry_num > 0)
650        // This is completely fault-tolerant - if session continuation fails for any reason
651        // (including agent crash, segfault, invalid session), it falls back to normal behavior
652        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        // Extract and validate the plan XML
668        let log_dir_path = Path::new(&log_dir);
669        let plan_content = read_last_planning_output(log_dir_path);
670
671        // Extract session info for potential retry (only if we don't have it yet)
672        // This is best-effort - if extraction fails, we just won't use session continuation
673        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        // Try to extract XML - if extraction fails, assume entire output is XML
684        // and validate it to get specific XSD errors for retry
685        let xml_to_validate = if let Some(xml_content) = extract_plan_xml(&plan_content) {
686            xml_content
687        } else {
688            // No XML tags found - assume the entire content is XML for validation
689            // This allows us to get specific XSD errors to send back to the agent
690            plan_content.clone()
691        };
692
693        // Try to validate against XSD
694        match validate_plan_xml(&xml_to_validate) {
695            Ok(plan_elements) => {
696                // XSD validation passed - format and write the plan
697                let formatted_xml = format_xml_for_display(&xml_to_validate);
698
699                // Convert XML to markdown format for PLAN.md
700                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                // Display the formatted plan
711                ctx.logger.info(&format!("\n{}", formatted_xml));
712
713                // Record execution history before returning
714                {
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                // XSD validation failed - check if we can retry
731                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                    // Store error for next retry attempt
737                    xsd_error = Some(error_msg);
738                    // Continue to next retry iteration
739                    continue;
740                } else {
741                    ctx.logger
742                        .error("  No more in-session XSD retries remaining");
743                    // Write placeholder and fail
744                    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    // Record execution history for failed planning (should never be reached since we always return above)
756    {
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
772/// Read the last planning output from logs.
773///
774/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/planning_1`.
775/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
776/// `.agent/logs/planning_1_ccs-glm_0.log`
777fn read_last_planning_output(log_prefix: &Path) -> String {
778    read_last_output_from_prefix(log_prefix)
779}
780
781/// Read the last development output from logs.
782///
783/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/development_1`.
784/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
785/// `.agent/logs/development_1_ccs-glm_0.log`
786fn read_last_development_output(log_prefix: &Path) -> String {
787    read_last_output_from_prefix(log_prefix)
788}
789
790/// Read the most recent log file matching a prefix pattern.
791///
792/// This is a shared helper for reading log output. Truncation of large prompts
793/// is handled centrally in `build_agent_command` to prevent E2BIG errors.
794fn read_last_output_from_prefix(log_prefix: &Path) -> String {
795    crate::pipeline::logfile::read_most_recent_logfile(log_prefix)
796}
797
798/// Format XSD error for display.
799fn format_xsd_error(error: &XsdValidationError) -> String {
800    format!(
801        "{} - expected: {}, found: {}",
802        error.element_path, error.expected, error.found
803    )
804}
805
806/// Format plan elements as markdown for PLAN.md.
807fn format_plan_as_markdown(elements: &PlanElements) -> String {
808    let mut result = String::new();
809
810    // Summary section
811    result.push_str("## Summary\n\n");
812    result.push_str(&elements.summary.context);
813    result.push_str("\n\n");
814
815    // Scope items
816    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    // Implementation steps
831    result.push_str("## Implementation Steps\n\n");
832    for step in &elements.steps {
833        // Step header
834        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        // Target files
865        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        // Location
885        if let Some(ref location) = step.location {
886            result.push_str(&format!("**Location:** {}\n\n", location));
887        }
888
889        // Rationale
890        if let Some(ref rationale) = step.rationale {
891            result.push_str(&format!("**Rationale:** {}\n\n", rationale));
892        }
893
894        // Content
895        result.push_str(&format_rich_content(&step.content));
896        result.push('\n');
897
898        // Dependencies
899        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    // Critical files
912    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    // Risks and mitigations
943    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    // Verification strategy
965    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
974/// Format rich content elements to markdown.
975fn 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                // Header row
1002                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                    // Infer column count from first row
1013                    result.push('|');
1014                    for _ in &first_row.cells {
1015                        result.push_str(" --- |");
1016                    }
1017                    result.push('\n');
1018                }
1019                // Data rows
1020                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
1046/// Format inline content elements.
1047fn 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
1064/// Format a list element with proper indentation.
1065fn 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
1093/// Verify that PLAN.md exists and is non-empty.
1094///
1095/// With orchestrator-controlled file I/O, `run_planning_step` always writes
1096/// PLAN.md (even if just a placeholder). This function checks if the file
1097/// exists and has meaningful content. If resuming and plan is missing,
1098/// re-run planning.
1099fn 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 resuming and plan is missing, re-run planning to recover
1113    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        // Check again after rerunning - orchestrator guarantees file exists
1119        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
1131/// Run fast check command.
1132fn 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
1165/// Handle commit creation after development changes are detected.
1166///
1167/// Creates a commit with an auto-generated message using the primary commit agent.
1168/// This is done by the orchestrator, not the agent, using fallback-aware commit
1169/// generation which tries multiple agents if needed.
1170fn handle_commit_after_development(
1171    ctx: &mut PhaseContext<'_>,
1172    iteration: u32,
1173) -> anyhow::Result<()> {
1174    let start_time = Instant::now();
1175    // Get the primary commit agent from the registry
1176    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        // Get the diff for commit message generation
1184        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        // Get git identity from config
1194        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                // No meaningful changes to commit (already handled by has_meaningful_changes)
1220                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                // Actual git operation failed - this is critical
1236                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                // Don't continue - this is a real error that needs attention
1253                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}