1pub mod config_init;
20pub mod context;
21pub mod detection;
22pub mod finalization;
23pub mod plumbing;
24pub mod resume;
25pub mod validation;
26
27use crate::agents::AgentRegistry;
28use crate::app::finalization::finalize_pipeline;
29use crate::app::resume::should_run_from;
30use crate::banner::print_welcome_banner;
31use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
32use crate::checkpoint::restore::{
33 calculate_start_iteration, calculate_start_reviewer_pass, should_skip_phase,
34};
35use crate::checkpoint::{
36 save_checkpoint, CheckpointBuilder, PipelineCheckpoint, PipelinePhase, RebaseState,
37};
38use crate::cli::{
39 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
40 handle_list_available_agents, handle_list_providers, handle_show_baseline,
41 handle_template_commands, prompt_template_selection, Args,
42};
43use crate::common::utils;
44use crate::files::protection::monitoring::PromptMonitor;
45use crate::files::{
46 create_prompt_backup, ensure_files, make_prompt_read_only, reset_context_for_isolation,
47 update_status, validate_prompt_md,
48};
49use crate::git_helpers::{
50 abort_rebase, cleanup_orphaned_marker, continue_rebase, get_conflicted_files,
51 get_default_branch, get_repo_root, get_start_commit_summary, is_main_or_master_branch,
52 rebase_onto, require_git_repo, reset_start_commit, save_start_commit, start_agent_phase,
53 RebaseResult,
54};
55use crate::logger::Colors;
56use crate::logger::Logger;
57use crate::phases::{run_development_phase, run_review_phase, PhaseContext};
58use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
59use crate::prompts::{get_stored_or_generate_prompt, template_context::TemplateContext};
60use std::env;
61use std::process::Command;
62
63use config_init::initialize_config;
64use context::PipelineContext;
65use detection::detect_project_stack;
66use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
67use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
68use validation::{
69 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
70};
71
72pub fn run(args: Args) -> anyhow::Result<()> {
91 let colors = Colors::new();
92 let logger = Logger::new(colors);
93
94 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
96 return Ok(()); };
98
99 let config_init::ConfigInitResult {
100 config,
101 registry,
102 config_path,
103 config_sources,
104 } = init_result;
105
106 let validated = resolve_required_agents(&config)?;
108 let developer_agent = validated.developer_agent;
109 let reviewer_agent = validated.reviewer_agent;
110
111 if handle_listing_commands(&args, ®istry, colors) {
113 return Ok(());
114 }
115
116 if args.recovery.diagnose {
118 handle_diagnose(colors, &config, ®istry, &config_path, &config_sources);
119 return Ok(());
120 }
121
122 validate_agent_chains(®istry, colors);
124
125 if handle_plumbing_commands(&args, &logger, colors)? {
127 return Ok(());
128 }
129
130 let Some(repo_root) = validate_and_setup_agents(
132 &config,
133 ®istry,
134 &developer_agent,
135 &reviewer_agent,
136 &config_path,
137 colors,
138 &logger,
139 )?
140 else {
141 return Ok(());
142 };
143
144 if args.rebase_flags.rebase_only {
146 let template_context =
147 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
148 return handle_rebase_only(&args, &config, &template_context, &logger, colors);
149 }
150
151 (prepare_pipeline_or_exit(PipelinePreparationParams {
153 args,
154 config,
155 registry,
156 developer_agent,
157 reviewer_agent,
158 repo_root,
159 logger,
160 colors,
161 })?)
162 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
163}
164
165fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
169 if args.agent_list.list_agents {
170 handle_list_agents(registry);
171 return true;
172 }
173 if args.agent_list.list_available_agents {
174 handle_list_available_agents(registry);
175 return true;
176 }
177 if args.provider_list.list_providers {
178 handle_list_providers(colors);
179 return true;
180 }
181
182 let template_cmds = &args.template_commands;
184 if template_cmds.init_templates_enabled()
185 || template_cmds.validate
186 || template_cmds.show.is_some()
187 || template_cmds.list
188 || template_cmds.list_all
189 || template_cmds.variables.is_some()
190 || template_cmds.render.is_some()
191 {
192 let _ = handle_template_commands(template_cmds, colors);
193 return true;
194 }
195
196 false
197}
198
199fn handle_plumbing_commands(args: &Args, logger: &Logger, colors: Colors) -> anyhow::Result<bool> {
204 if args.commit_display.show_commit_msg {
206 return handle_show_commit_msg().map(|()| true);
207 }
208
209 if args.commit_plumbing.apply_commit {
211 return handle_apply_commit(logger, colors).map(|()| true);
212 }
213
214 if args.commit_display.reset_start_commit {
216 require_git_repo()?;
217 let repo_root = get_repo_root()?;
218 env::set_current_dir(&repo_root)?;
219
220 return match reset_start_commit() {
221 Ok(result) => {
222 let short_oid = &result.oid[..8.min(result.oid.len())];
223 if result.fell_back_to_head {
224 logger.success(&format!(
225 "Starting commit reference reset to current HEAD ({})",
226 short_oid
227 ));
228 logger.info("On main/master branch - using HEAD as baseline");
229 } else if let Some(ref branch) = result.default_branch {
230 logger.success(&format!(
231 "Starting commit reference reset to merge-base with '{}' ({})",
232 branch, short_oid
233 ));
234 logger.info("Baseline set to common ancestor with default branch");
235 } else {
236 logger.success(&format!("Starting commit reference reset ({})", short_oid));
237 }
238 logger.info(".agent/start_commit has been updated");
239 Ok(true)
240 }
241 Err(e) => {
242 logger.error(&format!("Failed to reset starting commit: {e}"));
243 anyhow::bail!("Failed to reset starting commit");
244 }
245 };
246 }
247
248 if args.commit_display.show_baseline {
250 require_git_repo()?;
251 let repo_root = get_repo_root()?;
252 env::set_current_dir(&repo_root)?;
253
254 return match handle_show_baseline() {
255 Ok(()) => Ok(true),
256 Err(e) => {
257 logger.error(&format!("Failed to show baseline: {e}"));
258 anyhow::bail!("Failed to show baseline");
259 }
260 };
261 }
262
263 Ok(false)
264}
265
266struct PipelinePreparationParams {
270 args: Args,
271 config: crate::config::Config,
272 registry: AgentRegistry,
273 developer_agent: String,
274 reviewer_agent: String,
275 repo_root: std::path::PathBuf,
276 logger: Logger,
277 colors: Colors,
278}
279
280fn prepare_pipeline_or_exit(
284 params: PipelinePreparationParams,
285) -> anyhow::Result<Option<PipelineContext>> {
286 let PipelinePreparationParams {
287 args,
288 config,
289 registry,
290 developer_agent,
291 reviewer_agent,
292 repo_root,
293 mut logger,
294 colors,
295 } = params;
296
297 ensure_files(config.isolation_mode)?;
298
299 if config.isolation_mode {
301 reset_context_for_isolation(&logger)?;
302 }
303
304 logger = logger.with_log_file(".agent/logs/pipeline.log");
305
306 if args.recovery.dry_run {
308 let developer_display = registry.display_name(&developer_agent);
309 let reviewer_display = registry.display_name(&reviewer_agent);
310 handle_dry_run(
311 &logger,
312 colors,
313 &config,
314 &developer_display,
315 &reviewer_display,
316 &repo_root,
317 )?;
318 return Ok(None);
319 }
320
321 let template_context =
323 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
324
325 if args.commit_plumbing.generate_commit_msg {
327 handle_generate_commit_msg(
328 &config,
329 &template_context,
330 ®istry,
331 &logger,
332 colors,
333 &developer_agent,
334 &reviewer_agent,
335 )?;
336 return Ok(None);
337 }
338
339 let developer_display = registry.display_name(&developer_agent);
341 let reviewer_display = registry.display_name(&reviewer_agent);
342
343 let ctx = PipelineContext {
345 args,
346 config,
347 registry,
348 developer_agent,
349 reviewer_agent,
350 developer_display,
351 reviewer_display,
352 repo_root,
353 logger,
354 colors,
355 template_context,
356 };
357 Ok(Some(ctx))
358}
359
360fn validate_and_setup_agents(
365 config: &crate::config::Config,
366 registry: &AgentRegistry,
367 developer_agent: &str,
368 reviewer_agent: &str,
369 config_path: &std::path::Path,
370 colors: Colors,
371 logger: &Logger,
372) -> anyhow::Result<Option<std::path::PathBuf>> {
373 validate_agent_commands(
375 config,
376 registry,
377 developer_agent,
378 reviewer_agent,
379 config_path,
380 )?;
381
382 validate_can_commit(
384 config,
385 registry,
386 developer_agent,
387 reviewer_agent,
388 config_path,
389 )?;
390
391 require_git_repo()?;
393 let repo_root = get_repo_root()?;
394 env::set_current_dir(&repo_root)?;
395
396 let should_continue = setup_git_and_prompt_file(config, colors, logger)?;
398 if should_continue.is_none() {
399 return Ok(None);
400 }
401
402 Ok(Some(repo_root))
403}
404
405fn setup_git_and_prompt_file(
410 config: &crate::config::Config,
411 colors: Colors,
412 logger: &Logger,
413) -> anyhow::Result<Option<()>> {
414 let prompt_exists = std::path::Path::new("PROMPT.md").exists();
415
416 if config.behavior.interactive && !prompt_exists {
419 if let Some(template_name) = prompt_template_selection(colors) {
420 create_prompt_from_template(&template_name, colors)?;
421 println!();
422 logger.info(
423 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
424 );
425 logger.info(&format!(
426 "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
427 config.commit_msg
428 ));
429 return Ok(None);
430 }
431 println!();
432 logger.error("PROMPT.md not found in current directory.");
433 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
434 println!();
435 logger.info("To get started:");
436 logger.info(" ralph --init # Smart setup wizard");
437 logger.info(" ralph --init bug-fix # Create from Work Guide");
438 logger.info(" ralph --list-work-guides # See all Work Guides");
439 println!();
440 return Ok(None);
441 }
442
443 if !prompt_exists {
445 logger.error("PROMPT.md not found in current directory.");
446 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
447 println!();
448 logger.info("Quick start:");
449 logger.info(" ralph --init # Smart setup wizard");
450 logger.info(" ralph --init bug-fix # Create from Work Guide");
451 logger.info(" ralph --list-work-guides # See all Work Guides");
452 println!();
453 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
454 println!();
455 return Ok(None);
456 }
457
458 Ok(Some(()))
459}
460
461fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
463 let resume_result = offer_resume_if_checkpoint_exists(
465 &ctx.args,
466 &ctx.config,
467 &ctx.registry,
468 &ctx.logger,
469 &ctx.developer_agent,
470 &ctx.reviewer_agent,
471 );
472
473 let resume_result = match resume_result {
475 Some(result) => Some(result),
476 None => handle_resume_with_validation(
477 &ctx.args,
478 &ctx.config,
479 &ctx.registry,
480 &ctx.logger,
481 &ctx.developer_display,
482 &ctx.reviewer_display,
483 ),
484 };
485
486 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
487
488 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
490 use crate::checkpoint::RunContext;
491 RunContext::from_checkpoint(checkpoint)
492 } else {
493 use crate::checkpoint::RunContext;
494 RunContext::new()
495 };
496
497 let config = if let Some(ref checkpoint) = resume_checkpoint {
499 use crate::checkpoint::apply_checkpoint_to_config;
500 let mut restored_config = ctx.config.clone();
501 apply_checkpoint_to_config(&mut restored_config, checkpoint);
502 ctx.logger.info("Restored configuration from checkpoint:");
503 if checkpoint.cli_args.developer_iters > 0 {
504 ctx.logger.info(&format!(
505 " Developer iterations: {} (from checkpoint)",
506 checkpoint.cli_args.developer_iters
507 ));
508 }
509 if checkpoint.cli_args.reviewer_reviews > 0 {
510 ctx.logger.info(&format!(
511 " Reviewer passes: {} (from checkpoint)",
512 checkpoint.cli_args.reviewer_reviews
513 ));
514 }
515 restored_config
516 } else {
517 ctx.config.clone()
518 };
519
520 if let Some(ref checkpoint) = resume_checkpoint {
522 use crate::checkpoint::restore::restore_environment_from_checkpoint;
523 let restored_count = restore_environment_from_checkpoint(checkpoint);
524 if restored_count > 0 {
525 ctx.logger.info(&format!(
526 " Restored {} environment variable(s) from checkpoint",
527 restored_count
528 ));
529 }
530 }
531
532 let mut git_helpers = crate::git_helpers::GitHelpers::new();
534 cleanup_orphaned_marker(&ctx.logger)?;
535 start_agent_phase(&mut git_helpers)?;
536 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
537
538 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
540 print_pipeline_info_with_config(ctx, &config);
541 validate_prompt_and_setup_backup(ctx)?;
542
543 let mut prompt_monitor = setup_prompt_monitor(ctx);
545
546 let (_project_stack, review_guidelines) =
548 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
549
550 print_review_guidelines(ctx, review_guidelines.as_ref());
551 println!();
552
553 let (mut timer, mut stats) = (Timer::new(), Stats::new());
555 let mut phase_ctx = create_phase_context_with_config(
556 ctx,
557 &config,
558 &mut timer,
559 &mut stats,
560 review_guidelines.as_ref(),
561 &run_context,
562 resume_checkpoint.as_ref(),
563 );
564 save_start_commit_or_warn(ctx);
565
566 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
569 checkpoint.phase
570 } else {
571 PipelinePhase::Planning
572 };
573 setup_interrupt_context_for_pipeline(
574 initial_phase,
575 config.developer_iters,
576 config.reviewer_reviews,
577 &phase_ctx.execution_history,
578 &phase_ctx.prompt_history,
579 &run_context,
580 );
581
582 let _interrupt_guard = defer_clear_interrupt_context();
584
585 let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
587 if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
589 !checkpoint.cli_args.skip_rebase
590 } else {
591 ctx.args.rebase_flags.with_rebase
593 }
594 } else {
595 ctx.args.rebase_flags.with_rebase
596 };
597
598 if should_run_rebase {
600 run_initial_rebase(ctx, &mut phase_ctx, &run_context)?;
601 update_interrupt_context_from_phase(
603 &phase_ctx,
604 PipelinePhase::Planning,
605 config.developer_iters,
606 config.reviewer_reviews,
607 &run_context,
608 );
609 } else {
610 if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
612 let builder = CheckpointBuilder::new()
613 .phase(PipelinePhase::Planning, 0, config.developer_iters)
614 .reviewer_pass(0, config.reviewer_reviews)
615 .skip_rebase(true) .capture_from_context(
617 &config,
618 &ctx.registry,
619 &ctx.developer_agent,
620 &ctx.reviewer_agent,
621 &ctx.logger,
622 &run_context,
623 )
624 .with_execution_history(phase_ctx.execution_history.clone())
625 .with_prompt_history(phase_ctx.clone_prompt_history());
626
627 if let Some(checkpoint) = builder.build() {
628 let _ = save_checkpoint(&checkpoint);
629 }
630 }
631 update_interrupt_context_from_phase(
633 &phase_ctx,
634 PipelinePhase::Planning,
635 config.developer_iters,
636 config.reviewer_reviews,
637 &run_context,
638 );
639 }
640
641 run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
643 update_interrupt_context_from_phase(
645 &phase_ctx,
646 PipelinePhase::Development,
647 config.developer_iters,
648 config.reviewer_reviews,
649 &run_context,
650 );
651 check_prompt_restoration(ctx, &mut prompt_monitor, "development");
652 update_status("In progress.", config.isolation_mode)?;
653
654 run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
655 update_interrupt_context_from_phase(
657 &phase_ctx,
658 PipelinePhase::Review,
659 config.developer_iters,
660 config.reviewer_reviews,
661 &run_context,
662 );
663 check_prompt_restoration(ctx, &mut prompt_monitor, "review");
664
665 if should_run_rebase {
667 run_post_review_rebase(ctx, &mut phase_ctx, &run_context)?;
668 update_interrupt_context_from_phase(
670 &phase_ctx,
671 PipelinePhase::PostRebase,
672 config.developer_iters,
673 config.reviewer_reviews,
674 &run_context,
675 );
676 }
677
678 update_status("In progress.", config.isolation_mode)?;
679
680 run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
681
682 if config.features.checkpoint_enabled {
684 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
685 let builder = CheckpointBuilder::new()
686 .phase(
687 PipelinePhase::Complete,
688 config.developer_iters,
689 config.developer_iters,
690 )
691 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
692 .skip_rebase(skip_rebase)
693 .capture_from_context(
694 &config,
695 &ctx.registry,
696 &ctx.developer_agent,
697 &ctx.reviewer_agent,
698 &ctx.logger,
699 &run_context,
700 );
701
702 let builder = builder
703 .with_execution_history(phase_ctx.execution_history.clone())
704 .with_prompt_history(phase_ctx.clone_prompt_history());
705
706 if let Some(checkpoint) = builder.build() {
707 let _ = save_checkpoint(&checkpoint);
708 }
709 }
710
711 finalize_pipeline(
713 &mut agent_phase_guard,
714 &ctx.logger,
715 ctx.colors,
716 &config,
717 &timer,
718 &stats,
719 prompt_monitor,
720 );
721 Ok(())
722}
723
724fn setup_interrupt_context_for_pipeline(
729 phase: PipelinePhase,
730 total_iterations: u32,
731 total_reviewer_passes: u32,
732 execution_history: &crate::checkpoint::ExecutionHistory,
733 prompt_history: &std::collections::HashMap<String, String>,
734 run_context: &crate::checkpoint::RunContext,
735) {
736 use crate::interrupt::{set_interrupt_context, InterruptContext};
737
738 let (iteration, reviewer_pass) = match phase {
740 PipelinePhase::Development => (1, 0),
741 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
742 (total_iterations, 1)
743 }
744 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
745 (total_iterations, total_reviewer_passes)
746 }
747 _ => (0, 0),
748 };
749
750 let context = InterruptContext {
751 phase,
752 iteration,
753 total_iterations,
754 reviewer_pass,
755 total_reviewer_passes,
756 run_context: run_context.clone(),
757 execution_history: execution_history.clone(),
758 prompt_history: prompt_history.clone(),
759 };
760
761 set_interrupt_context(context);
762}
763
764fn update_interrupt_context_from_phase(
769 phase_ctx: &crate::phases::PhaseContext,
770 phase: PipelinePhase,
771 total_iterations: u32,
772 total_reviewer_passes: u32,
773 run_context: &crate::checkpoint::RunContext,
774) {
775 use crate::interrupt::{set_interrupt_context, InterruptContext};
776
777 let (iteration, reviewer_pass) = match phase {
779 PipelinePhase::Development => {
780 let iter = run_context.actual_developer_runs.max(1);
782 (iter, 0)
783 }
784 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
785 (total_iterations, run_context.actual_reviewer_runs.max(1))
786 }
787 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
788 (total_iterations, total_reviewer_passes)
789 }
790 _ => (0, 0),
791 };
792
793 let context = InterruptContext {
794 phase,
795 iteration,
796 total_iterations,
797 reviewer_pass,
798 total_reviewer_passes,
799 run_context: run_context.clone(),
800 execution_history: phase_ctx.execution_history.clone(),
801 prompt_history: phase_ctx.clone_prompt_history(),
802 };
803
804 set_interrupt_context(context);
805}
806
807fn defer_clear_interrupt_context() -> InterruptContextGuard {
813 InterruptContextGuard
814}
815
816struct InterruptContextGuard;
822
823impl Drop for InterruptContextGuard {
824 fn drop(&mut self) {
825 crate::interrupt::clear_interrupt_context();
826 }
827}
828
829fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
831 let prompt_validation =
832 validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
833 for err in &prompt_validation.errors {
834 ctx.logger.error(err);
835 }
836 for warn in &prompt_validation.warnings {
837 ctx.logger.warn(warn);
838 }
839 if !prompt_validation.is_valid() {
840 anyhow::bail!("PROMPT.md validation errors");
841 }
842
843 match create_prompt_backup() {
845 Ok(None) => {}
846 Ok(Some(warning)) => {
847 ctx.logger.warn(&format!(
848 "PROMPT.md backup created but: {warning}. Continuing anyway."
849 ));
850 }
851 Err(e) => {
852 ctx.logger.warn(&format!(
853 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
854 ));
855 }
856 }
857
858 match make_prompt_read_only() {
860 None => {}
861 Some(warning) => {
862 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
863 }
864 }
865
866 Ok(())
867}
868
869fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
871 match PromptMonitor::new() {
872 Ok(mut monitor) => {
873 if let Err(e) = monitor.start() {
874 ctx.logger.warn(&format!(
875 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
876 ));
877 None
878 } else {
879 if ctx.config.verbosity.is_debug() {
880 ctx.logger.info("Started real-time PROMPT.md monitoring");
881 }
882 Some(monitor)
883 }
884 }
885 Err(e) => {
886 ctx.logger.warn(&format!(
887 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
888 ));
889 None
890 }
891 }
892}
893
894fn print_review_guidelines(
896 ctx: &PipelineContext,
897 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
898) {
899 if let Some(guidelines) = review_guidelines {
900 ctx.logger.info(&format!(
901 "Review guidelines: {}{}{}",
902 ctx.colors.dim(),
903 guidelines.summary(),
904 ctx.colors.reset()
905 ));
906 }
907}
908
909fn create_phase_context_with_config<'ctx>(
911 ctx: &'ctx PipelineContext,
912 config: &'ctx crate::config::Config,
913 timer: &'ctx mut Timer,
914 stats: &'ctx mut Stats,
915 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
916 run_context: &'ctx crate::checkpoint::RunContext,
917 resume_checkpoint: Option<&PipelineCheckpoint>,
918) -> PhaseContext<'ctx> {
919 let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
921 let exec_history = checkpoint
922 .execution_history
923 .clone()
924 .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
925 let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
926 (exec_history, prompt_hist)
927 } else {
928 (
929 crate::checkpoint::execution_history::ExecutionHistory::new(),
930 std::collections::HashMap::new(),
931 )
932 };
933
934 PhaseContext {
935 config,
936 registry: &ctx.registry,
937 logger: &ctx.logger,
938 colors: &ctx.colors,
939 timer,
940 stats,
941 developer_agent: &ctx.developer_agent,
942 reviewer_agent: &ctx.reviewer_agent,
943 review_guidelines,
944 template_context: &ctx.template_context,
945 run_context: run_context.clone(),
946 execution_history,
947 prompt_history,
948 }
949}
950
951fn print_pipeline_info_with_config(ctx: &PipelineContext, config: &crate::config::Config) {
953 ctx.logger.info(&format!(
954 "Working directory: {}{}{}",
955 ctx.colors.cyan(),
956 ctx.repo_root.display(),
957 ctx.colors.reset()
958 ));
959 ctx.logger.info(&format!(
960 "Commit message: {}{}{}",
961 ctx.colors.cyan(),
962 config.commit_msg,
963 ctx.colors.reset()
964 ));
965}
966
967fn save_start_commit_or_warn(ctx: &PipelineContext) {
969 match save_start_commit() {
970 Ok(()) => {
971 if ctx.config.verbosity.is_debug() {
972 ctx.logger
973 .info("Saved starting commit for incremental diff generation");
974 }
975 }
976 Err(e) => {
977 ctx.logger.warn(&format!(
978 "Failed to save starting commit: {e}. \
979 Incremental diffs may be unavailable as a result."
980 ));
981 ctx.logger.info(
982 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
983 );
984 }
985 }
986
987 match get_start_commit_summary() {
989 Ok(summary) => {
990 if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale {
991 ctx.logger.info(&summary.format_compact());
992 if summary.is_stale {
993 ctx.logger.warn(
994 "Start commit is stale. Consider running: ralph --reset-start-commit",
995 );
996 } else if summary.commits_since > 5 {
997 ctx.logger
998 .info("Tip: Run 'ralph --show-baseline' for more details");
999 }
1000 }
1001 }
1002 Err(e) => {
1003 if ctx.config.verbosity.is_debug() {
1005 ctx.logger
1006 .warn(&format!("Failed to get start commit summary: {e}"));
1007 }
1008 }
1009 }
1010}
1011
1012fn check_prompt_restoration(
1014 ctx: &PipelineContext,
1015 prompt_monitor: &mut Option<PromptMonitor>,
1016 phase: &str,
1017) {
1018 if let Some(ref mut monitor) = prompt_monitor {
1019 if monitor.check_and_restore() {
1020 ctx.logger.warn(&format!(
1021 "PROMPT.md was deleted and restored during {phase} phase"
1022 ));
1023 }
1024 }
1025}
1026
1027fn run_development(
1029 ctx: &mut PhaseContext,
1030 args: &Args,
1031 resume_checkpoint: Option<&PipelineCheckpoint>,
1032) -> anyhow::Result<()> {
1033 ctx.logger
1034 .header("PHASE 1: Development", crate::logger::Colors::blue);
1035
1036 if let Some(checkpoint) = resume_checkpoint {
1038 if should_skip_phase(PipelinePhase::Development, checkpoint) {
1039 ctx.logger
1040 .info("Skipping development phase (checkpoint indicates it already completed)");
1041 return Ok(());
1042 }
1043 }
1044
1045 if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
1046 ctx.logger
1047 .info("Skipping development phase (resuming from a later checkpoint phase)");
1048 return Ok(());
1049 }
1050
1051 let start_iter = match resume_checkpoint {
1052 Some(checkpoint) => calculate_start_iteration(checkpoint, ctx.config.developer_iters),
1053 None => 1,
1054 };
1055
1056 let resume_context = if args.recovery.resume {
1057 resume_checkpoint.map(|c| c.resume_context())
1058 } else {
1059 None
1060 };
1061 let development_result = run_development_phase(ctx, start_iter, resume_context.as_ref())?;
1062
1063 if development_result.had_errors {
1064 ctx.logger
1065 .warn("Development phase completed with non-fatal errors");
1066 }
1067
1068 Ok(())
1069}
1070
1071fn run_review_and_fix(
1073 ctx: &mut PhaseContext,
1074 args: &Args,
1075 resume_checkpoint: Option<&PipelineCheckpoint>,
1076) -> anyhow::Result<()> {
1077 ctx.logger
1078 .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
1079
1080 let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
1082 || should_run_from(PipelinePhase::Fix, resume_checkpoint)
1083 || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
1084 || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
1085
1086 let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
1087 || resume_checkpoint
1088 .is_some_and(|c| matches!(c.phase, PipelinePhase::Fix | PipelinePhase::ReviewAgain));
1089
1090 if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
1091 let start_pass = match resume_checkpoint {
1092 Some(checkpoint) => {
1093 calculate_start_reviewer_pass(checkpoint, ctx.config.reviewer_reviews)
1094 }
1095 None => 1,
1096 };
1097
1098 let resume_context = if args.recovery.resume {
1099 resume_checkpoint
1100 .filter(|c| {
1101 matches!(
1102 c.phase,
1103 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain
1104 )
1105 })
1106 .map(|c| c.resume_context())
1107 } else {
1108 None
1109 };
1110
1111 let review_result = run_review_phase(ctx, start_pass, resume_context.as_ref())?;
1112 if review_result.completed_early {
1113 ctx.logger
1114 .success("Review phase completed early (no issues found)");
1115 }
1116 } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
1117 ctx.logger
1118 .info("Skipping review phase (reviewer_reviews=0)");
1119 } else if run_any_reviewer_phase {
1120 ctx.logger
1121 .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
1122 }
1123
1124 Ok(())
1128}
1129
1130fn run_final_validation(
1132 ctx: &PhaseContext,
1133 resume_checkpoint: Option<&PipelineCheckpoint>,
1134) -> anyhow::Result<()> {
1135 let Some(ref full_cmd) = ctx.config.full_check_cmd else {
1136 return Ok(());
1137 };
1138
1139 if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
1140 ctx.logger
1141 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
1142 ctx.logger
1143 .info("Skipping final validation (resuming from a later checkpoint phase)");
1144 return Ok(());
1145 }
1146
1147 let argv = utils::split_command(full_cmd)
1148 .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
1149 if argv.is_empty() {
1150 ctx.logger
1151 .warn("FULL_CHECK_CMD is empty; skipping final validation");
1152 return Ok(());
1153 }
1154
1155 if ctx.config.features.checkpoint_enabled {
1156 let builder = CheckpointBuilder::new()
1157 .phase(
1158 PipelinePhase::FinalValidation,
1159 ctx.config.developer_iters,
1160 ctx.config.developer_iters,
1161 )
1162 .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1163 .capture_from_context(
1164 ctx.config,
1165 ctx.registry,
1166 ctx.developer_agent,
1167 ctx.reviewer_agent,
1168 ctx.logger,
1169 &ctx.run_context,
1170 );
1171
1172 let builder = builder
1173 .with_execution_history(ctx.execution_history.clone())
1174 .with_prompt_history(ctx.prompt_history.clone());
1175
1176 if let Some(checkpoint) = builder.build() {
1177 let _ = save_checkpoint(&checkpoint);
1178 }
1179 }
1180
1181 ctx.logger
1182 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
1183 let display_cmd = utils::format_argv_for_log(&argv);
1184 ctx.logger.info(&format!(
1185 "Running full check: {}{}{}",
1186 ctx.colors.dim(),
1187 display_cmd,
1188 ctx.colors.reset()
1189 ));
1190
1191 let Some((program, arguments)) = argv.split_first() else {
1192 ctx.logger
1193 .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
1194 return Ok(());
1195 };
1196 let status = Command::new(program).args(arguments).status()?;
1197
1198 if status.success() {
1199 ctx.logger.success("Full check passed");
1200 } else {
1201 ctx.logger.error("Full check failed");
1202 anyhow::bail!("Full check failed");
1203 }
1204
1205 Ok(())
1206}
1207
1208fn handle_rebase_only(
1213 _args: &Args,
1214 config: &crate::config::Config,
1215 template_context: &TemplateContext,
1216 logger: &Logger,
1217 colors: Colors,
1218) -> anyhow::Result<()> {
1219 if is_main_or_master_branch()? {
1221 logger.warn("Already on main/master branch - rebasing on main is not recommended");
1222 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1223 logger.info(" git worktree add ../feature-branch feature-branch");
1224 logger.info("This allows multiple AI agents to work on different features simultaneously.");
1225 logger.info("Proceeding with rebase anyway as requested...");
1226 }
1227
1228 logger.header("Rebase to default branch", Colors::cyan);
1229
1230 match run_rebase_to_default(logger, colors) {
1231 Ok(RebaseResult::Success) => {
1232 logger.success("Rebase completed successfully");
1233 Ok(())
1234 }
1235 Ok(RebaseResult::NoOp { reason }) => {
1236 logger.info(&format!("No rebase needed: {reason}"));
1237 Ok(())
1238 }
1239 Ok(RebaseResult::Failed(err)) => {
1240 logger.error(&format!("Rebase failed: {err}"));
1241 anyhow::bail!("Rebase failed: {err}")
1242 }
1243 Ok(RebaseResult::Conflicts(_conflicts)) => {
1244 let conflicted_files = get_conflicted_files()?;
1246 if conflicted_files.is_empty() {
1247 logger.warn("Rebase reported conflicts but no conflicted files found");
1248 let _ = abort_rebase();
1249 return Ok(());
1250 }
1251
1252 logger.warn(&format!(
1253 "Rebase resulted in {} conflict(s), attempting AI resolution",
1254 conflicted_files.len()
1255 ));
1256
1257 match try_resolve_conflicts_without_phase_ctx(
1259 &conflicted_files,
1260 config,
1261 template_context,
1262 logger,
1263 colors,
1264 ) {
1265 Ok(true) => {
1266 logger.info("Continuing rebase after conflict resolution");
1268 match continue_rebase() {
1269 Ok(()) => {
1270 logger.success("Rebase completed successfully after AI resolution");
1271 Ok(())
1272 }
1273 Err(e) => {
1274 logger.error(&format!("Failed to continue rebase: {e}"));
1275 let _ = abort_rebase();
1276 anyhow::bail!("Rebase failed after conflict resolution")
1277 }
1278 }
1279 }
1280 Ok(false) => {
1281 logger.error("AI conflict resolution failed, aborting rebase");
1283 let _ = abort_rebase();
1284 anyhow::bail!("Rebase conflicts could not be resolved by AI")
1285 }
1286 Err(e) => {
1287 logger.error(&format!("Conflict resolution error: {e}"));
1288 let _ = abort_rebase();
1289 anyhow::bail!("Rebase conflict resolution failed: {e}")
1290 }
1291 }
1292 }
1293 Err(e) => {
1294 logger.error(&format!("Rebase failed: {e}"));
1295 anyhow::bail!("Rebase failed: {e}")
1296 }
1297 }
1298}
1299
1300fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
1313 let default_branch = get_default_branch()?;
1315 logger.info(&format!(
1316 "Rebasing onto {}{}{}",
1317 colors.cyan(),
1318 default_branch,
1319 colors.reset()
1320 ));
1321
1322 rebase_onto(&default_branch)
1324}
1325
1326fn run_initial_rebase(
1340 ctx: &PipelineContext,
1341 phase_ctx: &mut PhaseContext,
1342 run_context: &crate::checkpoint::RunContext,
1343) -> anyhow::Result<()> {
1344 ctx.logger.header("Pre-development rebase", Colors::cyan);
1345
1346 let step = ExecutionStep::new(
1348 "PreRebase",
1349 0,
1350 "pre_rebase_start",
1351 StepOutcome::success(None, vec![]),
1352 );
1353 phase_ctx.execution_history.add_step(step);
1354
1355 if ctx.config.features.checkpoint_enabled {
1357 let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
1358 let mut builder = CheckpointBuilder::new()
1359 .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
1360 .reviewer_pass(0, ctx.config.reviewer_reviews)
1361 .capture_from_context(
1362 &ctx.config,
1363 &ctx.registry,
1364 &ctx.developer_agent,
1365 &ctx.reviewer_agent,
1366 &ctx.logger,
1367 run_context,
1368 );
1369
1370 builder = builder
1372 .with_execution_history(phase_ctx.execution_history.clone())
1373 .with_prompt_history(phase_ctx.clone_prompt_history());
1374
1375 if let Some(mut checkpoint) = builder.build() {
1376 checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
1377 upstream_branch: default_branch,
1378 };
1379 let _ = save_checkpoint(&checkpoint);
1380 }
1381 }
1382
1383 match run_rebase_to_default(&ctx.logger, ctx.colors) {
1384 Ok(RebaseResult::Success) => {
1385 ctx.logger.success("Rebase completed successfully");
1386 let step = ExecutionStep::new(
1388 "PreRebase",
1389 0,
1390 "pre_rebase_complete",
1391 StepOutcome::success(None, vec![]),
1392 );
1393 phase_ctx.execution_history.add_step(step);
1394
1395 if ctx.config.features.checkpoint_enabled {
1397 let builder = CheckpointBuilder::new()
1398 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1399 .reviewer_pass(0, ctx.config.reviewer_reviews)
1400 .skip_rebase(true) .capture_from_context(
1402 &ctx.config,
1403 &ctx.registry,
1404 &ctx.developer_agent,
1405 &ctx.reviewer_agent,
1406 &ctx.logger,
1407 run_context,
1408 )
1409 .with_execution_history(phase_ctx.execution_history.clone())
1410 .with_prompt_history(phase_ctx.clone_prompt_history());
1411
1412 if let Some(checkpoint) = builder.build() {
1413 let _ = save_checkpoint(&checkpoint);
1414 }
1415 }
1416
1417 Ok(())
1418 }
1419 Ok(RebaseResult::NoOp { reason }) => {
1420 ctx.logger.info(&format!("No rebase needed: {reason}"));
1421 let step = ExecutionStep::new(
1423 "PreRebase",
1424 0,
1425 "pre_rebase_skipped",
1426 StepOutcome::skipped(reason.clone()),
1427 );
1428 phase_ctx.execution_history.add_step(step);
1429
1430 if ctx.config.features.checkpoint_enabled {
1432 let builder = CheckpointBuilder::new()
1433 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1434 .reviewer_pass(0, ctx.config.reviewer_reviews)
1435 .skip_rebase(true) .capture_from_context(
1437 &ctx.config,
1438 &ctx.registry,
1439 &ctx.developer_agent,
1440 &ctx.reviewer_agent,
1441 &ctx.logger,
1442 run_context,
1443 )
1444 .with_execution_history(phase_ctx.execution_history.clone())
1445 .with_prompt_history(phase_ctx.clone_prompt_history());
1446
1447 if let Some(checkpoint) = builder.build() {
1448 let _ = save_checkpoint(&checkpoint);
1449 }
1450 }
1451
1452 Ok(())
1453 }
1454 Ok(RebaseResult::Conflicts(_conflicts)) => {
1455 let conflicted_files = get_conflicted_files()?;
1457 if conflicted_files.is_empty() {
1458 ctx.logger
1459 .warn("Rebase reported conflicts but no conflicted files found");
1460 let _ = abort_rebase();
1461 return Ok(());
1462 }
1463
1464 let step = ExecutionStep::new(
1466 "PreRebase",
1467 0,
1468 "pre_rebase_conflict",
1469 StepOutcome::partial(
1470 "Rebase started".to_string(),
1471 format!("{} conflicts detected", conflicted_files.len()),
1472 ),
1473 );
1474 phase_ctx.execution_history.add_step(step);
1475
1476 if ctx.config.features.checkpoint_enabled {
1478 let mut builder = CheckpointBuilder::new()
1479 .phase(
1480 PipelinePhase::PreRebaseConflict,
1481 0,
1482 ctx.config.developer_iters,
1483 )
1484 .reviewer_pass(0, ctx.config.reviewer_reviews)
1485 .capture_from_context(
1486 &ctx.config,
1487 &ctx.registry,
1488 &ctx.developer_agent,
1489 &ctx.reviewer_agent,
1490 &ctx.logger,
1491 run_context,
1492 );
1493
1494 builder = builder
1496 .with_execution_history(phase_ctx.execution_history.clone())
1497 .with_prompt_history(phase_ctx.clone_prompt_history());
1498
1499 if let Some(mut checkpoint) = builder.build() {
1500 checkpoint.rebase_state = RebaseState::HasConflicts {
1501 files: conflicted_files.clone(),
1502 };
1503 let _ = save_checkpoint(&checkpoint);
1504 }
1505 }
1506
1507 ctx.logger.warn(&format!(
1508 "Rebase resulted in {} conflict(s), attempting AI resolution",
1509 conflicted_files.len()
1510 ));
1511
1512 match try_resolve_conflicts_with_fallback(
1514 &conflicted_files,
1515 &ctx.config,
1516 &ctx.template_context,
1517 &ctx.logger,
1518 ctx.colors,
1519 phase_ctx,
1520 "PreRebase",
1521 ) {
1522 Ok(true) => {
1523 ctx.logger
1525 .info("Continuing rebase after conflict resolution");
1526 match continue_rebase() {
1527 Ok(()) => {
1528 ctx.logger
1529 .success("Rebase completed successfully after AI resolution");
1530 let step = ExecutionStep::new(
1532 "PreRebase",
1533 0,
1534 "pre_rebase_resolution",
1535 StepOutcome::success(None, vec![]),
1536 );
1537 phase_ctx.execution_history.add_step(step);
1538
1539 if ctx.config.features.checkpoint_enabled {
1541 let builder = CheckpointBuilder::new()
1542 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
1543 .reviewer_pass(0, ctx.config.reviewer_reviews)
1544 .skip_rebase(true) .capture_from_context(
1546 &ctx.config,
1547 &ctx.registry,
1548 &ctx.developer_agent,
1549 &ctx.reviewer_agent,
1550 &ctx.logger,
1551 run_context,
1552 )
1553 .with_execution_history(phase_ctx.execution_history.clone())
1554 .with_prompt_history(phase_ctx.clone_prompt_history());
1555
1556 if let Some(checkpoint) = builder.build() {
1557 let _ = save_checkpoint(&checkpoint);
1558 }
1559 }
1560
1561 Ok(())
1562 }
1563 Err(e) => {
1564 ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
1565 let _ = abort_rebase();
1566 let step = ExecutionStep::new(
1568 "PreRebase",
1569 0,
1570 "pre_rebase_resolution",
1571 StepOutcome::partial(
1572 "Conflicts resolved by AI".to_string(),
1573 format!("Failed to continue rebase: {e}"),
1574 ),
1575 );
1576 phase_ctx.execution_history.add_step(step);
1577 Ok(()) }
1579 }
1580 }
1581 Ok(false) => {
1582 ctx.logger
1584 .warn("AI conflict resolution failed, aborting rebase");
1585 let _ = abort_rebase();
1586 let step = ExecutionStep::new(
1588 "PreRebase",
1589 0,
1590 "pre_rebase_resolution",
1591 StepOutcome::failure("AI conflict resolution failed".to_string(), true),
1592 );
1593 phase_ctx.execution_history.add_step(step);
1594 Ok(()) }
1596 Err(e) => {
1597 ctx.logger.error(&format!("Conflict resolution error: {e}"));
1598 let _ = abort_rebase();
1599 let step = ExecutionStep::new(
1601 "PreRebase",
1602 0,
1603 "pre_rebase_resolution",
1604 StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
1605 );
1606 phase_ctx.execution_history.add_step(step);
1607 Ok(()) }
1609 }
1610 }
1611 Ok(RebaseResult::Failed(err)) => {
1612 ctx.logger.error(&format!("Rebase failed: {err}"));
1613 let _ = abort_rebase();
1614 let step = ExecutionStep::new(
1616 "PreRebase",
1617 0,
1618 "pre_rebase_failed",
1619 StepOutcome::failure(format!("Rebase failed: {err}"), true),
1620 );
1621 phase_ctx.execution_history.add_step(step);
1622 Ok(()) }
1624 Err(e) => {
1625 ctx.logger
1626 .warn(&format!("Rebase failed, continuing without rebase: {e}"));
1627 let step = ExecutionStep::new(
1629 "PreRebase",
1630 0,
1631 "pre_rebase_error",
1632 StepOutcome::failure(format!("Rebase error: {e}"), true),
1633 );
1634 phase_ctx.execution_history.add_step(step);
1635 Ok(())
1636 }
1637 }
1638}
1639
1640fn run_post_review_rebase(
1654 ctx: &PipelineContext,
1655 phase_ctx: &mut PhaseContext,
1656 run_context: &crate::checkpoint::RunContext,
1657) -> anyhow::Result<()> {
1658 ctx.logger.header("Post-review rebase", Colors::cyan);
1659
1660 let step = ExecutionStep::new(
1662 "PostRebase",
1663 ctx.config.developer_iters,
1664 "post_rebase_start",
1665 StepOutcome::success(None, vec![]),
1666 );
1667 phase_ctx.execution_history.add_step(step);
1668
1669 if ctx.config.features.checkpoint_enabled {
1671 let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
1672 let mut builder = CheckpointBuilder::new()
1673 .phase(
1674 PipelinePhase::PostRebase,
1675 ctx.config.developer_iters,
1676 ctx.config.developer_iters,
1677 )
1678 .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1679 .capture_from_context(
1680 &ctx.config,
1681 &ctx.registry,
1682 &ctx.developer_agent,
1683 &ctx.reviewer_agent,
1684 &ctx.logger,
1685 run_context,
1686 );
1687
1688 builder = builder
1690 .with_execution_history(phase_ctx.execution_history.clone())
1691 .with_prompt_history(phase_ctx.clone_prompt_history());
1692
1693 if let Some(mut checkpoint) = builder.build() {
1694 checkpoint.rebase_state = RebaseState::PostRebaseInProgress {
1695 upstream_branch: default_branch,
1696 };
1697 let _ = save_checkpoint(&checkpoint);
1698 }
1699 }
1700
1701 match run_rebase_to_default(&ctx.logger, ctx.colors) {
1702 Ok(RebaseResult::Success) => {
1703 ctx.logger.success("Rebase completed successfully");
1704 let step = ExecutionStep::new(
1706 "PostRebase",
1707 ctx.config.developer_iters,
1708 "post_rebase_complete",
1709 StepOutcome::success(None, vec![]),
1710 );
1711 phase_ctx.execution_history.add_step(step);
1712
1713 if ctx.config.features.checkpoint_enabled {
1715 let builder = CheckpointBuilder::new()
1716 .phase(
1717 PipelinePhase::CommitMessage,
1718 ctx.config.developer_iters,
1719 ctx.config.developer_iters,
1720 )
1721 .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1722 .skip_rebase(true) .capture_from_context(
1724 &ctx.config,
1725 &ctx.registry,
1726 &ctx.developer_agent,
1727 &ctx.reviewer_agent,
1728 &ctx.logger,
1729 run_context,
1730 )
1731 .with_execution_history(phase_ctx.execution_history.clone())
1732 .with_prompt_history(phase_ctx.clone_prompt_history());
1733
1734 if let Some(checkpoint) = builder.build() {
1735 let _ = save_checkpoint(&checkpoint);
1736 }
1737 }
1738
1739 Ok(())
1740 }
1741 Ok(RebaseResult::NoOp { reason }) => {
1742 ctx.logger.info(&format!("No rebase needed: {reason}"));
1743 let step = ExecutionStep::new(
1745 "PostRebase",
1746 ctx.config.developer_iters,
1747 "post_rebase_skipped",
1748 StepOutcome::skipped(reason.clone()),
1749 );
1750 phase_ctx.execution_history.add_step(step);
1751
1752 if ctx.config.features.checkpoint_enabled {
1754 let builder = CheckpointBuilder::new()
1755 .phase(
1756 PipelinePhase::CommitMessage,
1757 ctx.config.developer_iters,
1758 ctx.config.developer_iters,
1759 )
1760 .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1761 .skip_rebase(true) .capture_from_context(
1763 &ctx.config,
1764 &ctx.registry,
1765 &ctx.developer_agent,
1766 &ctx.reviewer_agent,
1767 &ctx.logger,
1768 run_context,
1769 )
1770 .with_execution_history(phase_ctx.execution_history.clone())
1771 .with_prompt_history(phase_ctx.clone_prompt_history());
1772
1773 if let Some(checkpoint) = builder.build() {
1774 let _ = save_checkpoint(&checkpoint);
1775 }
1776 }
1777
1778 Ok(())
1779 }
1780 Ok(RebaseResult::Conflicts(_conflicts)) => {
1781 let conflicted_files = get_conflicted_files()?;
1783 if conflicted_files.is_empty() {
1784 ctx.logger
1785 .warn("Rebase reported conflicts but no conflicted files found");
1786 let _ = abort_rebase();
1787 return Ok(());
1788 }
1789
1790 let step = ExecutionStep::new(
1792 "PostRebase",
1793 ctx.config.developer_iters,
1794 "post_rebase_conflict",
1795 StepOutcome::partial(
1796 "Rebase started".to_string(),
1797 format!("{} conflicts detected", conflicted_files.len()),
1798 ),
1799 );
1800 phase_ctx.execution_history.add_step(step);
1801
1802 if ctx.config.features.checkpoint_enabled {
1804 let mut builder = CheckpointBuilder::new()
1805 .phase(
1806 PipelinePhase::PostRebaseConflict,
1807 ctx.config.developer_iters,
1808 ctx.config.developer_iters,
1809 )
1810 .reviewer_pass(ctx.config.reviewer_reviews, ctx.config.reviewer_reviews)
1811 .capture_from_context(
1812 &ctx.config,
1813 &ctx.registry,
1814 &ctx.developer_agent,
1815 &ctx.reviewer_agent,
1816 &ctx.logger,
1817 run_context,
1818 );
1819
1820 builder = builder
1822 .with_execution_history(phase_ctx.execution_history.clone())
1823 .with_prompt_history(phase_ctx.clone_prompt_history());
1824
1825 if let Some(mut checkpoint) = builder.build() {
1826 checkpoint.rebase_state = RebaseState::HasConflicts {
1827 files: conflicted_files.clone(),
1828 };
1829 let _ = save_checkpoint(&checkpoint);
1830 }
1831 }
1832
1833 ctx.logger.warn(&format!(
1834 "Rebase resulted in {} conflict(s), attempting AI resolution",
1835 conflicted_files.len()
1836 ));
1837
1838 match try_resolve_conflicts_with_fallback(
1840 &conflicted_files,
1841 &ctx.config,
1842 &ctx.template_context,
1843 &ctx.logger,
1844 ctx.colors,
1845 phase_ctx,
1846 "PostRebase",
1847 ) {
1848 Ok(true) => {
1849 ctx.logger
1851 .info("Continuing rebase after conflict resolution");
1852 match continue_rebase() {
1853 Ok(()) => {
1854 ctx.logger
1855 .success("Rebase completed successfully after AI resolution");
1856 let step = ExecutionStep::new(
1858 "PostRebase",
1859 ctx.config.developer_iters,
1860 "post_rebase_resolution",
1861 StepOutcome::success(None, vec![]),
1862 );
1863 phase_ctx.execution_history.add_step(step);
1864
1865 if ctx.config.features.checkpoint_enabled {
1867 let builder = CheckpointBuilder::new()
1868 .phase(
1869 PipelinePhase::CommitMessage,
1870 ctx.config.developer_iters,
1871 ctx.config.developer_iters,
1872 )
1873 .reviewer_pass(
1874 ctx.config.reviewer_reviews,
1875 ctx.config.reviewer_reviews,
1876 )
1877 .skip_rebase(true) .capture_from_context(
1879 &ctx.config,
1880 &ctx.registry,
1881 &ctx.developer_agent,
1882 &ctx.reviewer_agent,
1883 &ctx.logger,
1884 run_context,
1885 )
1886 .with_execution_history(phase_ctx.execution_history.clone())
1887 .with_prompt_history(phase_ctx.clone_prompt_history());
1888
1889 if let Some(checkpoint) = builder.build() {
1890 let _ = save_checkpoint(&checkpoint);
1891 }
1892 }
1893
1894 Ok(())
1895 }
1896 Err(e) => {
1897 ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
1898 let _ = abort_rebase();
1899 let step = ExecutionStep::new(
1901 "PostRebase",
1902 ctx.config.developer_iters,
1903 "post_rebase_resolution",
1904 StepOutcome::partial(
1905 "Conflicts resolved by AI".to_string(),
1906 format!("Failed to continue rebase: {e}"),
1907 ),
1908 );
1909 phase_ctx.execution_history.add_step(step);
1910 Ok(()) }
1912 }
1913 }
1914 Ok(false) => {
1915 ctx.logger
1917 .warn("AI conflict resolution failed, aborting rebase");
1918 let _ = abort_rebase();
1919 let step = ExecutionStep::new(
1921 "PostRebase",
1922 ctx.config.developer_iters,
1923 "post_rebase_resolution",
1924 StepOutcome::failure("AI conflict resolution failed".to_string(), true),
1925 );
1926 phase_ctx.execution_history.add_step(step);
1927 Ok(()) }
1929 Err(e) => {
1930 ctx.logger.error(&format!("Conflict resolution error: {e}"));
1931 let _ = abort_rebase();
1932 let step = ExecutionStep::new(
1934 "PostRebase",
1935 ctx.config.developer_iters,
1936 "post_rebase_resolution",
1937 StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
1938 );
1939 phase_ctx.execution_history.add_step(step);
1940 Ok(()) }
1942 }
1943 }
1944 Ok(RebaseResult::Failed(err)) => {
1945 ctx.logger.error(&format!("Rebase failed: {err}"));
1946 let _ = abort_rebase();
1947 let step = ExecutionStep::new(
1949 "PostRebase",
1950 ctx.config.developer_iters,
1951 "post_rebase_failed",
1952 StepOutcome::failure(format!("Rebase failed: {err}"), true),
1953 );
1954 phase_ctx.execution_history.add_step(step);
1955 Ok(()) }
1957 Err(e) => {
1958 ctx.logger
1959 .warn(&format!("Rebase failed, continuing without rebase: {e}"));
1960 let step = ExecutionStep::new(
1962 "PostRebase",
1963 ctx.config.developer_iters,
1964 "post_rebase_error",
1965 StepOutcome::failure(format!("Rebase error: {e}"), true),
1966 );
1967 phase_ctx.execution_history.add_step(step);
1968 Ok(())
1969 }
1970 }
1971}
1972
1973enum ConflictResolutionResult {
1977 WithJson(String),
1979 FileEditsOnly,
1981 Failed,
1983}
1984
1985fn try_resolve_conflicts_with_fallback(
1990 conflicted_files: &[String],
1991 config: &crate::config::Config,
1992 template_context: &TemplateContext,
1993 logger: &Logger,
1994 colors: Colors,
1995 phase_ctx: &mut PhaseContext<'_>,
1996 phase: &str,
1997) -> anyhow::Result<bool> {
1998 if conflicted_files.is_empty() {
1999 return Ok(false);
2000 }
2001
2002 logger.info(&format!(
2003 "Attempting AI conflict resolution for {} file(s)",
2004 conflicted_files.len()
2005 ));
2006
2007 let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
2008
2009 let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2012 let (resolution_prompt, was_replayed) =
2013 get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2014 build_resolution_prompt(&conflicts, template_context)
2015 });
2016
2017 if !was_replayed {
2019 phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2020 } else {
2021 logger.info(&format!(
2022 "Using stored prompt from checkpoint for determinism: {}",
2023 prompt_key
2024 ));
2025 }
2026
2027 match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
2028 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2029 match parse_and_validate_resolved_files(&resolved_content, logger) {
2032 Ok(resolved_files) => {
2033 write_resolved_files(&resolved_files, logger)?;
2034 }
2035 Err(_) => {
2036 }
2040 }
2041
2042 let remaining_conflicts = get_conflicted_files()?;
2044 if remaining_conflicts.is_empty() {
2045 Ok(true)
2046 } else {
2047 logger.warn(&format!(
2048 "{} conflicts remain after AI resolution",
2049 remaining_conflicts.len()
2050 ));
2051 Ok(false)
2052 }
2053 }
2054 Ok(ConflictResolutionResult::FileEditsOnly) => {
2055 logger.info("Agent resolved conflicts via file edits (no JSON output)");
2057
2058 let remaining_conflicts = get_conflicted_files()?;
2060 if remaining_conflicts.is_empty() {
2061 logger.success("All conflicts resolved via file edits");
2062 Ok(true)
2063 } else {
2064 logger.warn(&format!(
2065 "{} conflicts remain after AI resolution",
2066 remaining_conflicts.len()
2067 ));
2068 Ok(false)
2069 }
2070 }
2071 Ok(ConflictResolutionResult::Failed) => {
2072 logger.warn("AI conflict resolution failed");
2073 logger.info("Attempting to continue rebase anyway...");
2074
2075 match crate::git_helpers::continue_rebase() {
2077 Ok(()) => {
2078 logger.info("Successfully continued rebase");
2079 Ok(true)
2080 }
2081 Err(rebase_err) => {
2082 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
2083 Ok(false) }
2085 }
2086 }
2087 Err(e) => {
2088 logger.warn(&format!("AI conflict resolution failed: {e}"));
2089 logger.info("Attempting to continue rebase anyway...");
2090
2091 match crate::git_helpers::continue_rebase() {
2093 Ok(()) => {
2094 logger.info("Successfully continued rebase");
2095 Ok(true)
2096 }
2097 Err(rebase_err) => {
2098 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
2099 Ok(false) }
2101 }
2102 }
2103 }
2104}
2105
2106fn try_resolve_conflicts_without_phase_ctx(
2111 conflicted_files: &[String],
2112 config: &crate::config::Config,
2113 template_context: &TemplateContext,
2114 logger: &Logger,
2115 colors: Colors,
2116) -> anyhow::Result<bool> {
2117 use crate::agents::AgentRegistry;
2118 use crate::checkpoint::execution_history::ExecutionHistory;
2119 use crate::checkpoint::RunContext;
2120 use crate::pipeline::{Stats, Timer};
2121
2122 let registry = AgentRegistry::new()?;
2124 let mut timer = Timer::new();
2125 let mut stats = Stats::default();
2126
2127 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2128 let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2129
2130 let mut phase_ctx = PhaseContext {
2131 config,
2132 registry: ®istry,
2133 logger,
2134 colors: &colors,
2135 timer: &mut timer,
2136 stats: &mut stats,
2137 developer_agent,
2138 reviewer_agent,
2139 review_guidelines: None,
2140 template_context,
2141 run_context: RunContext::new(),
2142 execution_history: ExecutionHistory::new(),
2143 prompt_history: std::collections::HashMap::new(),
2144 };
2145
2146 try_resolve_conflicts_with_fallback(
2147 conflicted_files,
2148 config,
2149 template_context,
2150 logger,
2151 colors,
2152 &mut phase_ctx,
2153 "RebaseOnly",
2154 )
2155}
2156
2157fn collect_conflict_info_or_error(
2159 conflicted_files: &[String],
2160 logger: &Logger,
2161) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2162 use crate::prompts::collect_conflict_info;
2163
2164 let conflicts = match collect_conflict_info(conflicted_files) {
2165 Ok(c) => c,
2166 Err(e) => {
2167 logger.error(&format!("Failed to collect conflict info: {e}"));
2168 anyhow::bail!("Failed to collect conflict info");
2169 }
2170 };
2171 Ok(conflicts)
2172}
2173
2174fn build_resolution_prompt(
2176 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2177 template_context: &TemplateContext,
2178) -> String {
2179 build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2180 .unwrap_or_else(|_| String::new())
2181}
2182
2183fn build_enhanced_resolution_prompt(
2187 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2188 _branch_info: Option<()>, template_context: &TemplateContext,
2190) -> anyhow::Result<String> {
2191 use std::fs;
2192
2193 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2194 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2195
2196 Ok(
2198 crate::prompts::build_conflict_resolution_prompt_with_context(
2199 template_context,
2200 conflicts,
2201 prompt_md_content.as_deref(),
2202 plan_content.as_deref(),
2203 ),
2204 )
2205}
2206
2207fn run_ai_conflict_resolution(
2212 resolution_prompt: &str,
2213 config: &crate::config::Config,
2214 logger: &Logger,
2215 colors: Colors,
2216) -> anyhow::Result<ConflictResolutionResult> {
2217 use crate::agents::AgentRegistry;
2218 use crate::files::result_extraction::extract_last_result;
2219 use crate::pipeline::{
2220 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2221 };
2222 use std::io;
2223 use std::path::Path;
2224
2225 let log_dir = ".agent/logs/rebase_conflict_resolution";
2229
2230 let registry = AgentRegistry::new()?;
2231 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2232
2233 let mut runtime = PipelineRuntime {
2234 timer: &mut crate::pipeline::Timer::new(),
2235 logger,
2236 colors: &colors,
2237 config,
2238 #[cfg(any(test, feature = "test-utils"))]
2239 agent_executor: None,
2240 };
2241
2242 let validate_output: OutputValidator = |log_dir_path: &Path,
2245 validation_logger: &crate::logger::Logger|
2246 -> io::Result<bool> {
2247 match extract_last_result(log_dir_path) {
2248 Ok(Some(_)) => {
2249 Ok(true)
2251 }
2252 Ok(None) => {
2253 match crate::git_helpers::get_conflicted_files() {
2256 Ok(conflicts) if conflicts.is_empty() => {
2257 validation_logger
2258 .info("Agent resolved conflicts without JSON output (file edits only)");
2259 Ok(true) }
2261 Ok(conflicts) => {
2262 validation_logger.warn(&format!(
2263 "{} conflict(s) remain unresolved",
2264 conflicts.len()
2265 ));
2266 Ok(false) }
2268 Err(e) => {
2269 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2270 Ok(false) }
2272 }
2273 }
2274 Err(e) => {
2275 validation_logger.warn(&format!("Output validation check failed: {e}"));
2276 Ok(false) }
2278 }
2279 };
2280
2281 let mut fallback_config = FallbackConfig {
2282 role: crate::agents::AgentRole::Reviewer,
2283 base_label: "conflict resolution",
2284 prompt: resolution_prompt,
2285 logfile_prefix: log_dir,
2286 runtime: &mut runtime,
2287 registry: ®istry,
2288 primary_agent: reviewer_agent,
2289 output_validator: Some(validate_output),
2290 };
2291
2292 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2293
2294 if exit_code != 0 {
2295 return Ok(ConflictResolutionResult::Failed);
2296 }
2297
2298 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2301
2302 if remaining_conflicts.is_empty() {
2303 match extract_last_result(Path::new(log_dir)) {
2305 Ok(Some(content)) => {
2306 logger.info("Agent provided JSON output with resolved files");
2307 Ok(ConflictResolutionResult::WithJson(content))
2308 }
2309 Ok(None) => {
2310 logger.info("Agent resolved conflicts via file edits (no JSON output)");
2311 Ok(ConflictResolutionResult::FileEditsOnly)
2312 }
2313 Err(e) => {
2314 logger.warn(&format!(
2316 "Failed to extract JSON output but conflicts are resolved: {e}"
2317 ));
2318 Ok(ConflictResolutionResult::FileEditsOnly)
2319 }
2320 }
2321 } else {
2322 logger.warn(&format!(
2323 "{} conflict(s) remain after agent attempted resolution",
2324 remaining_conflicts.len()
2325 ));
2326 Ok(ConflictResolutionResult::Failed)
2327 }
2328}
2329
2330fn parse_and_validate_resolved_files(
2336 resolved_content: &str,
2337 logger: &Logger,
2338) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2339 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2340 anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2343 })?;
2344
2345 let resolved_files = match json.get("resolved_files") {
2346 Some(v) if v.is_object() => v.as_object().unwrap(),
2347 _ => {
2348 logger.info("Agent output missing 'resolved_files' object");
2349 anyhow::bail!("Agent output missing 'resolved_files' object");
2350 }
2351 };
2352
2353 if resolved_files.is_empty() {
2354 logger.info("No resolved files in JSON output");
2355 anyhow::bail!("No files were resolved by the agent");
2356 }
2357
2358 Ok(resolved_files.clone())
2359}
2360
2361fn write_resolved_files(
2363 resolved_files: &serde_json::Map<String, serde_json::Value>,
2364 logger: &Logger,
2365) -> anyhow::Result<usize> {
2366 use std::fs;
2367
2368 let mut files_written = 0;
2369 for (path, content) in resolved_files {
2370 if let Some(content_str) = content.as_str() {
2371 fs::write(path, content_str).map_err(|e| {
2372 logger.error(&format!("Failed to write {path}: {e}"));
2373 anyhow::anyhow!("Failed to write {path}: {e}")
2374 })?;
2375 logger.info(&format!("Resolved and wrote: {path}"));
2376 files_written += 1;
2377 if let Err(e) = crate::git_helpers::git_add_all() {
2379 logger.warn(&format!("Failed to stage {path}: {e}"));
2380 }
2381 }
2382 }
2383
2384 logger.success(&format!("Successfully resolved {files_written} file(s)"));
2385 Ok(files_written)
2386}