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::{phase_rank, should_run_from};
30use crate::banner::print_welcome_banner;
31use crate::checkpoint::{save_checkpoint, PipelineCheckpoint, PipelinePhase};
32use crate::cli::{
33 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
34 handle_list_available_agents, handle_list_providers, handle_show_baseline,
35 handle_template_commands, prompt_template_selection, Args,
36};
37use crate::common::utils;
38use crate::files::protection::monitoring::PromptMonitor;
39use crate::files::{
40 create_prompt_backup, ensure_files, make_prompt_read_only, reset_context_for_isolation,
41 update_status, validate_prompt_md,
42};
43use crate::git_helpers::{
44 abort_rebase, cleanup_orphaned_marker, continue_rebase, get_conflicted_files,
45 get_default_branch, get_repo_root, get_start_commit_summary, is_main_or_master_branch,
46 rebase_onto, require_git_repo, reset_start_commit, save_start_commit, start_agent_phase,
47 RebaseResult, RebaseStateMachine,
48};
49use crate::logger::Colors;
50use crate::logger::Logger;
51use crate::phases::{run_development_phase, run_review_phase, PhaseContext};
52use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
53use crate::prompts::template_context::TemplateContext;
54use std::env;
55use std::process::Command;
56
57use config_init::initialize_config;
58use context::PipelineContext;
59use detection::detect_project_stack;
60use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
61use resume::handle_resume;
62use validation::{
63 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
64};
65
66pub fn run(args: Args) -> anyhow::Result<()> {
85 let colors = Colors::new();
86 let logger = Logger::new(colors);
87
88 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
90 return Ok(()); };
92
93 let config_init::ConfigInitResult {
94 config,
95 registry,
96 config_path,
97 config_sources,
98 } = init_result;
99
100 let validated = resolve_required_agents(&config)?;
102 let developer_agent = validated.developer_agent;
103 let reviewer_agent = validated.reviewer_agent;
104
105 if handle_listing_commands(&args, ®istry, colors) {
107 return Ok(());
108 }
109
110 if args.recovery.diagnose {
112 handle_diagnose(colors, &config, ®istry, &config_path, &config_sources);
113 return Ok(());
114 }
115
116 validate_agent_chains(®istry, colors);
118
119 if handle_plumbing_commands(&args, &logger, colors)? {
121 return Ok(());
122 }
123
124 let Some(repo_root) = validate_and_setup_agents(
126 &config,
127 ®istry,
128 &developer_agent,
129 &reviewer_agent,
130 &config_path,
131 colors,
132 &logger,
133 )?
134 else {
135 return Ok(());
136 };
137
138 if args.rebase_flags.rebase_only {
140 let template_context =
141 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
142 return handle_rebase_only(&args, &config, &template_context, &logger, colors);
143 }
144
145 (prepare_pipeline_or_exit(PipelinePreparationParams {
147 args,
148 config,
149 registry,
150 developer_agent,
151 reviewer_agent,
152 repo_root,
153 logger,
154 colors,
155 })?)
156 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
157}
158
159fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
163 if args.agent_list.list_agents {
164 handle_list_agents(registry);
165 return true;
166 }
167 if args.agent_list.list_available_agents {
168 handle_list_available_agents(registry);
169 return true;
170 }
171 if args.provider_list.list_providers {
172 handle_list_providers(colors);
173 return true;
174 }
175
176 let template_cmds = &args.template_commands;
178 if template_cmds.init_templates_enabled()
179 || template_cmds.validate
180 || template_cmds.show.is_some()
181 || template_cmds.list
182 || template_cmds.list_all
183 || template_cmds.variables.is_some()
184 || template_cmds.render.is_some()
185 {
186 let _ = handle_template_commands(template_cmds, colors);
187 return true;
188 }
189
190 false
191}
192
193fn handle_plumbing_commands(args: &Args, logger: &Logger, colors: Colors) -> anyhow::Result<bool> {
198 if args.commit_display.show_commit_msg {
200 return handle_show_commit_msg().map(|()| true);
201 }
202
203 if args.commit_plumbing.apply_commit {
205 return handle_apply_commit(logger, colors).map(|()| true);
206 }
207
208 if args.commit_display.reset_start_commit {
210 require_git_repo()?;
211 let repo_root = get_repo_root()?;
212 env::set_current_dir(&repo_root)?;
213
214 return match reset_start_commit() {
215 Ok(result) => {
216 let short_oid = &result.oid[..8.min(result.oid.len())];
217 if result.fell_back_to_head {
218 logger.success(&format!(
219 "Starting commit reference reset to current HEAD ({})",
220 short_oid
221 ));
222 logger.info("On main/master branch - using HEAD as baseline");
223 } else if let Some(ref branch) = result.default_branch {
224 logger.success(&format!(
225 "Starting commit reference reset to merge-base with '{}' ({})",
226 branch, short_oid
227 ));
228 logger.info("Baseline set to common ancestor with default branch");
229 } else {
230 logger.success(&format!("Starting commit reference reset ({})", short_oid));
231 }
232 logger.info(".agent/start_commit has been updated");
233 Ok(true)
234 }
235 Err(e) => {
236 logger.error(&format!("Failed to reset starting commit: {e}"));
237 anyhow::bail!("Failed to reset starting commit");
238 }
239 };
240 }
241
242 if args.commit_display.show_baseline {
244 require_git_repo()?;
245 let repo_root = get_repo_root()?;
246 env::set_current_dir(&repo_root)?;
247
248 return match handle_show_baseline() {
249 Ok(()) => Ok(true),
250 Err(e) => {
251 logger.error(&format!("Failed to show baseline: {e}"));
252 anyhow::bail!("Failed to show baseline");
253 }
254 };
255 }
256
257 Ok(false)
258}
259
260struct PipelinePreparationParams {
264 args: Args,
265 config: crate::config::Config,
266 registry: AgentRegistry,
267 developer_agent: String,
268 reviewer_agent: String,
269 repo_root: std::path::PathBuf,
270 logger: Logger,
271 colors: Colors,
272}
273
274fn prepare_pipeline_or_exit(
278 params: PipelinePreparationParams,
279) -> anyhow::Result<Option<PipelineContext>> {
280 let PipelinePreparationParams {
281 args,
282 config,
283 registry,
284 developer_agent,
285 reviewer_agent,
286 repo_root,
287 mut logger,
288 colors,
289 } = params;
290
291 ensure_files(config.isolation_mode)?;
292
293 if config.isolation_mode {
295 reset_context_for_isolation(&logger)?;
296 }
297
298 logger = logger.with_log_file(".agent/logs/pipeline.log");
299
300 if args.recovery.dry_run {
302 let developer_display = registry.display_name(&developer_agent);
303 let reviewer_display = registry.display_name(&reviewer_agent);
304 handle_dry_run(
305 &logger,
306 colors,
307 &config,
308 &developer_display,
309 &reviewer_display,
310 &repo_root,
311 )?;
312 return Ok(None);
313 }
314
315 let template_context =
317 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
318
319 if args.commit_plumbing.generate_commit_msg {
321 handle_generate_commit_msg(
322 &config,
323 &template_context,
324 ®istry,
325 &logger,
326 colors,
327 &developer_agent,
328 &reviewer_agent,
329 )?;
330 return Ok(None);
331 }
332
333 let developer_display = registry.display_name(&developer_agent);
335 let reviewer_display = registry.display_name(&reviewer_agent);
336
337 let ctx = PipelineContext {
339 args,
340 config,
341 registry,
342 developer_agent,
343 reviewer_agent,
344 developer_display,
345 reviewer_display,
346 repo_root,
347 logger,
348 colors,
349 template_context,
350 };
351 Ok(Some(ctx))
352}
353
354fn validate_and_setup_agents(
359 config: &crate::config::Config,
360 registry: &AgentRegistry,
361 developer_agent: &str,
362 reviewer_agent: &str,
363 config_path: &std::path::Path,
364 colors: Colors,
365 logger: &Logger,
366) -> anyhow::Result<Option<std::path::PathBuf>> {
367 validate_agent_commands(
369 config,
370 registry,
371 developer_agent,
372 reviewer_agent,
373 config_path,
374 )?;
375
376 validate_can_commit(
378 config,
379 registry,
380 developer_agent,
381 reviewer_agent,
382 config_path,
383 )?;
384
385 require_git_repo()?;
387 let repo_root = get_repo_root()?;
388 env::set_current_dir(&repo_root)?;
389
390 let should_continue = setup_git_and_prompt_file(config, colors, logger)?;
392 if should_continue.is_none() {
393 return Ok(None);
394 }
395
396 Ok(Some(repo_root))
397}
398
399fn setup_git_and_prompt_file(
404 config: &crate::config::Config,
405 colors: Colors,
406 logger: &Logger,
407) -> anyhow::Result<Option<()>> {
408 let prompt_exists = std::path::Path::new("PROMPT.md").exists();
409
410 if config.behavior.interactive && !prompt_exists {
413 if let Some(template_name) = prompt_template_selection(colors) {
414 create_prompt_from_template(&template_name, colors)?;
415 println!();
416 logger.info(
417 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
418 );
419 logger.info(&format!(
420 "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
421 config.commit_msg
422 ));
423 return Ok(None);
424 }
425 println!();
426 logger.error("PROMPT.md not found in current directory.");
427 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
428 println!();
429 logger.info("To get started:");
430 logger.info(" ralph --init # Smart setup wizard");
431 logger.info(" ralph --init bug-fix # Create from Work Guide");
432 logger.info(" ralph --list-work-guides # See all Work Guides");
433 println!();
434 return Ok(None);
435 }
436
437 if !prompt_exists {
439 logger.error("PROMPT.md not found in current directory.");
440 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
441 println!();
442 logger.info("Quick start:");
443 logger.info(" ralph --init # Smart setup wizard");
444 logger.info(" ralph --init bug-fix # Create from Work Guide");
445 logger.info(" ralph --list-work-guides # See all Work Guides");
446 println!();
447 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
448 println!();
449 return Ok(None);
450 }
451
452 Ok(Some(()))
453}
454
455fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
457 let resume_checkpoint = handle_resume(
459 &ctx.args,
460 &ctx.logger,
461 &ctx.developer_display,
462 &ctx.reviewer_display,
463 );
464
465 let mut git_helpers = crate::git_helpers::GitHelpers::new();
467 cleanup_orphaned_marker(&ctx.logger)?;
468 start_agent_phase(&mut git_helpers)?;
469 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
470
471 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
473 print_pipeline_info(ctx);
474 validate_prompt_and_setup_backup(ctx)?;
475
476 let mut prompt_monitor = setup_prompt_monitor(ctx);
478
479 let (_project_stack, review_guidelines) =
481 detect_project_stack(&ctx.config, &ctx.repo_root, &ctx.logger, ctx.colors);
482
483 print_review_guidelines(ctx, review_guidelines.as_ref());
484 println!();
485
486 let (mut timer, mut stats) = (Timer::new(), Stats::new());
488 let mut phase_ctx =
489 create_phase_context(ctx, &mut timer, &mut stats, review_guidelines.as_ref());
490 save_start_commit_or_warn(ctx);
491
492 if ctx.args.rebase_flags.with_rebase {
494 run_initial_rebase(&ctx.config, &ctx.template_context, &ctx.logger, ctx.colors)?;
495 }
496
497 run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
499 check_prompt_restoration(ctx, &mut prompt_monitor, "development");
500 update_status("In progress.", ctx.config.isolation_mode)?;
501
502 run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
503 check_prompt_restoration(ctx, &mut prompt_monitor, "review");
504
505 if ctx.args.rebase_flags.with_rebase {
507 run_post_review_rebase(&ctx.config, &ctx.template_context, &ctx.logger, ctx.colors)?;
508 }
509
510 update_status("In progress.", ctx.config.isolation_mode)?;
511
512 run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
513
514 finalize_pipeline(
516 &mut agent_phase_guard,
517 &ctx.logger,
518 ctx.colors,
519 &ctx.config,
520 &timer,
521 &stats,
522 prompt_monitor,
523 );
524 Ok(())
525}
526
527fn print_pipeline_info(ctx: &PipelineContext) {
529 ctx.logger.info(&format!(
530 "Working directory: {}{}{}",
531 ctx.colors.cyan(),
532 ctx.repo_root.display(),
533 ctx.colors.reset()
534 ));
535 ctx.logger.info(&format!(
536 "Commit message: {}{}{}",
537 ctx.colors.cyan(),
538 ctx.config.commit_msg,
539 ctx.colors.reset()
540 ));
541}
542
543fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
545 let prompt_validation =
546 validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
547 for err in &prompt_validation.errors {
548 ctx.logger.error(err);
549 }
550 for warn in &prompt_validation.warnings {
551 ctx.logger.warn(warn);
552 }
553 if !prompt_validation.is_valid() {
554 anyhow::bail!("PROMPT.md validation errors");
555 }
556
557 match create_prompt_backup() {
559 Ok(None) => {}
560 Ok(Some(warning)) => {
561 ctx.logger.warn(&format!(
562 "PROMPT.md backup created but: {warning}. Continuing anyway."
563 ));
564 }
565 Err(e) => {
566 ctx.logger.warn(&format!(
567 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
568 ));
569 }
570 }
571
572 match make_prompt_read_only() {
574 None => {}
575 Some(warning) => {
576 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
577 }
578 }
579
580 Ok(())
581}
582
583fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
585 match PromptMonitor::new() {
586 Ok(mut monitor) => {
587 if let Err(e) = monitor.start() {
588 ctx.logger.warn(&format!(
589 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
590 ));
591 None
592 } else {
593 if ctx.config.verbosity.is_debug() {
594 ctx.logger.info("Started real-time PROMPT.md monitoring");
595 }
596 Some(monitor)
597 }
598 }
599 Err(e) => {
600 ctx.logger.warn(&format!(
601 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
602 ));
603 None
604 }
605 }
606}
607
608fn print_review_guidelines(
610 ctx: &PipelineContext,
611 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
612) {
613 if let Some(guidelines) = review_guidelines {
614 ctx.logger.info(&format!(
615 "Review guidelines: {}{}{}",
616 ctx.colors.dim(),
617 guidelines.summary(),
618 ctx.colors.reset()
619 ));
620 }
621}
622
623fn create_phase_context<'ctx>(
625 ctx: &'ctx PipelineContext,
626 timer: &'ctx mut Timer,
627 stats: &'ctx mut Stats,
628 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
629) -> PhaseContext<'ctx> {
630 PhaseContext {
631 config: &ctx.config,
632 registry: &ctx.registry,
633 logger: &ctx.logger,
634 colors: &ctx.colors,
635 timer,
636 stats,
637 developer_agent: &ctx.developer_agent,
638 reviewer_agent: &ctx.reviewer_agent,
639 review_guidelines,
640 template_context: &ctx.template_context,
641 }
642}
643
644fn save_start_commit_or_warn(ctx: &PipelineContext) {
646 match save_start_commit() {
647 Ok(()) => {
648 if ctx.config.verbosity.is_debug() {
649 ctx.logger
650 .info("Saved starting commit for incremental diff generation");
651 }
652 }
653 Err(e) => {
654 ctx.logger.warn(&format!(
655 "Failed to save starting commit: {e}. \
656 Incremental diffs may be unavailable as a result."
657 ));
658 ctx.logger.info(
659 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
660 );
661 }
662 }
663
664 match get_start_commit_summary() {
666 Ok(summary) => {
667 if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale {
668 ctx.logger.info(&summary.format_compact());
669 if summary.is_stale {
670 ctx.logger.warn(
671 "Start commit is stale. Consider running: ralph --reset-start-commit",
672 );
673 } else if summary.commits_since > 5 {
674 ctx.logger
675 .info("Tip: Run 'ralph --show-baseline' for more details");
676 }
677 }
678 }
679 Err(e) => {
680 if ctx.config.verbosity.is_debug() {
682 ctx.logger
683 .warn(&format!("Failed to get start commit summary: {e}"));
684 }
685 }
686 }
687}
688
689fn check_prompt_restoration(
691 ctx: &PipelineContext,
692 prompt_monitor: &mut Option<PromptMonitor>,
693 phase: &str,
694) {
695 if let Some(ref mut monitor) = prompt_monitor {
696 if monitor.check_and_restore() {
697 ctx.logger.warn(&format!(
698 "PROMPT.md was deleted and restored during {phase} phase"
699 ));
700 }
701 }
702}
703
704fn run_development(
706 ctx: &mut PhaseContext,
707 args: &Args,
708 resume_checkpoint: Option<&PipelineCheckpoint>,
709) -> anyhow::Result<()> {
710 ctx.logger
711 .header("PHASE 1: Development", crate::logger::Colors::blue);
712
713 let resume_phase = resume_checkpoint.map(|c| c.phase);
714 let resume_rank = resume_phase.map(phase_rank);
715
716 if resume_rank.is_some_and(|rank| rank >= phase_rank(PipelinePhase::Review)) {
717 ctx.logger
718 .info("Skipping development phase (checkpoint indicates it already completed)");
719 return Ok(());
720 }
721
722 if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
723 ctx.logger
724 .info("Skipping development phase (resuming from a later checkpoint phase)");
725 return Ok(());
726 }
727
728 let start_iter = match resume_phase {
729 Some(PipelinePhase::Planning | PipelinePhase::Development) => resume_checkpoint
730 .map_or(1, |c| c.iteration)
731 .clamp(1, ctx.config.developer_iters),
732 _ => 1,
733 };
734
735 let resuming_from_development =
736 args.recovery.resume && resume_phase == Some(PipelinePhase::Development);
737 let development_result = run_development_phase(ctx, start_iter, resuming_from_development)?;
738
739 if development_result.had_errors {
740 ctx.logger
741 .warn("Development phase completed with non-fatal errors");
742 }
743
744 Ok(())
745}
746
747fn run_review_and_fix(
749 ctx: &mut PhaseContext,
750 _args: &Args,
751 resume_checkpoint: Option<&PipelineCheckpoint>,
752) -> anyhow::Result<()> {
753 ctx.logger
754 .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
755
756 let resume_phase = resume_checkpoint.map(|c| c.phase);
757
758 let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
760 || should_run_from(PipelinePhase::Fix, resume_checkpoint)
761 || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
762 || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
763
764 let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
765 || resume_phase == Some(PipelinePhase::Fix)
766 || resume_phase == Some(PipelinePhase::ReviewAgain);
767
768 if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
769 let start_pass = match resume_phase {
770 Some(PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain) => {
771 resume_checkpoint
772 .map_or(1, |c| c.reviewer_pass)
773 .clamp(1, ctx.config.reviewer_reviews.max(1))
774 }
775 _ => 1,
776 };
777
778 let review_result = run_review_phase(ctx, start_pass)?;
779 if review_result.completed_early {
780 ctx.logger
781 .success("Review phase completed early (no issues found)");
782 }
783 } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
784 ctx.logger
785 .info("Skipping review phase (reviewer_reviews=0)");
786 } else if run_any_reviewer_phase {
787 ctx.logger
788 .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
789 }
790
791 Ok(())
795}
796
797fn run_final_validation(
799 ctx: &PhaseContext,
800 resume_checkpoint: Option<&PipelineCheckpoint>,
801) -> anyhow::Result<()> {
802 let Some(ref full_cmd) = ctx.config.full_check_cmd else {
803 return Ok(());
804 };
805
806 if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
807 ctx.logger
808 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
809 ctx.logger
810 .info("Skipping final validation (resuming from a later checkpoint phase)");
811 return Ok(());
812 }
813
814 let argv = utils::split_command(full_cmd)
815 .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
816 if argv.is_empty() {
817 ctx.logger
818 .warn("FULL_CHECK_CMD is empty; skipping final validation");
819 return Ok(());
820 }
821
822 if ctx.config.features.checkpoint_enabled {
823 let _ = save_checkpoint(&PipelineCheckpoint::new(
824 PipelinePhase::FinalValidation,
825 ctx.config.developer_iters,
826 ctx.config.developer_iters,
827 ctx.config.reviewer_reviews,
828 ctx.config.reviewer_reviews,
829 ctx.developer_agent,
830 ctx.reviewer_agent,
831 ));
832 }
833
834 ctx.logger
835 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
836 let display_cmd = utils::format_argv_for_log(&argv);
837 ctx.logger.info(&format!(
838 "Running full check: {}{}{}",
839 ctx.colors.dim(),
840 display_cmd,
841 ctx.colors.reset()
842 ));
843
844 let Some((program, arguments)) = argv.split_first() else {
845 ctx.logger
846 .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
847 return Ok(());
848 };
849 let status = Command::new(program).args(arguments).status()?;
850
851 if status.success() {
852 ctx.logger.success("Full check passed");
853 } else {
854 ctx.logger.error("Full check failed");
855 anyhow::bail!("Full check failed");
856 }
857
858 Ok(())
859}
860
861fn handle_rebase_only(
866 _args: &Args,
867 config: &crate::config::Config,
868 template_context: &TemplateContext,
869 logger: &Logger,
870 colors: Colors,
871) -> anyhow::Result<()> {
872 if is_main_or_master_branch()? {
874 logger.warn("Already on main/master branch - rebasing on main is not recommended");
875 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
876 logger.info(" git worktree add ../feature-branch feature-branch");
877 logger.info("This allows multiple AI agents to work on different features simultaneously.");
878 logger.info("Proceeding with rebase anyway as requested...");
879 }
880
881 logger.header("Rebase to default branch", Colors::cyan);
882
883 match run_rebase_to_default(logger, colors) {
884 Ok(RebaseResult::Success) => {
885 logger.success("Rebase completed successfully");
886 Ok(())
887 }
888 Ok(RebaseResult::NoOp { reason }) => {
889 logger.info(&format!("No rebase needed: {reason}"));
890 Ok(())
891 }
892 Ok(RebaseResult::Failed(err)) => {
893 logger.error(&format!("Rebase failed: {err}"));
894 anyhow::bail!("Rebase failed: {err}")
895 }
896 Ok(RebaseResult::Conflicts(_conflicts)) => {
897 let conflicted_files = get_conflicted_files()?;
899 if conflicted_files.is_empty() {
900 logger.warn("Rebase reported conflicts but no conflicted files found");
901 let _ = abort_rebase();
902 return Ok(());
903 }
904
905 logger.warn(&format!(
906 "Rebase resulted in {} conflict(s), attempting AI resolution",
907 conflicted_files.len()
908 ));
909
910 match try_resolve_conflicts_with_fallback(
912 &conflicted_files,
913 config,
914 template_context,
915 logger,
916 colors,
917 ) {
918 Ok(true) => {
919 logger.info("Continuing rebase after conflict resolution");
921 match continue_rebase() {
922 Ok(()) => {
923 logger.success("Rebase completed successfully after AI resolution");
924 Ok(())
925 }
926 Err(e) => {
927 logger.error(&format!("Failed to continue rebase: {e}"));
928 let _ = abort_rebase();
929 anyhow::bail!("Rebase failed after conflict resolution")
930 }
931 }
932 }
933 Ok(false) => {
934 logger.error("AI conflict resolution failed, aborting rebase");
936 let _ = abort_rebase();
937 anyhow::bail!("Rebase conflicts could not be resolved by AI")
938 }
939 Err(e) => {
940 logger.error(&format!("Conflict resolution error: {e}"));
941 let _ = abort_rebase();
942 anyhow::bail!("Rebase conflict resolution failed: {e}")
943 }
944 }
945 }
946 Err(e) => {
947 logger.error(&format!("Rebase failed: {e}"));
948 anyhow::bail!("Rebase failed: {e}")
949 }
950 }
951}
952
953fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
966 let default_branch = get_default_branch()?;
968 logger.info(&format!(
969 "Rebasing onto {}{}{}",
970 colors.cyan(),
971 default_branch,
972 colors.reset()
973 ));
974
975 rebase_onto(&default_branch)
977}
978
979fn run_initial_rebase(
993 config: &crate::config::Config,
994 template_context: &TemplateContext,
995 logger: &Logger,
996 colors: Colors,
997) -> anyhow::Result<()> {
998 if !config.features.auto_rebase {
1000 logger.info("Rebase disabled via config (auto_rebase=false)");
1001 return Ok(());
1002 }
1003
1004 logger.header("Pre-development rebase", Colors::cyan);
1005
1006 let default_branch = get_default_branch()?;
1008
1009 let mut state_machine: RebaseStateMachine =
1011 match RebaseStateMachine::load_or_create(default_branch.clone()) {
1012 Ok(mut machine) => {
1013 if machine.phase() == &crate::git_helpers::RebasePhase::NotStarted {
1016 machine =
1017 machine.with_max_recovery_attempts(config.features.max_recovery_attempts);
1018 }
1019 machine
1020 }
1021 Err(e) => {
1022 logger.warn(&format!("Failed to load rebase state machine: {e}"));
1023 return run_fallback_rebase(logger, colors, config, template_context);
1025 }
1026 };
1027
1028 let phase = state_machine.phase().clone();
1030 if phase != crate::git_helpers::RebasePhase::NotStarted {
1031 logger.info(&format!("Resuming rebase from phase: {:?}", phase));
1032 }
1033
1034 match run_rebase_with_state_machine(
1036 &mut state_machine,
1037 logger,
1038 colors,
1039 config,
1040 template_context,
1041 ) {
1042 Ok(RebaseResult::Success) => {
1043 logger.success("Rebase completed successfully");
1044 let _ = state_machine.clear_checkpoint();
1046 Ok(())
1047 }
1048 Ok(RebaseResult::NoOp { reason }) => {
1049 logger.info(&format!("No rebase needed: {reason}"));
1050 let _ = state_machine.clear_checkpoint();
1052 Ok(())
1053 }
1054 Ok(RebaseResult::Conflicts(_conflicts)) => {
1055 logger.success("Rebase completed successfully after conflict resolution");
1057 let _ = state_machine.clear_checkpoint();
1058 Ok(())
1059 }
1060 Ok(RebaseResult::Failed(err)) => {
1061 logger.error(&format!("Rebase failed: {err}"));
1062 anyhow::bail!("Rebase failed: {err}")
1063 }
1064 Err(e) => {
1065 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1066 Ok(())
1068 }
1069 }
1070}
1071
1072fn run_post_review_rebase(
1086 config: &crate::config::Config,
1087 template_context: &TemplateContext,
1088 logger: &Logger,
1089 colors: Colors,
1090) -> anyhow::Result<()> {
1091 if !config.features.auto_rebase {
1093 logger.info("Rebase disabled via config (auto_rebase=false)");
1094 return Ok(());
1095 }
1096
1097 logger.header("Post-review rebase", Colors::cyan);
1098
1099 let default_branch = get_default_branch()?;
1101
1102 let mut state_machine: RebaseStateMachine =
1104 match RebaseStateMachine::load_or_create(default_branch.clone()) {
1105 Ok(mut machine) => {
1106 if machine.phase() == &crate::git_helpers::RebasePhase::NotStarted {
1109 machine =
1110 machine.with_max_recovery_attempts(config.features.max_recovery_attempts);
1111 }
1112 machine
1113 }
1114 Err(e) => {
1115 logger.warn(&format!("Failed to load rebase state machine: {e}"));
1116 return run_fallback_rebase(logger, colors, config, template_context);
1118 }
1119 };
1120
1121 let phase = state_machine.phase().clone();
1123 if phase != crate::git_helpers::RebasePhase::NotStarted {
1124 logger.info(&format!("Resuming rebase from phase: {:?}", phase));
1125 }
1126
1127 match run_rebase_with_state_machine(
1129 &mut state_machine,
1130 logger,
1131 colors,
1132 config,
1133 template_context,
1134 ) {
1135 Ok(RebaseResult::Success) => {
1136 logger.success("Rebase completed successfully");
1137 let _ = state_machine.clear_checkpoint();
1139 Ok(())
1140 }
1141 Ok(RebaseResult::NoOp { reason }) => {
1142 logger.info(&format!("No rebase needed: {reason}"));
1143 let _ = state_machine.clear_checkpoint();
1145 Ok(())
1146 }
1147 Ok(RebaseResult::Conflicts(_conflicts)) => {
1148 logger.success("Rebase completed successfully after conflict resolution");
1150 let _ = state_machine.clear_checkpoint();
1151 Ok(())
1152 }
1153 Ok(RebaseResult::Failed(err)) => {
1154 logger.error(&format!("Rebase failed: {err}"));
1155 anyhow::bail!("Rebase failed: {err}")
1156 }
1157 Err(e) => {
1158 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1159 Ok(())
1161 }
1162 }
1163}
1164
1165enum ConflictResolutionResult {
1169 WithJson(String),
1171 FileEditsOnly,
1173 Failed,
1175}
1176
1177fn try_resolve_conflicts_with_fallback(
1182 conflicted_files: &[String],
1183 config: &crate::config::Config,
1184 template_context: &TemplateContext,
1185 logger: &Logger,
1186 colors: Colors,
1187) -> anyhow::Result<bool> {
1188 if conflicted_files.is_empty() {
1189 return Ok(false);
1190 }
1191
1192 logger.info(&format!(
1193 "Attempting AI conflict resolution for {} file(s)",
1194 conflicted_files.len()
1195 ));
1196
1197 let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
1198 let resolution_prompt = build_resolution_prompt(&conflicts, template_context);
1199
1200 match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1201 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1202 match parse_and_validate_resolved_files(&resolved_content, logger) {
1205 Ok(resolved_files) => {
1206 write_resolved_files(&resolved_files, logger)?;
1207 }
1208 Err(_) => {
1209 }
1213 }
1214
1215 let remaining_conflicts = get_conflicted_files()?;
1217 if remaining_conflicts.is_empty() {
1218 Ok(true)
1219 } else {
1220 logger.warn(&format!(
1221 "{} conflicts remain after AI resolution",
1222 remaining_conflicts.len()
1223 ));
1224 Ok(false)
1225 }
1226 }
1227 Ok(ConflictResolutionResult::FileEditsOnly) => {
1228 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1230
1231 let remaining_conflicts = get_conflicted_files()?;
1233 if remaining_conflicts.is_empty() {
1234 logger.success("All conflicts resolved via file edits");
1235 Ok(true)
1236 } else {
1237 logger.warn(&format!(
1238 "{} conflicts remain after AI resolution",
1239 remaining_conflicts.len()
1240 ));
1241 Ok(false)
1242 }
1243 }
1244 Ok(ConflictResolutionResult::Failed) => {
1245 logger.warn("AI conflict resolution failed");
1246 logger.info("Attempting to continue rebase anyway...");
1247
1248 match crate::git_helpers::continue_rebase() {
1250 Ok(()) => {
1251 logger.info("Successfully continued rebase");
1252 Ok(true)
1253 }
1254 Err(rebase_err) => {
1255 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1256 Ok(false) }
1258 }
1259 }
1260 Err(e) => {
1261 logger.warn(&format!("AI conflict resolution failed: {e}"));
1262 logger.info("Attempting to continue rebase anyway...");
1263
1264 match crate::git_helpers::continue_rebase() {
1266 Ok(()) => {
1267 logger.info("Successfully continued rebase");
1268 Ok(true)
1269 }
1270 Err(rebase_err) => {
1271 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1272 Ok(false) }
1274 }
1275 }
1276 }
1277}
1278
1279fn collect_conflict_info_or_error(
1281 conflicted_files: &[String],
1282 logger: &Logger,
1283) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
1284 use crate::prompts::collect_conflict_info;
1285
1286 let conflicts = match collect_conflict_info(conflicted_files) {
1287 Ok(c) => c,
1288 Err(e) => {
1289 logger.error(&format!("Failed to collect conflict info: {e}"));
1290 anyhow::bail!("Failed to collect conflict info");
1291 }
1292 };
1293 Ok(conflicts)
1294}
1295
1296fn build_resolution_prompt(
1298 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1299 template_context: &TemplateContext,
1300) -> String {
1301 build_enhanced_resolution_prompt(conflicts, None, template_context)
1302 .unwrap_or_else(|_| String::new())
1303}
1304
1305fn build_enhanced_resolution_prompt(
1310 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1311 branch_info: Option<&crate::prompts::BranchInfo>,
1312 template_context: &TemplateContext,
1313) -> anyhow::Result<String> {
1314 use std::fs;
1315
1316 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
1317 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
1318
1319 if let Some(info) = branch_info {
1321 Ok(crate::prompts::build_enhanced_conflict_resolution_prompt(
1322 template_context,
1323 conflicts,
1324 Some(info),
1325 prompt_md_content.as_deref(),
1326 plan_content.as_deref(),
1327 ))
1328 } else {
1329 Ok(
1331 crate::prompts::build_conflict_resolution_prompt_with_context(
1332 template_context,
1333 conflicts,
1334 prompt_md_content.as_deref(),
1335 plan_content.as_deref(),
1336 ),
1337 )
1338 }
1339}
1340
1341fn run_ai_conflict_resolution(
1346 resolution_prompt: &str,
1347 config: &crate::config::Config,
1348 logger: &Logger,
1349 colors: Colors,
1350) -> anyhow::Result<ConflictResolutionResult> {
1351 use crate::agents::AgentRegistry;
1352 use crate::files::result_extraction::extract_last_result;
1353 use crate::pipeline::{
1354 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
1355 };
1356 use std::io;
1357 use std::path::Path;
1358
1359 let log_dir = ".agent/logs/rebase_conflict_resolution";
1363
1364 let registry = AgentRegistry::new()?;
1365 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
1366
1367 let mut runtime = PipelineRuntime {
1368 timer: &mut crate::pipeline::Timer::new(),
1369 logger,
1370 colors: &colors,
1371 config,
1372 #[cfg(any(test, feature = "test-utils"))]
1373 agent_executor: None,
1374 };
1375
1376 let validate_output: OutputValidator = |log_dir_path: &Path,
1379 validation_logger: &crate::logger::Logger|
1380 -> io::Result<bool> {
1381 match extract_last_result(log_dir_path) {
1382 Ok(Some(_)) => {
1383 Ok(true)
1385 }
1386 Ok(None) => {
1387 match crate::git_helpers::get_conflicted_files() {
1390 Ok(conflicts) if conflicts.is_empty() => {
1391 validation_logger
1392 .info("Agent resolved conflicts without JSON output (file edits only)");
1393 Ok(true) }
1395 Ok(conflicts) => {
1396 validation_logger.warn(&format!(
1397 "{} conflict(s) remain unresolved",
1398 conflicts.len()
1399 ));
1400 Ok(false) }
1402 Err(e) => {
1403 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
1404 Ok(false) }
1406 }
1407 }
1408 Err(e) => {
1409 validation_logger.warn(&format!("Output validation check failed: {e}"));
1410 Ok(false) }
1412 }
1413 };
1414
1415 let mut fallback_config = FallbackConfig {
1416 role: crate::agents::AgentRole::Reviewer,
1417 base_label: "conflict resolution",
1418 prompt: resolution_prompt,
1419 logfile_prefix: log_dir,
1420 runtime: &mut runtime,
1421 registry: ®istry,
1422 primary_agent: reviewer_agent,
1423 output_validator: Some(validate_output),
1424 };
1425
1426 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
1427
1428 if exit_code != 0 {
1429 return Ok(ConflictResolutionResult::Failed);
1430 }
1431
1432 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
1435
1436 if remaining_conflicts.is_empty() {
1437 match extract_last_result(Path::new(log_dir)) {
1439 Ok(Some(content)) => {
1440 logger.info("Agent provided JSON output with resolved files");
1441 Ok(ConflictResolutionResult::WithJson(content))
1442 }
1443 Ok(None) => {
1444 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1445 Ok(ConflictResolutionResult::FileEditsOnly)
1446 }
1447 Err(e) => {
1448 logger.warn(&format!(
1450 "Failed to extract JSON output but conflicts are resolved: {e}"
1451 ));
1452 Ok(ConflictResolutionResult::FileEditsOnly)
1453 }
1454 }
1455 } else {
1456 logger.warn(&format!(
1457 "{} conflict(s) remain after agent attempted resolution",
1458 remaining_conflicts.len()
1459 ));
1460 Ok(ConflictResolutionResult::Failed)
1461 }
1462}
1463
1464fn parse_and_validate_resolved_files(
1470 resolved_content: &str,
1471 logger: &Logger,
1472) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
1473 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
1474 anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
1477 })?;
1478
1479 let resolved_files = match json.get("resolved_files") {
1480 Some(v) if v.is_object() => v.as_object().unwrap(),
1481 _ => {
1482 logger.info("Agent output missing 'resolved_files' object");
1483 anyhow::bail!("Agent output missing 'resolved_files' object");
1484 }
1485 };
1486
1487 if resolved_files.is_empty() {
1488 logger.info("No resolved files in JSON output");
1489 anyhow::bail!("No files were resolved by the agent");
1490 }
1491
1492 Ok(resolved_files.clone())
1493}
1494
1495fn write_resolved_files(
1497 resolved_files: &serde_json::Map<String, serde_json::Value>,
1498 logger: &Logger,
1499) -> anyhow::Result<usize> {
1500 use std::fs;
1501
1502 let mut files_written = 0;
1503 for (path, content) in resolved_files {
1504 if let Some(content_str) = content.as_str() {
1505 fs::write(path, content_str).map_err(|e| {
1506 logger.error(&format!("Failed to write {path}: {e}"));
1507 anyhow::anyhow!("Failed to write {path}: {e}")
1508 })?;
1509 logger.info(&format!("Resolved and wrote: {path}"));
1510 files_written += 1;
1511 if let Err(e) = crate::git_helpers::git_add_all() {
1513 logger.warn(&format!("Failed to stage {path}: {e}"));
1514 }
1515 }
1516 }
1517
1518 logger.success(&format!("Successfully resolved {files_written} file(s)"));
1519 Ok(files_written)
1520}
1521
1522fn run_rebase_with_state_machine(
1540 state_machine: &mut RebaseStateMachine,
1541 logger: &Logger,
1542 colors: Colors,
1543 config: &crate::config::Config,
1544 template_context: &TemplateContext,
1545) -> anyhow::Result<RebaseResult> {
1546 use crate::git_helpers::{detect_concurrent_git_operations, RebaseLock, RebasePhase};
1547
1548 let upstream_branch = state_machine.upstream_branch().to_string();
1549 logger.info(&format!(
1550 "Rebasing onto {}{}{}",
1551 colors.cyan(),
1552 upstream_branch,
1553 colors.reset()
1554 ));
1555
1556 state_machine.transition_to(RebasePhase::PreRebaseCheck)?;
1558
1559 let checkpoint = state_machine.checkpoint();
1561 logger.info(&format!(
1562 "Rebase checkpoint: upstream={}, phase={:?}, error_count={}",
1563 checkpoint.upstream_branch, checkpoint.phase, checkpoint.error_count
1564 ));
1565
1566 let _lock =
1568 RebaseLock::new().map_err(|e| anyhow::anyhow!("Failed to acquire rebase lock: {e}"))?;
1569
1570 if let Err(e) = crate::git_helpers::validate_git_state() {
1572 logger.warn(&format!("Git state validation failed: {e}"));
1573 let _ = crate::git_helpers::cleanup_stale_rebase_state();
1575 }
1576
1577 if let Ok(Some(operation)) = detect_concurrent_git_operations() {
1579 let operation_desc = operation.description();
1580 logger.warn(&format!(
1581 "Cannot start rebase: {operation_desc} already in progress"
1582 ));
1583 return Ok(RebaseResult::Failed(
1584 crate::git_helpers::RebaseErrorKind::ConcurrentOperation {
1585 operation: operation_desc,
1586 },
1587 ));
1588 }
1589
1590 if let Err(e) = crate::git_helpers::validate_rebase_preconditions() {
1593 logger.warn(&format!("Pre-rebase validation failed: {e}"));
1594 state_machine.record_error(format!("Pre-rebase validation failed: {e}"));
1595 return Ok(RebaseResult::NoOp {
1597 reason: format!("Pre-rebase validation failed: {e}"),
1598 });
1599 }
1600
1601 state_machine.transition_to(RebasePhase::RebaseInProgress)?;
1603
1604 match rebase_onto(&upstream_branch) {
1606 Ok(RebaseResult::Success) => {
1607 state_machine.transition_to(RebasePhase::RebaseComplete)?;
1609 if let Err(e) = crate::git_helpers::validate_post_rebase_state() {
1610 logger.warn(&format!("Post-rebase validation failed: {e}"));
1611 state_machine.record_error(format!("Post-rebase validation failed: {e}"));
1612 }
1615 Ok(RebaseResult::Success)
1616 }
1617 Ok(RebaseResult::NoOp { reason }) => {
1618 state_machine.transition_to(RebasePhase::RebaseComplete)?;
1619 Ok(RebaseResult::NoOp { reason })
1620 }
1621 Ok(RebaseResult::Conflicts(files)) => {
1622 state_machine.transition_to(RebasePhase::ConflictDetected)?;
1623 for file in &files {
1624 state_machine.record_conflict(file.clone());
1625 }
1626
1627 logger.warn(&format!(
1628 "Rebase resulted in {} conflict(s), attempting AI resolution",
1629 state_machine.unresolved_conflict_count()
1630 ));
1631
1632 let resolution_result = try_resolve_conflicts_with_state_machine(
1634 state_machine,
1635 config,
1636 template_context,
1637 logger,
1638 colors,
1639 )?;
1640
1641 if resolution_result {
1642 if state_machine.all_conflicts_resolved() {
1644 state_machine.transition_to(RebasePhase::CompletingRebase)?;
1646 logger.info("Continuing rebase after conflict resolution");
1647 match continue_rebase() {
1648 Ok(()) => {
1649 if let Err(e) = crate::git_helpers::validate_post_rebase_state() {
1651 logger.warn(&format!("Post-rebase validation failed: {e}"));
1652 state_machine
1653 .record_error(format!("Post-rebase validation failed: {e}"));
1654 }
1656 state_machine.transition_to(RebasePhase::RebaseComplete)?;
1657 Ok(RebaseResult::Success)
1658 }
1659 Err(e) => {
1660 state_machine.record_error(format!("Failed to continue rebase: {e}"));
1661 logger.warn(&format!("Failed to continue rebase: {e}"));
1662 let _ = state_machine.transition_to(RebasePhase::RebaseAborted);
1663 let _ = abort_rebase();
1664 Ok(RebaseResult::Failed(
1665 crate::git_helpers::RebaseErrorKind::ReferenceUpdateFailed {
1666 reason: format!("Failed to continue: {e}"),
1667 },
1668 ))
1669 }
1670 }
1671 } else {
1672 let remaining = state_machine.unresolved_conflict_count();
1674 state_machine
1675 .record_error(format!("AI resolution left {remaining} conflict(s)"));
1676 logger.warn(&format!(
1677 "AI resolution left {remaining} conflict(s) unresolved"
1678 ));
1679 let _ = state_machine.transition_to(RebasePhase::RebaseAborted);
1680 let _ = abort_rebase();
1681 Ok(RebaseResult::Failed(
1682 crate::git_helpers::RebaseErrorKind::ContentConflict { files },
1683 ))
1684 }
1685 } else {
1686 state_machine.record_error("AI conflict resolution failed".to_string());
1688 logger.warn("AI conflict resolution failed, aborting rebase");
1689 let _ = state_machine.transition_to(RebasePhase::RebaseAborted);
1690 let _ = abort_rebase();
1691 Ok(RebaseResult::Failed(
1692 crate::git_helpers::RebaseErrorKind::ContentConflict { files },
1693 ))
1694 }
1695 }
1696 Ok(RebaseResult::Failed(err)) => {
1697 state_machine.record_error(err.description());
1698 let _ = state_machine.transition_to(RebasePhase::RebaseAborted);
1699 Ok(RebaseResult::Failed(err))
1700 }
1701 Err(e) => {
1702 state_machine.record_error(format!("Rebase error: {e}"));
1703 Err(e.into())
1704 }
1705 }
1706}
1707
1708fn run_fallback_rebase(
1714 logger: &Logger,
1715 colors: Colors,
1716 config: &crate::config::Config,
1717 template_context: &TemplateContext,
1718) -> anyhow::Result<()> {
1719 logger.warn("Using fallback rebase mode (state machine unavailable)");
1720
1721 match run_rebase_to_default(logger, colors) {
1722 Ok(RebaseResult::Success) => {
1723 logger.success("Rebase completed successfully");
1724 Ok(())
1725 }
1726 Ok(RebaseResult::NoOp { reason }) => {
1727 logger.info(&format!("No rebase needed: {reason}"));
1728 Ok(())
1729 }
1730 Ok(RebaseResult::Failed(err)) => {
1731 logger.error(&format!("Rebase failed: {err}"));
1732 anyhow::bail!("Rebase failed: {err}")
1733 }
1734 Ok(RebaseResult::Conflicts(_conflicts)) => {
1735 let conflicted_files = get_conflicted_files()?;
1736 if conflicted_files.is_empty() {
1737 logger.warn("Rebase reported conflicts but no conflicted files found");
1738 let _ = abort_rebase();
1739 return Ok(());
1740 }
1741
1742 logger.warn(&format!(
1743 "Rebase resulted in {} conflict(s), attempting AI resolution",
1744 conflicted_files.len()
1745 ));
1746
1747 match try_resolve_conflicts_with_fallback(
1748 &conflicted_files,
1749 config,
1750 template_context,
1751 logger,
1752 colors,
1753 ) {
1754 Ok(true) => {
1755 logger.info("Continuing rebase after conflict resolution");
1756 match continue_rebase() {
1757 Ok(()) => {
1758 logger.success("Rebase completed successfully after AI resolution");
1759 Ok(())
1760 }
1761 Err(e) => {
1762 logger.warn(&format!("Failed to continue rebase: {e}"));
1763 let _ = abort_rebase();
1764 Ok(())
1765 }
1766 }
1767 }
1768 Ok(false) => {
1769 logger.warn("AI conflict resolution failed, aborting rebase");
1770 let _ = abort_rebase();
1771 Ok(())
1772 }
1773 Err(e) => {
1774 logger.error(&format!("Conflict resolution error: {e}"));
1775 let _ = abort_rebase();
1776 Ok(())
1777 }
1778 }
1779 }
1780 Err(e) => {
1781 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1782 Ok(())
1783 }
1784 }
1785}
1786
1787fn try_resolve_conflicts_with_state_machine(
1807 state_machine: &mut RebaseStateMachine,
1808 config: &crate::config::Config,
1809 template_context: &TemplateContext,
1810 logger: &Logger,
1811 colors: Colors,
1812) -> anyhow::Result<bool> {
1813 use crate::git_helpers::RebasePhase;
1814
1815 let conflicted_files = get_conflicted_files()?;
1817 if conflicted_files.is_empty() {
1818 logger.warn("No conflicted files found despite conflict state");
1819 return Ok(false);
1820 }
1821
1822 state_machine.transition_to(RebasePhase::ConflictResolutionInProgress)?;
1824
1825 let upstream_branch = state_machine.upstream_branch().to_string();
1827 let branch_info = match crate::prompts::collect_branch_info(&upstream_branch) {
1828 Ok(info) => {
1829 logger.info(&format!(
1830 "Branch context: {} diverging from {} by {} commit(s)",
1831 info.current_branch, info.upstream_branch, info.diverging_count
1832 ));
1833 Some(info)
1834 }
1835 Err(e) => {
1836 logger.warn(&format!(
1837 "Failed to collect branch info: {e}, continuing without it"
1838 ));
1839 None
1840 }
1841 };
1842
1843 let max_iterations = 3;
1845
1846 let mut previous_validation_failures = Vec::new();
1848
1849 for iteration in 1..=max_iterations {
1850 logger.info(&format!(
1851 "Conflict resolution cycle {iteration}/{max_iterations}",
1852 iteration = iteration,
1853 max_iterations = max_iterations
1854 ));
1855
1856 let conflicts = collect_conflict_info_or_error(&conflicted_files, logger)?;
1858 let resolution_prompt = if iteration == 1 {
1859 build_enhanced_resolution_prompt(&conflicts, branch_info.as_ref(), template_context)?
1861 } else {
1862 let failure_context = if previous_validation_failures.is_empty() {
1864 "Your previous resolution attempt was not successful.".to_string()
1865 } else {
1866 format!(
1867 "Your previous resolution attempt failed validation with these issues:\n\
1868 {}\n\nPlease address these specific issues in your next attempt.",
1869 previous_validation_failures
1870 .iter()
1871 .map(|s| format!("- {s}"))
1872 .collect::<Vec<_>>()
1873 .join("\n")
1874 )
1875 };
1876
1877 format!(
1878 "{}\n\n## Previous Resolution Failed\n\n\
1879 {}\n\n\
1880 **Validation Requirements**:\n\
1881 1. ALL conflict markers (<<<<<<<, =======, >>>>>>>) must be removed\n\
1882 2. The code must be syntactically valid (balanced brackets, etc.)\n\
1883 3. Files must be actually modified (not left unchanged)\n\
1884 4. Git must report no conflicted files after resolution\n\
1885 5. You must preserve the intent of BOTH sides where possible\n\n\
1886 **Guidance for this attempt**:\n\
1887 - Review each file carefully for remaining conflict markers\n\
1888 - Check for syntax errors like unbalanced braces/brackets/parentheses\n\
1889 - Ensure you're not accidentally leaving files unchanged\n\
1890 - Consider using the JSON output format to confirm your resolutions\n\n{}",
1891 build_enhanced_resolution_prompt(
1892 &conflicts,
1893 branch_info.as_ref(),
1894 template_context
1895 )?,
1896 failure_context,
1897 if iteration == max_iterations {
1898 "**FINAL ATTEMPT**: If conflicts remain after this attempt, manual intervention will be required. \
1899 Take extra care to ensure all validation criteria are met."
1900 } else {
1901 "Please try again with careful attention to the validation feedback above."
1902 }
1903 )
1904 };
1905
1906 match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1908 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1909 let resolved_files =
1912 match parse_and_validate_resolved_files(&resolved_content, logger) {
1913 Ok(files) => {
1914 write_resolved_files(&files, logger)?;
1916 Some(files)
1917 }
1918 Err(_) => {
1919 None
1922 }
1923 };
1924
1925 previous_validation_failures.clear();
1927
1928 match validate_conflict_resolution_detailed(logger, &conflicted_files) {
1930 Ok(validation_result) if validation_result.is_valid() => {
1931 if let Some(ref files) = resolved_files {
1933 for path in files.keys() {
1934 state_machine.record_resolution(path.clone());
1935 }
1936 }
1937 logger.success(&format!(
1938 "All conflicts resolved successfully after {} cycle(s)",
1939 iteration
1940 ));
1941 return Ok(true);
1942 }
1943 Ok(validation_result) => {
1944 if !validation_result.files_with_markers.is_empty() {
1946 previous_validation_failures.push(format!(
1947 "Files still have conflict markers: {}",
1948 validation_result.files_with_markers.join(", ")
1949 ));
1950 }
1951 if !validation_result.files_with_syntax_errors.is_empty() {
1952 previous_validation_failures.push(format!(
1953 "Files have syntax errors: {}",
1954 validation_result.files_with_syntax_errors.join(", ")
1955 ));
1956 }
1957 if !validation_result.unmodified_files.is_empty() {
1958 previous_validation_failures.push(format!(
1959 "Files were not modified: {}",
1960 validation_result.unmodified_files.join(", ")
1961 ));
1962 }
1963 let remaining = get_conflicted_files().unwrap_or_default();
1965 if !remaining.is_empty() {
1966 previous_validation_failures.push(format!(
1967 "Git still reports conflicts: {}",
1968 remaining.join(", ")
1969 ));
1970 }
1971
1972 state_machine.record_error(format!(
1973 "Conflict resolution validation failed: {}",
1974 validation_result.failure_summary()
1975 ));
1976 logger.warn(&format!(
1977 "Resolution validation failed: {}, retrying...",
1978 validation_result.failure_summary()
1979 ));
1980 }
1981 Err(e) => {
1982 previous_validation_failures.push(format!("Validation error: {e}"));
1983 state_machine.record_error(format!("Validation error: {e}"));
1984 logger.warn(&format!("Resolution validation error: {e}, retrying..."));
1985 }
1986 }
1987 }
1988 Ok(ConflictResolutionResult::FileEditsOnly) => {
1989 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1991
1992 previous_validation_failures.clear();
1994
1995 match validate_conflict_resolution_detailed(logger, &conflicted_files) {
1996 Ok(validation_result) if validation_result.is_valid() => {
1997 for file in &conflicted_files {
1999 state_machine.record_resolution(file.clone());
2000 }
2001 logger.success(&format!(
2002 "All conflicts resolved successfully after {} cycle(s)",
2003 iteration
2004 ));
2005 return Ok(true);
2006 }
2007 Ok(validation_result) => {
2008 if !validation_result.files_with_markers.is_empty() {
2010 previous_validation_failures.push(format!(
2011 "Files still have conflict markers: {}",
2012 validation_result.files_with_markers.join(", ")
2013 ));
2014 }
2015 if !validation_result.files_with_syntax_errors.is_empty() {
2016 previous_validation_failures.push(format!(
2017 "Files have syntax errors: {}",
2018 validation_result.files_with_syntax_errors.join(", ")
2019 ));
2020 }
2021
2022 state_machine.record_error(format!(
2023 "Conflict resolution validation failed: {}",
2024 validation_result.failure_summary()
2025 ));
2026 logger.warn(&format!(
2027 "Resolution validation failed: {}, retrying...",
2028 validation_result.failure_summary()
2029 ));
2030 }
2031 Err(e) => {
2032 previous_validation_failures.push(format!("Validation error: {e}"));
2033 state_machine.record_error(format!("Validation error: {e}"));
2034 logger.warn(&format!("Resolution validation error: {e}, retrying..."));
2035 }
2036 }
2037 }
2038 Ok(ConflictResolutionResult::Failed) | Err(_) => {
2039 logger.warn("AI conflict resolution attempt failed");
2040
2041 if iteration >= max_iterations {
2043 break;
2044 }
2045 }
2046 }
2047 }
2048
2049 logger.info("Resolution cycles exhausted, checking for manual resolution...");
2052 match crate::git_helpers::continue_rebase() {
2053 Ok(()) => {
2054 logger.info("Successfully continued rebase (possibly with manual resolution)");
2055 for file in &conflicted_files {
2057 state_machine.record_resolution(file.clone());
2058 }
2059 Ok(true)
2060 }
2061 Err(rebase_err) => {
2062 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
2063 Ok(false)
2064 }
2065 }
2066}
2067
2068#[derive(Debug, Clone, Default)]
2072struct ConflictValidationResult {
2073 pub files_with_markers: Vec<String>,
2075 pub files_with_syntax_errors: Vec<String>,
2077 pub unmodified_files: Vec<String>,
2079 pub is_valid: bool,
2081}
2082
2083impl ConflictValidationResult {
2084 pub fn is_valid(&self) -> bool {
2086 self.is_valid
2087 }
2088
2089 pub fn failure_summary(&self) -> String {
2091 let mut parts = Vec::new();
2092
2093 if !self.files_with_markers.is_empty() {
2094 parts.push(format!(
2095 "{} file(s) still have conflict markers",
2096 self.files_with_markers.len()
2097 ));
2098 }
2099 if !self.files_with_syntax_errors.is_empty() {
2100 parts.push(format!(
2101 "{} file(s) have syntax errors",
2102 self.files_with_syntax_errors.len()
2103 ));
2104 }
2105 if !self.unmodified_files.is_empty() {
2106 parts.push(format!(
2107 "{} file(s) were not modified",
2108 self.unmodified_files.len()
2109 ));
2110 }
2111
2112 if parts.is_empty() {
2113 "No specific issues detected".to_string()
2114 } else {
2115 parts.join(", ")
2116 }
2117 }
2118}
2119
2120fn validate_file_syntax(extension: &str, content: &str) -> anyhow::Result<()> {
2134 match extension {
2135 "rs" => {
2137 let open_braces = content.matches('{').count();
2138 let close_braces = content.matches('}').count();
2139 let open_parens = content.matches('(').count();
2140 let close_parens = content.matches(')').count();
2141 let open_brackets = content.matches('[').count();
2142 let close_brackets = content.matches(']').count();
2143
2144 if open_braces != close_braces {
2145 anyhow::bail!("Unbalanced braces: {open_braces} open, {close_braces} close");
2146 }
2147 if open_parens != close_parens {
2148 anyhow::bail!("Unbalanced parentheses: {open_parens} open, {close_parens} close");
2149 }
2150 if open_brackets != close_brackets {
2151 anyhow::bail!("Unbalanced brackets: {open_brackets} open, {close_brackets} close");
2152 }
2153 Ok(())
2154 }
2155 "py" => {
2157 let lines: Vec<&str> = content.lines().collect();
2159 for (i, line) in lines.iter().enumerate() {
2160 if line.contains('\t') && line.matches(' ').count() > 0 {
2162 anyhow::bail!("Line {}: mixed tabs and spaces", i + 1);
2163 }
2164 }
2165 Ok(())
2166 }
2167 "js" | "ts" | "jsx" | "tsx" => {
2169 let open_braces = content.matches('{').count();
2170 let close_braces = content.matches('}').count();
2171 let open_parens = content.matches('(').count();
2172 let close_parens = content.matches(')').count();
2173 let open_brackets = content.matches('[').count();
2174 let close_brackets = content.matches(']').count();
2175
2176 if open_braces != close_braces {
2177 anyhow::bail!("Unbalanced braces: {open_braces} open, {close_braces} close");
2178 }
2179 if open_parens != close_parens {
2180 anyhow::bail!("Unbalanced parentheses: {open_parens} open, {close_parens} close");
2181 }
2182 if open_brackets != close_brackets {
2183 anyhow::bail!("Unbalanced brackets: {open_brackets} open, {close_brackets} close");
2184 }
2185 Ok(())
2186 }
2187 "json" => {
2189 let open_braces = content.matches('{').count();
2190 let close_braces = content.matches('}').count();
2191 let open_brackets = content.matches('[').count();
2192 let close_brackets = content.matches(']').count();
2193
2194 if open_braces != close_braces {
2195 anyhow::bail!("Unbalanced braces: {open_braces} open, {close_braces} close");
2196 }
2197 if open_brackets != close_brackets {
2198 anyhow::bail!("Unbalanced brackets: {open_brackets} open, {close_brackets} close");
2199 }
2200 Ok(())
2201 }
2202 "yaml" | "yml" => {
2204 for line in content.lines() {
2206 if line.starts_with('\t') {
2208 anyhow::bail!("YAML files should not use tabs for indentation");
2209 }
2210 }
2211 Ok(())
2212 }
2213 _ => Ok(()),
2215 }
2216}
2217
2218fn validate_conflict_resolution_detailed(
2232 logger: &Logger,
2233 original_conflicts: &[String],
2234) -> anyhow::Result<ConflictValidationResult> {
2235 use std::fs;
2236
2237 let mut validation_result = ConflictValidationResult::default();
2238
2239 for path in original_conflicts {
2241 match fs::read_to_string(path) {
2242 Ok(content) => {
2243 let has_markers = content.contains("<<<<<<<")
2245 || content.contains("=======")
2246 || content.contains(">>>>>>>");
2247
2248 if has_markers {
2249 validation_result.files_with_markers.push(path.clone());
2250 logger.warn(&format!("File {} still contains conflict markers", path));
2251 }
2252
2253 if let Some(ext) = std::path::Path::new(path).extension() {
2255 if let Some(ext_str) = ext.to_str() {
2256 if validate_file_syntax(ext_str, &content).is_err() {
2257 validation_result
2258 .files_with_syntax_errors
2259 .push(path.clone());
2260 logger.warn(&format!("File {} may have syntax errors", path));
2261 }
2262 }
2263 }
2264 }
2265 Err(e) => {
2266 logger.warn(&format!("Failed to read file {}: {}", path, e));
2267 validation_result.files_with_markers.push(path.clone());
2269 }
2270 }
2271 }
2272
2273 let remaining_conflicts = get_conflicted_files()?;
2275 if !remaining_conflicts.is_empty() {
2276 logger.warn(&format!(
2277 "Git still reports {} conflicted file(s): {}",
2278 remaining_conflicts.len(),
2279 remaining_conflicts.join(", ")
2280 ));
2281 }
2282
2283 for path in original_conflicts {
2285 if remaining_conflicts.contains(path)
2286 && !validation_result.files_with_markers.contains(path)
2287 {
2288 logger.warn(&format!(
2291 "File {} is still marked as conflicted by Git",
2292 path
2293 ));
2294 }
2295 }
2296
2297 validation_result.is_valid = validation_result.files_with_markers.is_empty()
2298 && validation_result.files_with_syntax_errors.is_empty()
2299 && remaining_conflicts.is_empty();
2300
2301 Ok(validation_result)
2302}