Skip to main content

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_with_workspace, CheckpointBuilder, PipelinePhase};
13use crate::files::llm_output_extraction::xsd_validation::XsdValidationError;
14use crate::files::llm_output_extraction::{
15    archive_xml_file_with_workspace, extract_development_result_xml, extract_plan_xml,
16    extract_xml_with_file_fallback_with_workspace, format_xml_for_display,
17    validate_development_result_xml, validate_plan_xml, xml_paths, PlanElements,
18};
19use crate::files::{delete_plan_file_with_workspace, update_status_with_workspace};
20use crate::git_helpers::{git_snapshot, CommitResultFallback};
21use crate::logger::print_progress;
22use crate::phases::commit::commit_with_generated_message;
23use crate::phases::get_primary_commit_agent;
24use crate::phases::integrity::ensure_prompt_integrity;
25use crate::pipeline::{run_xsd_retry_with_session, PipelineRuntime, XsdRetryConfig};
26use crate::prompts::{
27    get_stored_or_generate_prompt, prompt_developer_iteration_xml_with_context,
28    prompt_developer_iteration_xsd_retry_with_context, prompt_planning_xml_with_context,
29    prompt_planning_xsd_retry_with_context, ContextLevel,
30};
31use std::path::Path;
32
33use super::context::PhaseContext;
34
35use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
36
37use std::time::Instant;
38
39/// Result of the development phase.
40pub struct DevelopmentResult {
41    /// Whether any errors occurred during the phase.
42    pub had_errors: bool,
43}
44
45/// Run the development phase.
46///
47/// This phase runs `developer_iters` iterations, each consisting of:
48/// 1. Planning: Create PLAN.md from PROMPT.md
49/// 2. Execution: Execute the plan
50/// 3. Cleanup: Delete PLAN.md
51///
52/// # Arguments
53///
54/// * `ctx` - The phase context containing shared state
55/// * `start_iter` - The iteration to start from (for resume support)
56/// * `resume_context` - Optional resume context for resumed sessions
57///
58/// # Returns
59///
60/// Returns `Ok(DevelopmentResult)` on success, or an error if a critical failure occurs.
61pub fn run_development_phase(
62    ctx: &mut PhaseContext<'_>,
63    start_iter: u32,
64    resume_context: Option<&ResumeContext>,
65) -> anyhow::Result<DevelopmentResult> {
66    let mut had_errors = false;
67    let mut prev_snap = git_snapshot()?;
68    let developer_context = ContextLevel::from(ctx.config.developer_context);
69
70    for i in start_iter..=ctx.config.developer_iters {
71        ctx.logger.subheader(&format!(
72            "Iteration {} of {}",
73            i, ctx.config.developer_iters
74        ));
75        print_progress(i, ctx.config.developer_iters, "Overall");
76
77        let resuming_into_development = resume_context.is_some() && i == start_iter;
78
79        // Step 1: Create PLAN from PROMPT (skip if resuming into development)
80        if resuming_into_development {
81            ctx.logger
82                .info("Resuming at development step; skipping plan generation");
83        } else {
84            run_planning_step(ctx, i)?;
85        }
86
87        // Verify PLAN.md was created (required)
88        let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
89        if !plan_ok {
90            anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
91        }
92        ctx.logger.success("PLAN.md created");
93
94        // Save checkpoint at start of development phase (if enabled)
95        if ctx.config.features.checkpoint_enabled {
96            let builder = CheckpointBuilder::new()
97                .phase(PipelinePhase::Development, i, ctx.config.developer_iters)
98                .reviewer_pass(0, ctx.config.reviewer_reviews)
99                .capture_from_context(
100                    ctx.config,
101                    ctx.registry,
102                    ctx.developer_agent,
103                    ctx.reviewer_agent,
104                    ctx.logger,
105                    &ctx.run_context,
106                )
107                .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
108                .with_execution_history(ctx.execution_history.clone())
109                .with_prompt_history(ctx.clone_prompt_history());
110
111            if let Some(checkpoint) = builder.build() {
112                let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
113            }
114        }
115
116        // 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_with_workspace(
122            ctx.workspace,
123            "Starting development iteration",
124            ctx.config.isolation_mode,
125        )?;
126
127        // Run development iteration with XML extraction and XSD validation
128        let dev_result = run_development_iteration_with_xml_retry(
129            ctx,
130            i,
131            developer_context,
132            resuming_into_development,
133            resume_context,
134            None,
135        )?;
136
137        if dev_result.had_error {
138            ctx.logger.error(&format!(
139                "Iteration {i} encountered an error but continuing"
140            ));
141            had_errors = true;
142        }
143
144        // Record stats
145        ctx.stats.developer_runs_completed += 1;
146
147        // Record execution history
148        {
149            let dev_start_time = Instant::now(); // Note: this is after the iteration runs
150            let duration = dev_start_time.elapsed().as_secs();
151            let outcome = if dev_result.had_error {
152                StepOutcome::failure("Agent exited with non-zero code".to_string(), true)
153            } else {
154                StepOutcome::success(
155                    dev_result.summary.clone(),
156                    dev_result.files_changed.clone().unwrap_or_default(),
157                )
158            };
159            let step = ExecutionStep::new("Development", i, "dev_run", outcome)
160                .with_agent(ctx.developer_agent)
161                .with_duration(duration);
162            ctx.execution_history.add_step(step);
163        }
164        update_status_with_workspace(
165            ctx.workspace,
166            "Completed progress step",
167            ctx.config.isolation_mode,
168        )?;
169
170        // Log the development result
171        if let Some(ref summary) = dev_result.summary {
172            ctx.logger
173                .info(&format!("Development summary: {}", summary));
174        }
175
176        let snap = git_snapshot()?;
177        if snap == prev_snap {
178            if snap.is_empty() {
179                ctx.logger
180                    .warn("No git-status change detected (repository is clean)");
181            } else {
182                ctx.logger.warn(&format!(
183                    "No git-status change detected (existing changes: {})",
184                    snap.lines().count()
185                ));
186            }
187        } else {
188            ctx.logger.success(&format!(
189                "Repository modified ({} file(s) changed)",
190                snap.lines().count()
191            ));
192            ctx.stats.changes_detected += 1;
193            handle_commit_after_development(ctx, i)?;
194        }
195        prev_snap = snap;
196
197        // Run fast check if configured
198        if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
199            run_fast_check(ctx, fast_cmd, i)?;
200        }
201
202        // Periodic restoration check - ensure PROMPT.md still exists
203        // This catches agent deletions and restores from backup
204        ensure_prompt_integrity(ctx.workspace, ctx.logger, "development", i);
205
206        // Step 3: Delete the PLAN
207        ctx.logger.info("Deleting PLAN.md...");
208        if let Err(err) = delete_plan_file_with_workspace(ctx.workspace) {
209            ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
210        }
211        ctx.logger.success("PLAN.md deleted");
212
213        // Save checkpoint after iteration completes (if enabled)
214        // This checkpoint captures the completed iteration so resume won't re-run it
215        if ctx.config.features.checkpoint_enabled {
216            let next_iteration = i + 1;
217            let builder = CheckpointBuilder::new()
218                .phase(
219                    PipelinePhase::Development,
220                    next_iteration,
221                    ctx.config.developer_iters,
222                )
223                .reviewer_pass(0, ctx.config.reviewer_reviews)
224                .capture_from_context(
225                    ctx.config,
226                    ctx.registry,
227                    ctx.developer_agent,
228                    ctx.reviewer_agent,
229                    ctx.logger,
230                    &ctx.run_context,
231                )
232                .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
233                .with_execution_history(ctx.execution_history.clone())
234                .with_prompt_history(ctx.clone_prompt_history());
235
236            if let Some(checkpoint) = builder.build() {
237                let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
238            }
239        }
240    }
241
242    Ok(DevelopmentResult { had_errors })
243}
244
245/// Result of a single development iteration.
246#[derive(Debug)]
247pub struct DevIterationResult {
248    /// Whether an error occurred during iteration.
249    pub had_error: bool,
250    /// Optional summary of what was done.
251    pub summary: Option<String>,
252    /// Optional list of files changed.
253    pub files_changed: Option<Vec<String>>,
254}
255
256/// Run a single development iteration with XML extraction and XSD validation retry loop.
257///
258/// This function implements a nested loop structure:
259/// - **Outer loop (continuation)**: Continue while status != "completed" (max 100)
260/// - **Inner loop (XSD retry)**: Retry XSD validation with error feedback (max 100)
261///
262/// The continuation logic ignores non-XSD errors and only looks for valid XML.
263/// If XML passes XSD validation with status="completed", we're done for this iteration.
264/// If XML passes XSD validation with status="partial", we continue the outer loop.
265/// If XML passes XSD validation with status="failed", we continue the outer loop.
266///
267/// The development iteration produces side effects (file changes) as its primary output.
268/// The XML status is secondary - we use it for logging/tracking but don't fail the
269/// entire iteration if XML is missing or invalid.
270pub fn run_development_iteration_with_xml_retry(
271    ctx: &mut PhaseContext<'_>,
272    iteration: u32,
273    _developer_context: ContextLevel,
274    _resuming_into_development: bool,
275    _resume_context: Option<&ResumeContext>,
276    _agent: Option<&str>,
277) -> anyhow::Result<DevIterationResult> {
278    let prompt_md = ctx
279        .workspace
280        .read(Path::new("PROMPT.md"))
281        .unwrap_or_default();
282    let plan_md = ctx
283        .workspace
284        .read(Path::new(".agent/PLAN.md"))
285        .unwrap_or_default();
286    let log_dir = format!(".agent/logs/developer_{iteration}");
287
288    let max_xsd_retries = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize;
289    let max_continuations = crate::reducer::state::MAX_DEV_VALIDATION_RETRY_ATTEMPTS as usize; // Safety limit to prevent infinite loops
290    let mut final_summary: Option<String> = None;
291    let mut final_files_changed: Option<Vec<String>> = None;
292    let mut had_any_error = false;
293
294    // Outer loop: Continue until agent returns status="completed" or we hit the limit
295    'continuation: for continuation_num in 0..max_continuations {
296        let is_continuation = continuation_num > 0;
297        if is_continuation {
298            ctx.logger.info(&format!(
299                "Continuation {} of {} (status was not 'completed')",
300                continuation_num, max_continuations
301            ));
302        }
303
304        let mut xsd_error: Option<String> = None;
305        let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
306
307        // Inner loop: XSD validation retry with error feedback
308        // Session continuation allows the AI to retain memory between XSD retries
309        for retry_num in 0..max_xsd_retries {
310            let is_retry = retry_num > 0;
311            let total_attempts = continuation_num * max_xsd_retries + retry_num + 1;
312
313            // Before each retry, check if the XML file is writable and clean up if locked
314            // This prevents "permission denied" errors from stale file handles
315            if is_retry {
316                use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
317
318                let xml_path = Path::new(
319                    crate::files::llm_output_extraction::xml_paths::DEVELOPMENT_RESULT_XML,
320                );
321                let _ = check_and_cleanup_xml_before_retry_with_workspace(
322                    ctx.workspace,
323                    xml_path,
324                    ctx.logger,
325                );
326            }
327
328            // For initial attempt, use XML prompt
329            // For retries, use XSD retry prompt with error feedback
330            let dev_prompt = if !is_retry && !is_continuation {
331                // First attempt ever - use initial XML prompt
332                let prompt_key = format!("development_{}", iteration);
333                let (prompt, was_replayed) =
334                    get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
335                        prompt_developer_iteration_xml_with_context(
336                            ctx.template_context,
337                            &prompt_md,
338                            &plan_md,
339                        )
340                    });
341
342                if !was_replayed {
343                    ctx.capture_prompt(&prompt_key, &prompt);
344                } else {
345                    ctx.logger.info(&format!(
346                        "Using stored prompt from checkpoint for determinism: {}",
347                        prompt_key
348                    ));
349                }
350
351                prompt
352            } else if !is_continuation {
353                // XSD retry only (no continuation yet)
354                ctx.logger.info(&format!(
355                    "  In-session retry {}/{} for XSD validation (total attempt: {})",
356                    retry_num,
357                    max_xsd_retries - 1,
358                    total_attempts
359                ));
360                if let Some(ref error) = xsd_error {
361                    ctx.logger.info(&format!("  XSD error: {}", error));
362                }
363
364                let last_output = read_last_development_output(Path::new(&log_dir), ctx.workspace);
365
366                prompt_developer_iteration_xsd_retry_with_context(
367                    ctx.template_context,
368                    &prompt_md,
369                    &plan_md,
370                    xsd_error.as_deref().unwrap_or("Unknown error"),
371                    &last_output,
372                    ctx.workspace,
373                )
374            } else if !is_retry {
375                // Continuation only (first XSD attempt after continuation)
376                ctx.logger.info(&format!(
377                    "  Continuation attempt {} (XSD validation attempt {}/{})",
378                    total_attempts, 1, max_xsd_retries
379                ));
380
381                prompt_developer_iteration_xml_with_context(
382                    ctx.template_context,
383                    &prompt_md,
384                    &plan_md,
385                )
386            } else {
387                // Both continuation and XSD retry
388                ctx.logger.info(&format!(
389                    "  Continuation retry {}/{} for XSD validation (total attempt: {})",
390                    retry_num,
391                    max_xsd_retries - 1,
392                    total_attempts
393                ));
394                if let Some(ref error) = xsd_error {
395                    ctx.logger.info(&format!("  XSD error: {}", error));
396                }
397
398                let last_output = read_last_development_output(Path::new(&log_dir), ctx.workspace);
399
400                prompt_developer_iteration_xsd_retry_with_context(
401                    ctx.template_context,
402                    &prompt_md,
403                    &plan_md,
404                    xsd_error.as_deref().unwrap_or("Unknown error"),
405                    &last_output,
406                    ctx.workspace,
407                )
408            };
409
410            // Run the agent with session continuation for XSD retries
411            // This is completely fault-tolerant - if session continuation fails for any reason
412            // (including agent crash, segfault, invalid session), it falls back to normal behavior
413            let exit_code = {
414                let mut runtime = PipelineRuntime {
415                    timer: ctx.timer,
416                    logger: ctx.logger,
417                    colors: ctx.colors,
418                    config: ctx.config,
419                    executor: ctx.executor,
420                    executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
421                    workspace: ctx.workspace,
422                };
423                let base_label = format!(
424                    "run #{}{}",
425                    iteration,
426                    if is_continuation {
427                        format!(" (continuation {})", continuation_num)
428                    } else {
429                        String::new()
430                    }
431                );
432                let mut xsd_retry_config = XsdRetryConfig {
433                    role: AgentRole::Developer,
434                    base_label: &base_label,
435                    prompt: &dev_prompt,
436                    logfile_prefix: &log_dir,
437                    runtime: &mut runtime,
438                    registry: ctx.registry,
439                    primary_agent: _agent.unwrap_or(ctx.developer_agent),
440                    session_info: session_info.as_ref(),
441                    retry_num,
442                    output_validator: None,
443                    workspace: ctx.workspace,
444                };
445                run_xsd_retry_with_session(&mut xsd_retry_config)?
446            };
447
448            // Track if any agent run had an error (for final result)
449            if exit_code != 0 {
450                had_any_error = true;
451            }
452
453            // Extract and validate the development result XML
454            let log_dir_path = Path::new(&log_dir);
455            let dev_content = read_last_development_output(log_dir_path, ctx.workspace);
456
457            // Extract session info for potential retry (only if we don't have it yet)
458            // This is best-effort - if extraction fails, we just won't use session continuation
459            if session_info.is_none() {
460                if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
461                    ctx.logger.info(&format!(
462                        "  [dev] Extracting session from {:?} with parser {:?}",
463                        log_dir_path, agent_config.json_parser
464                    ));
465                    session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
466                        log_dir_path,
467                        agent_config.json_parser,
468                        Some(ctx.developer_agent),
469                        ctx.workspace,
470                    );
471                    if let Some(ref info) = session_info {
472                        ctx.logger.info(&format!(
473                            "  [dev] Extracted session: agent={}, session_id={}...",
474                            info.agent_name,
475                            &info.session_id[..8.min(info.session_id.len())]
476                        ));
477                    } else {
478                        ctx.logger
479                            .warn("  [dev] Failed to extract session info from log");
480                    }
481                }
482            }
483
484            // Try file-based extraction first - allows agents to write XML to .agent/tmp/development_result.xml
485            let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
486                ctx.workspace,
487                Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
488                &dev_content,
489                extract_development_result_xml,
490            )
491            .unwrap_or_else(|| {
492                // No XML found anywhere - assume entire log content is XML for validation
493                // This allows us to get specific XSD errors to send back to the agent
494                dev_content.clone()
495            });
496
497            // Try to validate against XSD
498            match validate_development_result_xml(&xml_to_validate) {
499                Ok(result_elements) => {
500                    // XSD validation passed - format and log the result
501                    let formatted_xml = format_xml_for_display(&xml_to_validate);
502
503                    // Archive the XML file for debugging (moves to .xml.processed)
504                    archive_xml_file_with_workspace(
505                        ctx.workspace,
506                        Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
507                    );
508
509                    if is_retry {
510                        ctx.logger
511                            .success(&format!("Status validated after {} retries", retry_num));
512                    } else {
513                        ctx.logger.success("Status extracted and validated (XML)");
514                    }
515
516                    // Display the formatted status
517                    ctx.logger.info(&format!("\n{}", formatted_xml));
518
519                    // Store the results
520                    final_summary = Some(result_elements.summary.clone());
521                    final_files_changed = result_elements
522                        .files_changed
523                        .as_ref()
524                        .map(|f| f.lines().map(|s| s.to_string()).collect());
525
526                    // Check the status to determine if we should continue
527                    if result_elements.is_completed() {
528                        // Status is "completed" - we're done with this iteration
529                        return Ok(DevIterationResult {
530                            had_error: had_any_error,
531                            summary: final_summary,
532                            files_changed: final_files_changed,
533                        });
534                    } else if result_elements.is_partial() {
535                        // Status is "partial" - continue the outer loop
536                        ctx.logger
537                            .info("Status is 'partial' - continuing with same iteration");
538                        continue 'continuation;
539                    } else if result_elements.is_failed() {
540                        // Status is "failed" - continue the outer loop
541                        ctx.logger
542                            .warn("Status is 'failed' - continuing with same iteration");
543                        continue 'continuation;
544                    }
545                }
546                Err(xsd_err) => {
547                    // XSD validation failed - check if we can retry
548                    let error_msg = format_xsd_error(&xsd_err);
549                    ctx.logger
550                        .warn(&format!("  XSD validation failed: {}", error_msg));
551
552                    if retry_num < max_xsd_retries - 1 {
553                        // Store error for next retry attempt
554                        xsd_error = Some(error_msg);
555                        // Continue to next XSD retry iteration
556                        continue;
557                    } else {
558                        ctx.logger
559                            .warn("  No more in-session XSD retries remaining");
560                        // Fall through to return what we have
561                        break 'continuation;
562                    }
563                }
564            }
565        }
566
567        // If we've exhausted XSD retries, break the continuation loop
568        ctx.logger
569            .warn("XSD retry loop exhausted - stopping continuation");
570        break;
571    }
572
573    // If we get here, we exhausted the continuation limit or XSD retries
574    Ok(DevIterationResult {
575        had_error: had_any_error,
576        summary: final_summary.or_else(|| {
577            Some(format!(
578                "Continuation stopped after {} attempts",
579                max_continuations * max_xsd_retries
580            ))
581        }),
582        files_changed: final_files_changed,
583    })
584}
585
586/// Run the planning step to create PLAN.md.
587///
588/// The orchestrator ALWAYS extracts and writes PLAN.md from agent XML output.
589/// Uses XSD validation with retry loop to ensure valid XML format.
590pub fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
591    let start_time = Instant::now();
592    // Save checkpoint at start of planning phase (if enabled)
593    if ctx.config.features.checkpoint_enabled {
594        let builder = CheckpointBuilder::new()
595            .phase(
596                PipelinePhase::Planning,
597                iteration,
598                ctx.config.developer_iters,
599            )
600            .reviewer_pass(0, ctx.config.reviewer_reviews)
601            .capture_from_context(
602                ctx.config,
603                ctx.registry,
604                ctx.developer_agent,
605                ctx.reviewer_agent,
606                ctx.logger,
607                &ctx.run_context,
608            )
609            .with_executor_from_context(std::sync::Arc::clone(&ctx.executor_arc))
610            .with_execution_history(ctx.execution_history.clone())
611            .with_prompt_history(ctx.clone_prompt_history());
612
613        if let Some(checkpoint) = builder.build() {
614            let _ = save_checkpoint_with_workspace(ctx.workspace, &checkpoint);
615        }
616    }
617
618    ctx.logger.info("Creating plan from PROMPT.md...");
619    update_status_with_workspace(
620        ctx.workspace,
621        "Starting planning phase",
622        ctx.config.isolation_mode,
623    )?;
624
625    // Read PROMPT.md content to include directly in the planning prompt
626    // This prevents agents from discovering PROMPT.md through file exploration,
627    // which reduces the risk of accidental deletion.
628    let prompt_md_content = ctx.workspace.read(Path::new("PROMPT.md")).ok();
629
630    // Note: We don't set is_resume for planning since planning runs on each iteration.
631    // The resume context is set during the development execution step.
632    let prompt_key = format!("planning_{}", iteration);
633    let prompt_md_str = prompt_md_content.as_deref().unwrap_or("");
634
635    // Use prompt replay if available, otherwise generate new prompt
636    let (plan_prompt, was_replayed) =
637        get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
638            prompt_planning_xml_with_context(
639                ctx.template_context,
640                Some(prompt_md_str),
641                ctx.workspace,
642            )
643        });
644
645    // Capture the planning prompt for checkpoint/resume (only if newly generated)
646    if !was_replayed {
647        ctx.capture_prompt(&prompt_key, &plan_prompt);
648    } else {
649        ctx.logger.info(&format!(
650            "Using stored prompt from checkpoint for determinism: {}",
651            prompt_key
652        ));
653    }
654
655    let log_dir = format!(".agent/logs/planning_{iteration}");
656    let plan_path = Path::new(".agent/PLAN.md");
657
658    // Ensure .agent directory exists
659    if let Some(parent) = plan_path.parent() {
660        ctx.workspace.create_dir_all(parent)?;
661    }
662
663    // In-session retry loop with XSD validation feedback
664    // Session continuation allows the AI to retain memory between XSD retries
665    let max_retries = crate::reducer::state::MAX_VALIDATION_RETRY_ATTEMPTS as usize;
666    let mut xsd_error: Option<String> = None;
667    let mut session_info: Option<crate::pipeline::session::SessionInfo> = None;
668
669    for retry_num in 0..max_retries {
670        // Before each retry, check if the XML file is writable and clean up if locked
671        if retry_num > 0 {
672            use crate::files::io::check_and_cleanup_xml_before_retry_with_workspace;
673            let xml_path = Path::new(crate::files::llm_output_extraction::xml_paths::PLAN_XML);
674            let _ = check_and_cleanup_xml_before_retry_with_workspace(
675                ctx.workspace,
676                xml_path,
677                ctx.logger,
678            );
679        }
680
681        // For initial attempt, use XML prompt
682        // For retries, use XSD retry prompt with error feedback
683        let plan_prompt = if retry_num == 0 {
684            plan_prompt.clone()
685        } else {
686            ctx.logger.info(&format!(
687                "  In-session retry {}/{} for XSD validation",
688                retry_num,
689                max_retries - 1
690            ));
691            if let Some(ref error) = xsd_error {
692                ctx.logger.info(&format!("  XSD error: {}", error));
693            }
694
695            // Read the last output for retry context (used as fallback if session continuation fails)
696            let last_output = read_last_planning_output(Path::new(&log_dir), ctx.workspace);
697
698            prompt_planning_xsd_retry_with_context(
699                ctx.template_context,
700                prompt_md_str,
701                xsd_error.as_deref().unwrap_or("Unknown error"),
702                &last_output,
703                ctx.workspace,
704            )
705        };
706
707        let mut runtime = PipelineRuntime {
708            timer: ctx.timer,
709            logger: ctx.logger,
710            colors: ctx.colors,
711            config: ctx.config,
712            executor: ctx.executor,
713            executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
714            workspace: ctx.workspace,
715        };
716
717        // Use session continuation for XSD retries (retry_num > 0)
718        // This is completely fault-tolerant - if session continuation fails for any reason
719        // (including agent crash, segfault, invalid session), it falls back to normal behavior
720        let mut xsd_retry_config = XsdRetryConfig {
721            role: AgentRole::Developer,
722            base_label: &format!("planning #{}", iteration),
723            prompt: &plan_prompt,
724            logfile_prefix: &log_dir,
725            runtime: &mut runtime,
726            registry: ctx.registry,
727            primary_agent: ctx.developer_agent,
728            session_info: session_info.as_ref(),
729            retry_num,
730            output_validator: None,
731            workspace: ctx.workspace,
732        };
733
734        let _exit_code = run_xsd_retry_with_session(&mut xsd_retry_config)?;
735
736        // Extract and validate the plan XML
737        let log_dir_path = Path::new(&log_dir);
738        let plan_content = read_last_planning_output(log_dir_path, ctx.workspace);
739
740        // Extract session info for potential retry (only if we don't have it yet)
741        // This is best-effort - if extraction fails, we just won't use session continuation
742        if session_info.is_none() {
743            if let Some(agent_config) = ctx.registry.resolve_config(ctx.developer_agent) {
744                session_info = crate::pipeline::session::extract_session_info_from_log_prefix(
745                    log_dir_path,
746                    agent_config.json_parser,
747                    Some(ctx.developer_agent),
748                    ctx.workspace,
749                );
750            }
751        }
752
753        // Try file-based extraction first - allows agents to write XML to .agent/tmp/plan.xml
754        let xml_to_validate = extract_xml_with_file_fallback_with_workspace(
755            ctx.workspace,
756            Path::new(xml_paths::PLAN_XML),
757            &plan_content,
758            extract_plan_xml,
759        )
760        .unwrap_or_else(|| {
761            // No XML found anywhere - assume entire log content is XML for validation
762            // This allows us to get specific XSD errors to send back to the agent
763            plan_content.clone()
764        });
765
766        // Try to validate against XSD
767        match validate_plan_xml(&xml_to_validate) {
768            Ok(plan_elements) => {
769                // XSD validation passed - format and write the plan
770                let formatted_xml = format_xml_for_display(&xml_to_validate);
771
772                // Convert XML to markdown format for PLAN.md
773                let markdown = format_plan_as_markdown(&plan_elements);
774                ctx.workspace.write(plan_path, &markdown)?;
775
776                // Archive the XML file for debugging (moves to .xml.processed)
777                archive_xml_file_with_workspace(ctx.workspace, Path::new(xml_paths::PLAN_XML));
778
779                if retry_num > 0 {
780                    ctx.logger
781                        .success(&format!("Plan validated after {} retries", retry_num));
782                } else {
783                    ctx.logger.success("Plan extracted and validated (XML)");
784                }
785
786                // Display the formatted plan
787                ctx.logger.info(&format!("\n{}", formatted_xml));
788
789                // Record execution history before returning
790                {
791                    let duration = start_time.elapsed().as_secs();
792                    let step = ExecutionStep::new(
793                        "Planning",
794                        iteration,
795                        "plan_generation",
796                        StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
797                    )
798                    .with_agent(ctx.developer_agent)
799                    .with_duration(duration);
800                    ctx.execution_history.add_step(step);
801                }
802
803                return Ok(());
804            }
805            Err(xsd_err) => {
806                // XSD validation failed - check if we can retry
807                let error_msg = format_xsd_error(&xsd_err);
808                ctx.logger
809                    .warn(&format!("  XSD validation failed: {}", error_msg));
810
811                if retry_num < max_retries - 1 {
812                    // Store error for next retry attempt
813                    xsd_error = Some(error_msg);
814                    // Continue to next retry iteration
815                    continue;
816                } else {
817                    ctx.logger
818                        .error("  No more in-session XSD retries remaining");
819                    // Write placeholder and fail
820                    let placeholder = "# Plan\n\nAgent produced no valid XML output. Only XML format is accepted.\n";
821                    ctx.workspace.write(plan_path, placeholder)?;
822                    anyhow::bail!(
823                        "Planning agent did not produce valid XML output after {} attempts",
824                        max_retries
825                    );
826                }
827            }
828        }
829    }
830
831    // Record execution history for failed planning (should never be reached since we always return above)
832    {
833        let duration = start_time.elapsed().as_secs();
834        let step = ExecutionStep::new(
835            "Planning",
836            iteration,
837            "plan_generation",
838            StepOutcome::failure("No valid XML output produced".to_string(), false),
839        )
840        .with_agent(ctx.developer_agent)
841        .with_duration(duration);
842        ctx.execution_history.add_step(step);
843    }
844
845    anyhow::bail!("Planning failed after {} XSD retry attempts", max_retries)
846}
847
848/// Read the last planning output from logs.
849///
850/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/planning_1`.
851/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
852/// `.agent/logs/planning_1_ccs-glm_0.log`
853fn read_last_planning_output(
854    log_prefix: &Path,
855    workspace: &dyn crate::workspace::Workspace,
856) -> String {
857    read_last_output_from_prefix(log_prefix, workspace)
858}
859
860/// Read the last development output from logs.
861///
862/// The `log_prefix` is a path prefix (not a directory) like `.agent/logs/development_1`.
863/// Actual log files are named `{prefix}_{agent}_{model}.log`, e.g.:
864/// `.agent/logs/development_1_ccs-glm_0.log`
865fn read_last_development_output(
866    log_prefix: &Path,
867    workspace: &dyn crate::workspace::Workspace,
868) -> String {
869    read_last_output_from_prefix(log_prefix, workspace)
870}
871
872/// Read the most recent log file matching a prefix pattern.
873///
874/// This is a shared helper for reading log output. Truncation of large prompts
875/// is handled centrally in `build_agent_command` to prevent E2BIG errors.
876fn read_last_output_from_prefix(
877    log_prefix: &Path,
878    workspace: &dyn crate::workspace::Workspace,
879) -> String {
880    crate::pipeline::logfile::read_most_recent_logfile(log_prefix, workspace)
881}
882
883/// Format XSD error for display.
884fn format_xsd_error(error: &XsdValidationError) -> String {
885    format!(
886        "{} - expected: {}, found: {}",
887        error.element_path, error.expected, error.found
888    )
889}
890
891/// Format plan elements as markdown for PLAN.md.
892fn format_plan_as_markdown(elements: &PlanElements) -> String {
893    let mut result = String::new();
894
895    // Summary section
896    result.push_str("## Summary\n\n");
897    result.push_str(&elements.summary.context);
898    result.push_str("\n\n");
899
900    // Scope items
901    result.push_str("### Scope\n\n");
902    for item in &elements.summary.scope_items {
903        if let Some(ref count) = item.count {
904            result.push_str(&format!("- **{}** {}", count, item.description));
905        } else {
906            result.push_str(&format!("- {}", item.description));
907        }
908        if let Some(ref category) = item.category {
909            result.push_str(&format!(" ({})", category));
910        }
911        result.push('\n');
912    }
913    result.push('\n');
914
915    // Implementation steps
916    result.push_str("## Implementation Steps\n\n");
917    for step in &elements.steps {
918        // Step header
919        let step_type_str = match step.step_type {
920            crate::files::llm_output_extraction::xsd_validation_plan::StepType::FileChange => {
921                "file-change"
922            }
923            crate::files::llm_output_extraction::xsd_validation_plan::StepType::Action => "action",
924            crate::files::llm_output_extraction::xsd_validation_plan::StepType::Research => {
925                "research"
926            }
927        };
928        let priority_str = step.priority.map_or(String::new(), |p| {
929            format!(
930                " [{}]",
931                match p {
932                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Critical =>
933                        "critical",
934                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::High =>
935                        "high",
936                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Medium =>
937                        "medium",
938                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Low =>
939                        "low",
940                }
941            )
942        });
943
944        result.push_str(&format!(
945            "### Step {} ({}){}:  {}\n\n",
946            step.number, step_type_str, priority_str, step.title
947        ));
948
949        // Target files
950        if !step.target_files.is_empty() {
951            result.push_str("**Target Files:**\n");
952            for tf in &step.target_files {
953                let action_str = match tf.action {
954                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
955                        "create"
956                    }
957                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
958                        "modify"
959                    }
960                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
961                        "delete"
962                    }
963                };
964                result.push_str(&format!("- `{}` ({})\n", tf.path, action_str));
965            }
966            result.push('\n');
967        }
968
969        // Location
970        if let Some(ref location) = step.location {
971            result.push_str(&format!("**Location:** {}\n\n", location));
972        }
973
974        // Rationale
975        if let Some(ref rationale) = step.rationale {
976            result.push_str(&format!("**Rationale:** {}\n\n", rationale));
977        }
978
979        // Content
980        result.push_str(&format_rich_content(&step.content));
981        result.push('\n');
982
983        // Dependencies
984        if !step.depends_on.is_empty() {
985            result.push_str("**Depends on:** ");
986            let deps: Vec<String> = step
987                .depends_on
988                .iter()
989                .map(|d| format!("Step {}", d))
990                .collect();
991            result.push_str(&deps.join(", "));
992            result.push_str("\n\n");
993        }
994    }
995
996    // Critical files
997    result.push_str("## Critical Files\n\n");
998    result.push_str("### Primary Files\n\n");
999    for pf in &elements.critical_files.primary_files {
1000        let action_str = match pf.action {
1001            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
1002                "create"
1003            }
1004            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
1005                "modify"
1006            }
1007            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
1008                "delete"
1009            }
1010        };
1011        if let Some(ref est) = pf.estimated_changes {
1012            result.push_str(&format!("- `{}` ({}) - {}\n", pf.path, action_str, est));
1013        } else {
1014            result.push_str(&format!("- `{}` ({})\n", pf.path, action_str));
1015        }
1016    }
1017    result.push('\n');
1018
1019    if !elements.critical_files.reference_files.is_empty() {
1020        result.push_str("### Reference Files\n\n");
1021        for rf in &elements.critical_files.reference_files {
1022            result.push_str(&format!("- `{}` - {}\n", rf.path, rf.purpose));
1023        }
1024        result.push('\n');
1025    }
1026
1027    // Risks and mitigations
1028    result.push_str("## Risks & Mitigations\n\n");
1029    for rp in &elements.risks_mitigations {
1030        let severity_str = rp.severity.map_or(String::new(), |s| {
1031            format!(
1032                " [{}]",
1033                match s {
1034                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Low =>
1035                        "low",
1036                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Medium =>
1037                        "medium",
1038                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::High =>
1039                        "high",
1040                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Critical =>
1041                        "critical",
1042                }
1043            )
1044        });
1045        result.push_str(&format!("**Risk{}:** {}\n", severity_str, rp.risk));
1046        result.push_str(&format!("**Mitigation:** {}\n\n", rp.mitigation));
1047    }
1048
1049    // Verification strategy
1050    result.push_str("## Verification Strategy\n\n");
1051    for (i, v) in elements.verification_strategy.iter().enumerate() {
1052        result.push_str(&format!("{}. **{}**\n", i + 1, v.method));
1053        result.push_str(&format!("   Expected: {}\n\n", v.expected_outcome));
1054    }
1055
1056    result
1057}
1058
1059/// Format rich content elements to markdown.
1060fn format_rich_content(
1061    content: &crate::files::llm_output_extraction::xsd_validation_plan::RichContent,
1062) -> String {
1063    use crate::files::llm_output_extraction::xsd_validation_plan::ContentElement;
1064
1065    let mut result = String::new();
1066
1067    for element in &content.elements {
1068        match element {
1069            ContentElement::Paragraph(p) => {
1070                result.push_str(&format_inline_content(&p.content));
1071                result.push_str("\n\n");
1072            }
1073            ContentElement::CodeBlock(cb) => {
1074                let lang = cb.language.as_deref().unwrap_or("");
1075                result.push_str(&format!("```{}\n", lang));
1076                result.push_str(&cb.content);
1077                if !cb.content.ends_with('\n') {
1078                    result.push('\n');
1079                }
1080                result.push_str("```\n\n");
1081            }
1082            ContentElement::Table(t) => {
1083                if let Some(ref caption) = t.caption {
1084                    result.push_str(&format!("**{}**\n\n", caption));
1085                }
1086                // Header row
1087                if !t.columns.is_empty() {
1088                    result.push_str("| ");
1089                    result.push_str(&t.columns.join(" | "));
1090                    result.push_str(" |\n");
1091                    result.push('|');
1092                    for _ in &t.columns {
1093                        result.push_str(" --- |");
1094                    }
1095                    result.push('\n');
1096                } else if let Some(first_row) = t.rows.first() {
1097                    // Infer column count from first row
1098                    result.push('|');
1099                    for _ in &first_row.cells {
1100                        result.push_str(" --- |");
1101                    }
1102                    result.push('\n');
1103                }
1104                // Data rows
1105                for row in &t.rows {
1106                    result.push_str("| ");
1107                    let cells: Vec<String> = row
1108                        .cells
1109                        .iter()
1110                        .map(|c| format_inline_content(&c.content))
1111                        .collect();
1112                    result.push_str(&cells.join(" | "));
1113                    result.push_str(" |\n");
1114                }
1115                result.push('\n');
1116            }
1117            ContentElement::List(l) => {
1118                result.push_str(&format_list(l, 0));
1119                result.push('\n');
1120            }
1121            ContentElement::Heading(h) => {
1122                let prefix = "#".repeat(h.level as usize);
1123                result.push_str(&format!("{} {}\n\n", prefix, h.text));
1124            }
1125        }
1126    }
1127
1128    result
1129}
1130
1131/// Format inline content elements.
1132fn format_inline_content(
1133    content: &[crate::files::llm_output_extraction::xsd_validation_plan::InlineElement],
1134) -> String {
1135    use crate::files::llm_output_extraction::xsd_validation_plan::InlineElement;
1136
1137    content
1138        .iter()
1139        .map(|e| match e {
1140            InlineElement::Text(s) => s.clone(),
1141            InlineElement::Emphasis(s) => format!("**{}**", s),
1142            InlineElement::Code(s) => format!("`{}`", s),
1143            InlineElement::Link { href, text } => format!("[{}]({})", text, href),
1144        })
1145        .collect::<Vec<_>>()
1146        .join("")
1147}
1148
1149/// Format a list element with proper indentation.
1150fn format_list(
1151    list: &crate::files::llm_output_extraction::xsd_validation_plan::List,
1152    indent: usize,
1153) -> String {
1154    use crate::files::llm_output_extraction::xsd_validation_plan::ListType;
1155
1156    let mut result = String::new();
1157    let indent_str = "  ".repeat(indent);
1158
1159    for (i, item) in list.items.iter().enumerate() {
1160        let marker = match list.list_type {
1161            ListType::Ordered => format!("{}. ", i + 1),
1162            ListType::Unordered => "- ".to_string(),
1163        };
1164
1165        result.push_str(&indent_str);
1166        result.push_str(&marker);
1167        result.push_str(&format_inline_content(&item.content));
1168        result.push('\n');
1169
1170        if let Some(ref nested) = item.nested_list {
1171            result.push_str(&format_list(nested, indent + 1));
1172        }
1173    }
1174
1175    result
1176}
1177
1178/// Verify that PLAN.md exists and is non-empty.
1179///
1180/// With orchestrator-controlled file I/O, `run_planning_step` always writes
1181/// PLAN.md (even if just a placeholder). This function checks if the file
1182/// exists and has meaningful content. If resuming and plan is missing,
1183/// re-run planning.
1184fn verify_plan_exists(
1185    ctx: &mut PhaseContext<'_>,
1186    iteration: u32,
1187    resuming_into_development: bool,
1188) -> anyhow::Result<bool> {
1189    let plan_path = Path::new(".agent/PLAN.md");
1190
1191    let plan_ok = ctx
1192        .workspace
1193        .exists(plan_path)
1194        .then(|| ctx.workspace.read(plan_path).ok())
1195        .flatten()
1196        .is_some_and(|s| !s.trim().is_empty());
1197
1198    // If resuming and plan is missing, re-run planning to recover
1199    if !plan_ok && resuming_into_development {
1200        ctx.logger
1201            .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
1202        run_planning_step(ctx, iteration)?;
1203
1204        // Check again after rerunning - orchestrator guarantees file exists
1205        let plan_ok = ctx
1206            .workspace
1207            .exists(plan_path)
1208            .then(|| ctx.workspace.read(plan_path).ok())
1209            .flatten()
1210            .is_some_and(|s| !s.trim().is_empty());
1211
1212        return Ok(plan_ok);
1213    }
1214
1215    Ok(plan_ok)
1216}
1217
1218/// Run fast check command.
1219fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
1220    let argv = crate::common::split_command(fast_cmd)
1221        .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
1222    if argv.is_empty() {
1223        ctx.logger
1224            .warn("FAST_CHECK_CMD is empty; skipping fast check");
1225        return Ok(());
1226    }
1227
1228    let display_cmd = crate::common::format_argv_for_log(&argv);
1229    ctx.logger.info(&format!(
1230        "Running fast check: {}{}{}",
1231        ctx.colors.dim(),
1232        display_cmd,
1233        ctx.colors.reset()
1234    ));
1235
1236    let Some((program, cmd_args)) = argv.split_first() else {
1237        ctx.logger
1238            .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
1239        return Ok(());
1240    };
1241    let args_refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect();
1242    let output = ctx.executor.execute(program, &args_refs, &[], None)?;
1243    let status = output.status;
1244
1245    if status.success() {
1246        ctx.logger.success("Fast check passed");
1247    } else {
1248        ctx.logger.warn("Fast check had issues (non-blocking)");
1249    }
1250
1251    Ok(())
1252}
1253
1254/// Handle commit creation after development changes are detected.
1255///
1256/// Creates a commit with an auto-generated message using the primary commit agent.
1257/// This is done by the orchestrator, not the agent, using fallback-aware commit
1258/// generation which tries multiple agents if needed.
1259fn handle_commit_after_development(
1260    ctx: &mut PhaseContext<'_>,
1261    iteration: u32,
1262) -> anyhow::Result<()> {
1263    let start_time = Instant::now();
1264    // Get the primary commit agent from the registry
1265    let commit_agent = get_primary_commit_agent(ctx);
1266
1267    if let Some(agent) = commit_agent {
1268        ctx.logger.info(&format!(
1269            "Creating commit with auto-generated message (agent: {agent})..."
1270        ));
1271
1272        // Get the diff for commit message generation
1273        let diff = match crate::git_helpers::git_diff() {
1274            Ok(d) => d,
1275            Err(e) => {
1276                ctx.logger
1277                    .error(&format!("Failed to get diff for commit: {e}"));
1278                return Err(anyhow::anyhow!(e));
1279            }
1280        };
1281
1282        // Get git identity from config
1283        let git_name = ctx.config.git_user_name.as_deref();
1284        let git_email = ctx.config.git_user_email.as_deref();
1285
1286        let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
1287
1288        match result {
1289            CommitResultFallback::Success(oid) => {
1290                ctx.logger
1291                    .success(&format!("Commit created successfully: {oid}"));
1292                ctx.stats.commits_created += 1;
1293
1294                {
1295                    let duration = start_time.elapsed().as_secs();
1296                    let step = ExecutionStep::new(
1297                        "Development",
1298                        iteration,
1299                        "commit",
1300                        StepOutcome::success(Some(oid.to_string()), vec![]),
1301                    )
1302                    .with_agent(&agent)
1303                    .with_duration(duration);
1304                    ctx.execution_history.add_step(step);
1305                }
1306            }
1307            CommitResultFallback::NoChanges => {
1308                // No meaningful changes to commit (already handled by has_meaningful_changes)
1309                ctx.logger.info("No commit created (no meaningful changes)");
1310
1311                {
1312                    let duration = start_time.elapsed().as_secs();
1313                    let step = ExecutionStep::new(
1314                        "Development",
1315                        iteration,
1316                        "commit",
1317                        StepOutcome::skipped("No meaningful changes to commit".to_string()),
1318                    )
1319                    .with_duration(duration);
1320                    ctx.execution_history.add_step(step);
1321                }
1322            }
1323            CommitResultFallback::Failed(err) => {
1324                // Actual git operation failed - this is critical
1325                ctx.logger.error(&format!(
1326                    "Failed to create commit (git operation failed): {err}"
1327                ));
1328
1329                {
1330                    let duration = start_time.elapsed().as_secs();
1331                    let step = ExecutionStep::new(
1332                        "Development",
1333                        iteration,
1334                        "commit",
1335                        StepOutcome::failure(err.to_string(), false),
1336                    )
1337                    .with_duration(duration);
1338                    ctx.execution_history.add_step(step);
1339                }
1340
1341                // Don't continue - this is a real error that needs attention
1342                return Err(anyhow::anyhow!(err));
1343            }
1344        }
1345    } else {
1346        ctx.logger
1347            .warn("Unable to get primary commit agent for commit");
1348
1349        {
1350            let duration = start_time.elapsed().as_secs();
1351            let step = ExecutionStep::new(
1352                "Development",
1353                iteration,
1354                "commit",
1355                StepOutcome::failure("No commit agent available".to_string(), true),
1356            )
1357            .with_duration(duration);
1358            ctx.execution_history.add_step(step);
1359        }
1360    }
1361
1362    Ok(())
1363}